哈喽,我是老刘
几年前分享了一篇GestureDetector嵌套ListView的文章
因为文章中只给出了关键部位的代码,别的运用的技术也偏底层
所以许多同学私信我要完好的源码
这儿把原先的方案整理一下,别的也给出完好的代码供大家参阅
咱们先一点一点来看这个问题
滑动封闭组件
首要大家一定在各种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无法呼应用户的手势了
咱们下面就来处理这个问题
GestureDetector嵌套ListView
首要咱们要知道为什么嵌入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的翻滚
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学习的知识地图。