Flutter realizes the effect of seat selection in the cinema!


I received a demand for imitation cinemas. I almost searched Baidu, Google and stack overflow last week. We can only write one by ourselves if we don't find the effect realized by fluent. This article only talks about ideas, and you need to do it yourself. As long as you understand the following ideas, the implementation is very simple.

Effect drawing directly on

Vertical screen:

Horizontal screen:

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

Layout analysis

The middle seat = > matrix is implemented through Column nested Row, not through GridView (sliding conflict, which will be described below)

Left navigation bar = > a simple Column (ListView cannot be used, which will also cause sliding conflict)

Interaction analysis & Implementation

Zoom in and out drag effect:

For the effect of zooming in and out drag, fluent now has its own component interactive viewer

Through this component, the effect of zooming in and out can be perfectly realized. Component properties are not explained here. They are relatively simple. You can click the link above to learn about them.

Here are two key attributes:

1. Callback event

  • Interaction start - onInteractionStart
  • Interactive update - onInteractionUpdate
  • End of interaction: onInteractionEnd

2. Transformation controller

You can use this class to control the zoom in and zoom out effect through code

Drag the navigation bar to zoom in and out with the seat table:

The left navigation bar follows the enlargement and contraction of the middle seat, and the positioning of the number of rows does not deviate:

Those things mentioned above are generally conceivable and easy to realize. The real difficulty of this interaction effect is the following sliding effect.

Since the left navigation bar is fixed on the leftmost side and the seat table can be dragged in full screen, the seat table and navigation bar cannot be placed in a zoom component, otherwise when the seat table is enlarged, the navigation bar will be directly enlarged out of the screen. Therefore, our idea is to take the navigation bar and seat table as sub components of the Stack, and then the seat table can zoom in and out, and let the navigation bar zoom in and out with the seat table. The author has tried many methods here:

Method 1:

Both the left navigation bar and the middle seat table use interactive viewer

Then, the effect synchronization is realized through the callback event of interactive viewer and transform controller


Failed. The principle of transformationController is Matrix4 generic ValueNotifier (four-dimensional matrix). Simple moving and zooming can also be realized. The author can not completely clone a zooming in and zooming out drag effect.. Ladies and gentlemen, if linear algebra is very awesome, you can try it.

Method 2:

The shuttle has a synchronized scrolling component called linked_scroll_controller

It can bind two scrollcontrollers together to realize synchronous scrolling.

So let the left navigation bar use ListView, the middle seat table use interactive viewer to nest GridView, and then bind the ListView and the ScrollController of GridView to realize synchronous scrolling.


Failed. The sliding of interactive viewer is implemented through Matrix4, which conflicts with the sliding of ListView.

Synchronous scrolling is implemented, but zooming in and out dragging cannot be performed.

Method 3:

You can't escape using interactive viewer. Otherwise, it's too troublesome to realize the effect of zooming in and out by yourself. If it can be like linked above_ scroll_ Like the controller, it is perfect to copy the scaling effect of the interactive viewer to another interactive viewer.

This is the idea of method 1, but it cannot be completed with the open interface and controller of interactive viewer. At this time, you need to read and understand the source code of interactive viewer to see if there is any inspiration.

  Widget build(BuildContext context) {
    Widget child = Transform(
      transform: _transformationController.value,
      child: KeyedSubtree(
        key: _childKey,
        child: widget.child,

    if (!widget.constrained) {
      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,

I'm surprised to see that interactive viewer has encapsulated all the methods for us.

Note that the GestureDetector and gesture interaction methods of the entire interactive viewer above are actually onScaleEnd, onScaleStart and onScaleUpdate.

Then we only need to pass the parameters of the three methods recalled by the seat table component into the navigation bar component, and then delete the GestureDetector of the navigation bar component to make the navigation bar component accept only the gesture interaction parameters from the seat table component.

We only need to rewrite two interactive viewers, one as the master component (seat table) and the other as the slave component (navigation bar), and open the interactive viewerstate. When the seat table component recalls the three methods of gesture, we can pass the parameters of the three methods into the navigation bar component through key.

_onInteractionUpdate(ScaleUpdateDetails details) {
    if (controller.fromInteractiveViewKey.currentState != null) {

  _onInteractionStart(ScaleStartDetails details) {
    if (controller.fromInteractiveViewKey.currentState != null) {

  _onInteractionEnd(ScaleEndDetails details) {
    if (controller.fromInteractiveViewKey.currentState != null) {

Without any processing, the parameters are copied into the navigation bar component. We can achieve the effect of synchronous scaling and dragging!

Special attention must be paid here: the height of a single item of the seat table and navigation bar components must be exactly the same, including margin and padding, otherwise dislocation will still occur

So far, the biggest difficulty of synchronous scaling and sliding has been solved.

The bottom spring frame is suspended above the seat table:

After clicking the seat, the pop-up box at the bottom will pop up to cover part of the seat table, but the seat table can continue to drag upward to display the data of the last row

At first glance, it's not difficult, but it's a little complicated when you think about it. First of all, make it clear that the display area of the seat table contains the bottom pop-up frame. Because the bottom pop-up frame is suspended on the seat table, we can only use margin instead of padding. Therefore, according to the height of the pop-up frame at the bottom of the design drawing, we can set the marginBottom to this height, but there will be a problem:

When the whole seat table is enlarged, the margin part will also be enlarged synchronously, which will lead to the larger the space between the seat table and the lower space.


We need to get the current magnification and dynamically adjust the margin. The current magnification is x times, and the original margin is y, then the current enlarged margin=Y/X, y is known, and we only need to know X. But in_ In the onInteractionUpdate interface, X is not the current magnification, but the magnification after the last scaling. Namely:

  • Initial 1.0 times.
  • For the first time, it is magnified to 2 times, and the magnification of interface callback is 2
  • For the second time, the magnification is 3 times, and the magnification of the interface callback is 1.5 (1.5 times larger than the first time).

And what's more serious is that after zooming in to maxScale, the interface will continue to callback the magnification. This bothers us. Later, after reading the source code, we found that the current magnification parameter of the original magnification we want is in the interactive viewer 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 =
    final double totalScale =currentScale * scale;
    //Changed the algorithm
    // 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(


    final double clampedScale = clampedTotalScale / currentScale;
    return matrix.clone()..scale(clampedScale);

Note the scaleCallback above, which is the callback method implemented by the author. The clampedTotalScale is the current magnification of the initial magnification, that is, the initial magnification is 1.0 times, the first magnification is 2 times, the interface callback magnification is 2 times, the second magnification is 3 times, and the interface callback magnification is 3 times (3 times larger than the initial magnification).

And clampedTotalScale is always within the range of minScale and maxScale. It's very convenient to use.

I commented out an algorithm in the above code. The effect of this code is:

When the child in the interactive viewer is fully displayed, it cannot be reduced. That is, minScale depends not only on the value we set, but also on the child display effect of the interactive viewer. I don't need this limitation here, so I'll comment it out.

In fact, if you want to perfectly realize the effect given by the UI, you need to use margin in many places, such as the up, down, left and right margins of the seat table. As long as you get the clampedTotalScale above, you can calculate dynamically, which is very convenient.

Horizontal and vertical screen adaptation effect

The gif diagram above has a horizontal screen effect. The horizontal and vertical screen switching also uses the official API, orientation builder, which is also very simple to use. Here is a note for UI adaptation:

Because ScreenUtil (UI adaptation) is used in the author's project, when the vertical screen is displayed, the UI dimension diagram of the vertical screen is imported, and the dimension is used at the end w for adaptation. When the horizontal screen is used, the UI dimension diagram of the horizontal screen is passed in (in fact, the width and height of the vertical screen are inverted), and then the dimension is used at the end h for adaptation. In this way, the horizontal and vertical screens can be perfectly adapted, and the remaining details can be fine tuned.

Initial magnification

As shown in the effect drawing above, when entering or switching between horizontal and vertical screens for the first time, when the layout of the seat table is too many (the default display is not lower), it shall be reduced as much as possible to display more content (the lower limit shall be reduced to minScale); when the layout of the seat table is too few (the screen is empty by default), it shall be enlarged as much as possible until the full screen is displayed (the upper limit shall be enlarged to maxScale).

The above effect can be summarized as follows: display as much as possible on the premise of complete display.

Interactive viewer does not have the initial magnification parameter, and the default entry is 1.0 times magnification. Here we need to calculate the initial magnification by ourselves.


If screenUtil is available, the following calculation shall distinguish between horizontal and vertical screens, which shall be used at the end of horizontal screen w. For vertical screen h. The padding of special-shaped screen does not distinguish between horizontal and vertical screens, and the system will change it automatically

  • 1. Display area of the whole seat table:

Screen height - padding up and down of special-shaped screen - height of bottom suspension frame in vertical screen (0 if horizontal suspension frame is not at the bottom) - height of title bar and height of some other layouts added by yourself. Screen width - left and right padding of special-shaped screen - width of the right suspension frame in horizontal screen (0 if the suspension frame is not on the right in vertical screen) - width of the navigation bar (the width of the navigation bar also needs to be dynamically calculated according to the magnification) - other layout widths added by yourself.

  • 2. Calculate the width, height and padding of the seat table item under the initial magnification (1.0), which is directly according to the design drawing.
  • 3. Obtain the x-axis and y-axis of the current seat table. That is, there are several seats in each row, and there are several rows of seats in total.
  • 4. The calculation assumes that the width and height of each item are displayed in the table of all seats.

Use the above 1 The width and height of the display area of the resulting seat table are divided by x and y of the seat table, respectively,

  • 5. Will 2 Divide the width of by 4 Width, i.e. the value SX to be scaled when the X axis is fully displayed,

Will 2 Divide your height by 4 Height, that is, if the Y axis is fully displayed, the value SY to be scaled,

  • 6. Compare SX and SY values and take the small value defaultS (as large as possible on the premise of complete display as possible)
  • 7. If defaultS is within the range of minScale and maxScale, take defaultS, otherwise take the boundary value of the interval.


Use the transformation controller to zoom the interactive viewer to defaultS

//Seating table
mainTransformationController.value = Matrix4.identity()..scale(defaultS);
//Navigation bar
fromTransformationController.value = Matrix4.identity()..scale(defaultS);

Note that the seat table and navigation bar should be zoomed here.

Scale dynamic margin

Finally, don't forget to scale various margin s that need dynamic calculation to the defaultS value.

If there is a horizontal and vertical screen switching effect, the initial magnification value is dynamically calculated during each horizontal and vertical screen switching. It should be noted that the dynamically calculated margin should be set as the initial value during each calculation (that is, the margin value when the zoom size is 1.0).

Sometimes you can't think of it, just look at the source code, and you'll be impressed immediately.

Advanced notes of Android advanced development system, latest interview review notes PDF, My GitHub

end of document

Your favorite collection is my greatest encouragement!
Welcome to follow me, share Android dry goods and exchange Android technology.
If you have any opinions on the article or any technical problems, please leave a message in the comment area for discussion!

Keywords: Android Flutter

Added by sharke on Tue, 11 Jan 2022 12:10:49 +0200