Original address: juejin.cn/post/695046…

Reproduced please sign, plagiarism is strictly prohibited

Direct renderings:

Vertical screen:


Initialize the zoom in and out effect of the adaptive screen:


Received a request for a copy of the movie theater and last week almost searched Baidu, Google, StackOverflow. I couldn’t find any effect with Flutter, so I had to write my own.

This article speaks train of thought only, concrete realization still needs everybody to see officer oneself start work. As long as you understand the idea below, it is very simple to achieve

Layout analysis

  1. The middle seat => matrix, implemented by Column nested Row, cannot be implemented by GridView (sliding conflict, explained below)

  2. Left navigation bar => a simple Column (can’t use ListView, which also causes sliding conflicts)

Interaction analysis & implementation

Zoom in and out drag effect:

Flutter now has an InteractiveViewer component for zooming in and out of drag

This component is perfect for zooming in and out. Component properties here do not expand the explanation, relatively simple, you can click the link above to understand.

Here are two key attributes:

First, callback events

  1. Interaction starts onInteractionStart
  2. Interactive update onInteractionUpdate
  3. Interaction end onInteractionEnd


You can use this class to control zooming in and out through your code

Drag the navigation bar to zoom in and out with the seating chart:

The left navigation bar follows the zooming in and out of the middle seat, as well as the positioning of the number of lines:

Those things mentioned above are generally imaginable and easy to achieve. The real difficulty with this interaction is the follow slide effect.

Since the left navigation bar is fixed to the far left, and the seating chart can be dragged full screen, the seating chart and the navigation bar cannot be in the same zoom component,

Otherwise, when the seating chart is enlarged, the navigation bar will be directly enlarged out of the screen.

So the idea is to use the navigation bar and the seating chart as children of the Stack, and then the seating chart can zoom in and out, and the navigation bar can zoom in and out with the seating chart.

I have tried a number of methods here:

Method one:

Both the left navigation bar and the middle seating chart use the InteractiveViewer,

The effect synchronization is then achieved through the callback event and transformation controller of the InteractiveViewer


Failure, the principle of transformationController is Matrix4 generic ValueNotifier (four dimension matrix), simple mobile amplification can also be achieved, completely clone a zoom in and out drag effect, I can not do. You can try linear algebra if it’s really cool.

Method 2:

Flutter has a synchronous scroll component called linked_scroll_controller that binds two scrollcontrollers together to achieve synchronous scroll.

So let the left navigation bar use the ListView, and the middle seat table use the InteractiveViewer nested GridView, and then bind the ListView to the GridView’s ScrollController for synchronous scrolling.


Failed, InteractiveViewer sliding is implemented via Matrix4, and ListView sliding conflicts. Synchronous scrolling is implemented, but zooming and zooming cannot be performed.

Method 3:

You can’t get away with using InteractiveViewer, otherwise it’s a pain to make your own zoom in and out. If you can do as linked_scroll_controller does, Copy the InteractiveViewer’s scaling effect into another InteractiveViewer, and you’re good.

Is the idea of method one, but with the InteractiveViewer open interface and controller, can not be completed, this time you need to read and understand the source code of the InteractiveViewer, see if there is any inspiration.

@override Widget build(BuildContext context) { Widget child = Transform( transform: _transformationController.value, child: KeyedSubtree( key: _childKey, child: widget.child, ), ); if (! Child = OverflowBox(alignment: alignment. TopLeft, minWidth: 0.0, minHeight: 0.0, maxWidth: double.infinity, maxHeight: double.infinity, // maxHeight: 220.w, child: child, ); } if (widget.clipBehavior ! = Clip.none) { child = ClipRRect( clipBehavior: widget.clipBehavior, child: child, ); } // A GestureDetector allows the detection of panning and zooming gestures on // the child. return Listener( key: _parentKey, onPointerSignal: _receivedPointerSignal, child: GestureDetector( behavior: HitTestBehavior.opaque, // Necessary when panning off screen. dragStartBehavior: DragStartBehavior.start, onScaleEnd: onScaleEnd, onScaleStart: onScaleStart, onScaleUpdate: onScaleUpdate, child: child, ), ); }Copy the code

The InteractiveViewer has wrapped all the methods for us.

Note that the above GestureDetector, the whole InteractiveViewer gesture interaction method, is actually onScaleEnd, onScaleStart, onScaleUpdate these three methods.

Then we just need to pass the parameters of the three methods called back by the seat chart component into the navigation bar component, and then delete the GestureDetector of the navigation bar component so that the navigation bar component only accepts the gesture interaction parameters from the seat chart component.

We simply override two Interactive Viewers, one for the master component (seating table) and one for the slave component (navigation bar), and open the InteractiveView State. When the seating table component calls back to the three methods of the gesture, we pass the parameters of the three methods to the navigation bar component by key.

_onInteractionUpdate(ScaleUpdateDetails details) { if (controller.fromInteractiveViewKey.currentState ! = null) { controller.fromInteractiveViewKey.currentState.onScaleUpdate(details); } } _onInteractionStart(ScaleStartDetails details) { if (controller.fromInteractiveViewKey.currentState ! = null) { controller.fromInteractiveViewKey.currentState.onScaleStart(details); } } _onInteractionEnd(ScaleEndDetails details) { if (controller.fromInteractiveViewKey.currentState ! = null) { controller.fromInteractiveViewKey.currentState.onScaleEnd(details); }}Copy the code

Pass parameters copied into the navigation bar component without any processing at all. We can scale synchronously!

Special attention must be paid here: the height of each item of the seating chart and navigation bar component must be exactly the same, including margin and padding, or there will still be dislocation

At this point, the biggest difficulty of simultaneous scaling and sliding is solved.

The bottom frame floats above the seating chart:

After clicking on a seat, the bottom popup box pops up, covering part of the seating chart, but the seating chart can continue to drag up to display the last row of data

This doesn’t seem difficult at first glance, but it’s a little complicated when you think about it.

First of all,

The clear seating chart display area contains the bottom cartridge, because the bottom cartridge is suspended above the seating chart,

So we can only use margin and not padding, so depending on the height of the popbox at the bottom of the design, we can set marginBottom to this height, but there is a problem:

When the whole seating chart enlarges, the margin will also be enlarged synchronously, which will result in the larger the space is, the larger the gap between the seating chart and the bottom.


We need to get the current magnification multiple, dynamically adjust the margin,

If the current magnification is X times and the original margin is Y, then the current enlarged margin=Y/X

We know Y, we just need to know X.

However, in the _onInteractionUpdate interface, X is not a multiple of the current scale, but a multiple of the last scale since the last scale.

That is: initial 1.0 times,

The magnification of the interface callback is 2 for the first time

The second magnification is 3 times, and the interface callback is 1.5 times larger (1.5 times larger than the first one).

Even worse, the interface continues to call back the magnification after scaling to maxScale. And that’s what bothers us,

When we read the source code later, we found that the current magnification parameter that we wanted from the original magnification was in the InteractiveViewer class

// Return a new matrix representing the given matrix after applying the given // scale. Matrix4 _matrixScale(Matrix4 Matrix, double scale) {if (scale == 1.0) {return matrix.clone(); } assert(scale ! = 0.0). // Don't allow a scale that results in an overall scale beyond min/max // scale. final double currentScale = _transformationController.value.getMaxScaleOnAxis(); final double totalScale =currentScale * scale; // Final double totalScale = math.max(// currentScale * scale, // // Ensure that the scale cannot make the child so big that it can't fit // // inside the boundaries (in either direction). // math.max( // _viewport.width / _boundaryRect.width, // _viewport.height / _boundaryRect.height, // ), / /); final double clampedTotalScale = totalScale.clamp( widget.minScale, widget.maxScale, ); widget.scaleCallback? .call(clampedTotalScale); final double clampedScale = clampedTotalScale / currentScale; return matrix.clone().. scale(clampedScale); }Copy the code

Note the scaleCallback above, which is the author’s own callback method, where the clampedTotalScale is the current magnification we want from the initial scale,

That is: initial 1.0 times,

The magnification of the interface callback is 2 for the first time

The second magnification is 3 times, and the interface callback is 3 times larger than the initial magnification.

And the clampedTotalScale is always in the range of minScale and maxScale. It’s very handy to use.

The above code has an algorithm that I have commented out. The effect of this code is:

The minScale depends not only on the value we set, but also on the child display of the InteractiveViewer. I don’t need this limitation here, so I comment it out.

In fact, if you want to perfectly achieve the effect given by UI, there are many places to use margin, such as the upper, lower and left margin of the seating table, as long as you get the clampedTotalScale above, it can be dynamically calculated, which is very convenient.

Horizontal and vertical screen adaptation effect

The GIF above has a landscape effect and uses the official API, OrientationBuilder, which is also very easy to use. Here is a UI adaptation note:

Since ScreenUtil (UI adaptive) is used in the author’s project, the UI size diagram of portrait screen is passed in for portrait screen, and the end of the size is used for adaptation. W; the UI size diagram of portrait screen is passed in for landscape screen (in fact, width and height of portrait screen are inverted), and the end of the size is used for adaptation. This will almost perfectly fit the horizontal and vertical screens, and the rest of the details can be tweaked.

Initial magnification

As rendering in entering or somehow the screen switch for the first time, when the seating chart layout too much (the default display during), narrow as far as possible to display more content (lower limit to minScale), when the seating chart layout too little (when the default display screen is empty), enlarge as much as possible until the full screen (ceiling magnified to maxScale).

The above effect can be summarized as: as large as possible under the premise of displaying as complete as possible.

The InteractiveViewer does not have an initial magnification parameter. The default entry is 1.0 magnification.

So we’re going to have to figure out the initial magnification.

To calculate

(If screenUtil is used, please pay attention to distinguish between horizontal and vertical screens in the following calculation. For horizontal screen, the ending is used with.w, while for vertical screen, the system will automatically change the padding of the irregular screen.)

  1. The entire seating chart display area,

    Screen height – Top and bottom padding-height of the bottom hover box for portrait (0 if the hover box is not at the bottom) – title bar height and the height of any other layout you add.

    Screen width – Left and right padding-right floating box width for landscape (0 if the floating box is not on the right for portrait) – Navigation bar width (this navigation bar width also needs to be dynamically calculated based on zoom in/out) – Other layout width added by yourself.

  2. Calculate the width, height and padding of the seating table item at the initial magnification (1.0).

  3. Gets the x and y axes of the current seating chart. So how many seats per row, how many rows.

  4. The calculation assumes that the width and height of each item are to be displayed in all seating tables.

    That is, divide the width and height of the seating chart display area obtained in 1 above by the seating chart x and Y respectively.

  5. Divide the width of 2. by 4.width, i.e., the value SX that needs to be scaled if the X-axis is fully displayed,

    Divide the height of 2 by the height of 4.

  6. Compare the values of SX and SY with the small value of defaultS (as large as possible but as complete as possible).

  7. If the defaultS are within the minScale and maxScale ranges, the defaultS are used, and vice versa.

The zoom

Scale the InteractiveViewer to defaultS with the transformationController

/ / charts mainTransformationController. Value = Matrix4. Identity (.). scale(defaultS); / / navigation fromTransformationController. Value = Matrix4. Identity (.). scale(defaultS);Copy the code

Note here that both the seating chart and the navigation bar are scaled.

Zooming dynamic margin

Finally, don’t forget to scale the various margins that need to be evaluated dynamically to defaultS as well

If there is a horizontal/vertical switching effect, the initial magnification value is dynamically calculated at each time of horizontal/vertical switching.

It should be noted that the margin of the dynamic calculation should be set to the initial value (i.e. the margin value when the scale is 1.0) during each calculation.


So far all the effects have been achieved, the rest I believe you can also fix, very simple.

Due to the flexibility of this component and the confidentiality of the company’s project, the code will not be passed on, so you can leave a message if you have any questions.

Sometimes can’t think of the source code, will be immediately inspired.