【-Flutter组件篇- 】1.20新增组件InteractiveViewer

7,639 阅读4分钟

0、前言

Flutter更新到1.20,出了一个新组件InteractiveViewer,主要对移动、缩放的手势交互进行封装,简化使用。

移动缩放
家族: StatefulWidget
源码行数: 1207
依赖的核心组件: GestureDetector、Transform、ClipRect、OverflowBox
123

1、子组件的移动

属性名类型默认值简介
alignPanAxisboolfalse沿轴拖动
boundaryMarginEdgeInsetsEdgeInsets.zero边界边矩
panEnabledbooltrue是否可平移
childWidget@required子组件

移动缩放
  • 如左图,灰色区域是InteractiveViewer的上级区域。
  • boundaryMargin是可移动的限定边距。默认是EdgeInsets.zero,即被定死,不能移动
  • panEnabled可指定是否支持移动,默认为true
  • alignPanAxis 指定是否沿轴拖动,默认为false(左图)。当为true时,按下后只能沿某个轴向进行拖动(如右图)

  • 示例代码
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'),
        ),
      ),
    );
  }
}


2、子组件缩放

属性名类型默认值简介
maxScaledouble2.5最大放大倍数
minScaledouble0.8最小缩小倍数
scaleEnabledbooltrue是否可缩放

  • scaleEnabled为是否开启缩放,maxScale和minScale分别确定放大缩小的倍数限值。

估计百分之九十的人都很难触发缩放效果,昨天在群里讨论后。Alex给出了手势触发情况: 先把一只手指放上去,边移动边放第二只。 同时提出了一个issues: [InteractiveViewer] Hard to scale when two fingers tap down at the same


  • 示例代码
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'),
        ),
      ),
    );
  }
}

3、constrained属性

属性名类型默认值简介
constrainedbooltrue受约束的


关于constrained属性,源码中给了一个小demo。这里的表格可以上下滚动,左右滑动。constrained默认为true,当子组件比InteractiveViewer区域大时,将constrained设为false, 子组件将被赋予无限的约束。

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];
}

4、回调事件

属性名类型默认值简介
onInteractionEndGestureScaleEndCallbacknull交互结束回调
onInteractionStartGestureScaleStartCallbacknull交互开始回调
onInteractionUpdateGestureScaleUpdateCallbacknull交互更新回调

  • onInteractionStart

当触碰时,onInteractionStart 会回调ScaleStartDetails对象
focalPoint 是相对于屏幕左上角的偏移量。
localFocalPoint是相对于父容器区域左上角的偏移量。

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

  • onInteractionUpdate

当手指滑动时,onInteractionUpdate 会回调ScaleUpdateDetails对象
focalPoint 是相对于屏幕左上角的偏移量。
localFocalPoint是相对于父容器区域左上角的偏移量。
scale缩放量。
horizontalScale水平缩放量。
verticalScale竖直缩放量。
rotation旋转量。------ 这里说明能监听到旋转量

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
)

  • onInteractionEnd

当手指滑动时,onInteractionEnd 会回调ScaleEndDetails对象
velocity 水平和竖直方向的速度量。

onInteractionEnd----
ScaleEndDetails(velocity: Velocity(0.0, 0.0))

  • 示例代码
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());
  }
}

5.变换控制器 transformationController

属性名类型默认值简介
transformationControllerTransformationControllernull变化控制器


可以通过transformationController进行变换控制,如上面通过按钮进行复位、移动
TransformationController是一个Matrix4泛型的ValueNotifier 所以可以通过改变TransformationController.value来对子组件进行高级的变换操作,Matrix4的强大,你懂得...

class TransformationController extends ValueNotifier<Matrix4> {

  • 示例代码
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: Colors.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: Colors.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: Colors.green,
        shape: CircleBorder(
          side: BorderSide(width: 2.0, color: Color(0xFFFFDFDFDF)),
        ),
        onPressed: () {
          var temp = _transformationController.value.clone();
          temp.translate(_x + 4);
          _transformationController.value = temp;
        });
  }
}

6.InteractiveViewer的核心源码

Listener组件 + GestureDetector组件 实现手势交互相关功能及回调
Transform组件通过transformationController的Matrix4进行变换
如果 constrained=false 外会附加一层ClipRect+OverflowBox

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

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