Flutter模仿抖音TikTok手势交互(二)
在之前的手势交互文章中,我们学习了GestureDetector、Transform和Hero动画,并在TikTok中实现了几种手势交互效果。
我们来看看这个实现的效果:
Gif:user-gold-cdn.xitu.io/2019/4/25/1…
Github地址:github.com/ditclear/十…
下载体验
交互分解
这次主要包括两个下拉交互:下拉刷新和下拉返回。
向下滑动刷新

Gif:user-gold-cdn.xitu.io/2019/4/25/1…
手指向下滑动时,offsetY发生变化。这里有两个变化:
- Transparency
- y轴方向偏移
Transparency 这里可以使用Flutter提供的Opacity组件,专门用来改变透明度,使用也很简单。
Opacity(
opacity: currentOpacity,
child:childWidget
)
复制代码
只需传入不透明度即可。我们需要做的就是用offsetY 来表示不透明度的值。
值得注意的是,这里有两个透明度变化:顶部导航栏和下拉更新内容文本。
而且两者不会同时出现。当你向下拖动时,顶部导航栏会逐渐变成透明,当达到一定距离(可以认为是最大下拉距离的一半)时,就会隐藏。 向下拖动 文字更新内容后才会出现,其透明度逐渐变得不透明。
我们设置下拉菜单的最大滑动距离为40,那么临界点就是20。
从这里我们可以推导出顶部导航栏透明度变化的公式下拉距离。
opacity = 1 - offsetY / 20
因为offsetY最大可以为40,并且不透明度必须在0到1之间。因此,最终的公式为:
opacity = max offset(Y, / 1) 20)
具体实现:
向下拖动时,记录总偏移量offsetY
,检查是否大于0且不超过最大距离。
onVerticalDragUpdate: (details) {
final tempY = offsetY + details.delta.dy / 2;
if (currentIndex == 0) {
//最大下拉距离不超过40
if (tempY > 0) {
if (tempY < 40) {
setState(() {
offsetY = tempY;
});
} else if (offsetY != 40) {
setState(() {
setState(() {
offsetY = 40;
});
});
// 当下拉到最大距离时,触发震动效果
vibrate();
}
}
} else {
offsetY = 0;
}
}
复制代码
当向下拖动到最大距离时,可以使用震动产生震动效果。
当手指离开屏幕时,执行另一个动画,将OffsetY设置为0,并通过setState告诉UI渲染。
// 下拉结束
onVerticalDragEnd: (_) {
if (offsetY != 0) {
animateToTop();
}
}
/// 滑动到顶部
///
/// [offsetY] to 0.0
void animateToTop() {
animationControllerY =
AnimationController(duration: Duration(milliseconds: offsetY.abs() * 1000 ~/ 40), vsync: this);
final curve = CurvedAnimation(parent: animationControllerY, curve: Curves.easeOutCubic);
animationY = Tween(begin: offsetY, end: 0.0).animate(curve)
..addListener(() {
setState(() {
offsetY = animationY.value;
});
});
animationControllerY.forward();
}
复制代码
下拉返回

Gif:user-gold-cdn.xitu.io/2019/4/25/1…
简单来说,下拉的时候整个页面Y方向平移-轴方向,当超过指定距离时,直接退出本页面。
// 滑动截止时
onPanEnd: (_) {
if (offsetY > 100) {
// 下拉距离超过100,即退出页面
Navigator.pop(context);
} else if (offsetY > 0) {
// 下拉距离小于100,恢复原样
animateToBottom(screenHeight);
} else if (offsetY < 0) {
// 上拉根据是否已经显示评论框 [isCommentShow]和offsetY来判断是展开还是收缩
if (!isCommentShow && offsetY.abs() > screenHeight * 0.2) {
if (offsetY.abs() > screenHeight * 0.2) {
animateToTop(screenHeight);
} else {
animateToBottom(screenHeight);
}
} else {
if (offsetY.abs() > screenHeight * 0.4) {
animateToTop(screenHeight);
} else {
animateToBottom(screenHeight);
}
}
}
},
复制代码
如果页面有偏移,请在最外层添加一层Transform.translate
。注意,offset必须大于0。
Transform.translate(
offset: Offset(0, max(0, offsetY)),
child: childWidget
)
复制代码
当你完成了申诉的基本逻辑后,运行后你会发现和你预想的还是有一些差别。
背景是不透明的。当时我以为是黑色背景,需要设置背景颜色的透明度。然而,我在ThemeData中尝试了各种可疑的颜色,发现没有效果。后来我想是PageRoute的原因。 。
当我们调用Navigator
进行推送操作时,我们必须发送一个PageRoute
,比如常用的Material❀路线或CupertinoPageRoute
,这是PageRoute
中的opaque
表示不透明,并且始终为真。
@override
bool get opaque => true;
复制代码
为了解决上述问题,这里复制一份MaterialPageRoute
的源代码,然后将opaque
更改为false。
/// copy 一份 MaterialPageRoute,修改opaque
class TransparentPage<T> extends PageRoute<T> {
//...
/// false 代表背景透明
@override
bool get opaque => false;
//...
}
复制代码
最后,在上一篇文章中,有同学问如何将列表中的Hero动画化。我还在代码中进行了模拟,以确保英雄标签是相同的。在新旧路由之间切换时,Flutter 将为相同的标签设置动画。 Hero 小部件是动画的。
/// detail_page.dart
child: Hero(
tag: "detail_$currentIndex",
child: GestureDetector(
child:PageView(
onPageChanged: (index) {
setState(() {
currentIndex = index;
});
},
//..
),
),
)
/// right_page.dart
child: Hero(
tag: "detail_0",
child: Image.asset(
assets/detail.png",
fit: BoxFit.fill,
),
),
复制代码
写在最后
有了手势交互的经验,实现以上效果并不难。
需要花点时间的问题是背景颜色,但是Flutter是开源的,注释和用例都写得很详细。如果Flutter框架不能满足您的需求,您可以完全修改底层Flutter源码来实现。期望的效果。
至此,本文的内容就结束了。还剩下一步来处理手势冲突。这个内容会放在下一篇文章中。关注我,获取最新文章。
Github地址:github.com/ditclear/ti…
作者:ditclear
来源:掘金
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。