0、前言
Flutter更新到1.20,出了一个新组件
InteractiveViewer
,主要对移动、缩放的手势交互进行封装,简化使用。
移动 | 缩放 ---|--- |
家族: StatefulWidget 源码行数: 1207 依赖的核心组件: GestureDetector、Transform、ClipRect、OverflowBox
- 此组件已加入FlutterUnit,欢迎star~
1 | 2 | 3 ---|---|--- | |
1、子组件的移动
属性名 | 类型| 默认值 | 简介 ---|---|---|--- alignPanAxis | bool | false| 沿轴拖动 boundaryMargin | EdgeInsets | EdgeInsets.zero |边界边矩 panEnabled | bool |true |是否可平移 child | Widget | @required |子组件
移动 | 缩放 ---|--- |
- 如左图,灰色区域是InteractiveViewer的上级区域。
boundaryMargin
是可移动的限定边距。默认是EdgeInsets.zero,即被定死,不能移动panEnabled
可指定是否支持移动,默认为truealignPanAxis
指定是否沿轴拖动,默认为false(左图)。当为true时,按下后只能沿某个轴向进行拖动(如右图)
示例代码
```dart 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、子组件缩放
属性名 | 类型| 默认值 | 简介 ---|---|---|--- maxScale | double | 2.5 | 最大放大倍数 minScale | double | 0.8 | 最小缩小倍数 scaleEnabled | bool |true |是否可缩放
scaleEnabled
为是否开启缩放,maxScale和minScale分别确定放大缩小的倍数限值。
估计百分之九十的人都很难触发缩放效果,昨天在群里讨论后。Alex给出了手势触发情况: 先把一只手指放上去,边移动边放第二只。
同时提出了一个issues: [InteractiveViewer] Hard to scale when two fingers tap down at the same
示例代码
```dart 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属性
属性名 | 类型| 默认值 | 简介 ---|---|---|--- constrained | bool |true |受约束的
关于constrained属性,源码中给了一个小demo。这里的表格可以
上下滚动,左右滑动
。constrained默认为true,当子组件比InteractiveViewer区域大时,将constrained设为false, 子组件将被赋予无限的约束。
```dart 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 buildRows(int rowCount, int columnCount) { return [ for (int row = 0; row < rowCount; row += 1) TableRow( children: [ 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、回调事件
属性名 | 类型| 默认值 | 简介 ---|---|---|--- onInteractionEnd | GestureScaleEndCallback | null | 交互结束回调 onInteractionStart | GestureScaleStartCallback | null | 交互开始回调 onInteractionUpdate | GestureScaleUpdateCallback | null | 交互更新回调
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
属性名 | 类型| 默认值 | 简介 ---|---|---|--- transformationController | TransformationController |null |变化控制器
可以通过
transformationController
进行变换控制,如上面通过按钮进行复位、移动TransformationController
是一个Matrix4
泛型的ValueNotifier 所以可以通过改变TransformationController.value来对子组件进行高级的变换操作,Matrix4
的强大,你懂得...
class TransformationController extends ValueNotifier<Matrix4> {
示例代码
``` class InteractiveViewerDemo3 extends StatefulWidget { @override _InteractiveViewerDemo3State createState() => _InteractiveViewerDemo3State(); }
class _InteractiveViewerDemo3State extends State with SingleTickerProviderStateMixin { final TransformationController _transformationController = TransformationController(); Animation _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.navigatebefore, 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.navigatenext, 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,
),
);
} }
```