Peripherals of FlutterUnit | In-depth analysis of iOS gesture fallback issues

theme: cyanosis

1. The emergence of the problem

Since I have been testing on an Android machine before, I have not run it on iOS. Recently, FlutterUnit released the iOS version, and the most feedback received is: Swipe back fails. At first I thought it was just the fault of WillPopScope, but I found that after jumping to many common interfaces, iOS cannot return to sliding. Then I felt very strange, there must be a demon when things go wrong, so let’s find out.

|Android Interface | iOS Interface | | — | — | | 5nP3gtb3NzLXByb2Nlc3M9aW1hZ2UvcmVzaXplLGhfMzA=,g_se,x_0,y_0,t_100″ alt=”143.gif” style=”outline: none;”> | 144.gif |

In the above iOS interface, click About Bee to enter the interface and you can swipe back normally, but you cannot swipe back when you jump to Account Information. Therefore, it is natural to think of checking the relevant source code location to see the difference in processing. Then you can see that About Bees is redirected through MaterialPageRoute, and Account Information is redirected through Right2LeftRouter. Then I checked several other interfaces that cannot be rolled back, and I can hammer it out:

There is a problem with the auxiliary routing class of the interface jump animation that I encapsulated before!!!

image.png

2. Jump animation about routing

Right2LeftRouter is an assistant that can jump animation from left to right when jumping interface. This was written 4 years ago, and the related articles are: Flutter Welfare | Hero Transition Component Sharing – Bonus-Route Animation Tool Class , I didn’t expect to be dug up today. We all know the effect of MaterialPageRoute jump, which is transparency + zoom animation in Android. This can be seen in the source code, the following is the logic of building a jump animation: get PageTransitionsTheme according to the context to execute:

image.png

It can be seen that different platforms have different default animation transformation effects. For example, the android platform uses ZoomPageTransitionsBuilder, and ios and macos use CupertinoPageTransitionsBuilder.

image.png

Follow up and see: ZoomPageTransitionsBuilder uses the _ZoomEnterTransition component when entering, which defines the transparency and zoom animation. This is the root of the transparency + zoom animation jump interface on the Android platform.

image.png

In CupertinoPageTransitionsBuilder, it is easy to see that SlideTransition is used, that is, sliding left and right.

image.png

So XXXTransitionsBuilder can determine the interface routing transition animation, and the default value provided in PageTransitionsTheme means that we can specify the style of the platform transition animation through the theme. You can also customize XXXTransitionsBuilder to achieve your own style, this piece will be studied and sorted out in detail when you have time. Stop here, and write an article in the future. Now focus on how to modify the previous routing jump animation tool so that it can meet the requirements.

3. The twists and turns of the road to correction

The idea at the beginning (partial): In fact, it is very simple. The previous inheritance is that PageRouteBuilder cannot fulfill the requirements, and it and MaterialPageRoute are brothers, both of which are derived classes of PageRoute. So just change it to inherit MaterialPageRoute, the biggest problem is how to modify the jump animation effect of MaterialPageRoute.

From the above we know that the core of decision jump animation is the buildTransitions method, which is defined in MaterialRouteTransitionMixin and mixed in by MaterialPageRoute. So there are two ways to change the animation processing method:

Method 1, modify the transition constructor provided in PageTransitionsTheme
The second way is to inherit from MaterialPageRoute, override buildTransitions, and provide the implementation yourself.

From the perspective of coupling, the first method is better, you can customize XXXTransitionsBuilder to plug in and out at will, and you can control the effect of each platform. Here, because the previous code is an inheritance system, in order not to destroy the existing code, method 2 is also used here. The advantage of this approach is that you can access and control more details, such as the duration of the animation. Each has its own advantages and disadvantages, and the implementation is also the operation of the animator, and there is no difference in essence.

The following is the processing of jumping animation routing from right to left, overriding buildTransitions to control the animation effect, and overriding transitionDuration to control the duration.

“`dart //right—>left class Right2LeftRouter

extends MaterialPageRoute

{ final Widget child; final Duration duration; final Curve curve;

Right2LeftRouter({ required this.child, this.duration = const Duration(milliseconds: 300), this.curve = Curves.fastOutSlowIn, }) : super(builder: (_) => child);

@override Widget buildTransitions( BuildContext context, Animation

animation, animation

secondaryAnimation, Widget child, ) { return SlideTransition( position: Tween

( begin: const Offset(1.0, 0.0), end: const Offset(0.0, 0.0), ).animate(CurvedAnimation(parent: animation, curve: curve)), child: child, ); }

@override Duration get transitionDuration => duration; } “`

But the result is counterproductive, and iOS uses the above Right2LeftRouter to slide back. The only difference between this and MaterialPageRoute is that buildTransitions is customized. So now all the clues point to the problem: CupertinoPageTransitionsBuilder, it must respond to the back gesture internally.

From this point of view, it is more troublesome to customize the jump transition animation of iOS. The back gesture is handled in CupertinoPageTransitionsBuilder, so the official implication is: use mine obediently, don’t mess around.

But I’m not a good kid. The default animation of iOS is to enter the page from right to left, but if you want to implement other animations such as transparency gradient entry and support gestures to fall back, it will be more troublesome. How can you catch a tiger if you don’t enter the tiger’s den, go explore the way.

4. Go deep into the tiger’s den and analyze the behavior of CupertinoPageTransitionsBuilder

As shown below, the CupertinoPageTransitionsBuilder uses the buildTransitions logic handled by the CupertinoRouteTransitionMixin.buildPageTransitions static method:

image.png

If you continue to follow up, you can find that there is a _CupertinoBackGestureDetector gesture detection component under CupertinoPageTransition. It is easy to see from the name that it handles iOS fallback gesture events. It is not difficult to see from here that the back gesture of iOS in Flutter is a component behavior, while the back gesture in Android is a system behavior.

image.png

This is why the above inherits from MaterialPageRoute , and cannot complete the fallback effect. You have not planted a seed, of course nothing will sprout.

Let’s take a look at the specific processing of the _CupertinoBackGestureDetector fallback detection component: there are two callback functions, both logics are completed by static methods:

enabledCallback: A function that returns a bool value, used to indicate whether a fallback is possible.
onStartPopGesture : Returns the function of _CupertinoBackGestureController, when the back gesture is triggered.

First of all, it can be seen from the class declaration that it is a StatefulWidget, that is to say, it needs to maintain the state quantity internally, and the focus is on the construction logic of the state class and the maintenance logic of the state quantity.

image.png

Then, get straight to the point and see what’s built into it. The following is the build method of the corresponding state class, which is not very complicated. Stack it through Stack, place a drag area through PositionedDirectional, and use Listener to listen to gesture events. The GestureDetector is not used here, probably because it is only a horizontal drag event, and it is more straightforward to use a Listener.

image.png

As can be seen from the above, the dragged area defaults to 20 logical pixels, which is shown in the red box below. If you set a large horizontal padding for MediaQuery, the width can go beyond 20, but the application rarely sets global horizontal padding.

dart const double _kBackGestureWidth = 20.0;

image.png

5. Learn the handling of gesture events from the source code

Here you can learn how to handle horizontal drag events through Listener in the source code. Since the Listener component can only listen to the onPointerDown event, that is, the touch point is pressed, an additional thing is needed to track the behavior track of the touch point, which is the gesture detector. If you have read the booklet “Flutter Gesture Exploration-Controlling the World”, you may be familiar with it.

As shown below, the HorizontalDragGestureRecognizer horizontal drag gesture detector is maintained in the state class. The gesture detector is created when the state is initialized and needs to be destroyed when dispose. This is why the component is StatefulWidget.

image.png

After the horizontal drag gesture detector is created, the next step is to associate the detector with the touch point. This event is very obvious, that is, the Listener component listens to when the contact is pressed, as shown below. In fact, if you think about it carefully, this is no different from the logic processing of the GestureDetector component.

image.png

At this point, the logic of gesture events is very clear. HorizontalDragGestureRecognizer detects touch points and triggers related callbacks at corresponding times, such as when dragging starts and dragging updates. What the detector provides is the data already carried by the event type. As for what reaction the interface needs to make according to the event and data, it needs to be handled by the outside world in the callback.

image.png

The handler is _CupertinoBackGestureController, which will be created by OnStartPopGesture callback when dragging starts, which is the _startPopGesture static method seen above. When dragging and updating, this object is also processed through dragUpdate. That is to say, _CupertinoBackGestureController has the ability to change the interface shape. For example, when dragging to update, the dragUpdate method will make the interface offset. Where does its magic come from?

It can be seen from the source code that it holds an animation controller, which is very clear: the routing jump animation is essentially transformed through the animation controller. If the interface is a puppet, then the animation controller is the string that controls the puppet, that is to say, _CupertinoBackGestureController holds the string holder .

image.png

One last note: how the lift is held by the _CupertinoBackGestureController . In fact, it is easy to understand that some people are born with a golden key in their mouths.

image.png

At this point, all the clues to solve the case have been sorted out. Now if you want to customize the jump animation and also want iOS to support fallback, you can copy the code related to the magic animation transformation of CupertinoPageTransitionsBuilder.

6. What should be paid attention to if you want to customize animation routing animation

In fact, after looking at the source code, I know that CupertinoPageTransition is essentially based on SlideTransition, and several animation curves have been processed. To be honest, the effect is quite good. Then the tattered animation thing I wrote before suddenly became unsavory. As shown below, what I wrote before was a simple SlideTransition.

image.png

In general, the animation effects of Flutter are basically enough. Think about whether it is really necessary to do some more fancy jump animations. The following are four built-in jump animations in Flutter, but only _CupertinoBackGestureDetector handles the verification of iOS back gestures. So if you want to customize the animation effect, you must modify the implementation of CupertinoPageTransitionsBuilder.

image.png

Let’s take a transparency gradient animation as an example. For example, click the search box on the home page in FlutterUnit, and the transparency will gradually change to jump to the search page. If you want iOS to also have transparency animation, you need to modify CupertinoPageTransitionsBuilder to handle it. For example, define a FadePageRouter here to handle transparency gradient routing:

The following is the core code, the main thing is to take the CupertinoBackGestureDetector over, and set it up for the child when it is on the iOS platform. In this way, iOS can handle the fallback event, the code is detailed in: fadepageroute.dart. If you want to define other animations, you can handle them in buildTransitions according to animation.

“`dart class FadePageRoute

extends MaterialPageRoute

{ final Widget child; final Duration duration; final Curve? curve;

FadePageRoute({ required this.child, this.duration = const Duration(milliseconds: 300), this.curve, }) : super(builder: (_) => child);

@override Widget buildTransitions( BuildContext context, Animation

animation, animation

secondaryAnimation, Widget child, ) { if (Platform.isIOS) { child = CupertinoBackGestureDetector( enabledCallback: () => isPopGestureEnabled

(this), onStartPopGesture: () => startPopGesture

(this), child: child, ); }

if (curve != null) {
  animation = CurvedAnimation(
    parent: animation,
    curve: curve!,
  );
}

return FadeTransition(
  opacity: Tween(begin: 0.1, end: 1.0).animate(animation),
  child: child,
);

}

@override Duration get transitionDuration => duration;

} “`

This eloquent article is about to write 10,000 words. Through this exploration, I have a lot of new understanding of routing animation. It also gives a better way to customize routing animation. I hope that everyone can also learn some knowledge from the processing of the source code, instead of just taking the answer to solve the problem, but to cultivate their ability to solve the problem. WillPopScope I took a look at the source code, and I have some problems with the iOS rollback gesture. I will introduce it separately in the next article, so this article is here, thank you for watching~