赞
踩
哈喽,我是老刘
几年前分享了一篇GestureDetector嵌套ListView的文章
Flutter多控件滑动事件联动(滑动冲突处理) - 知乎 (zhihu.com)
由于文章中只给出了关键部位的代码,另外使用的技术也偏底层
所以很多同学私信我要完整的源码
这里把原先的方案整理一下,另外也给出完整的代码供大家参考
我们先一点一点来看这个问题
首先大家一定在各种App中见过这个滑动关闭组件
就是手指向下滑动,组件跟随手指移动
手指抬起后组件滑出屏幕
我们先来实现这个组件,然后来讨论一下如果组件中的内容时一个ListView,要怎么处理
如果这个组件的内容时固定内容,不是ListView这样的可滚动组件
实现起来其实很简单
我这里直接放源码
- import 'package:flutter/material.dart';
-
- class CloseOnSwipeDownWidget extends StatefulWidget {
- final Widget child;
-
- const CloseOnSwipeDownWidget({
- Key? key,
- required this.child,
- }) : super(key: key);
-
- @override
- CloseOnSwipeDownWidgetState createState() => CloseOnSwipeDownWidgetState();
- }
-
- class CloseOnSwipeDownWidgetState extends State
- with TickerProviderStateMixin {
- double yOffset = 0.0;
- double initialPosition = 0.0;
- bool isAnimatingOut = false;
- int animTime = 0;
-
- @override
- void initState() {
- super.initState();
- }
-
- @override
- Widget build(BuildContext context) {
- return GestureDetector(
- onVerticalDragDown: (details) {
- initialPosition = details.globalPosition.dy;
- },
- onVerticalDragUpdate: (details) {
- double updatedPosition = details.globalPosition.dy;
- double deltaY = updatedPosition - initialPosition;
-
- animTime = 0;
-
- setState(() {
- yOffset = yOffset + deltaY;
- initialPosition = updatedPosition;
- });
- },
- onVerticalDragEnd: (details) {
- animTime = 300;
-
- if (yOffset > 200) {
- // 触发滑出动画
- _startSlideOutAnimation();
- } else {
- // 触发返回原始位置的动画
- _startReturnToOriginalPositionAnimation();
- }
- },
- child: Stack(
- children: [
- AnimatedPositioned( // 组件跟随手指位移,以及抬起手指后组件移动动画
- duration: Duration(milliseconds: animTime),
- curve: Curves.easeInOut,
- top: yOffset,
- left: 0,
- right: 0,
- child: widget.child,
- onEnd: () {
- if(isAnimatingOut) {
- Navigator.of(context).pop();
- }
- },
- ),
- ],
- ),
- );
- }
-
- // 开始滑出动画
- void _startSlideOutAnimation() {
- setState(() {
- isAnimatingOut = true;
- yOffset = MediaQuery.of(context).size.height;
- });
- }
-
- // 开始返回原始位置的动画
- void _startReturnToOriginalPositionAnimation() {
- setState(() {
- yOffset = 0.0;
- });
- }
- }
这里的原理很简单
就是通过GestureDetector检测用户的滑动行为
并且通过AnimatedPositioned将用户手指的每一段位移转换成整个组件的移动
并且在最终手指抬起时,通过AnimatedPositioned的动画效果让组件移出屏幕或者复位
我们写一个页面来使用这个组件
- class TestPage extends StatelessWidget {
- const TestPage({super.key});
-
- @override
- Widget build(BuildContext context) {
- return Scaffold(
- backgroundColor: Colors.white,
- body: CloseOnSwipeDownWidget(
- child: Column(
- children: [
- SizedBox(
- height: MediaQuery.of(context).size.height - 500,
- ), // 让组件内容在页面底部
- Container(
- height: 500,
- color: Colors.blue,
- alignment: Alignment.bottomCenter,
- child: const Center(
- child: Text('我是内容'),
- ),
- ),
- ],
- ),
- ),
- );
- }
- }
看一下效果
那么现在如果把传入的内容换成一个ListView
你会发现ListView内部的内容可以正常滑动
但是外部的GestureDetector无法响应用户的手势了
我们下面就来解决这个问题
首先我们要知道为什么嵌入ListView后GestureDetector会失效
这是Flutter的竞技场机制导致的
用户的一个滑动行为其实在底层时通过down、move和up三种事件完成的
当一个down事件出现后,如果手指按下的坐标位置有多个组件可以响应滑动事件
就是我们目前例子中的GestureDetector嵌套ListView的场景
Flutter框架会将这些组件都加入竞技场
然后通过一定的逻辑选择一个组件胜出
通常同类组件嵌套时最内层的组件胜出
胜出的组件会处理接下来的move和up事件,其它组件则不会继续处理这些事件了
在GestureDetector嵌套ListView的场景中
ListView最终胜出,所以后续的事件都交由ListView处理
而GestureDetector收不到后续的事件,也就不会响应用户的手势了
因此,我们解决这个问题的第一步就是要让GestureDetector在这种场景下也能收到后续的事件
其实要做到这一步很简单
GestureDetector真正处理用户手势事件的是内部的Recognizer
比如处理上下滑动的是VerticalDragGestureRecognizer
而Recognizer在竞技场失败后也可以单方面宣布自己胜出
这样即使在竞技场失败了,GestureDetector也能收到后续的手势事件
因此我们现定义一个单方面宣布胜出的Recognizer
- class _MyVerticalDragGestureRecognizer extends VerticalDragGestureRecognizer {
- @override
- void rejectGesture(int pointer) {
- // 单方面宣布自己胜出
- acceptGesture(pointer);
- }
- }
接下来,把这个Recognizer加入到GestureDetector中
这时就需要用到一个GestureDetector的底层组件RawGestureDetector
通过它我们可以自己指定需要的Recognizer
RawGestureDetector( gestures: { _MyVerticalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers< _MyVerticalDragGestureRecognizer>( () => _MyVerticalDragGestureRecognizer(), (_MyVerticalDragGestureRecognizer recognizer) { recognizer ..onStart = (DragStartDetails details) { } ..onUpdate = (DragUpdateDetails details) { } ..onEnd = (DragEndDetails details) { }; }), }, child: ... );
这其中的onStart、onUpdate和onEnd 方法
就对应了GestureDetector中的onVerticalDragDown、onVerticalDragUpdate和onVerticalDragEnd方法
好的,到目前为止,我们已经解决了竞技场造成的只有ListView能收到手势事件的问题
但是这样的话就会造成用户滑动,内外两层组件都在移动的问题
因此,接下来我们就来解决如何两个滑动组件如何相互配合
ListView是ScrollView的子类
所有的ScrollView都会在滚动过程中沿着组件树向上发出各种滚动状态变化的通知
通过监听这些通知事件,就可以判断ScrollView的滚动状态
- NotificationListener( // 监听内部ListView的滑动变化
- onNotification: (ScrollNotification notification) {
- if (notification is OverscrollNotification && notification.overscroll < 0) {
- // 用户向下滑动,ListView已经滑动到顶部,处理GestureDetector的滑动事件
- } else if (notification is ScrollUpdateNotification) {
- // 用户在ListView中执行滑动动作,关闭外部GestureDetector的滑动处理
- } else {
-
- }
-
- return false;
- },
- child: //ListView
- ),
好的,把这些组合起来,完整的代码如下
- import 'package:flutter/gestures.dart';
- import 'package:flutter/material.dart';
-
- class CloseOnSwipeDownWidget2 extends StatefulWidget {
- final Widget child;
-
- const CloseOnSwipeDownWidget2({
- Key? key,
- required this.child,
- }) : super(key: key);
-
- @override
- CloseOnSwipeDownWidget2State createState() => CloseOnSwipeDownWidget2State();
- }
-
- class CloseOnSwipeDownWidget2State extends State
- with TickerProviderStateMixin {
- double yOffset = 0.0;
- double initialPosition = 0.0;
- bool isAnimatingOut = false;
- int animTime = 0;
- bool needDrag = true;
-
- @override
- void initState() {
- super.initState();
- }
-
- @override
- Widget build(BuildContext context) {
- return RawGestureDetector(
- gestures: {
- _MyVerticalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<
- _MyVerticalDragGestureRecognizer>(
- () => _MyVerticalDragGestureRecognizer(),
- (_MyVerticalDragGestureRecognizer recognizer) {
- recognizer
- ..onStart = (DragStartDetails details) {
- initialPosition = details.globalPosition.dy;
- }
- ..onUpdate = (DragUpdateDetails details) {
- if (!needDrag) {
- return;
- }
- double updatedPosition = details.globalPosition.dy;
- double deltaY = updatedPosition - initialPosition;
-
- animTime = 0;
-
- setState(() {
- yOffset = yOffset + deltaY;
- initialPosition = updatedPosition;
- });
- }
- ..onEnd = (DragEndDetails details) {
- animTime = 300;
-
- if (yOffset > 200) {
- // 触发滑出动画
- _startSlideOutAnimation();
- } else {
- // 触发返回原始位置的动画
- _startReturnToOriginalPositionAnimation();
- }
- };
- }),
- },
- child: Stack(
- children: [
- AnimatedPositioned(
- duration: Duration(milliseconds: animTime),
- curve: Curves.easeInOut,
- top: yOffset,
- left: 0,
- right: 0,
- child: NotificationListener( // 监听内部ListView的滑动变化
- onNotification: (ScrollNotification notification) {
- if (notification is OverscrollNotification && notification.overscroll < 0) {
- // 用户向下滑动,ListView已经滑动到顶部,处理GestureDetector的滑动事件
- needDrag = true;
- } else if (notification is ScrollUpdateNotification) {
- // 用户在ListView中执行滑动动作,关闭外部GestureDetector的滑动处理
- needDrag = false;
- } else {
- }
-
- return false;
- },
- child: widget.child,
- ),
- onEnd: () {
- if (isAnimatingOut) {
- Navigator.of(context).pop();
- }
- },
- ),
- ],
- ),
- );
- }
-
- // 开始滑出动画
- void _startSlideOutAnimation() {
- setState(() {
- isAnimatingOut = true;
- yOffset = MediaQuery.of(context).size.height;
- });
- }
-
- // 开始返回原始位置的动画
- void _startReturnToOriginalPositionAnimation() {
- setState(() {
- yOffset = 0.0;
- });
- }
- }
-
- class _MyVerticalDragGestureRecognizer extends VerticalDragGestureRecognizer {
- bool needDrag = true;
-
- @override
- void rejectGesture(int pointer) {
- // 单方面宣布自己胜出
- acceptGesture(pointer);
- }
- }
简单来说就是通过needDrag来判断外部GestureDetector是否跟随用户手势移动
needDrag的值基于监听ListView的状态
当ListView已经滑动到顶部,就开始响应用户的手势动作
下面是使用这个组件的代码
- class TestPage2 extends StatelessWidget {
- const TestPage2({super.key});
-
- @override
- Widget build(BuildContext context) {
- return Scaffold(
- backgroundColor: Colors.white,
- body: CloseOnSwipeDownWidget2(
- child: Column(
- children: [
- SizedBox(
- height: MediaQuery.of(context).size.height - 500,
- ), // 让组件内容在页面底部
- Container(
- height: 500,
- color: Colors.blue,
- child: ListView.builder(
- itemCount: 20,
- itemBuilder: (context, index) {
- return ListTile(
- title: Text('index $index'),
- );
- }),
- ),
- ],
- ),
- ),
- );
- }
- }
实现效果如下
好了,关于手势组件嵌套的问题就先聊到这里
如果看到这里的同学有学习Flutter的兴趣,欢迎联系老刘,我们互相学习。
点击免费领老刘整理的《Flutter开发手册》,覆盖90%应用开发场景。
可以作为Flutter学习的知识地图。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。