Last active
November 24, 2020 21:50
-
-
Save alhafoudh/ce139a745c39efccc6e5fb6b4b1b399b to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Usage: | |
CustomRoute(page: SomewPage, transitionsBuilder: CustomTransitions.withoutShadow) | |
*/ | |
import 'dart:ui'; | |
import 'package:flutter/foundation.dart'; | |
import 'package:flutter/gestures.dart'; | |
import 'package:flutter/widgets.dart'; | |
import 'dart:math'; | |
/// A [BoxPainter] used to draw the page transition shadow using gradients. | |
class _CupertinoEdgeShadowPainter extends BoxPainter { | |
_CupertinoEdgeShadowPainter( | |
this._decoration, | |
VoidCallback onChange, | |
) : assert(_decoration != null), | |
super(onChange); | |
final _CupertinoEdgeShadowDecoration _decoration; | |
@override | |
void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) { | |
final LinearGradient gradient = _decoration.edgeGradient; | |
if (gradient == null) return; | |
// The drawable space for the gradient is a rect with the same size as | |
// its parent box one box width on the start side of the box. | |
final TextDirection textDirection = configuration.textDirection; | |
assert(textDirection != null); | |
double deltaX; | |
switch (textDirection) { | |
case TextDirection.rtl: | |
deltaX = configuration.size.width; | |
break; | |
case TextDirection.ltr: | |
deltaX = -configuration.size.width; | |
break; | |
} | |
final Rect rect = (offset & configuration.size).translate(deltaX, 0.0); | |
final Paint paint = Paint() | |
..shader = gradient.createShader(rect, textDirection: textDirection); | |
canvas.drawRect(rect, paint); | |
} | |
} | |
// A custom [Decoration] used to paint an extra shadow on the start edge of the | |
// box it's decorating. It's like a [BoxDecoration] with only a gradient except | |
// it paints on the start side of the box instead of behind the box. | |
// | |
// The [edgeGradient] will be given a [TextDirection] when its shader is | |
// created, and so can be direction-sensitive; in this file we set it to a | |
// gradient that uses an AlignmentDirectional to position the gradient on the | |
// end edge of the gradient's box (which will be the edge adjacent to the start | |
// edge of the actual box we're supposed to paint in). | |
class _CupertinoEdgeShadowDecoration extends Decoration { | |
const _CupertinoEdgeShadowDecoration({this.edgeGradient}); | |
// A gradient to draw to the left of the box being decorated. | |
// Alignments are relative to the original box translated one box | |
// width to the left. | |
final LinearGradient edgeGradient; | |
// Linearly interpolate between two edge shadow decorations decorations. | |
// | |
// The `t` argument represents position on the timeline, with 0.0 meaning | |
// that the interpolation has not started, returning `a` (or something | |
// equivalent to `a`), 1.0 meaning that the interpolation has finished, | |
// returning `b` (or something equivalent to `b`), and values in between | |
// meaning that the interpolation is at the relevant point on the timeline | |
// between `a` and `b`. The interpolation can be extrapolated beyond 0.0 and | |
// 1.0, so negative values and values greater than 1.0 are valid (and can | |
// easily be generated by curves such as [Curves.elasticInOut]). | |
// | |
// Values for `t` are usually obtained from an [Animation<double>], such as | |
// an [AnimationController]. | |
// | |
// See also: | |
// | |
// * [Decoration.lerp]. | |
static _CupertinoEdgeShadowDecoration lerp( | |
_CupertinoEdgeShadowDecoration a, | |
_CupertinoEdgeShadowDecoration b, | |
double t, | |
) { | |
assert(t != null); | |
if (a == null && b == null) return null; | |
return _CupertinoEdgeShadowDecoration( | |
edgeGradient: LinearGradient.lerp(a?.edgeGradient, b?.edgeGradient, t), | |
); | |
} | |
@override | |
_CupertinoEdgeShadowDecoration lerpFrom(Decoration a, double t) { | |
if (a is _CupertinoEdgeShadowDecoration) | |
return _CupertinoEdgeShadowDecoration.lerp(a, this, t); | |
return _CupertinoEdgeShadowDecoration.lerp(null, this, t); | |
} | |
@override | |
_CupertinoEdgeShadowDecoration lerpTo(Decoration b, double t) { | |
if (b is _CupertinoEdgeShadowDecoration) | |
return _CupertinoEdgeShadowDecoration.lerp(this, b, t); | |
return _CupertinoEdgeShadowDecoration.lerp(this, null, t); | |
} | |
@override | |
_CupertinoEdgeShadowPainter createBoxPainter([VoidCallback onChanged]) { | |
return _CupertinoEdgeShadowPainter(this, onChanged); | |
} | |
@override | |
bool operator ==(Object other) { | |
if (other.runtimeType != runtimeType) return false; | |
return other is _CupertinoEdgeShadowDecoration && | |
other.edgeGradient == edgeGradient; | |
} | |
@override | |
int get hashCode => edgeGradient.hashCode; | |
@override | |
void debugFillProperties(DiagnosticPropertiesBuilder properties) { | |
super.debugFillProperties(properties); | |
properties | |
.add(DiagnosticsProperty<LinearGradient>('edgeGradient', edgeGradient)); | |
} | |
} | |
// Offset from offscreen to the right to fully on screen. | |
final Animatable<Offset> _kRightMiddleTween = Tween<Offset>( | |
begin: const Offset(1.0, 0.0), | |
end: Offset.zero, | |
); | |
// Offset from fully on screen to 1/3 offscreen to the left. | |
final Animatable<Offset> _kMiddleLeftTween = Tween<Offset>( | |
begin: Offset.zero, | |
end: const Offset(-1.0 / 3.0, 0.0), | |
); | |
class CupertinoPageTransitionWithoutShadow extends StatelessWidget { | |
/// Creates an iOS-style page transition. | |
/// | |
/// * `primaryRouteAnimation` is a linear route animation from 0.0 to 1.0 | |
/// when this screen is being pushed. | |
/// * `secondaryRouteAnimation` is a linear route animation from 0.0 to 1.0 | |
/// when another screen is being pushed on top of this one. | |
/// * `linearTransition` is whether to perform the transitions linearly. | |
/// Used to precisely track back gesture drags. | |
CupertinoPageTransitionWithoutShadow({ | |
Key key, | |
@required Animation<double> primaryRouteAnimation, | |
@required Animation<double> secondaryRouteAnimation, | |
@required this.child, | |
@required bool linearTransition, | |
}) : assert(linearTransition != null), | |
_primaryPositionAnimation = (linearTransition | |
? primaryRouteAnimation | |
: CurvedAnimation( | |
// The curves below have been rigorously derived from plots of native | |
// iOS animation frames. Specifically, a video was taken of a page | |
// transition animation and the distance in each frame that the page | |
// moved was measured. A best fit bezier curve was the fitted to the | |
// point set, which is linearToEaseIn. Conversely, easeInToLinear is the | |
// reflection over the origin of linearToEaseIn. | |
parent: primaryRouteAnimation, | |
curve: Curves.linearToEaseOut, | |
reverseCurve: Curves.easeInToLinear, | |
)) | |
.drive(_kRightMiddleTween), | |
_secondaryPositionAnimation = (linearTransition | |
? secondaryRouteAnimation | |
: CurvedAnimation( | |
parent: secondaryRouteAnimation, | |
curve: Curves.linearToEaseOut, | |
reverseCurve: Curves.easeInToLinear, | |
)) | |
.drive(_kMiddleLeftTween), | |
super(key: key); | |
// When this page is coming in to cover another page. | |
final Animation<Offset> _primaryPositionAnimation; | |
// When this page is becoming covered by another page. | |
final Animation<Offset> _secondaryPositionAnimation; | |
/// The widget below this widget in the tree. | |
final Widget child; | |
@override | |
Widget build(BuildContext context) { | |
assert(debugCheckHasDirectionality(context)); | |
final TextDirection textDirection = Directionality.of(context); | |
return SlideTransition( | |
position: _secondaryPositionAnimation, | |
textDirection: textDirection, | |
transformHitTests: false, | |
child: SlideTransition( | |
position: _primaryPositionAnimation, | |
textDirection: textDirection, | |
child: child, | |
), | |
); | |
} | |
} | |
const double _kBackGestureWidth = 20.0; | |
const double _kMinFlingVelocity = 1.0; // Screen widths per second. | |
// An eyeballed value for the maximum time it takes for a page to animate forward | |
// if the user releases a page mid swipe. | |
const int _kMaxDroppedSwipePageForwardAnimationTime = 800; // Milliseconds. | |
// The maximum time for a page to get reset to it's original position if the | |
// user releases a page mid swipe. | |
const int _kMaxPageBackAnimationTime = 300; // Milliseconds. | |
/// A controller for an iOS-style back gesture. | |
/// | |
/// This is created by a [CupertinoPageRoute] in response from a gesture caught | |
/// by a [_CupertinoBackGestureDetector] widget, which then also feeds it input | |
/// from the gesture. It controls the animation controller owned by the route, | |
/// based on the input provided by the gesture detector. | |
/// | |
/// This class works entirely in logical coordinates (0.0 is new page dismissed, | |
/// 1.0 is new page on top). | |
/// | |
/// The type `T` specifies the return type of the route with which this gesture | |
/// detector controller is associated. | |
class _CupertinoBackGestureController<T> { | |
/// Creates a controller for an iOS-style back gesture. | |
/// | |
/// The [navigator] and [controller] arguments must not be null. | |
_CupertinoBackGestureController({ | |
@required this.navigator, | |
@required this.controller, | |
}) : assert(navigator != null), | |
assert(controller != null) { | |
navigator.didStartUserGesture(); | |
} | |
final AnimationController controller; | |
final NavigatorState navigator; | |
/// The drag gesture has changed by [fractionalDelta]. The total range of the | |
/// drag should be 0.0 to 1.0. | |
void dragUpdate(double delta) { | |
controller.value -= delta; | |
} | |
/// The drag gesture has ended with a horizontal motion of | |
/// [fractionalVelocity] as a fraction of screen width per second. | |
void dragEnd(double velocity) { | |
// Fling in the appropriate direction. | |
// AnimationController.fling is guaranteed to | |
// take at least one frame. | |
// | |
// This curve has been determined through rigorously eyeballing native iOS | |
// animations. | |
const Curve animationCurve = Curves.fastLinearToSlowEaseIn; | |
bool animateForward; | |
// If the user releases the page before mid screen with sufficient velocity, | |
// or after mid screen, we should animate the page out. Otherwise, the page | |
// should be animated back in. | |
if (velocity.abs() >= _kMinFlingVelocity) | |
animateForward = velocity <= 0; | |
else | |
animateForward = controller.value > 0.5; | |
if (animateForward) { | |
// The closer the panel is to dismissing, the shorter the animation is. | |
// We want to cap the animation time, but we want to use a linear curve | |
// to determine it. | |
final int droppedPageForwardAnimationTime = min( | |
lerpDouble( | |
_kMaxDroppedSwipePageForwardAnimationTime, 0, controller.value) | |
.floor(), | |
_kMaxPageBackAnimationTime, | |
); | |
controller.animateTo(1.0, | |
duration: Duration(milliseconds: droppedPageForwardAnimationTime), | |
curve: animationCurve); | |
} else { | |
// This route is destined to pop at this point. Reuse navigator's pop. | |
navigator.pop(); | |
// The popping may have finished inline if already at the target destination. | |
if (controller.isAnimating) { | |
// Otherwise, use a custom popping animation duration and curve. | |
final int droppedPageBackAnimationTime = lerpDouble( | |
0, _kMaxDroppedSwipePageForwardAnimationTime, controller.value) | |
.floor(); | |
controller.animateBack(0.0, | |
duration: Duration(milliseconds: droppedPageBackAnimationTime), | |
curve: animationCurve); | |
} | |
} | |
if (controller.isAnimating) { | |
// Keep the userGestureInProgress in true state so we don't change the | |
// curve of the page transition mid-flight since CupertinoPageTransition | |
// depends on userGestureInProgress. | |
AnimationStatusListener animationStatusCallback; | |
animationStatusCallback = (AnimationStatus status) { | |
navigator.didStopUserGesture(); | |
controller.removeStatusListener(animationStatusCallback); | |
}; | |
controller.addStatusListener(animationStatusCallback); | |
} else { | |
navigator.didStopUserGesture(); | |
} | |
} | |
} | |
/// This is the widget side of [_CupertinoBackGestureController]. | |
/// | |
/// This widget provides a gesture recognizer which, when it determines the | |
/// route can be closed with a back gesture, creates the controller and | |
/// feeds it the input from the gesture recognizer. | |
/// | |
/// The gesture data is converted from absolute coordinates to logical | |
/// coordinates by this widget. | |
/// | |
/// The type `T` specifies the return type of the route with which this gesture | |
/// detector is associated. | |
class _CupertinoBackGestureDetector<T> extends StatefulWidget { | |
const _CupertinoBackGestureDetector({ | |
Key key, | |
@required this.enabledCallback, | |
@required this.onStartPopGesture, | |
@required this.child, | |
}) : assert(enabledCallback != null), | |
assert(onStartPopGesture != null), | |
assert(child != null), | |
super(key: key); | |
final Widget child; | |
final ValueGetter<bool> enabledCallback; | |
final ValueGetter<_CupertinoBackGestureController<T>> onStartPopGesture; | |
@override | |
_CupertinoBackGestureDetectorState<T> createState() => | |
_CupertinoBackGestureDetectorState<T>(); | |
} | |
class _CupertinoBackGestureDetectorState<T> | |
extends State<_CupertinoBackGestureDetector<T>> { | |
_CupertinoBackGestureController<T> _backGestureController; | |
HorizontalDragGestureRecognizer _recognizer; | |
@override | |
void initState() { | |
super.initState(); | |
_recognizer = HorizontalDragGestureRecognizer(debugOwner: this) | |
..onStart = _handleDragStart | |
..onUpdate = _handleDragUpdate | |
..onEnd = _handleDragEnd | |
..onCancel = _handleDragCancel; | |
} | |
@override | |
void dispose() { | |
_recognizer.dispose(); | |
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); | |
// This can be called even if start is not called, paired with the "down" event | |
// that we don't consider here. | |
_backGestureController?.dragEnd(0.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; | |
} | |
return null; | |
} | |
@override | |
Widget build(BuildContext context) { | |
assert(debugCheckHasDirectionality(context)); | |
// For devices with notches, the drag area needs to be larger on the side | |
// that has the notch. | |
double dragAreaWidth = Directionality.of(context) == TextDirection.ltr | |
? MediaQuery.of(context).padding.left | |
: MediaQuery.of(context).padding.right; | |
dragAreaWidth = max(dragAreaWidth, _kBackGestureWidth); | |
return Stack( | |
fit: StackFit.passthrough, | |
children: <Widget>[ | |
widget.child, | |
PositionedDirectional( | |
start: 0.0, | |
width: dragAreaWidth, | |
top: 0.0, | |
bottom: 0.0, | |
child: Listener( | |
onPointerDown: _handlePointerDown, | |
behavior: HitTestBehavior.translucent, | |
), | |
), | |
], | |
); | |
} | |
} | |
class CustomTransitions { | |
static const RouteTransitionsBuilder withoutShadow = _withoutShadow; | |
static Widget _withoutShadow( | |
BuildContext context, | |
Animation<double> animation, | |
Animation<double> secondaryAnimation, | |
Widget child) { | |
return CupertinoPageTransitionWithoutShadow( | |
primaryRouteAnimation: animation, | |
secondaryRouteAnimation: secondaryAnimation, | |
linearTransition: false, | |
child: child, | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment