拿去吧你!Flutter 仿抖音个人主页下拉拖拽效果
引言
最近产品经理看到抖音的个人主页下拉效果很不错,让我也实现一个,如果是native还好办,开源成熟的库一大堆,可我是Flutter呐🤣,业内成熟可用的库非常有限,最终跟产品经理batte失败后,没办法只能参考native代码硬肝出来。
效果图
整体构思
实现拖拽滑动功能,关键在于对手势事件的识别。在 Flutter 中,可使用Listener
来监听触摸事件,如下所示:
Listener(
onPointerDown: (result) {
},
onPointerMove: (result) {
},
onPointerUp: (_) {
}
在手指滑动的过程中不断的刷新背景图高度是不是就可以实现图片的拉伸效果呢?我们这里图片加载库使用CachedNetworkImage,高度在156的基础上动态识别手指的滑动距离extraPicHeight
CachedNetworkImage(
width: double.infinity,
height: 156 + extraPicHeight,
imageUrl: backgroundUrl,
fit: fitType,
)
识别到手指滑动就不断的刷新拉伸高度extraPicHeight
,flutter setState
内部已经做了优化,不用担心性能问题,实际效果体验很不错。
setState(() {
extraPicHeight;
});
经过实验思路是没有问题,那么监听哪些事件,extraPicHeight
到底怎么计算,有什么边界值还考虑到呢?我们从手势的顺序开始梳理一下。
首先按压屏幕会识别到触碰屏幕起点,也就是initialDx
initialDy
,对于下拉拖拽我们关心更多的是纵向坐标result.position.dy
onPointerDown: (result) {
initialDy = result.position.dy;
initialDx = result.position.dx;
},
当手指在屏幕滑动会触发onPointerMovew
,result.position.dy
代表的就是手势滑动的位置
onPointerMove: (result) {
//手指的移动时
// updatePicHeight(result.position.dy); //自定义方法,图片的放大由它完成。
},
这边处理逻辑比较复杂,我们先抽成函数updatePicHeight
updatePicHeight(changed) {
//。。。已省略不重要细节代码
extraPicHeight += changed - prev_dy; //新的一个y值减去前一次的y值然后累加,作为加载到图片上的高度。
debugPrint('extraPicHeight updatePicHeight : $extraPicHeight');
//这里是为了限制我们的最大拉伸效果
if (extraPicHeight > 300) {
extraPicHeight = 300;
}
if (extraPicHeight > 0) {
setState(() {
prev_dy = changed;
});
}
}
这里简化了很多细节逻辑,核心目的就是要不断的累加我们的拖动距离来计算extraPicHeight
高度,这里的changed是我们手指的y坐标,滑动的距离需要减去上次滑动的回调y,所以我们必须声明一个过去y坐标的变量也就是prev_dy
,通过通过 changed - prev_dy
就可以得出真正滑动的距离,然后我们不断累加 extraPicHeight += changed - prev_dy
就是图片的拉伸距离。
手指下拉以后图片确实拉伸了,但是松开手后发现回不去了🤣因为我们还需要处理图回去的问题,既然可以通过setState把图片高度拉高,我们也可以通过setState把图片高度刷回去,核心要思考的是如何平滑的让图片自己缩回去呢?有经验的你一定想到动画了。
flutter这里的动画库是Tween
,Tween
可以通过addListener
监听距离的回调,当距离变化不断刷新图片高度
anim = Tween(begin: extraPicHeight, end: 0.0).animate(animationController)
..addListener(() {
setState(() {
extraPicHeight = anim.value;
fitType = BoxFit.cover;
});
});
prev_dy = 0; //同样归零
动画的效果最终由控制器animationController
来决定,这里给了一个300ms的时间还不错,可以根据自己业务扩展
animationController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 300));
所有在手抬起的时候执行我们的动画runAnimate
函数即可
onPointerUp: (_) {
//当手指抬起离开屏幕时
if (isVerticalMove) {
if (extraPicHeight < 0) {
extraPicHeight = 0;
prev_dy = 0;
return;
}
debugPrint('extraPicHeight onPointerUp : $extraPicHeight');
runAnimate(); //动画执行
animationController.forward(from: 0); //重置动画
}
},
整体的技术方案履完了,之后就是细节问题了
问题1:横行稍微有倾角的滑动也会导致页面拖拽,比如侧滑返回上一页面
这是由于手指滑动的角度没有限制, 这里我们计算一下滑动倾角,超过45度无效,角度计算通过x,y坐标计算tan函数即可
onPointerMove: (result) {
double deltaY = result.position.dy - initialDy;
double deltaX = result.position.dx - initialDx;
double angle =
(deltaY == 0) ? 90 : atan(deltaX.abs() / deltaY.abs()) * 180 / pi;
debugPrint('onPointerMove angle : $angle');
if (angle < 45) {
isVerticalMove = true; // It's a valid vertical movement
updatePicHeight(result
.position.dy); // Custom method to handle vertical movement
} else {
isVerticalMove =
false; // It's not a valid vertical movement, ignore it
}
}
问题2:图片高度变了,为啥没有拉伸啊!
图片拉伸取决于你图片库的加载配置,以flutter举例,我们的图片库是CachedNetworkImage
CachedNetworkImage(
width: double.infinity,
height: 156 + extraPicHeight,
imageUrl: backgroundUrl,
fit: fitType,
)
加载效果取决于fit,默认不变形我们使用cover,拉伸时使用fitHeight
或者fill
updatePicHeight(changed) {
if (prev_dy == 0) {
//如果是手指第一次点下时,我们不希望图片大小就直接发生变化,所以进行一个判定。
prev_dy = changed;
}
if (extraPicHeight > 0) {
//当我们加载到图片上的高度大于某个值的时候,改变图片的填充方式,让它由以宽度填充变为以高度填充,从而实现了图片视角上的放大。
fitType = BoxFit.fitHeight;
} else {
fitType = BoxFit.cover;
}
extraPicHeight += changed - prev_dy; //新的一个y值减去前一次的y值然后累加,作为加载到图片上的高度。
debugPrint('extraPicHeight updatePicHeight : $extraPicHeight');
if (extraPicHeight > 300) {
extraPicHeight = 300;
}
if (extraPicHeight > 0) {
setState(() {
prev_dy = changed;
fitType = fitType;
});
}
}
最后看下组件如何布局
CustomScrollView(
physics: const NeverScrollableScrollPhysics(),
slivers: <Widget>[
SliverToBoxAdapter(
child: buildTopWidget(),
),
SliverToBoxAdapter(
child: Column(
children: contents,
),
)
]
),
)
整个列表使用CustomScrollView
,因为在flutter上用他才能实现这种变化效果,未来还可以扩展顶部导航栏的变化需求。buildTopWidget
就是我们头部组件,包括内部的背景图,但是整个组件和背景图的高度都是依赖extraPicHeight
变化的,contents
是我们的内容,当头部组件挤压,会正常跟随滑动到底部。
全局变量依赖以下参数就够了,核心要注意的就是边界值问题,什么时候把状态值重置问题。
//初始坐标
double initialDy = 0;
double initialDx = 0;
double extraPicHeight = 0; //初始化要加载到图片上的高度
late double prev_dy; //前一次滑动y
//是否是垂直滑动
bool isVerticalMove = false;
//动画器
late AnimationController animationController;
late Animation<double> anim;
技术语言不是我分享的核心,解决这个需求的技术思维路线是我们大家可以借鉴学习的。
如果你有任何疑问可以通过掘金联系我,如果文章对你有所启发,希望能得到你的点赞、关注和收藏,这是我持续写作的最大动力。Thanks~
来源:juejin.cn/post/7419248277382021135