前言
业务场景为,在摄像机的播放画面上,按下手指左右上下滑动,摄像机跟随滑动。
由于播放页面使用了photo_view提供了画面图片的放大缩小和拖拽功能,导致和嵌套的要实现监听滑动的GestureDetector存在冲突的问题。
最终实现的解决方案为,在photo_vew默认状态下,让GestureDetector接收手指滑动事件,摄像机跟随旋转;在photo_vew放大状态下,GestureDetector不接收事件,让photo_vew处理放大图片的拖拽。比较好的解决了用户的使用场景问题
问题描述
onPanDown实现
首先,正常思路肯定是直接在Photoview的外部包裹GestureDetector,然后重写onPanDown,onPanUpdate方法,如下
body: GestureDetector(
onPanDown: (e) {
print('fuxiao:按下 $e');
},
onPanStart: (e) {
print('fuxiao:开始 $e');
},
onPanCancel: () {
print('fuxiao:取消');
},
onPanEnd: (e) {
print('fuxiao:结束');
},
onPanUpdate: (e) {
print('fuxiao:更新$e');
},
child: Container(
constraints: BoxConstraints.expand(
height: MediaQuery.of(context).size.height,
),
// child: Container(
// color: Colors.blue,
// width: 400,
// height: 400,
// )
child: PhotoView(
imageProvider: imageProvider,
loadingBuilder: loadingBuilder,
backgroundDecoration: backgroundDecoration,
minScale: minScale,
maxScale: maxScale,
initialScale: initialScale,
basePosition: basePosition,
filterQuality: filterQuality,
disableGestures: disableGestures,
errorBuilder: errorBuilder,
),
),
),
结果存在两个问题
1、onPanUpdate第一次滑动不触发,而触发onPanCancel方法,猜想是PhotoView处理事件导致的,上层树的事件取消,通过将上面注释代码打开验证,确实是PhotoView导致的问题
2、由于重写onPanDown方法,导致PhotoView,双指缩放画面功能失效
两个功能都不能正常工作,尝试解决这个问题,在网上浏览资料发现,GestureDetector并没有提供,双指的按下检测方法。
所以希望通过双指按下PhotoView处理事件,单指按下外层GestureDetector处理事件的方法行不通。
onHorizontalDragUpdate 实现
通过查看GestureDetector构造方法发现,提供了水平和竖直方向的检测方法
水平拖拽
- onHorizontalDragStart 水平移动开始
- onHorizontalDragUpdate 水平方向移动
- onHorizontalDragEnd 水平移动结束
垂直拖拽
- onVerticalDragStart 垂直移动开始
- onVerticalDragUpdate 垂直移动
- onVerticalDragEnd 垂直移动结束
通过重写上述方法,实际打印log发现,onHorizontalDragUpdate和onVerticalDragUpdate在单手移动屏幕时总会优先调用,代码如下
body: GestureDetector(
behavior: HitTestBehavior.opaque,
onHorizontalDragUpdate: (e) {
print('fuxiao:水平$e');
},
onVerticalDragUpdate: (e) {
print('fuxiao:垂直$e');
},
child: Container(
constraints: BoxConstraints.expand(
height: MediaQuery.of(context).size.height,
),
child: PhotoView(
imageProvider: imageProvider,
loadingBuilder: loadingBuilder,
backgroundDecoration: backgroundDecoration,
minScale: minScale,
maxScale: maxScale,
initialScale: initialScale,
basePosition: basePosition,
filterQuality: filterQuality,
disableGestures: disableGestures,
errorBuilder: errorBuilder,
),
),
),
手指在屏幕上滑动,打印log
I/flutter ( 5191): fuxiao:水平DragUpdateDetails(Offset(1.1, 0.0))
I/flutter ( 5191): fuxiao:水平DragUpdateDetails(Offset(1.1, 0.0))
I/flutter ( 5191): fuxiao:水平DragUpdateDetails(Offset(1.8, 0.0))
I/flutter ( 5191): fuxiao:垂直DragUpdateDetails(Offset(0.0, -2.5))
I/flutter ( 5191): fuxiao:垂直DragUpdateDetails(Offset(0.0, -2.9))
I/flutter ( 5191): fuxiao:垂直DragUpdateDetails(Offset(0.0, -2.5))
并且,删除了onPanDown的重写方法,PhotoView双指放大缩小的功能也正常了,这样我们的需求基本上已经可以实现了,只不过还有一点小小的优化:PhotoView在画面放大状态下,左右滑动是移动画面,而不是回调onHorizontalDragUpdate方法
image跟相册中预览图片的效果是相似的。
而如果重写onHorizontalDragUpdate,会导致画面移动失效
image可以看到,放大以后,按下水平手指,水平滑动画面没有跟随移动,事件被上层消费了
解决的思路就是,当PhotoView放大或缩小状态时,禁止onHorizontalDragUpdate的调用
冲突解决
可以猜想,PhotoView应该提供了缩放状态的监听,查看PhotoView的构造方法
PhotoView({
Key? key,
required this.imageProvider,
this.loadingBuilder,
this.backgroundDecoration,
this.gaplessPlayback = false,
this.heroAttributes,
/// 缩放状态监听
this.scaleStateChangedCallback,
this.enableRotation = false,
this.controller,
this.scaleStateController,
this.maxScale,
this.minScale,
this.initialScale,
this.basePosition,
this.scaleStateCycle,
this.onTapUp,
this.onTapDown,
this.onScaleEnd,
this.customSize,
this.gestureDetectorBehavior,
this.tightMode,
this.filterQuality,
this.disableGestures,
this.errorBuilder,
this.enablePanAlways,
}) : child = null,
childSize = null,
super(key: key);
/// A [Function] to be called whenever the scaleState changes, this happens when the user double taps the content ou start to pinch-in.
final ValueChanged<PhotoViewScaleState>? scaleStateChangedCallback;
/// A way to represent the step of the "doubletap gesture cycle" in which PhotoView is.
enum PhotoViewScaleState {
initial,
covering,
originalSize,
zoomedIn,
zoomedOut,
}
这样就拿到了,PhotoView画面状态变化回调,只需要做一个处理,当state为initial时,才允许GestureDetector监听滑动事件,其他情况,走PhotoView的拖拽
结论
最终代码如下
这里直接贴上photo_view中example里的common_example_wrapper.dart代码
import 'package:flutter/material.dart';
import 'package:photo_view/photo_view.dart';
class CommonExampleRouteWrapper extends StatefulWidget {
const CommonExampleRouteWrapper({
this.imageProvider,
this.loadingBuilder,
this.backgroundDecoration,
this.minScale,
this.maxScale,
this.initialScale,
this.basePosition = Alignment.center,
this.filterQuality = FilterQuality.none,
this.disableGestures,
this.errorBuilder,
this.scaleChangedListener
});
final ImageProvider? imageProvider;
final LoadingBuilder? loadingBuilder;
final BoxDecoration? backgroundDecoration;
final dynamic minScale;
final dynamic maxScale;
final dynamic initialScale;
final Alignment basePosition;
final FilterQuality filterQuality;
final bool? disableGestures;
final ImageErrorWidgetBuilder? errorBuilder;
final ValueChanged<PhotoViewScaleState>? scaleChangedListener;
@override
_CommonExampleRouteWrapperState createState() => _CommonExampleRouteWrapperState();
}
class _CommonExampleRouteWrapperState extends State<CommonExampleRouteWrapper> {
ValueChanged<PhotoViewScaleState>? _scaleChangedListener;
bool canZoomControl = true;
GestureDragUpdateCallback? updateCallback;
@override
void initState() {
updateCallback = (e) {
print('fuxiao: 滑动:$e');
};
if(widget.scaleChangedListener == null) {
_scaleChangedListener = (PhotoViewScaleState statue) {
print('fuxiao: 状态:$statue');
switch(statue) {
case PhotoViewScaleState.initial:
canZoomControl = true;
break;
default:
canZoomControl = false;
break;
}
setState(() {
});
};
} else {
_scaleChangedListener = widget.scaleChangedListener;
}
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: GestureDetector(
onHorizontalDragUpdate: canZoomControl ? updateCallback : null,
onVerticalDragUpdate: canZoomControl ? updateCallback : null,
child: Container(
constraints: BoxConstraints.expand(
height: MediaQuery.of(context).size.height,
),
child: PhotoView(
imageProvider: widget.imageProvider,
loadingBuilder: widget.loadingBuilder,
backgroundDecoration: widget.backgroundDecoration,
scaleStateChangedCallback: _scaleChangedListener,
minScale: widget.minScale,
maxScale: widget.maxScale,
initialScale: widget.initialScale,
basePosition: widget.basePosition,
filterQuality: widget.filterQuality,
disableGestures: widget.disableGestures,
errorBuilder: widget.errorBuilder,
),
),
),
);
}
}
最终效果如下
最终方案总结
本文主要分析解决了,photo_view嵌套GestureDetector滑动监听的冲突问题,暂时先记录问题的解决,原理有时间再写一篇分析。
大家如果有遇到相同情况,并且有更好的解决方案,欢迎在评论区交流分享,感谢~
网友评论