mirror of
https://github.com/wgh136/pixes.git
synced 2025-09-27 04:57:23 +00:00
445 lines
12 KiB
Dart
445 lines
12 KiB
Dart
import 'dart:math';
|
|
import 'dart:ui';
|
|
import 'package:flutter/gestures.dart';
|
|
import 'package:fluent_ui/fluent_ui.dart';
|
|
import 'package:pixes/foundation/app.dart';
|
|
|
|
const double _kBackGestureWidth = 20.0;
|
|
const int _kMaxDroppedSwipePageForwardAnimationTime = 800;
|
|
const int _kMaxPageBackAnimationTime = 300;
|
|
const double _kMinFlingVelocity = 1.0;
|
|
|
|
class AppPageRoute<T> extends PageRoute<T> with _AppRouteTransitionMixin {
|
|
/// Construct a MaterialPageRoute whose contents are defined by [builder].
|
|
AppPageRoute({
|
|
required this.builder,
|
|
super.settings,
|
|
this.maintainState = true,
|
|
super.fullscreenDialog,
|
|
super.allowSnapshotting = true,
|
|
super.barrierDismissible = false,
|
|
this.enableIOSGesture = true,
|
|
this.preventRebuild = true,
|
|
this.isRoot = false,
|
|
}) {
|
|
assert(opaque);
|
|
}
|
|
|
|
/// Builds the primary contents of the route.
|
|
final WidgetBuilder builder;
|
|
|
|
@override
|
|
Widget buildContent(BuildContext context) {
|
|
return builder(context);
|
|
}
|
|
|
|
@override
|
|
final bool maintainState;
|
|
|
|
@override
|
|
String get debugLabel => '${super.debugLabel}(${settings.name})';
|
|
|
|
@override
|
|
final bool enableIOSGesture;
|
|
|
|
@override
|
|
final bool preventRebuild;
|
|
|
|
@override
|
|
final bool isRoot;
|
|
|
|
static void updateBackButton() {
|
|
Future.delayed(const Duration(milliseconds: 300), () {
|
|
StateController.findOrNull(tag: "back_button")?.update();
|
|
});
|
|
}
|
|
}
|
|
|
|
mixin _AppRouteTransitionMixin<T> on PageRoute<T> {
|
|
/// Builds the primary contents of the route.
|
|
@protected
|
|
Widget buildContent(BuildContext context);
|
|
|
|
@override
|
|
Duration get transitionDuration => const Duration(milliseconds: 300);
|
|
|
|
@override
|
|
Color? get barrierColor => null;
|
|
|
|
@override
|
|
String? get barrierLabel => null;
|
|
|
|
@override
|
|
bool canTransitionTo(TransitionRoute<dynamic> nextRoute) {
|
|
// Don't perform outgoing animation if the next route is a fullscreen dialog.
|
|
return nextRoute is PageRoute && !nextRoute.fullscreenDialog;
|
|
}
|
|
|
|
bool get enableIOSGesture;
|
|
|
|
bool get preventRebuild;
|
|
|
|
Widget? _child;
|
|
|
|
bool get isRoot;
|
|
|
|
@override
|
|
Widget buildPage(
|
|
BuildContext context,
|
|
Animation<double> animation,
|
|
Animation<double> secondaryAnimation,
|
|
) {
|
|
Widget result;
|
|
|
|
if (preventRebuild) {
|
|
result = _child ?? (_child = buildContent(context));
|
|
} else {
|
|
result = buildContent(context);
|
|
}
|
|
|
|
return Semantics(
|
|
scopesRoute: true,
|
|
explicitChildNodes: true,
|
|
child: result,
|
|
);
|
|
}
|
|
|
|
static bool _isPopGestureEnabled<T>(PageRoute<T> route) {
|
|
if (route.isFirst ||
|
|
route.willHandlePopInternally ||
|
|
route.popDisposition == RoutePopDisposition.doNotPop ||
|
|
route.fullscreenDialog ||
|
|
route.animation!.status != AnimationStatus.completed ||
|
|
route.secondaryAnimation!.status != AnimationStatus.dismissed ||
|
|
route.navigator!.userGestureInProgress) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
@override
|
|
Widget buildTransitions(BuildContext context, Animation<double> animation,
|
|
Animation<double> secondaryAnimation, Widget child) {
|
|
if (isRoot) {
|
|
return EntrancePageTransition(
|
|
animation: CurvedAnimation(
|
|
parent: animation,
|
|
curve: FluentTheme.of(context).animationCurve,
|
|
),
|
|
child: enableIOSGesture && App.isIOS
|
|
? IOSBackGestureDetector(
|
|
gestureWidth: _kBackGestureWidth,
|
|
enabledCallback: () => _isPopGestureEnabled<T>(this),
|
|
onStartPopGesture: () => _startPopGesture(this),
|
|
child: child)
|
|
: child,
|
|
);
|
|
}
|
|
|
|
return DrillInPageTransition(
|
|
animation: CurvedAnimation(
|
|
parent: animation,
|
|
curve: FluentTheme.of(context).animationCurve,
|
|
),
|
|
child: enableIOSGesture && App.isIOS
|
|
? IOSBackGestureDetector(
|
|
gestureWidth: _kBackGestureWidth,
|
|
enabledCallback: () => _isPopGestureEnabled<T>(this),
|
|
onStartPopGesture: () => _startPopGesture(this),
|
|
child: child)
|
|
: child,
|
|
);
|
|
}
|
|
|
|
IOSBackGestureController _startPopGesture(PageRoute<T> route) {
|
|
return IOSBackGestureController(route.controller!, route.navigator!);
|
|
}
|
|
}
|
|
|
|
class IOSBackGestureController {
|
|
final AnimationController controller;
|
|
|
|
final NavigatorState navigator;
|
|
|
|
IOSBackGestureController(this.controller, this.navigator) {
|
|
navigator.didStartUserGesture();
|
|
}
|
|
|
|
void dragEnd(double velocity) {
|
|
const Curve animationCurve = Curves.fastLinearToSlowEaseIn;
|
|
final bool animateForward;
|
|
|
|
if (velocity.abs() >= _kMinFlingVelocity) {
|
|
animateForward = velocity <= 0;
|
|
} else {
|
|
animateForward = controller.value > 0.5;
|
|
}
|
|
|
|
if (animateForward) {
|
|
final droppedPageForwardAnimationTime = min(
|
|
lerpDouble(
|
|
_kMaxDroppedSwipePageForwardAnimationTime, 0, controller.value)!
|
|
.floor(),
|
|
_kMaxPageBackAnimationTime,
|
|
);
|
|
controller.animateTo(1.0,
|
|
duration: Duration(milliseconds: droppedPageForwardAnimationTime),
|
|
curve: animationCurve);
|
|
} else {
|
|
navigator.pop();
|
|
if (controller.isAnimating) {
|
|
final droppedPageBackAnimationTime = lerpDouble(
|
|
0, _kMaxDroppedSwipePageForwardAnimationTime, controller.value)!
|
|
.floor();
|
|
controller.animateBack(0.0,
|
|
duration: Duration(milliseconds: droppedPageBackAnimationTime),
|
|
curve: animationCurve);
|
|
}
|
|
}
|
|
|
|
if (controller.isAnimating) {
|
|
late AnimationStatusListener animationStatusCallback;
|
|
animationStatusCallback = (status) {
|
|
navigator.didStopUserGesture();
|
|
controller.removeStatusListener(animationStatusCallback);
|
|
};
|
|
controller.addStatusListener(animationStatusCallback);
|
|
} else {
|
|
navigator.didStopUserGesture();
|
|
}
|
|
}
|
|
|
|
void dragUpdate(double delta) {
|
|
controller.value -= delta;
|
|
}
|
|
}
|
|
|
|
class IOSBackGestureDetector extends StatefulWidget {
|
|
const IOSBackGestureDetector(
|
|
{required this.enabledCallback,
|
|
required this.child,
|
|
required this.gestureWidth,
|
|
required this.onStartPopGesture,
|
|
super.key});
|
|
|
|
final double gestureWidth;
|
|
|
|
final bool Function() enabledCallback;
|
|
|
|
final IOSBackGestureController Function() onStartPopGesture;
|
|
|
|
final Widget child;
|
|
|
|
@override
|
|
State<IOSBackGestureDetector> createState() => _IOSBackGestureDetectorState();
|
|
}
|
|
|
|
class _IOSBackGestureDetectorState extends State<IOSBackGestureDetector> {
|
|
IOSBackGestureController? _backGestureController;
|
|
|
|
late HorizontalDragGestureRecognizer _recognizer;
|
|
|
|
@override
|
|
void dispose() {
|
|
_recognizer.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_recognizer = HorizontalDragGestureRecognizer(debugOwner: this)
|
|
..onStart = _handleDragStart
|
|
..onUpdate = _handleDragUpdate
|
|
..onEnd = _handleDragEnd
|
|
..onCancel = _handleDragCancel;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
var dragAreaWidth = Directionality.of(context) == TextDirection.ltr
|
|
? MediaQuery.of(context).padding.left
|
|
: MediaQuery.of(context).padding.right;
|
|
dragAreaWidth = max(dragAreaWidth, widget.gestureWidth);
|
|
return Stack(
|
|
fit: StackFit.passthrough,
|
|
children: <Widget>[
|
|
widget.child,
|
|
Positioned(
|
|
width: dragAreaWidth,
|
|
top: 0.0,
|
|
bottom: 0.0,
|
|
left: 0,
|
|
child: Listener(
|
|
onPointerDown: _handlePointerDown,
|
|
behavior: HitTestBehavior.translucent,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
void _handlePointerDown(PointerDownEvent event) {
|
|
if (widget.enabledCallback()) _recognizer.addPointer(event);
|
|
}
|
|
|
|
void _handleDragCancel() {
|
|
assert(mounted);
|
|
_backGestureController?.dragEnd(0.0);
|
|
_backGestureController = null;
|
|
}
|
|
|
|
double _convertToLogical(double value) {
|
|
switch (Directionality.of(context)) {
|
|
case TextDirection.rtl:
|
|
return -value;
|
|
case TextDirection.ltr:
|
|
return value;
|
|
}
|
|
}
|
|
|
|
void _handleDragEnd(DragEndDetails details) {
|
|
assert(mounted);
|
|
assert(_backGestureController != null);
|
|
_backGestureController!.dragEnd(_convertToLogical(
|
|
details.velocity.pixelsPerSecond.dx / context.size!.width));
|
|
_backGestureController = null;
|
|
}
|
|
|
|
void _handleDragStart(DragStartDetails details) {
|
|
assert(mounted);
|
|
assert(_backGestureController == null);
|
|
_backGestureController = widget.onStartPopGesture();
|
|
}
|
|
|
|
void _handleDragUpdate(DragUpdateDetails details) {
|
|
assert(mounted);
|
|
assert(_backGestureController != null);
|
|
_backGestureController!.dragUpdate(
|
|
_convertToLogical(details.primaryDelta! / context.size!.width));
|
|
}
|
|
}
|
|
|
|
const _kSideBarWidth = 420.0;
|
|
|
|
class SideBarRoute<T> extends PopupRoute<T> {
|
|
SideBarRoute(this.child);
|
|
|
|
final Widget child;
|
|
|
|
@override
|
|
Color? get barrierColor => Colors.transparent;
|
|
|
|
@override
|
|
bool get barrierDismissible => true;
|
|
|
|
@override
|
|
String? get barrierLabel => "side bar";
|
|
|
|
@override
|
|
Widget buildPage(BuildContext context, Animation<double> animation,
|
|
Animation<double> secondaryAnimation) {
|
|
return Align(
|
|
alignment: Alignment.centerRight,
|
|
child: Stack(
|
|
children: [
|
|
Positioned(
|
|
right: 0,
|
|
top: 0,
|
|
bottom: 0,
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: FluentTheme.of(context)
|
|
.micaBackgroundColor
|
|
.toOpacity(0.98),
|
|
borderRadius: const BorderRadius.only(
|
|
topLeft: Radius.circular(4),
|
|
bottomLeft: Radius.circular(4))),
|
|
constraints: BoxConstraints(
|
|
maxWidth:
|
|
min(_kSideBarWidth, MediaQuery.of(context).size.width)),
|
|
width: double.infinity,
|
|
child: child,
|
|
),
|
|
)
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Duration get transitionDuration => const Duration(milliseconds: 200);
|
|
|
|
static bool _isPopGestureEnabled<T>(PopupRoute<T> route) {
|
|
if (route.isFirst ||
|
|
route.willHandlePopInternally ||
|
|
route.popDisposition == RoutePopDisposition.doNotPop ||
|
|
route.animation!.status != AnimationStatus.completed ||
|
|
route.secondaryAnimation!.status != AnimationStatus.dismissed ||
|
|
route.navigator!.userGestureInProgress) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool get enableIOSGesture => true;
|
|
|
|
@override
|
|
Widget buildTransitions(BuildContext context, Animation<double> animation,
|
|
Animation<double> secondaryAnimation, Widget child) {
|
|
var offset =
|
|
Tween<Offset>(begin: const Offset(1, 0), end: const Offset(0, 0));
|
|
return SlideTransition(
|
|
position: offset.animate(CurvedAnimation(
|
|
parent: animation,
|
|
curve: Curves.fastOutSlowIn,
|
|
)),
|
|
child: enableIOSGesture
|
|
? IOSBackGestureDetector(
|
|
gestureWidth: _kBackGestureWidth,
|
|
enabledCallback: () => _isPopGestureEnabled<T>(this),
|
|
onStartPopGesture: () => _startPopGesture(this),
|
|
child: child)
|
|
: child,
|
|
);
|
|
}
|
|
|
|
IOSBackGestureController _startPopGesture(PopupRoute<T> route) {
|
|
return IOSBackGestureController(route.controller!, route.navigator!);
|
|
}
|
|
}
|
|
|
|
class EntrancePageTransition extends StatelessWidget {
|
|
/// Creates an entrance page transition
|
|
const EntrancePageTransition({
|
|
super.key,
|
|
required this.child,
|
|
required this.animation,
|
|
});
|
|
|
|
/// The widget to be animated
|
|
final Widget child;
|
|
|
|
/// The animation to drive this transition
|
|
final Animation<double> animation;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ColoredBox(
|
|
color: FluentTheme.of(context).micaBackgroundColor,
|
|
child: SlideTransition(
|
|
position: Tween<Offset>(
|
|
begin: const Offset(0, 0.1),
|
|
end: Offset.zero,
|
|
).animate(animation),
|
|
child: FadeTransition(
|
|
opacity: animation,
|
|
child: child,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|