A few days ago, a fellow in the FlutterCandies group asked how to implement the parallax effect of the IOS slide-back route, just as I was about to implement the IOS parallax route animation that pops up from the bottom.

Before we get down to business, here are a few things to look at:

· How to understand Flutter routing source code design? | creators camp ii – the nuggets (juejin. Cn)

PageRoutBuilder Secondary Animation is always 0 · Issue #94642 · flutter/flutter (github.com)

[iOS 13] new fullscreen stack type route transition · Issue #33798 · flutter/flutter (github.com)

Lateral spreads parallax

Pharaoh said, do not know how to read the source code.

  1. Let’s start with the basics. Official stuffPageRouteBuilderConvenient implementation of routing, we only care about it hereopaqueProperties andbuildPageAs well asbuildTransitionsTwo methods.

Opaque indicates whether to draw the old route after the new route is added to the stack. The remaining two methods function as their names, but pay attention to animation and secondaryAnimation. They will be used in route linkage later.

  @override
  final bool opaque;
  
  @override
  Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
    return pageBuilder(context, animation, secondaryAnimation);
  }

  @override
  Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
    return transitionsBuilder(context, animation, secondaryAnimation, child);
  }
Copy the code
  1. Now, let’s seeCupertinoPageRoutebuildTransitionsHow it’s done.

This is where it gets interesting. CupertinoPageTransition and _CupertinoBackGestureDetector, the former of sideslip and parallax effect, which deal with gesture to make way for the follow pointer by animation.

  static Widget buildPageTransitions<T>(
    PageRoute<T> route,
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
    Widget child,
  ) {

    final bool linearTransition = isPopGestureInProgress(route);
    if (route.fullscreenDialog) {
      return CupertinoFullscreenDialogTransition(
        primaryRouteAnimation: animation,
        secondaryRouteAnimation: secondaryAnimation,
        linearTransition: linearTransition,
        child: child,
      );
    } else {
      // Non-dialog goes here. The animation shown above returns the Widget
      returnCupertinoPageTransition( 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);
  }
Copy the code
  1. Look again atCupertinoPageTransitionWhat the hell is going on

Everything is a Widget. There are two slidetransitions nested internally. The first one is driven by secondaryAnimation, and the second one by animation. Just to keep an impression here, because parallax animation is relevant.

class CupertinoPageTransition extends StatelessWidget {

  CupertinoPageTransition({
    Key? key,
    required Animation<double> primaryRouteAnimation,
    required Animation<double> secondaryRouteAnimation,
    required this.child,
    required bool linearTransition,
  }) 
  
  ...
  
  @override
  Widget build(BuildContext 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

Here, to recap, the route class’s buildTransitions method is used to build the route animation, and its parameters include animation and secondaryAnimation to drive the animation. So where do they come from? By whom? Now we have a small high energy wave.

Parse the routing animation source tree

Take a look at CupertinoPageRoute’s parent class and its various functions.

Since this is a route animation, all we care about is what is hidden in the TransitionRoute.

Weeds, a AnimationController, two Animation < double >, notice the _secondaryAnimation is a ProxyAnimation, defaults to kAlwaysDismissedAnimation, An Animation

that always has a value of 0.


Animation<double>? _animation;

final ProxyAnimation _secondaryAnimation = ProxyAnimation(kAlwaysDismissedAnimation);

// Create AnimationController
AnimationController createAnimationController() {
    assert(! _transitionCompleter.isCompleted,'Cannot reuse a $runtimeType after disposing it.');
    final Duration duration = transitionDuration;
    final Duration reverseDuration = reverseTransitionDuration;
    assert(duration ! =null && duration >= Duration.zero);
    return AnimationController(
      duration: duration,
      reverseDuration: reverseDuration,
      debugLabel: debugLabel,
      vsync: navigator!,
    );
  }
  
// Return the Animation corresponding to controller
Animation<double> createAnimation() {
    return_controller! .view; }Copy the code

Eh, did you forget the AnimationController and animation?

It’s hidden in the install method. Don’t know what install does? See recommended reading at the beginning of this article.)

  @override
  voidinstall() { _controller = createAnimationController(); _animation = createAnimation() .. addStatusListener(_handleStatusChanged);super.install();
    if (_animation!.isCompleted && overlayEntries.isNotEmpty) {
      overlayEntries.first.opaque = opaque;
    }
  }
Copy the code

Do you think these are the parameters to buildTransition at this point? ModalRoute gives you another layer of ProxyAnimation, as for why a layer of Proxy, we won’t go into the details here.

  // This is ModalRoute's install method
  @override
  void install() {
    super.install();  // super is the installation of TransitionRoute
    _animationProxy = ProxyAnimation(super.animation);
    _secondaryAnimationProxy = ProxyAnimation(super.secondaryAnimation);
  }
Copy the code

Can’t see through the clouds? That’s fine. It doesn’t affect the rest of the reading. At this point, the parallax implementation hasn’t happened yet.

Parse the route animation call process

Who is calling buildTrasition?

Package/lib/SRC/widgets/route. The inside of the dart _ModalScope in doing this, it’s a StatefulWidget. (Everything is a widget)

When the new route is pushed, the build method is triggered. There is too much code here, only part of it is captured. Your route animations are all in the AnimatedBuilder.

Let’s see what happens when we push a route

    // Suppose the above is a MaterialApp
     MaterialButton(
        child: Text("go next Page"),
        onPressed: () async {
           Navigator.push(context,
              CupertinoPageRoute(
                  builder: (context) {
                    return Scaffold(
                      appBar: AppBar(
                        title: Text("Sideslip parallax"),
                      ),
                      body: Center(
                        child: Text("New route"),),); })); });Copy the code

The call stack associated with the routing animation is as follows

Pop it again. Well, discovery always executes _updateSecondaryAnimation.

By now, the above view is very foggy. It doesn’t matter if you don’t understand it. You probably know the function of each method and don’t care about the details. Just know that Install initializes the route animation controller and the _updateSecondaryAnimation method updates secondaryAnimation (the same parameter used for buildTransition).

Parallax routing is officially unveiled

Now, start All in on the front

In the page we started with, the default MaterialPageRoute provided by the MaterialApp, its buildTrasiton is different, it displays different route animations depending on the platform, the default is from the bottom up, IOS will slide parallax, and the rest will fade in and out.

  / / MaterialRouteTransitionMixin buildTrasiton
  @override
  Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
    final PageTransitionsTheme theme = Theme.of(context).pageTransitionsTheme;
    return theme.buildTransitions<T>(this, context, animation, secondaryAnimation, child);
  }
  
  / / theme buildTransitions
    Widget buildTransitions<T>(
    PageRoute<T> route,
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
    Widget child,
  ) {
    TargetPlatform platform = Theme.of(context).platform;

    if (CupertinoRouteTransitionMixin.isPopGestureInProgress(route))   // Keep an eye on this
      platform = TargetPlatform.iOS;

    final PageTransitionsBuilder matchingBuilder =
      builders[platform] ?? const FadeUpwardsPageTransitionsBuilder();
    return matchingBuilder.buildTransitions<T>(route, context, animation, secondaryAnimation, child);
  }
Copy the code
To start, we currently have only one routing stackMaterialPageRoutewhenpush CupertinoPageRouteAt that time, something wonderful happened !!!!!!!

Remember the _updateSecondaryAnimation mentioned above?

  1. When you arepush“Is called firstCupertinoPageRoute_updateSecondaryAnimationMethod,nextRoutesaidCupertinoPageRouteThe next route, due topushAfter the current routing stackMaterialPageRouteCupertinoPageRouteCupertinoPageRouteThere are no new routes after that, so yesnull.

Here is the route animation for CupertinoPageRoute

  1. After that, executeMaterialPageRoute_updateSecondaryAnimationAt this point, the incomingnextRouteParameter is freshpushCupertinoPageRoute, pass it in, in_updateSecondaryAnimationIn this method, there are all kinds of thingsif else,CupertinoPageRouteAnimationControllerSet toMaterialPageRoutesecondaryAnimation(ProxyAnimation)parent.

The following is the current route animation information of the previous route, MaterialPageRoute

  1. What happens to the parallax effect when you start to sideslip?

It should be mentioned that the value of the AnimationController changes from 0.0 -> 1.0 when the route animation is started to full entry

Remember why there are two SlideTrasition? One controls sliding parallax effects and one controls side slide routing.

IsPopGestureInProgress (Route) returns True when gestural sliding is detected (this route is MaterialPageRoute), selecting two SlideTrasition versions of the animation, The slidetrasnesting effect is a stack of two. Here is the Offset used for both.

// The interface slides completely in from the right
// 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,
);

// The interface draws 1/3 of the distance relative to itself from the center to the left
// 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));Copy the code

This is the sideslip,MaterialPageRouteRoute animation information of.

Because MaterialPageRoute initial was in the middle of the screen, so the corresponding AnimationController. Value = 1.0

This figure is the animation information of the MaterialPageRoute after clicking Push. Its controller is always 1.0 and the controller of the new route keeps increasing. The corresponding effect is that the old route stays the same and the new route is entered from the right, but there is no parallax effect at this time.

The following figure shows the animation information of the MaterialPageRoute after the complete push and then sideslip back to the center of the screen. Animation is 1, indicating that in the center of the screen, secondaryAnimation is 0.5, corresponding to the left offset of 0.15, which corresponds to the parallax effect.

And CupertinoPageRoute at this time (not), animation as a value of 0.5, secondaryAnimation 0.0 (kAlwaysDismissedAnimation), The effect is that the route above slides out half the screen.

  1. How does sideslip work?

See route.dart for Cupertino Package. _CupertinoBackGestureController simple dragUpdate and _CupertinoBackGestureDetector this StatefulWidget build, take a look at will.

 void dragUpdate(double delta) {
    controller.value -= delta;
  }
  
   @override
  Widget build(BuildContext context) {
    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,
          ),
        ),
      ],
    );
  }
Copy the code

Ready to freeze your hands, rub a parallax routing animation

The _updateSecondaryAnimation method controls the parent of secondaryAnimation and allows the old route to borrow the animation controller from the new route.

Take a look at_updateSecondaryAnimationThe source of

To get rid of animation linkage and to animate old and new routes together, the flutter official designs a contract on the TransitionRoute, which is the first if statement corresponding to _updateSecondaryAnimation

      // Both return true by default, and subclasses can be overridden to determine whether linkage is possible
      // Whether to update the animation of the previous route when the current route state changes
      bool canTransitionFrom(TransitionRoute<dynamic> previousRoute) => true;
      // Whether to update the animation when the new route is pushed
      bool canTransitionTo(TransitionRoute<dynamic> nextRoute) => true;
Copy the code

All clear, start much faster, afraid of no effect, all return True is done.

Now analyze the parallax effect shown in figure 2 at the beginning.

The state of the following routes is saved, so the new route selects to inherit PopupRoute, clipping and scaling after sliding, and the new route slides into the interface from the bottom up. Parent is the AnimationController of the new page. For the old route, we choose to inherit PageRoute.

  / / the new road
  @override
  Widget buildTransitions(BuildContext context, Animation<double> animation,
      Animation<double> secondaryAnimation, Widget child) {
    final double topOffset = paddingTop / MediaQuery.of(context).size.height;

    final Animation<Offset> position = Tween<Offset>(
            begin: const Offset(0.0.1.0), end: Offset(0.0, topOffset))
        .animate(animation);
    returnSlideTransition( position: position, ... ) ; }/ / the old route
  @override
  Widget buildTransitions(
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
    Widget child,
  ) {
    final Animation<Offset> position =
        Tween<Offset>(begin: const Offset(1.0.0.0), end: Offset.zero)
            .animate(animation);
    final Animation<double> scaleFactor =
        Tween<double>(begin: 1.0, end: 0.94).animate(secondaryAnimation);

    return SlideTransition(
      position: position,
      child: ScaleTransition(
        scale: scaleFactor,
        child: ClipRRect(
            borderRadius: secondaryAnimation.isDismissed
                ? BorderRadius.zero
                : const BorderRadius.only(
                    topLeft: Radius.circular(35.0),
                    topRight: Radius.circular(35.0)),
            child: child),
      ),
    );
  }
Copy the code

Finally add a drag gesture processing (refer to the official source code), and then write too much.

Let’s take a look at the effect, missing a billion little details, here I don’t bother to go into the details.

conclusion

Not in the route, then the effect will have hands. But it is also interesting to read the source code of Flutter and then to be enlightened.

This implementation is actually the community had a package named modal_bottom_sheet | Flutter package (Flutter – IO. Cn), However, his implementation is to create an AnimationControler to control the animation controller in didChangeNext calls, which is different from the implementation of this article. The similarity is that you have to build two routes yourself, as this is determined by the contract required for linkage.

Chinouo/flutter_modal_bottom_route, also welcome to join FlutterCandies die 🤪, crouching tiger hidden dragon in the group!

Finally, I wish everyone the year of the Tiger coding dragon jing tiger fierce! 😀