Files
MWClash/lib/widgets/side_sheet.dart
chen08209 ed7868282a Add android separates the core process
Support core status check and force restart

Optimize proxies page and access page

Update flutter and pub dependencies

Update go version

Optimize more details
2025-09-23 15:23:58 +08:00

656 lines
18 KiB
Dart

import 'dart:ui';
import 'package:fl_clash/common/color.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 ??
RoundedSuperellipseBorder(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.a != 0 && !offstage) {
assert(barrierColor != barrierColor.opacity0);
final Animation<Color?> color = animation!.drive(
ColorTween(
begin: barrierColor.opacity0,
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 WidgetBuilder builder,
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,
ImageFilter? filter,
}) {
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: builder,
filter: filter,
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,
),
);
}
// class ModalAppBar extends StatelessWidget {
// final String title;
//
// const ModalAppBar({
// super.key,
// required this.title,
// });
//
// @override
// Widget build(BuildContext context) {
// return AppBar(
// automaticallyImplyLeading: false,
// title: Text(title),
// centerTitle: false,
// actions: const [
// SizedBox(
// height: kToolbarHeight,
// width: kToolbarHeight,
// child: CloseButton(),
// )
// ],
// );
// }
// }