Files
MWClash/lib/widgets/side_sheet.dart
chen08209 82be4cc45f Optimize proxies page
Support mouse drag scroll

Adjust desktop ui
2024-07-17 14:52:15 +08:00

651 lines
18 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
const Duration _bottomSheetEnterDuration = Duration(milliseconds: 300);
const Duration _bottomSheetExitDuration = Duration(milliseconds: 200);
const Curve _modalBottomSheetCurve = Easing.standardDecelerate;
const double _defaultScrollControlDisabledMaxHeightRatio = 9.0 / 16.0;
class SideSheet extends StatefulWidget {
const SideSheet({
super.key,
this.animationController,
this.enableDrag = true,
this.showDragHandle,
this.dragHandleColor,
this.dragHandleSize,
this.onDragStart,
this.onDragEnd,
this.backgroundColor,
this.shadowColor,
this.elevation,
this.shape,
this.clipBehavior,
this.constraints,
required this.onClosing,
required this.builder,
}) : assert(elevation == null || elevation >= 0.0);
final AnimationController? animationController;
final VoidCallback onClosing;
final WidgetBuilder builder;
final bool enableDrag;
final bool? showDragHandle;
final Color? dragHandleColor;
final Size? dragHandleSize;
final BottomSheetDragStartHandler? onDragStart;
final BottomSheetDragEndHandler? onDragEnd;
final Color? backgroundColor;
final Color? shadowColor;
final double? elevation;
final ShapeBorder? shape;
final Clip? clipBehavior;
final BoxConstraints? constraints;
@override
State<SideSheet> createState() => _SideSheetState();
static AnimationController createAnimationController(TickerProvider vsync) {
return AnimationController(
duration: _bottomSheetEnterDuration,
reverseDuration: _bottomSheetExitDuration,
debugLabel: 'SideSheet',
vsync: vsync,
);
}
}
class _SideSheetState extends State<SideSheet> {
final GlobalKey _childKey = GlobalKey(debugLabel: 'SideSheet child');
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final Color color = widget.backgroundColor ?? colorScheme.surface;
final Color surfaceTintColor = colorScheme.surfaceTint;
final Color shadowColor = widget.shadowColor ?? Colors.transparent;
final double elevation = widget.elevation ?? 0;
final ShapeBorder shape = widget.shape ??
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(0),
);
final BoxConstraints constraints = widget.constraints ??
const BoxConstraints(
maxWidth: 320,
minWidth: 320
);
final Clip clipBehavior = widget.clipBehavior ?? Clip.none;
Widget sideSheet = Material(
key: _childKey,
color: color,
elevation: elevation,
surfaceTintColor: surfaceTintColor,
shadowColor: shadowColor,
shape: shape,
clipBehavior: clipBehavior,
child: widget.builder(context),
);
return ConstrainedBox(
constraints: constraints,
child: sideSheet,
);
}
}
typedef _SizeChangeCallback<Size> = void Function(Size);
class _SideSheetLayoutWithSizeListener extends SingleChildRenderObjectWidget {
const _SideSheetLayoutWithSizeListener({
required this.onChildSizeChanged,
required this.animationValue,
required this.isScrollControlled,
required this.scrollControlDisabledMaxHeightRatio,
super.child,
});
final _SizeChangeCallback<Size> onChildSizeChanged;
final double animationValue;
final bool isScrollControlled;
final double scrollControlDisabledMaxHeightRatio;
@override
_RenderSideSheetLayoutWithSizeListener createRenderObject(
BuildContext context) {
return _RenderSideSheetLayoutWithSizeListener(
onChildSizeChanged: onChildSizeChanged,
animationValue: animationValue,
isScrollControlled: isScrollControlled,
scrollControlDisabledMaxHeightRatio: scrollControlDisabledMaxHeightRatio,
);
}
@override
void updateRenderObject(BuildContext context,
_RenderSideSheetLayoutWithSizeListener renderObject) {
renderObject.onChildSizeChanged = onChildSizeChanged;
renderObject.animationValue = animationValue;
renderObject.isScrollControlled = isScrollControlled;
renderObject.scrollControlDisabledMaxHeightRatio =
scrollControlDisabledMaxHeightRatio;
}
}
class _RenderSideSheetLayoutWithSizeListener extends RenderShiftedBox {
_RenderSideSheetLayoutWithSizeListener({
RenderBox? child,
required _SizeChangeCallback<Size> onChildSizeChanged,
required double animationValue,
required bool isScrollControlled,
required double scrollControlDisabledMaxHeightRatio,
}) : _onChildSizeChanged = onChildSizeChanged,
_animationValue = animationValue,
_isScrollControlled = isScrollControlled,
_scrollControlDisabledMaxHeightRatio =
scrollControlDisabledMaxHeightRatio,
super(child);
Size _lastSize = Size.zero;
_SizeChangeCallback<Size> get onChildSizeChanged => _onChildSizeChanged;
_SizeChangeCallback<Size> _onChildSizeChanged;
set onChildSizeChanged(_SizeChangeCallback<Size> newCallback) {
if (_onChildSizeChanged == newCallback) {
return;
}
_onChildSizeChanged = newCallback;
markNeedsLayout();
}
double get animationValue => _animationValue;
double _animationValue;
set animationValue(double newValue) {
if (_animationValue == newValue) {
return;
}
_animationValue = newValue;
markNeedsLayout();
}
bool get isScrollControlled => _isScrollControlled;
bool _isScrollControlled;
set isScrollControlled(bool newValue) {
if (_isScrollControlled == newValue) {
return;
}
_isScrollControlled = newValue;
markNeedsLayout();
}
double get scrollControlDisabledMaxHeightRatio =>
_scrollControlDisabledMaxHeightRatio;
double _scrollControlDisabledMaxHeightRatio;
set scrollControlDisabledMaxHeightRatio(double newValue) {
if (_scrollControlDisabledMaxHeightRatio == newValue) {
return;
}
_scrollControlDisabledMaxHeightRatio = newValue;
markNeedsLayout();
}
Size _getSize(BoxConstraints constraints) {
return constraints.constrain(constraints.biggest);
}
@override
double computeMinIntrinsicWidth(double height) {
final double width =
_getSize(BoxConstraints.tightForFinite(height: height)).width;
if (width.isFinite) {
return width;
}
return 0.0;
}
@override
double computeMaxIntrinsicWidth(double height) {
final double width =
_getSize(BoxConstraints.tightForFinite(height: height)).width;
if (width.isFinite) {
return width;
}
return 0.0;
}
@override
double computeMinIntrinsicHeight(double width) {
final double height =
_getSize(BoxConstraints.tightForFinite(width: width)).height;
if (height.isFinite) {
return height;
}
return 0.0;
}
@override
double computeMaxIntrinsicHeight(double width) {
final double height =
_getSize(BoxConstraints.tightForFinite(width: width)).height;
if (height.isFinite) {
return height;
}
return 0.0;
}
@override
Size computeDryLayout(BoxConstraints constraints) {
return _getSize(constraints);
}
BoxConstraints _getConstraintsForChild(BoxConstraints constraints) {
return BoxConstraints(
maxHeight: constraints.maxHeight,
);
}
Offset _getPositionForChild(Size size, Size childSize) {
return Offset(size.width - childSize.width * animationValue, 0.0);
}
@override
void performLayout() {
size = _getSize(constraints);
if (child != null) {
final BoxConstraints childConstraints =
_getConstraintsForChild(constraints);
assert(childConstraints.debugAssertIsValid(isAppliedConstraint: true));
child!.layout(
childConstraints,
parentUsesSize: !childConstraints.isTight,
);
final BoxParentData childParentData = child!.parentData! as BoxParentData;
childParentData.offset = _getPositionForChild(
size,
childConstraints.isTight ? childConstraints.smallest : child!.size,
);
final Size childSize =
childConstraints.isTight ? childConstraints.smallest : child!.size;
if (_lastSize != childSize) {
_lastSize = childSize;
_onChildSizeChanged.call(_lastSize);
}
}
}
}
class _ModalSideSheet<T> extends StatefulWidget {
const _ModalSideSheet({
super.key,
required this.route,
this.backgroundColor,
this.elevation,
this.shape,
this.clipBehavior,
this.constraints,
this.isScrollControlled = false,
this.scrollControlDisabledMaxHeightRatio =
_defaultScrollControlDisabledMaxHeightRatio,
this.enableDrag = true,
this.showDragHandle = false,
});
final ModalSideSheetRoute<T> route;
final bool isScrollControlled;
final double scrollControlDisabledMaxHeightRatio;
final Color? backgroundColor;
final double? elevation;
final ShapeBorder? shape;
final Clip? clipBehavior;
final BoxConstraints? constraints;
final bool enableDrag;
final bool showDragHandle;
@override
_ModalSideSheetState<T> createState() => _ModalSideSheetState<T>();
}
class _ModalSideSheetState<T> extends State<_ModalSideSheet<T>> {
ParametricCurve<double> animationCurve = _modalBottomSheetCurve;
String _getRouteLabel(MaterialLocalizations localizations) {
switch (Theme.of(context).platform) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
return '';
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
return localizations.dialogLabel;
}
}
EdgeInsets _getNewClipDetails(Size topLayerSize) {
return EdgeInsets.fromLTRB(0, 0, 0, topLayerSize.height);
}
@override
Widget build(BuildContext context) {
assert(debugCheckHasMediaQuery(context));
assert(debugCheckHasMaterialLocalizations(context));
final MaterialLocalizations localizations =
MaterialLocalizations.of(context);
final String routeLabel = _getRouteLabel(localizations);
return AnimatedBuilder(
animation: widget.route.animation!,
child: SideSheet(
animationController: widget.route._animationController,
onClosing: () {
if (widget.route.isCurrent) {
Navigator.pop(context);
}
},
builder: widget.route.builder,
backgroundColor: widget.backgroundColor,
elevation: widget.elevation,
shape: widget.shape,
clipBehavior: widget.clipBehavior,
constraints: widget.constraints,
enableDrag: widget.enableDrag,
showDragHandle: widget.showDragHandle,
),
builder: (BuildContext context, Widget? child) {
final double animationValue = animationCurve.transform(
widget.route.animation!.value,
);
return Semantics(
scopesRoute: true,
namesRoute: true,
label: routeLabel,
explicitChildNodes: true,
child: ClipRect(
child: _SideSheetLayoutWithSizeListener(
onChildSizeChanged: (Size size) {
widget.route._didChangeBarrierSemanticsClip(
_getNewClipDetails(size),
);
},
animationValue: animationValue,
isScrollControlled: widget.isScrollControlled,
scrollControlDisabledMaxHeightRatio:
widget.scrollControlDisabledMaxHeightRatio,
child: child,
),
),
);
},
);
}
}
class ModalSideSheetRoute<T> extends PopupRoute<T> {
ModalSideSheetRoute(
{required this.builder,
this.capturedThemes,
this.barrierLabel,
this.barrierOnTapHint,
this.backgroundColor,
this.elevation,
this.shape,
this.clipBehavior,
this.constraints,
this.modalBarrierColor,
this.isDismissible = true,
this.isScrollControlled = false,
this.scrollControlDisabledMaxHeightRatio =
_defaultScrollControlDisabledMaxHeightRatio,
super.settings,
this.transitionAnimationController,
this.anchorPoint,
this.useSafeArea = false,
super.filter});
final WidgetBuilder builder;
final CapturedThemes? capturedThemes;
final bool isScrollControlled;
final double scrollControlDisabledMaxHeightRatio;
final Color? backgroundColor;
final double? elevation;
final ShapeBorder? shape;
final Clip? clipBehavior;
final BoxConstraints? constraints;
final Color? modalBarrierColor;
final bool isDismissible;
final AnimationController? transitionAnimationController;
final Offset? anchorPoint;
final bool useSafeArea;
final String? barrierOnTapHint;
final ValueNotifier<EdgeInsets> _clipDetailsNotifier =
ValueNotifier<EdgeInsets>(EdgeInsets.zero);
@override
void dispose() {
_clipDetailsNotifier.dispose();
super.dispose();
}
bool _didChangeBarrierSemanticsClip(EdgeInsets newClipDetails) {
if (_clipDetailsNotifier.value == newClipDetails) {
return false;
}
_clipDetailsNotifier.value = newClipDetails;
return true;
}
@override
Duration get transitionDuration => _bottomSheetEnterDuration;
@override
Duration get reverseTransitionDuration => _bottomSheetExitDuration;
@override
bool get barrierDismissible => isDismissible;
@override
final String? barrierLabel;
@override
Color get barrierColor => modalBarrierColor ?? Colors.black54;
AnimationController? _animationController;
@override
AnimationController createAnimationController() {
assert(_animationController == null);
if (transitionAnimationController != null) {
_animationController = transitionAnimationController;
willDisposeAnimationController = false;
} else {
_animationController = SideSheet.createAnimationController(navigator!);
}
return _animationController!;
}
@override
Widget buildPage(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
final Widget content = DisplayFeatureSubScreen(
anchorPoint: anchorPoint,
child: Builder(
builder: (BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return _ModalSideSheet<T>(
route: this,
backgroundColor: backgroundColor ?? colorScheme.surface,
elevation: elevation ?? 0,
shape: shape,
clipBehavior: clipBehavior,
constraints: constraints,
isScrollControlled: isScrollControlled,
scrollControlDisabledMaxHeightRatio:
scrollControlDisabledMaxHeightRatio,
);
},
),
);
final Widget sideSheet = content;
return capturedThemes?.wrap(sideSheet) ?? sideSheet;
}
@override
Widget buildModalBarrier() {
if (barrierColor.alpha != 0 && !offstage) {
assert(barrierColor != barrierColor.withOpacity(0.0));
final Animation<Color?> color = animation!.drive(
ColorTween(
begin: barrierColor.withOpacity(0.0),
end: barrierColor,
).chain(
CurveTween(curve: barrierCurve),
),
);
return AnimatedModalBarrier(
color: color,
dismissible: barrierDismissible,
semanticsLabel: barrierLabel,
barrierSemanticsDismissible: semanticsDismissible,
clipDetailsNotifier: _clipDetailsNotifier,
semanticsOnTapHint: barrierOnTapHint,
);
} else {
return ModalBarrier(
dismissible: barrierDismissible,
semanticsLabel: barrierLabel,
barrierSemanticsDismissible: semanticsDismissible,
clipDetailsNotifier: _clipDetailsNotifier,
semanticsOnTapHint: barrierOnTapHint,
);
}
}
}
Future<T?> showModalSideSheet<T>({
required BuildContext context,
required Widget body,
required String title,
Color? backgroundColor,
String? barrierLabel,
double? elevation,
ShapeBorder? shape,
Clip? clipBehavior,
BoxConstraints? constraints,
Color? barrierColor,
bool isScrollControlled = false,
double scrollControlDisabledMaxHeightRatio =
_defaultScrollControlDisabledMaxHeightRatio,
bool useRootNavigator = false,
bool isDismissible = true,
bool useSafeArea = false,
RouteSettings? routeSettings,
AnimationController? transitionAnimationController,
Offset? anchorPoint,
}) {
assert(debugCheckHasMediaQuery(context));
assert(debugCheckHasMaterialLocalizations(context));
final NavigatorState navigator =
Navigator.of(context, rootNavigator: useRootNavigator);
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
return navigator.push(ModalSideSheetRoute<T>(
builder: (context) {
return Column(
children: [
Flexible(
flex: 0,
child: Row(
children: [
const SizedBox(
height: kToolbarHeight,
width: kToolbarHeight,
child: BackButton(),
),
const SizedBox(
width: 8,
),
Expanded(
child: Text(
title,
style: Theme.of(context).textTheme.titleMedium,
),
),
const SizedBox(
height: kToolbarHeight,
width: kToolbarHeight,
child: CloseButton(),
),
],
),
),
Expanded(
flex: 1,
child: body,
),
],
);
},
capturedThemes:
InheritedTheme.capture(from: context, to: navigator.context),
isScrollControlled: isScrollControlled,
scrollControlDisabledMaxHeightRatio: scrollControlDisabledMaxHeightRatio,
barrierLabel: barrierLabel ?? localizations.scrimLabel,
barrierOnTapHint:
localizations.scrimOnTapHint(localizations.bottomSheetLabel),
backgroundColor: backgroundColor,
elevation: elevation,
shape: shape,
clipBehavior: clipBehavior,
constraints: constraints,
isDismissible: isDismissible,
modalBarrierColor:
barrierColor ?? Theme.of(context).bottomSheetTheme.modalBarrierColor,
settings: routeSettings,
transitionAnimationController: transitionAnimationController,
anchorPoint: anchorPoint,
useSafeArea: useSafeArea,
));
}