Skip to content

Instantly share code, notes, and snippets.

@toda-bps
Last active February 20, 2024 13:09
Show Gist options
  • Save toda-bps/f0d99dc5e87be019dbd71ebdfb7da435 to your computer and use it in GitHub Desktop.
Save toda-bps/f0d99dc5e87be019dbd71ebdfb7da435 to your computer and use it in GitHub Desktop.
CustomCupertinoPageTransitionsBuilder
import 'dart:math';
import 'dart:ui' show lerpDouble;
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class CustomCupertinoPageTransitionsBuilder extends PageTransitionsBuilder {
const CustomCupertinoPageTransitionsBuilder();
@override
Widget buildTransitions<T>(
PageRoute<T> route,
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return _CustomCupertinoRouteTransitionMixin.buildPageTransitions<T>(
route,
context,
animation,
secondaryAnimation,
child,
);
}
}
/////////////////////////////////////////////////////////////////////////////
///
/// Customized CupertinoRouteTransitionMixin
///
/// based on:
/// https://github.com/flutter/flutter/blob/35c52e7bee/packages/flutter/lib/src/cupertino/route.dart
///
const Color _kCupertinoPageTransitionBarrierColor = Color(0x18000000);
mixin _CustomCupertinoRouteTransitionMixin<T> on PageRoute<T> {
@protected
Widget buildContent(BuildContext context);
String? get title;
ValueNotifier<String?>? _previousTitle;
ValueListenable<String?> get previousTitle {
assert(
_previousTitle != null,
'Cannot read the previousTitle for a route '
'that has not yet been installed',
);
return _previousTitle!;
}
@override
void dispose() {
_previousTitle?.dispose();
super.dispose();
}
@override
void didChangePrevious(Route<dynamic>? previousRoute) {
final previousTitleString = previousRoute is CupertinoRouteTransitionMixin
? previousRoute.title
: null;
if (_previousTitle == null) {
_previousTitle = ValueNotifier<String?>(previousTitleString);
} else {
_previousTitle!.value = previousTitleString;
}
super.didChangePrevious(previousRoute);
}
@override
Duration get transitionDuration => const Duration(milliseconds: 500);
@override
Color? get barrierColor =>
fullscreenDialog ? null : _kCupertinoPageTransitionBarrierColor;
@override
String? get barrierLabel => null;
@override
bool canTransitionTo(TransitionRoute<dynamic> nextRoute) {
return nextRoute is CupertinoRouteTransitionMixin &&
!nextRoute.fullscreenDialog;
}
static bool isPopGestureInProgress(PageRoute<dynamic> route) {
return route.navigator!.userGestureInProgress;
}
bool get popGestureInProgress => isPopGestureInProgress(this);
bool get popGestureEnabled => _isPopGestureEnabled(this);
static bool _isPopGestureEnabled<T>(PageRoute<T> route) {
if (route.isFirst) {
return false;
}
if (route.willHandlePopInternally) {
return false;
}
// ignore: deprecated_member_use
if (route.hasScopedWillPopCallback ||
route.popDisposition == RoutePopDisposition.doNotPop) {
return false;
}
if (route.fullscreenDialog) {
return false;
}
if (route.animation!.status != AnimationStatus.completed) {
return false;
}
if (route.secondaryAnimation!.status != AnimationStatus.dismissed) {
return false;
}
if (isPopGestureInProgress(route)) {
return false;
}
return true;
}
@override
Widget buildPage(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) {
final child = buildContent(context);
return Semantics(
scopesRoute: true,
explicitChildNodes: true,
child: child,
);
}
// Customize:
// Whether pop animation is currently animating
static bool _popAnimationInProgress = false;
static _CupertinoBackGestureController<T> _startPopGesture<T>(
PageRoute<T> route,
) {
assert(_isPopGestureEnabled(route));
// Customize:
// Track pop animation status to determine whether to use linearTransition
// in buildPageTransitions()
_popAnimationInProgress = true;
final controller = route.controller!;
late final AnimationStatusListener animationStatusCallback;
animationStatusCallback = (status) {
if (status == AnimationStatus.dismissed) {
_popAnimationInProgress = false;
controller.removeStatusListener(animationStatusCallback);
}
};
controller.addStatusListener(animationStatusCallback);
return _CupertinoBackGestureController<T>(
navigator: route.navigator!,
controller: controller,
);
}
static Widget buildPageTransitions<T>(
PageRoute<T> route,
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
// Customize:
// Consider _popAnimationInProgress
final linearTransition =
isPopGestureInProgress(route) || _popAnimationInProgress;
if (route.fullscreenDialog) {
return CupertinoFullscreenDialogTransition(
primaryRouteAnimation: animation,
secondaryRouteAnimation: secondaryAnimation,
linearTransition: linearTransition,
child: child,
);
} else {
return CupertinoPageTransition(
primaryRouteAnimation: animation,
secondaryRouteAnimation: secondaryAnimation,
linearTransition: linearTransition,
child: _CupertinoBackGestureDetector<T>(
enabledCallback: () => _isPopGestureEnabled<T>(route),
onStartPopGesture: () => _startPopGesture<T>(route),
child: child,
),
);
}
}
@override
Widget buildTransitions(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return buildPageTransitions<T>(
this,
context,
animation,
secondaryAnimation,
child,
);
}
}
class _CupertinoBackGestureDetector<T> extends StatefulWidget {
const _CupertinoBackGestureDetector({
required this.enabledCallback,
required this.onStartPopGesture,
required this.child,
super.key,
});
final Widget child;
final ValueGetter<bool> enabledCallback;
final ValueGetter<_CupertinoBackGestureController<T>> onStartPopGesture;
@override
_CupertinoBackGestureDetectorState<T> createState() =>
_CupertinoBackGestureDetectorState<T>();
}
const double _kBackGestureWidth = 20;
const double _kMinFlingVelocity = 1;
const int _kMaxDroppedSwipePageForwardAnimationTime = 800;
const int _kMaxPageBackAnimationTime = 300;
class _CupertinoBackGestureDetectorState<T>
extends State<_CupertinoBackGestureDetector<T>> {
_CupertinoBackGestureController<T>? _backGestureController;
late HorizontalDragGestureRecognizer _recognizer;
@override
void initState() {
super.initState();
_recognizer = HorizontalDragGestureRecognizer(debugOwner: this)
..onStart = _handleDragStart
..onUpdate = _handleDragUpdate
..onEnd = _handleDragEnd
..onCancel = _handleDragCancel;
}
@override
void dispose() {
_recognizer.dispose();
if (_backGestureController != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_backGestureController?.navigator.mounted ?? false) {
_backGestureController?.navigator.didStopUserGesture();
}
_backGestureController = null;
});
}
super.dispose();
}
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),
);
}
void _handleDragEnd(DragEndDetails details) {
assert(mounted);
assert(_backGestureController != null);
_backGestureController!.dragEnd(
_convertToLogical(
details.velocity.pixelsPerSecond.dx / context.size!.width,
),
);
_backGestureController = null;
}
void _handleDragCancel() {
assert(mounted);
_backGestureController?.dragEnd(0);
_backGestureController = null;
}
void _handlePointerDown(PointerDownEvent event) {
if (widget.enabledCallback()) {
_recognizer.addPointer(event);
}
}
double _convertToLogical(double value) {
switch (Directionality.of(context)) {
case TextDirection.rtl:
return -value;
case TextDirection.ltr:
return value;
}
}
@override
Widget build(BuildContext context) {
assert(debugCheckHasDirectionality(context));
var dragAreaWidth = Directionality.of(context) == TextDirection.ltr
? MediaQuery.paddingOf(context).left
: MediaQuery.paddingOf(context).right;
dragAreaWidth = max(dragAreaWidth, _kBackGestureWidth);
return Stack(
fit: StackFit.passthrough,
children: <Widget>[
widget.child,
PositionedDirectional(
start: 0,
width: dragAreaWidth,
top: 0,
bottom: 0,
child: Listener(
onPointerDown: _handlePointerDown,
behavior: HitTestBehavior.translucent,
),
),
],
);
}
}
class _CupertinoBackGestureController<T> {
_CupertinoBackGestureController({
required this.navigator,
required this.controller,
}) {
navigator.didStartUserGesture();
}
final AnimationController controller;
final NavigatorState navigator;
void dragUpdate(double delta) {
controller.value -= delta;
}
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 int droppedPageForwardAnimationTime = min(
lerpDouble(
_kMaxDroppedSwipePageForwardAnimationTime,
0,
controller.value,
)!
.floor(),
_kMaxPageBackAnimationTime,
);
controller.animateTo(
1,
duration: Duration(milliseconds: droppedPageForwardAnimationTime),
curve: animationCurve,
);
} else {
navigator.pop();
if (controller.isAnimating) {
final droppedPageBackAnimationTime = lerpDouble(
0,
_kMaxDroppedSwipePageForwardAnimationTime,
controller.value,
)!
.floor();
controller.animateBack(
0,
duration: Duration(milliseconds: droppedPageBackAnimationTime),
curve: animationCurve,
);
}
}
// Customize:
// Always call didStopUserGesture() immediately
// to enable gestures on the underlying page.
navigator.didStopUserGesture();
}
}
/////////////////////////////////////////////////////////////////////////////
///
/// Sample App
///
void main() => runApp(const _App());
@immutable
class _SamplePage extends StatelessWidget {
const _SamplePage({
required this.path,
required this.next,
});
final String? path;
final String next;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(path ?? ''),
),
body: ListView.builder(
itemBuilder: (context, index) {
return ListTile(
title: Text('row $index'),
onTap: () {
GoRouter.of(context).go(next);
},
);
},
),
);
}
}
@immutable
class _LastPage extends StatelessWidget {
const _LastPage({required this.path});
final String? path;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(path ?? ''),
),
);
}
}
final _routerConfig = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => _SamplePage(
path: state.path,
next: '/second',
),
routes: [
GoRoute(
path: 'second',
builder: (context, state) => _SamplePage(
path: state.path,
next: '/second/last',
),
routes: [
GoRoute(
path: 'last',
builder: (context, state) => _LastPage(path: state.path),
),
],
),
],
),
],
);
@immutable
class _CustomPageTransitionsTheme extends PageTransitionsTheme {
const _CustomPageTransitionsTheme();
static const _builder = CustomCupertinoPageTransitionsBuilder();
@override
Widget buildTransitions<T>(
PageRoute<T> route,
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return _builder.buildTransitions<T>(
route,
context,
animation,
secondaryAnimation,
child,
);
}
}
@immutable
class _CustomScrollBehavior extends MaterialScrollBehavior {
const _CustomScrollBehavior();
@override
Set<PointerDeviceKind> get dragDevices => PointerDeviceKind.values.toSet();
}
@immutable
class _App extends StatelessWidget {
const _App();
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: _routerConfig,
theme: ThemeData(
pageTransitionsTheme: const _CustomPageTransitionsTheme(),
),
scrollBehavior: const _CustomScrollBehavior(),
);
}
}
@BazinC
Copy link

BazinC commented Feb 19, 2024

Hi @toda-bps, thanks for your hard work trying to fix flutter/flutter#48225!
I want to try your workaround but I have one question:
Why do you need a _CustomScrollBehavior in your example?
Thank you :)

@toda-bps
Copy link
Author

@BazinC

Why do you need a _CustomScrollBehavior in your example?

ScrollBehavior is now set to provide the same scrolling experience on web, mobile, and desktop.
This is just a sample and is not essential for a workaround.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment