引言
最近产品经理看到抖音的个人主页下拉效果很不错,让我也实现一个,如果是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
updatePicHeight(changed) {
extraPicHeight += changed - prev_dy;
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
updatePicHeight(result
.position.dy)
} else {
isVerticalMove =
false
}
}
问题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
debugPrint('extraPicHeight updatePicHeight : $extraPicHeight')
if (extraPicHeight > 300) {
extraPicHeight = 300
}
if (extraPicHeight > 0) {
setState(() {
prev_dy = changed
fitType = fitType
})
}
}
最后看下组件如何布局
CustomScrollView(
physics: const NeverScrollableScrollPhysics(),
slivers: [
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
//是否是垂直滑动
bool isVerticalMove = false
//动画器
late AnimationController animationController
late Animation anim
技术语言不是我分享的核心,解决这个需求的技术思维路线是我们大家可以借鉴学习的。
如果你有任何疑问可以通过掘金联系我,如果文章对你有所启发,希望能得到你的点赞、关注和收藏,这是我持续写作的最大动力。Thanks~
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.dandroid.cn/22203,转载请注明出处。
评论0