Flutter 小技巧之霓虹灯文本的「故障」效果的实现
Posted 恋猫de小郭
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Flutter 小技巧之霓虹灯文本的「故障」效果的实现相关的知识,希望对你有一定的参考价值。
如下图所示,最近通过群友的问题在 codepen.io 上看到了一个文本「抽动」的动画实现,看起来就像是生活中常见的「霓虹灯招牌」故障时的「抽动」效果,而本篇的目标通过「抄袭」这个实现,帮助大家理解 Flutter 里的一些实现小技巧。
这个效果在 codepen 上是通过 CSS 实现的,实现思路 codepen 上的 Glitch Walkthrough 大致有提示,但是 Flutter 没有强大的 CSS,那么如何将它「复刻」到 Flutter 上就是本篇的核心要点。
不得不说 CSS 很强大,要在 Flutter 上实现类似的效果还是比较「折腾」。
而要在 Flutter 上实现类似 Glitch Walkthrough 的效果,大致上我们需要处理:
- 类似霓虹灯效果的文本
- 文本内容撕裂的效果
- 文本变形闪动的效果
那么接下来我们就按照这个流程来实现一个 Flutter 上的 Glitch Walkthrough 。
霓虹灯文本
这一步其实相对简单,Flutter 的 TextStyle
提供了 shadows
配置,通过它可以快速实现一个「会发光」的文本。
我们这里通过两个 Shadow
来实现「发光」的视觉效果,核心就是利用 Shadow
的 blurRadius
来让背景出现一定程度的模糊发散,然后两个 Shadow
形成不一样的颜色深度和发散效果,从而达到看起来「发亮」的效果。
如下图是没有填充文本颜色时
Shadow
的效果。
最后,如下代码所示,我们只需要通过 foreground
给文本补充下颜色,就可以看到如下图所示的类似「霓虹灯」效果的文本。
当然这里你不想用
foreground
,只用简单的color
也可以。
Text(
widget.text,
style: TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
foreground: Paint()
..style = PaintingStyle.fill
..strokeWidth = 5
..color = Colors.white,
shadows: [
Shadow(
blurRadius: 10,
color: Colors.white,
offset: Offset(0, 0),
),
Shadow(
blurRadius: 20,
color: Colors.white30,
offset: Offset(0, 0),
),
],
),
)
这里提个题外话,其实类似的思路用在图片上也可以实现「发光」的效果,如下代码所示,通过 Stack 嵌套两个 Image
,然后中间通过 BackdropFilter
的 ImageFilter
做一层模糊,让底下的图片模糊后发散产生类似「发光」的效果。
var child = Image.asset(
'static/test_logo.png',
width: 250,
);
return Stack(
children: [
child,
Positioned.fill(
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: blurRadius,
sigmaY: blurRadius,
),
child: Container(color: Colors.transparent),
),
),
child,
],
)
);
如下图所示,图片最终可以通过自己的色彩产生类似「发光」的效果,当然这部分只是额外的拓展内容,和我们要实现的效果无关。
文本撕裂
这部分可以说是需求效果的核心,这里我们需要用到 ClipPath
和 Polygon
,通过 Polygon
来实现随机的多边形路径,然后利用 ClipPath
对文本内容进行随机的路径裁剪。
虽然说用 Polygon
, 但是 Flutter 官方并没有直接提供类似前端 CSS 的 Polygon
多边形 API 支持,但是社区总有「好心人」,我们可以直接使用 Flutter 上类似的第三方库: polygon: ^0.1.0
。
简单说
Polygon
就是按照 step 对Path
的moveTo
和quadraticBezierTo
等 API 进行了封装。
Flutter 上的 Polygon
取值范围是 -1 ~ 1 ,也就是按照比例决定位置,比如 - 1 就是起始点, 1 就是最大宽高, 更具体如下面的代码所示,这里利用 Polygon
添加了三个点,最终这三个点形成的 Path 会绘制出一个三角形。
List<Offset> generatePoint()
List<Offset> points = [];
points.add(Offset(-1, -1));
points.add(Offset(-1, 0));
points.add(Offset(0, -1));
return points;
如下代码所示,那如果如果 point 的数量多了,就可以形成一系列不规则的形状,比如下面代码随机添加了 60 个点的位置,可以看到此时屏幕上的白色 Container
被裁剪成「凌乱」的形状。
List<Offset> generatePoint()
List<Offset> points = [];
points.add(Offset(-1.00, -0.76));
points.add(Offset(0.06, -0.76));
points.add(Offset(0.06, -0.48));
points.add(Offset(-0.50, -0.48));
points.add(Offset(-0.50, 0.72));
points.add(Offset(-0.38, 0.72));
points.add(Offset(-0.38, -1.00));
points.add(Offset(0.06, -1.00));
points.add(Offset(0.06, 0.67));
points.add(Offset(0.84, 0.67));
points.add(Offset(0.84, 0.63));
points.add(Offset(0.39, 0.63));
points.add(Offset(0.39, -0.42));
points.add(Offset(0.56, -0.42));
points.add(Offset(0.56, 0.30));
points.add(Offset(0.37, 0.30));
points.add(Offset(0.37, 0.32));
points.add(Offset(0.54, 0.32));
points.add(Offset(0.54, -0.09));
points.add(Offset(0.70, -0.09));
points.add(Offset(0.70, -0.48));
points.add(Offset(0.94, -0.48));
points.add(Offset(0.94, -0.43));
points.add(Offset(0.67, -0.43));
points.add(Offset(0.67, -0.31));
points.add(Offset(0.08, -0.31));
points.add(Offset(0.08, 0.78));
points.add(Offset(-0.40, 0.78));
points.add(Offset(-0.40, 0.15));
points.add(Offset(0.65, 0.15));
points.add(Offset(0.65, 0.00));
points.add(Offset(0.36, 0.00));
points.add(Offset(0.36, -0.28));
points.add(Offset(0.24, -0.28));
points.add(Offset(0.24, -0.80));
points.add(Offset(-0.76, -0.80));
points.add(Offset(-0.76, -0.31));
points.add(Offset(0.19, -0.31));
points.add(Offset(0.19, 0.13));
points.add(Offset(0.96, 0.13));
points.add(Offset(0.96, 0.65));
points.add(Offset(-0.80, 0.65));
points.add(Offset(-0.80, 0.06));
points.add(Offset(0.82, 0.06));
points.add(Offset(0.82, 0.67));
points.add(Offset(0.60, 0.67));
points.add(Offset(0.60, 0.65));
points.add(Offset(-0.19, 0.65));
return points;
如果这时候把白色 Container
换成文本内容,那么我们就可以如下图所示的效果,看起来像不像一帧状态下文本的「错乱」效果?后面我们只需要每次生成一帧这样的 Path ,就可以实现文本动态「撕裂」的需求。
我们只需要把这个实现做成随机输出,然后每次生成一个
Path
就可以了。
如下代码所示,我们通过 generatePoint
方法,每次随机生成 60 个点,然后将这些点通过 computePath
转化为 Path,然后继承 CustomClipper
配置到 getClip
方法里,在需要的时候(tear
)对 child 按 Path 进行裁剪。
注意这里的
i % 2
,为的是让上次的 x 或者 y 可以是同一个位置,在连接上能连续。
class RandomTearingClipper extends CustomClipper<Path>
bool tear;
RandomTearingClipper(this.tear);
List<Offset> generatePoint()
List<Offset> points = [];
var x = -1.0;
var y = -1.0;
for (var i = 0; i < 60; i++)
if (i % 2 != 0)
x = Random().nextDouble() * (Random().nextBool() ? -1 : 1);
else
y = Random().nextDouble() * (Random().nextBool() ? -1 : 1);
points.add(Offset(x, y));
return points;
Path getClip(Size size)
var points = generatePoint();
var polygon = Polygon(points);
if (tear)
return polygon.computePath(rect: Offset.zero & size);
else
return Path()..addRect(Offset.zero & size);
bool shouldReclip(RandomTearingClipper oldClipper) => true;
接着,我们只需要设置一个定期器,然后将前面的「霓虹灯文本」和「故障裁剪效果」配置到 ClipPath
上,如下图所示,我们就可以看到文本的随机撕裂效果。
timer = Timer.periodic(Duration(milliseconds: 400), (timer)
tearFunction();
);
return ClipPath(
child: Center(
child: Text(
widget.text,
style: TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
foreground: Paint()
..style = PaintingStyle.fill
..strokeWidth = 1
..color = Colors.white,
shadows: [
Shadow(
blurRadius: 10,
color: Colors.white,
offset: Offset(0, 0),
),
Shadow(
blurRadius: 20,
color: Colors.white30,
offset: Offset(0, 0),
),
],
),
),
),
clipper: RandomTearingClipper(tear),
);
此时看起来还不够形象。
变形闪动
为了达到我们预期的效果,最后我们还需要做一些特殊处理,比如再实现两个形状、颜色和位置不一样「霓虹灯文本」,为的就是实现「变形和闪动」的效果替换。
比如如下代码所示,通过 ShaderMask
可以实现一个渐变效果的的文本,这是用来在闪动的时候,提供一个短暂替换和色彩加深的作用。
ShaderMask(
blendMode: BlendMode.srcATop,
shaderCallback: (bounds)
return LinearGradient(
colors: [Colors.blue, Colors.green, Colors.red],
stops: [0.0, 0.5, 1.0],
).createShader(bounds);
,
child:
类似的我们还可以实现一个「变形」的文本,在之前的白色「霓虹灯」文本基础上增加「斜体」和「颜色变淡」等处理,用来闪动的时候提供「变形」的作用。
最后我们再将之前的 ClipPath
添加到它们上面,并增加一个 transform
实现文本四周随意移动的效果支持,如下图所示,此时的效果已经肉眼可见的接近我们的需求。
transform:
Matrix4.translationValues(randomPosition(4), randomPosition(4), 0),
double randomPosition(position)
return Random().nextInt(position).toDouble() *
(Random().nextBool() ? -1 : 1);
最后我们将这几个文本效果用 Stack
组合起来,然后再在定时器里不停去切换「故障」和「正常」的文本状态,并且随机选择展示不同的 「故障」状态。
timer = Timer.periodic(Duration(milliseconds: 400), (timer)
tearFunction();
);
timer2 = Timer.periodic(Duration(milliseconds: 600), (timer)
tearFunction();
);
tearFunction()
count++;
tear = count % 2 == 0;
if (tear == true)
setState(() );
Future.delayed(Duration(milliseconds: 150), ()
setState(()
tear = false;
);
);
Widget build(BuildContext context)
var status = Random().nextInt(3);
return Stack(
children: [
if (tear && (status == 1)) renderTearText1(RandomTearingClipper(tear)),
if (!tear || (tear && status != 2))
renderMainText(RandomTearingClipper(tear)),
if (tear && status == 2) renderTearText2(RandomTearingClipper(tear)),
],
);
最终效果如下图所示,这里还额外对后面两个文本做了一个 ClipRect
处理,闪动切换的时候只展示部分内容,这样在「故障」时的切换不会显得太过生硬,可以看到简单的 CSS 效
Flutter 小技巧之 ListView 和 PageView 的各种花式嵌套
这次的 Flutter 小技巧是 ListView
和 PageView
的花式嵌套,不同 Scrollable
的嵌套冲突问题相信大家不会陌生,今天就通过 ListView
和 PageView
的三种嵌套模式带大家收获一些不一样的小技巧。
正常嵌套
最常见的嵌套应该就是横向 PageView
加纵向 ListView
的组合,一般情况下这个组合不会有什么问题,除非你硬是要斜着滑。
最近刚好遇到好几个人同时在问:“斜滑 ListView
容易切换到 PageView
滑动” 的问题,如下 GIF 所示,当用户在滑动 ListView
时,滑动角度带上倾斜之后,可能就会导致滑动的是 PageView
而不是 ListView
。
虽然从我个人体验上并不觉得这是个问题,但是如果产品硬是要你修改,难道要自己重写 PageView
的手势响应吗?
我们简单看一下,不管是 PageView
还是 ListView
它们的滑动效果都来自于 Scrollable
,而 Scrollable
内部针对不同方向的响应,是通过 RawGestureDetector
完成:
VerticalDragGestureRecognizer
处理垂直方向的手势HorizontalDragGestureRecognizer
处理水平方向的手势
所以简单看它们响应的判断逻辑,可以看到一个很有趣的方法 computeHitSlop
: 根据 pointer 的类型确定当然命中需要的最小像素,触摸默认是 kTouchSlop (18.0)。
看到这你有没有灵光一闪:如果我们把 PageView
的 touchSlop 修改了,是不是就可以调整它响应的灵敏度? 恰好在 computeHitSlop
方法里,它可以通过 DeviceGestureSettings
来配置,而 DeviceGestureSettings
来自于 MediaQuery
,所以如下代码所示:
body: MediaQuery(
///调高 touchSlop 到 50 ,这样 pageview 滑动可能有点点影响,
///但是大概率处理了斜着滑动触发的问题
data: MediaQuery.of(context).copyWith(
gestureSettings: DeviceGestureSettings(
touchSlop: 50,
)),
child: PageView(
scrollDirection: Axis.horizontal,
pageSnapping: true,
children: [
HandlerListView(),
HandlerListView(),
],
),
),
复制代码
小技巧一:通过嵌套一个 MediaQuery
,然后调整 gestureSettings
的 touchSlop
从而修改 PageView
的灵明度 ,另外不要忘记,还需要把 ListView
的 touchSlop
切换会默认 的 kTouchSlop
:
class HandlerListView extends StatefulWidget
@override
_MyListViewState createState() => _MyListViewState();
class _MyListViewState extends State<HandlerListView>
@override
Widget build(BuildContext context)
return MediaQuery(
///这里 touchSlop 需要调回默认
data: MediaQuery.of(context).copyWith(
gestureSettings: DeviceGestureSettings(
touchSlop: kTouchSlop,
)),
child: ListView.separated(
itemCount: 15,
itemBuilder: (context, index)
return ListTile(
title: Text('Item $index'),
);
,
separatorBuilder: (context, index)
return const Divider(
thickness: 3,
);
,
),
);
复制代码
最后我们看一下效果,如下 GIF 所示,现在就算你斜着滑动,也很触发 PageView
的水平滑动,只有横向移动时才会触发 PageView
的手势,当然, 如果要说这个粗暴的写法有什么问题的话,大概就是降低了 PageView
响应的灵敏度。
同方向 PageView 嵌套 ListView
介绍完常规使用,接着来点不一样的,在垂直切换的 PageView
里嵌套垂直滚动的 ListView
, 你第一感觉是不是觉得不靠谱,为什么会有这样的场景?
对于产品来说,他们不会考虑你如何实现的问题,他们只会拍着脑袋说淘宝可以,为什么你不行,所以如果是你,你会怎么做?
而关于这个需求,社区目前讨论的结果是:把 PageView
和 ListView
的滑动禁用,然后通过 RawGestureDetector
自己管理。
如果对实现逻辑分析没兴趣,可以直接看本小节末尾的 源码链接 。
看到自己管理先不要慌,虽然要自己实现 PageView
和 ListView
的手势分发,但是其实并不需要重写 PageView
和 ListView
,我们可以复用它们的 Darg
响应逻辑,如下代码所示:
- 通过
NeverScrollableScrollPhysics
禁止了PageView
和ListView
的滚动效果 - 通过顶部
RawGestureDetector
的VerticalDragGestureRecognizer
自己管理手势事件 - 配置
PageController
和ScrollController
用于获取状态
body: RawGestureDetector(
gestures: <Type, GestureRecognizerFactory>
VerticalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<
VerticalDragGestureRecognizer>(
() => VerticalDragGestureRecognizer(),
(VerticalDragGestureRecognizer instance)
instance
..onStart = _handleDragStart
..onUpdate = _handleDragUpdate
..onEnd = _handleDragEnd
..onCancel = _handleDragCancel;
)
,
behavior: HitTestBehavior.opaque,
child: PageView(
controller: _pageController,
scrollDirection: Axis.vertical,
///屏蔽默认的滑动响应
physics: const NeverScrollableScrollPhysics(),
children: [
ListView.builder(
controller: _listScrollController,
///屏蔽默认的滑动响应
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index)
return ListTile(title: Text('List Item $index'));
,
itemCount: 30,
),
Container(
color: Colors.green,
child: Center(
child: Text(
'Page View',
style: TextStyle(fontSize: 50),
),
),
)
],
),
),
接着我们看 _handleDragStart
实现,如下代码所示,在产生手势 details
时,我们主要判断:
- 通过
ScrollController
判断ListView
是否可见 - 判断触摸位置是否在
ListIView
范围内 - 根据状态判断通过哪个
Controller
去生产Drag
对象,用于响应后续的滑动事件
void _handleDragStart(DragStartDetails details)
///先判断 Listview 是否可见或者可以调用
///一般不可见时 hasClients false ,因为 PageView 也没有 keepAlive
if (_listScrollController?.hasClients == true &&
_listScrollController?.position.context.storageContext != null)
///获取 ListView 的 renderBox
final RenderBox? renderBox = _listScrollController
?.position.context.storageContext
.findRenderObject() as RenderBox;
///判断触摸的位置是否在 ListView 内
///不在范围内一般是因为 ListView 已经滑动上去了,坐标位置和触摸位置不一致
if (renderBox?.paintBounds
.shift(renderBox.localToGlobal(Offset.zero))
.contains(details.globalPosition) ==
true)
_activeScrollController = _listScrollController;
_drag = _activeScrollController?.position.drag(details, _disposeDrag);
return;
///这时候就可以认为是 PageView 需要滑动
_activeScrollController = _pageController;
_drag = _pageController?.position.drag(details, _disposeDrag);
前面我们主要在触摸开始时,判断需要响应的对象时 ListView
还是 PageView
,然后通过 _activeScrollController
保存当然响应对象,并且通过 Controller 生成用于响应手势信息的 Drag
对象。
简单说:滑动事件发生时,默认会建立一个
Drag
用于处理后续的滑动事件,Drag
会对原始事件进行加工之后再给到ScrollPosition
去触发后续滑动效果。
接着在 _handleDragUpdate
方法里,主要是判断响应是不是需要切换到 PageView
:
- 如果不需要就继续用前面得到的
_drag?.update(details)
响应ListView
滚动 - 如果需要就通过
_pageController
切换新的_drag
对象用于响应
void _handleDragUpdate(DragUpdateDetails details)
if (_activeScrollController == _listScrollController &&
///手指向上移动,也就是快要显示出底部 PageView
details.primaryDelta! < 0 &&
///到了底部,切换到 PageView
_activeScrollController?.position.pixels ==
_activeScrollController?.position.maxScrollExtent)
///切换相应的控制器
_activeScrollController = _pageController;
_drag?.cancel();
///参考 Scrollable 里
///因为是切换控制器,也就是要更新 Drag
///拖拽流程要切换到 PageView 里,所以需要 DragStartDetails
///所以需要把 DragUpdateDetails 变成 DragStartDetails
///提取出 PageView 里的 Drag 相应 details
_drag = _pageController?.position.drag(
DragStartDetails(
globalPosition: details.globalPosition,
localPosition: details.localPosition),
_disposeDrag);
_drag?.update(details);
这里有个小知识点:如上代码所示,我们可以简单通过
details.primaryDelta
判断滑动方向和移动的是否是主轴
最后如下 GIF 所示,可以看到 PageView
嵌套 ListView
同方向滑动可以正常运行了,但是目前还有个两个小问题,从图示可以看到:
- 在切换之后
ListView
的位置没有保存下来 - 产品要求去除
ListView
的边缘溢出效果
所以我们需要对 ListView
做一个 KeepAlive ,然后用简单的方法去除 Android 边缘滑动的 Material 效果:
- 通过
with AutomaticKeepAliveClientMixin
让ListView
在切换之后也保持滑动位置 - 通过
ScrollConfiguration.of(context).copyWith(overscroll: false)
快速去除 Scrollable 的边缘 Material 效果
child: PageView(
controller: _pageController,
scrollDirection: Axis.vertical,
///去掉 Android 上默认的边缘拖拽效果
scrollBehavior:
ScrollConfiguration.of(context).copyWith(overscroll: false),
///对 PageView 里的 ListView 做 KeepAlive 记住位置
class KeepAliveListView extends StatefulWidget
final ScrollController? listScrollController;
final int itemCount;
KeepAliveListView(
required this.listScrollController,
required this.itemCount,
);
@override
KeepAliveListViewState createState() => KeepAliveListViewState();
class KeepAliveListViewState extends State<KeepAliveListView>
with AutomaticKeepAliveClientMixin
@override
Widget build(BuildContext context)
super.build(context);
return ListView.builder(
controller: widget.listScrollController,
///屏蔽默认的滑动响应
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index)
return ListTile(title: Text('List Item $index'));
,
itemCount: widget.itemCount,
);
@override
bool get wantKeepAlive => true;
所以这里我们有解锁了另外一个小技巧:通过 ScrollConfiguration.of(context).copyWith(overscroll: false)
快速去除 Android 滑动到边缘的 Material 2效果,为什么说 Material2, 因为 Material3 上变了,具体可见: Flutter 3 下的 ThemeExtensions 和 Material3 。
本小节源码可见: github.com/CarGuo/gsy_…
同方向 ListView 嵌套 PageView
那还有没有更非常规的?答案是肯定的,毕竟产品的小脑袋,怎么会想不到在垂直滑动的 ListView
里嵌套垂直切换的 PageView
这种需求。
有了前面的思路,其实实现这个逻辑也是异曲同工:把 PageView
和 ListView
的滑动禁用,然后通过 RawGestureDetector
自己管理,不同的就是手势方法分发的差异。
RawGestureDetector(
gestures: <Type, GestureRecognizerFactory>
VerticalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<
VerticalDragGestureRecognizer>(
() => VerticalDragGestureRecognizer(),
(VerticalDragGestureRecognizer instance)
instance
..onStart = _handleDragStart
..onUpdate = _handleDragUpdate
..onEnd = _handleDragEnd
..onCancel = _handleDragCancel;
)
,
behavior: HitTestBehavior.opaque,
child: ListView.builder(
///屏蔽默认的滑动响应
physics: NeverScrollableScrollPhysics(),
controller: _listScrollController,
itemCount: 5,
itemBuilder: (context, index)
if (index == 0)
return Container(
height: 300,
child: KeepAlivePageView(
pageController: _pageController,
itemCount: itemCount,
),
);
return Container(
height: 300,
color: Colors.greenAccent,
child: Center(
child: Text(
"Item $index",
style: TextStyle(fontSize: 40, color: Colors.blue),
),
));
),
)
同样是在 _handleDragStart
方法里,这里首先需要判断:
ListView
如果已经滑动过,就不响应顶部PageView
的事件- 如果此时
ListView
处于顶部未滑动,判断手势位置是否在PageView
里,如果是响应PageView
的事件
void _handleDragStart(DragStartDetails details)
///只要不是顶部,就不响应 PageView 的滑动
///所以这个判断只支持垂直 PageView 在 ListView 的顶部
if (_listScrollController.offset > 0)
_activeScrollController = _listScrollController;
_drag = _listScrollController.position.drag(details, _disposeDrag);
return;
///此时处于 ListView 的顶部
if (_pageController.hasClients)
///获取 PageView
final RenderBox renderBox =
_pageController.position.context.storageContext.findRenderObject()
as RenderBox;
///判断触摸范围是不是在 PageView
final isDragPageView = renderBox.paintBounds
.shift(renderBox.localToGlobal(Offset.zero))
.contains(details.globalPosition);
///如果在 PageView 里就切换到 PageView
if (isDragPageView)
_activeScrollController = _pageController;
_drag = _activeScrollController.position.drag(details, _disposeDrag);
return;
///不在 PageView 里就继续响应 ListView
_activeScrollController = _listScrollController;
_drag = _listScrollController.position.drag(details, _disposeDrag);
接着在 _handleDragUpdate
方法里,判断如果 PageView
已经滑动到最后一页,也将滑动事件切换到 ListView
void _handleDragUpdate(DragUpdateDetails details)
var scrollDirection = _activeScrollController.position.userScrollDirection;
///判断此时响应的如果还是 _pageController,是不是到了最后一页
if (_activeScrollController == _pageController &&
scrollDirection == ScrollDirection.reverse &&
///是不是到最后一页了,到最后一页就切换回 pageController
(_pageController.page != null &&
_pageController.page! >= (itemCount - 1)))
///切换回 ListView
_activeScrollController = _listScrollController;
_drag?.cancel();
_drag = _listScrollController.position.drag(
DragStartDetails(
globalPosition: details.globalPosition,
localPosition: details.localPosition),
_disposeDrag);
_drag?.update(details);
当然,同样还有 KeepAlive 和去除列表 Material 边缘效果,最后运行效果如下 GIF 所示。
本小节源码可见:github.com/CarGuo/gsy_…
最后再补充一个小技巧:如果你需要 Flutter 打印手势竞技的过程,可以配置 debugPrintGestureArenaDiagnostics = true;
来让 Flutter 输出手势竞技的处理过程。
import 'package:flutter/gestures.dart';
void main()
debugPrintGestureArenaDiagnostics = true;
runApp(MyApp());
复制代码
最后
最后总结一下,本篇介绍了如何通过 Darg
解决各种因为嵌套而导致的手势冲突,相信大家也知道了如何利用 Controller
和 Darg
来快速自定义一些滑动需求,例如 ListView
联动 ListView
的差量滑动效果:
///listView 联动 listView
class ListViewLinkListView extends StatefulWidget
@override
_ListViewLinkListViewState createState() => _ListViewLinkListViewState();
class _ListViewLinkListViewState extends State<ListViewLinkListView>
ScrollController _primaryScrollController = ScrollController();
ScrollController _subScrollController = ScrollController();
Drag? _primaryDrag;
Drag? _subDrag;
@override
void initState()
super.initState();
@override
void dispose()
_primaryScrollController.dispose();
_subScrollController.dispose();
super.dispose();
void _handleDragStart(DragStartDetails details)
_primaryDrag =
_primaryScrollController.position.drag(details, _disposePrimaryDrag);
_subDrag = _subScrollController.position.drag(details, _disposeSubDrag);
void _handleDragUpdate(DragUpdateDetails details)
_primaryDrag?.update(details);
///除以10实现差量效果
_subDrag?.update(DragUpdateDetails(
sourceTimeStamp: details.sourceTimeStamp,
delta: details.delta / 30,
primaryDelta: (details.primaryDelta ?? 0) / 30,
globalPosition: details.globalPosition,
localPosition: details.localPosition));
void _handleDragEnd(DragEndDetails details)
_primaryDrag?.end(details);
_subDrag?.end(details);
void _handleDragCancel()
_primaryDrag?.cancel();
_subDrag?.cancel();
void _disposePrimaryDrag()
_primaryDrag = null;
void _disposeSubDrag()
_subDrag = null;
@override
Widget build(BuildContext context)
return Scaffold(
appBar: AppBar(
title: Text("ListViewLinkListView"),
),
body: RawGestureDetector(
gestures: <Type, GestureRecognizerFactory>
VerticalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<
VerticalDragGestureRecognizer>(
() => VerticalDragGestureRecognizer(),
(VerticalDragGestureRecognizer instance)
instance
..onStart = _handleDragStart
..onUpdate = _handleDragUpdate
..onEnd = _handleDragEnd
..onCancel = _handleDragCancel;
)
,
behavior: HitTestBehavior.opaque,
child: ScrollConfiguration(
///去掉 Android 上默认的边缘拖拽效果
behavior:
ScrollConfiguration.of(context).copyWith(overscroll: false),
child: Row(
children: [
new Expanded(
child: ListView.builder(
///屏蔽默认的滑动响应
physics: NeverScrollableScrollPhysics(),
controller: _primaryScrollController,
itemCount: 55,
itemBuilder: (context, index)
return Container(
height: 300,
color: Colors.greenAccent,
child: Center(
child: Text(
"Item $index",
style: TextStyle(
fontSize: 40, color: Colors.blue),
),
));
)),
new SizedBox(
width: 5,
),
new Expanded(
child: ListView.builder(
///屏蔽默认的滑动响应
physics: NeverScrollableScrollPhysics(),
controller: _subScrollController,
itemCount: 55,
itemBuilder: (context, index)
return Container(
height: 300,
color: Colors.deepOrange,
child: Center(
child: Text(
"Item $index",
style:
TextStyle(fontSize: 40, color: Colors.white),
),
),
);
),
),
],
),
),
));
以上是关于Flutter 小技巧之霓虹灯文本的「故障」效果的实现的主要内容,如果未能解决你的问题,请参考以下文章
Flutter 小技巧之 ListView 和 PageView 的各种花式嵌套
Flutter 小技巧之 ListView 和 PageView 的各种花式嵌套