Github.com/yumi0629/Fl…

Recently, a member of our group asked us how to modify cupertino ageroute into animation. Our goal is to achieve the following effect:

Some people may think, isn’t this just self effect? We can compare with the native effect:

Obviously, the entry animation is different, and the default is a transition from right to left. So, can this entry animation be changed? CupertinoPageRoute’s existing API does not have this interface, so we need to change it.

Design of Flutter routing animation

Before we get started, I think it’s important to talk about the Flutter route animation design. The push and pop animations of A Flutter route are grouped. If the push Animation is Animation A, then the pop Animation is Animation A.rise (). TransitionRoute = TransitionRoute

@override
  TickerFuture didPush() { assert(_controller ! = null,'$runtimeType.didPush called before calling install() or after calling dispose().'); assert(! _transitionCompleter.isCompleted,'Cannot reuse a $runtimeType after disposing it.');
    _animation.addStatusListener(_handleStatusChanged);
    return_controller.forward(); } @override bool didPop(T result) { assert(_controller ! = null,'$runtimeType.didPop called before calling install() or after calling dispose().'); assert(! _transitionCompleter.isCompleted,'Cannot reuse a $runtimeType after disposing it.');
    _result = result;
    _controller.reverse();
    return super.didPop(result);
  }
Copy the code

It is clear that _controller.forward() is executed for push and _controller.reverse() is executed for POP. So, in CupertinoPageRoute, the default transition is going to be right to left, because the slide back is going to be left to right, so push is going to be right to left.

About animation design in CupertinoPageRoute

Now that you’ve got a basic understanding of routing animation, let’s take a look at CupertinoPageRoute’s animation design. The inheritance of CupertinoPageRoute is as follows: CupertinoPageRoute –> PageRoute –> ModalRoute –> TransitionRoute –> OverlayRoute –> Route. In CupertinoPageRoute, route transitions are created using buildTransitions:

@override
  Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
    return buildPageTransitions<T>(this, context, animation, secondaryAnimation, child);
  }
Copy the code

The parent of this method comes from ModalRoute and is used in class _ModalScopeState. We can see that the page is wrapped in an AnimatedBuilder control. Cooperate with the widget. The route. BuildTransitions can realize all kinds of animation effects:

Class _ModalScopeState<T> extends State<_ModalScope<T>> {······ @override Widget Build (BuildContext context) {return _ModalScopeStatus(
      route: widget.route,
      isCurrent: widget.route.isCurrent, // _routeSetState is called if this updates
      canPop: widget.route.canPop, // _routeSetState is called if this updates
      child: Offstage(
        offstage: widget.route.offstage, // _routeSetState is called if this updates
        child: PageStorage(
          bucket: widget.route._storageBucket, // immutable
          child: FocusScope(
            node: focusScopeNode, // immutable
            child: RepaintBoundary(
              child: AnimatedBuilder(
                animation: _listenable, // immutable
                builder: (BuildContext context, Widget child) {
                  returnwidget.route.buildTransitions( context, widget.route.animation, widget.route.secondaryAnimation, IgnorePointer( ignoring: widget.route.animation? .status == AnimationStatus.reverse, child: child, ), ); }, child: _page ?? = RepaintBoundary( key: widget.route._subtreeKey, // immutable child: Builder( builder: (BuildContext context) {returnwidget.route.buildPage( context, widget.route.animation, widget.route.secondaryAnimation, ); }, ((), ((), ((), ((), ((), ((); }......}Copy the code

So when is _ModalScope mounted to the route? Continuing with the ModalRoute source code, createOverlayEntries() initializes this _ModalScope:

 @override
  Iterable<OverlayEntry> createOverlayEntries() sync* {
    yield _modalBarrier = OverlayEntry(builder: _buildModalBarrier);
    yield OverlayEntry(builder: _buildModalScope, maintainState: maintainState);
  }

Widget _buildModalScope(BuildContext context) {
    return_modalScopeCache ?? = _ModalScope<T>( key: _scopeKey, route: this, // _ModalScope calls buildTransitions() and buildChild(), defined above ); }Copy the code

CreateOverlayEntries () is called in the Install () method of OverlayRoute:

@override void install(OverlayEntry insertionPoint) { assert(_overlayEntries.isEmpty); _overlayEntries.addAll(createOverlayEntries()); navigator.overlay? .insertAll(_overlayEntries, above: insertionPoint); super.install(insertionPoint); }Copy the code

The install() method is called when the route is inserted into the navigator, at which point the Flutter fills the overlayEntries and adds them to the overlay. This is done by Route, not Navigator, because Route is also responsible for removing overlayEntries, so add and remove operations are symmetrical. These together will be: when routing intall widget. The route. The buildTransitions give AnimatedBuilder provides a way to animated Transitions, which makes routing to move up. So, must change CupertinoPageRoute into animation, will rewrite the widget. The route. BuildTransitions method.

Custom CupertinoPageTransition

Analysis system of CupertinoPageTransition

@override
  Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
    return buildPageTransitions<T>(this, context, animation, secondaryAnimation, child);
  }

static Widget buildPageTransitions<T>(
    PageRoute<T> route,
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
    Widget child,
  ) {
    if (route.fullscreenDialog) {
      return CupertinoFullscreenDialogTransition(
        animation: animation,
        child: child,
      );
    } else {
      returnCupertinoPageTransition( primaryRouteAnimation: animation, secondaryRouteAnimation: secondaryAnimation, linearTransition: isPopGestureInProgress(route), child: _CupertinoBackGestureDetector<T>( enabledCallback: () => _isPopGestureEnabled<T>(route), onStartPopGesture: () => _startPopGesture<T>(route), child: child, ), ); }}Copy the code

Here are two parameters in the buildTransitions() method: animation and secondaryAnimation.

  • When the Navigator pushes a new route, the new routeanimationFrom 0.0–>1.0; The animation changes from 1.0- >0.0 when the Navigator pops the top route (for example, by clicking the back key).
  • When the Navigator pushes a new route, the original topmost route is usedsecondaryAnimationFrom 0.0–>1.0; SecondaryAnimation changes from 1.0- >0.0 when routing the top pop route.

To put it simply, animation is how I come in and go out by myself, while secondaryAnimation is how I come in and go out when others cover me.

So, we need to make some changes to the animation, and secondaryAnimation doesn’t care about it.

class CupertinoPageTransition 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 primary transition linearly. /// Used to precisely track back gesture drags. CupertinoPageTransition({ 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 distancein 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),
       _primaryShadowAnimation =
           (linearTransition
             ? primaryRouteAnimation
             : CurvedAnimation(
                 parent: primaryRouteAnimation,
                 curve: Curves.linearToEaseOut,
               )
           ).drive(_kGradientShadowTween),
       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;
  final Animation<Decoration> _primaryShadowAnimation;

  /// 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: DecoratedBoxTransition( decoration: _primaryShadowAnimation, child: child, ), ), ); }}Copy the code

CupertinoPageTransition source, is actually wrap page in a SlideTransition, while the child was a _CupertinoBackGestureDetector, with a gesture that we don’t have to change, no matter it. We need to change the SlideTransition to use our own transition for push and keep the original animation and gesture controls for POP.

Modify SlideTransition

Let’s be clear about our purpose. Here’s what we want to achieve:

SlideTransition(Position: Push? Our own push animation: system, own _primaryPositionAnimation textDirection: textDirection, child: DecoratedBoxTransition( decoration: widget._primaryShadowAnimation, child: widget.child, ), ),Copy the code

So the final thing to solve is to determine whether the current is a push or a pop. At first, I planned to use displacement to calculate, moving right is POP, moving left is push, but push is moved with gesture, the user can pull the page left and right blind jb slide, so this scheme pass; Then I change my mind and listen for the state of the animation. When the animation is finished, I change the value of the “push” variable:

@override
  void initState() {
    super.initState();
    widget.primaryRouteAnimation.addStatusListener((status) {
      print("status:$status");
      if(status == AnimationStatus.completed) { isPush = ! isPush;setState(() {
          print("setState isFrom = ${isPush}");
        });
      } 
  }

@override
  Widget build(BuildContext context) {
    assert(debugCheckHasDirectionality(context));
    final TextDirection textDirection = Directionality.of(context);
    return SlideTransition(
      position: widget._secondaryPositionAnimation,
      textDirection: textDirection,
      transformHitTests: false,
      child: SlideTransition(
        position: isPush
            ? widget._primaryPositionAnimationPush
            : widget._primaryPositionAnimation,
        textDirection: textDirection,
        child: DecoratedBoxTransition(
          decoration: widget._primaryShadowAnimation,
          child: widget.child,
        ),
      ),
    );
  }
Copy the code

One of _primaryPositionAnimationPush is our custom push animation:

_primaryPositionAnimationPush = (linearTransition
                ? primaryRouteAnimation
                : CurvedAnimation(
                    parent: primaryRouteAnimation,
                    curve: Curves.linearToEaseOut,
                    reverseCurve: Curves.easeInToLinear,
                  ))
            .drive(_kTweenPush);

final Animatable<Offset> _kTweenPush = Tween<Offset>(
  begin: Offset.zero,
  end: Offset.zero,
);
Copy the code

It’s important to note that CupertinoPageTransition is a StatelessWidget, but we’re talking about state changes, so we need to make it a StatefulWidget. This is basically done, but there is a small bug. If the user cancelled the completed slide while sliding on push, the animation still walked on completed, and the isPush state is not correct. We can print the status of primaryRouteAnimation under different operations and find the following results:

  • Push: forward –> completed
  • When normal pop: forward –> reverse –> dismissed
  • Pop slides halfway to cancel: forward –> completed

This log also reflects that the pop animation is actually the reverse of the push animation. We modify the listener of primaryRouteAnimation according to this rule:

@override
  void initState() {
    super.initState();
    widget.primaryRouteAnimation.addStatusListener((status) {
      print("status:$status");
      if (status == AnimationStatus.completed) {
        isPush = false;
        setState(() {
          print("setState isFrom = ${isPush}");
        });
      } else if (status == AnimationStatus.dismissed) {
        isPush = true;
        setState(() {
          print("setState isFrom = ${isPush}"); }); }}); }Copy the code

Run it. It’s exactly what we need. We can modify _kTweenPush to implement a variety of push transformations:

  • _kTweenPush = Tween(begin: const Offset(0.0, 1.0),end: Offset. Zero);

  • _kTweenPush = Tween(begin: const Offset(1.0, 1.0),end: Offset. Zero,);

_kRightMiddleTween = Tween(begin: const Offset(1.0, 1.0),end: offset.zero);

Anyway, all kinds of SAO operation, you can try.

What if I want to add a fade-in animation?

The CupertinoPageTransition route is a SlideTransition. If you want to implement another transition, you need to change the build() method:

_primaryPositionAnimationPush = (linearTransition
                ? primaryRouteAnimation
                : CurvedAnimation(
                    parent: primaryRouteAnimation,
                    curve: Curves.linearToEaseOut,
                    reverseCurve: Curves.easeInToLinear,
                  ))
            .drive(Tween<double>(
          begin: 0.0,
          end: 1.0,
        )),

@override
  Widget build(BuildContext context) {
    assert(debugCheckHasDirectionality(context));
    final TextDirection textDirection = Directionality.of(context);
    return SlideTransition(
        position: widget._secondaryPositionAnimation,
        textDirection: textDirection,
        transformHitTests: false,
        child: isPush
            ? FadeTransition(
                opacity: widget._primaryPositionAnimationPush,
                child: widget.child,
              )
            : SlideTransition(
                position: widget._primaryPositionAnimation,
                textDirection: textDirection,
                child: DecoratedBoxTransition(
                  decoration: widget._primaryShadowAnimation,
                  child: widget.child,
                ),
              ));
  }
Copy the code

As for the rest of what size, rotation, etc., try yourself, with xxxTransition control can be achieved.

What if I want to change the animation time?

If you want to change a Duration, you can override CupertinoPageRoute’s get transitionDuration method.

class MyCupertinoPageRoute<T> extends CupertinoPageRoute<T> {
@override
  Duration get transitionDuration => const Duration(seconds: 3);
}
Copy the code