Skip to content

Instantly share code, notes, and snippets.

@alhafoudh
Last active November 24, 2020 21:50
Show Gist options
  • Save alhafoudh/ce139a745c39efccc6e5fb6b4b1b399b to your computer and use it in GitHub Desktop.
Save alhafoudh/ce139a745c39efccc6e5fb6b4b1b399b to your computer and use it in GitHub Desktop.
/*
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