0, preface

Flutter has been updated to 1.20 with a new component, the InteractiveViewer, which encapsulates and simplifies gesture interactions for movement and scaling.

mobile The zoom
Core components: GestureDetector, Transform, ClipRect, OverflowBoxCopy the code
  • This component has been added to FlutterUnit, welcome star~
1 2 3

1. Movement of child components

The property name type The default value Introduction to the
alignPanAxis bool false Drag along the shaft
boundaryMargin EdgeInsets EdgeInsets.zero Boundary edge moment
panEnabled bool true Whether it can be shifted
child Widget @required Child components

mobile The zoom
  • As shown on the left, the gray area is the upper area of the InteractiveViewer.
  • boundaryMarginIs a movable bounding margin. The default is EdgeInsets. Zero, which is fixed and cannot be moved
  • panEnabledYou can specify whether movement is supported or not. Default is true
  • alignPanAxisSpecifies whether to drag along the axis. Default is false(left image). When true, you can only drag along an axis (as shown on the right)

  • The sample code
class InteractiveViewerDemo extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 150,
      color: Colors.grey.withAlpha(33),
      child: InteractiveViewer(
// alignPanAxis: true,
        panEnabled: true,
        boundaryMargin: EdgeInsets.all(40.0),
        child: Container(
          child: Image.asset('assets/images/caver.jpeg'),),),); }}Copy the code

2. Child component scaling

The property name type The default value Introduction to the
maxScale double 2.5 Maximum magnification
minScale double 0.8 Minimum reduction factor
scaleEnabled bool true Scalable or not

  • scaleEnabledTo enable scaling, maxScale and minScale determine the multiples of scaling and zooming respectively.

It is estimated that 90% of the people have difficulty triggering the zoom effect after discussing it in the group yesterday. Alex gives the gesture trigger: first place one finger on it, then move the second finger as you go. [InteractiveViewer] Hard to scale when two fingers tap down at the same


  • The sample code
class InteractiveViewerDemo extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 150,
      color: Colors.grey.withAlpha(33),
      child: InteractiveViewer(
// alignPanAxis: true,
        boundaryMargin: EdgeInsets.all(40.0),
        maxScale: 2.5,
        minScale: 0.3,
        panEnabled: true,
        scaleEnabled: true,
        child: Container(
          child: Image.asset('assets/images/caver.jpeg'),),),); }}Copy the code

3. Constrained attribute

The property name type The default value Introduction to the
constrained bool true constrained


For the constrained property, a small demo is given in the source code. The table here can scroll up and down, slide left and right. The default for constrained is true, and when the subcomponent is larger than the InteractiveViewer region, set the constraint to false, and the subcomponent will be constrained indefinitely.

class InteractiveViewerDemo2 extends StatelessWidget {

  Widget build(BuildContext context) {
    const int _rowCount = 20;
    const int _columnCount = 4;

    return Container(
      width: 300,
      height: 200,
      child: InteractiveViewer(
        constrained: false,
        scaleEnabled: false,
        child: Table(
          columnWidths: <int, TableColumnWidth>{
            for (int column = 0; column < _columnCount; column += 1)
              column: const FixedColumnWidth(150.0),
          },
          children: buildRows(_rowCount, _columnCount),
        ),
      ),
    );
  }

  List<TableRow> buildRows(int rowCount, int columnCount) {
    return <TableRow>[
          for (int row = 0; row < rowCount; row += 1)
            TableRow(
              children: <Widget>[
                for (int column = 0; column < columnCount; column += 1)
                  Container(
                    margin: EdgeInsets.all(2),
                    height: 50,
                    alignment: Alignment.center,
                    color: _colorful(row,column),
                    child: Text('($row.$column) ',style: TextStyle(fontSize: 20,color: Colors.white),),
                  ),
              ],
            ),
        ];
  }

  final colors = [Colors.red,Colors.yellow,Colors.blue,Colors.green];
  final colors2 = [Colors.yellow,Colors.blue,Colors.green,Colors.red];

  _colorful(int row, int column ) => row % 2= =0? colors[column]:colors2[column]; }Copy the code

4. Callback events

The property name type The default value Introduction to the
onInteractionEnd GestureScaleEndCallback null End of interaction callback
onInteractionStart GestureScaleStartCallback null The interaction starts a callback
onInteractionUpdate GestureScaleUpdateCallback null Interactive update callback

  • onInteractionStart

When touched, onInteractionStart calls back to the ScaleStartDetails object

FocalPoint is the offset relative to the upper-left corner of the screen. LocalFocalPoint is the offset relative to the upper-left corner of the parent container region.

ScaleStartDetails(focalPoint: Offset(306.0, 168.7), localFocalPoint: Offset(50.4, 63.7))Copy the code

  • onInteractionUpdate

OnInteractionUpdate calls back to the ScaleUpdateDetails object when the finger is swiped

FocalPoint is the offset relative to the upper-left corner of the screen. LocalFocalPoint is the offset relative to the upper-left corner of the parent container region. Scale Scale. HorizontalScale horizontalScale. VerticalScale specifies the verticalScale. Rotation. —— indicates that rotation can be monitored

OnInteractionUpdate ---- ScaleUpdateDetails(focalPoint: Offset(6.4, 13.7), localFocalPoint: Offset(6.4, 13.7), scale: 1.0, horizontalScale: 1.0, verticalScale: 1.0, Rotation: 0.0)Copy the code

  • onInteractionEnd

As the finger slides, onInteractionEnd calls back to the ScaleEndDetails object

Velocity A quantity of velocity in both horizontal and vertical directions.

OnInteractionEnd -- ScaleEndDetails (velocity, velocity (0.0, 0.0))Copy the code

  • The sample code
class InteractiveViewerDemo extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 150,
      color: Colors.grey.withAlpha(33),
      child: InteractiveViewer(
        boundaryMargin: EdgeInsets.all(40.0),
        maxScale: 2.5,
        minScale: 0.3,
        panEnabled: true,
        scaleEnabled: true,
        child: Container(
          child: Image.asset('assets/images/caver.jpeg'),
        ),
        onInteractionStart: _onInteractionStart,
        onInteractionUpdate: _onInteractionUpdate,
        onInteractionEnd: _onInteractionEnd,
      ),
    );
  }

  void _onInteractionStart(ScaleStartDetails details) {
    print('onInteractionStart----' + details.toString());
  }

  void _onInteractionUpdate(ScaleUpdateDetails details) {
    print('onInteractionUpdate----' + details.toString());
  }

  void _onInteractionEnd(ScaleEndDetails details) {
    print('onInteractionEnd----' + details.toString());
  }
}
Copy the code

5. Transform the controllertransformationController

The property name type The default value Introduction to the
transformationController TransformationController null Change controller


TransformationController can be used for transformation control, such as reset and move through the above buttons

TransformationController is a Matrix4 generic ValueNotifier so can change TransformationController. The child component value for senior transformation operations, Matrix4 strong, You know…

class TransformationController extends ValueNotifier<Matrix4> {
Copy the code

  • The sample code
class InteractiveViewerDemo3 extends StatefulWidget { @override _InteractiveViewerDemo3State createState() => _InteractiveViewerDemo3State(); } class _InteractiveViewerDemo3State extends State<InteractiveViewerDemo3> with SingleTickerProviderStateMixin { final TransformationController _transformationController = TransformationController(); Animation<Matrix4> _animationReset; AnimationController _controllerReset; void _onAnimateReset() { _transformationController.value = _animationReset.value; if (! _controllerReset.isAnimating) { _animationReset? .removeListener(_onAnimateReset); _animationReset = null; _controllerReset.reset(); } } void _animateResetInitialize() { _controllerReset.reset(); _animationReset = Matrix4Tween( begin: _transformationController.value, end: Matrix4.identity(), ).animate(_controllerReset); _animationReset.addListener(_onAnimateReset); _controllerReset.forward(); } void _animateResetStop() { _controllerReset.stop(); _animationReset? .removeListener(_onAnimateReset); _animationReset = null; _controllerReset.reset(); } void _onInteractionStart(ScaleStartDetails details) { if (_controllerReset.status == AnimationStatus.forward) { _animateResetStop(); } } @override void initState() { super.initState(); _controllerReset = AnimationController( vsync: this, duration: const Duration(milliseconds: 400), ); } @override void dispose() { _controllerReset.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Wrap( direction: Axis.vertical, spacing: 10, crossAxisAlignment: WrapCrossAlignment.center, alignment: WrapAlignment.center, children: [ Container( height: 150, color: Colors.grey.withAlpha(33), child: InteractiveViewer( boundaryMargin: EdgeInsets. All (40), transformationController: _transformationController, minScale: 0.1, maxScale: 1.8, onInteractionStart: _onInteractionStart, child: Container(child: Image.asset('assets/images/caver.jpeg'), ), ), ), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _buildButton(), _buildButton2(), _buildButton3(), ], ) ], ); } Widget _buildButton() { return MaterialButton( child: Icon( Icons.refresh, color: Colors.white, ), color: Color.green, shape: CircleBorder(side: BorderSide(width: 2.0, color: color (0xFFFFDFDFDF)),), onPressed: _animateResetInitialize); } var _x = 0.0; Widget _buildButton2() { return MaterialButton( child: Icon( Icons.navigate_before, color: Colors.white, ), color: Color.green, shape: CircleBorder(side: BorderSide(width: 2.0, color: color (0xFFFFDFDFDF)),), onPressed: () { var temp = _transformationController.value.clone(); temp.translate(_x - 4); _transformationController.value = temp; }); } Widget _buildButton3() { return MaterialButton( child: Icon( Icons.navigate_next, color: Colors.white, ), color: Color.green, shape: CircleBorder(side: BorderSide(width: 2.0, color: color (0xFFFFDFDFDF)),), onPressed: () { var temp = _transformationController.value.clone(); temp.translate(_x + 4); _transformationController.value = temp; }); }}Copy the code

6.InteractiveViewer core source code

The Transform component transforms through the transformationController’s Matrix4 if constrained=false A layer of ClipRect+OverflowBox will be attached.

@override Widget build(BuildContext context) { Widget child = Transform( transform: _transformationController.value, child: KeyedSubtree( key: _childKey, child: widget.child, ), ); if (! Child = ClipRect(child: OverflowBox(alignment: alignment. TopLeft, minWidth: 0.0, minHeight: 0.0, maxWidth: double. Infinity, maxHeight: double. Infinity, 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. onScaleEnd: _onScaleEnd, onScaleStart: _onScaleStart, onScaleUpdate: _onScaleUpdate, child: child, ), ); }}Copy the code