国际惯例先上实现效果图,仿真度百分之99。
微信每个人都使用,首页的交互过程大家应该都很清楚,就不细说了。
我们直接来拆分需求:
- 手指下拉过程一个大球逐渐变成三个小球
- 滑动一定距离小程序界面逐渐显示出来,继续下拉进入小程序界面
- 小程序界面上拉超过一定距离可以回到首页
不要慌我们一个一个来实现,还是很简单的,有手就行。
下拉过程中三个小球变化效果实现
下拉过程中仔细观察小球变化,我们可以得到以下信息,小球的大小从0 逐渐变化到最大,如果继续下拉,大球会生出两个小球,然后变小,直至与小球大小一样,继续下拉三个小球同时隐藏。
先定义几个变量
ini
val maxRadius = remember { 15f }
val minRadius = remember { 10f }
val startDistance = remember { 0f }
val endDistance = remember { 60f }
val openMiniThreshold = remember { 0.25f }
以上变量分别表示大球和小球的半径和小球的位移距离,因为大球和小球其实差别不是很大,所以相差5个px就可以了。
我们用Canvas 来绘制小球。
scss
Canvas(
modifier = Modifier
.fillMaxWidth()
.height(height)
.background(Color(0xffededed))
) {
//绘制大球
drawCircle(
Color.Gray,
if (scrollPercent <= openMiniThreshold / 3) lerp(
0f,
maxRadius,
fraction = scrollPercent / (openMiniThreshold / 3)
) else lerp(
maxRadius,
minRadius,
fraction = (scrollPercent.minus(openMiniThreshold / 3)) / ((openMiniThreshold.minus(
openMiniThreshold / 3
)))
)
)
//绘制两个小球
if (scrollPercent > openMiniThreshold / 3) {
repeat(2) {
translate(
left = lerp(
startDistance,
endDistance,
fraction = (scrollPercent.minus(openMiniThreshold / 3)) / ((openMiniThreshold.minus(
openMiniThreshold / 3
)))
).withSign((if (it == 0) 1 else -1))
) {
drawCircle(Color.Gray, minRadius)
}
}
}
}
仔细观察微信下拉我们可以看到其实滑动到屏幕大约四分之一小球就消失了,打开小程序界面的阈值取0.25就差不多了,当到达阈值三分之一处大球就变化到最大了,继续下滑大球变小,小球从大球生出来,小球的移动直接用translate移动滑动画布就行了。
下拉过程显示小程序界面
小球消失后,小程序界面显示出来,这时候小程序界面是缩小的,并且上面有一层和topbar颜色一样的蒙层。这个蒙层透明度的变化我们可以看到大概是从1变化到0。
蒙层我们直接在小程序界面用drawWithContent 来实现
scss
.drawWithContent {
drawContent()
drawRect(
size = size,
color = Color(0xffededed).copy(
alpha = lerp(
1f,
0f,
scrollPercent
.minus(openMiniThreshold)
.coerceAtLeast(0f) / (1 - openMiniThreshold).coerceAtLeast(
0.1f
)
)
)
)
}
小程序界面的缩放在graphicsLayer中操作就行了,缩放的最小值大概为0.8
ini
LazyColumn(modifier = Modifier
.graphicsLayer {
scaleX = scrollPercent.coerceAtLeast(0.8f)
scaleY = scrollPercent.coerceAtLeast(0.8f)
translationY = offsetY
}, content = {})
因为首页和小程序界面其实都是用LazyColumn 实现,LazyColumn 用scrollable 来实现滚动的,scrollable支持嵌套滚动,所以我们直接用嵌套滚动NestedScrollConnection来实现这个下拉过程。
kotlin
override fun onPreScroll(
available: Offset,
source: NestedScrollSource
): Offset {
if (abs(offsetY) <= visibilityThreshold) {
return Offset.Zero
}
if (source == NestedScrollSource.Drag) {
if (flingAnimator.isRunning) {
scope.launch {
flingAnimator.stop()
}
}
var consumedY = 0f
if (offsetY + available.y < 0) {
consumedY = offsetY + available.y
offsetY = 0f
} else {
offsetY += available.y
consumedY += available.y
}
return Offset(0f, consumedY)
}
return Offset.Zero
}
override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
): Offset {
if (source == NestedScrollSource.Drag) {
if (available != Offset.Zero) {
offsetY = (available.y + offsetY).coerceIn(
0f,
500f
)
}
return Offset(0f, available.y)
}
return Offset.Zero
}
override suspend fun onPreFling(available: Velocity): Velocity {
if (offsetY != 0f) {
if (flingAnimator.isRunning) {
flingAnimator.stop()
}
flingAnimator = Animatable(offsetY)
flingAnimator.animateTo(
if (scrollPercent > openMiniThreshold) constraints.maxHeight.toFloat() else 0f,
springSpec
) {
offsetY = value
}.endState.velocity
return Velocity(0f, available.y)
}
return Velocity(0f, 0f)
}
上面代码其实就是一些消费距离的计算和动画的处理,compose 的动画api设计还是很好的,用Animatable 来实现就好了。手指离开屏幕会触发Fling ,然后又会由fling触发一些列的onPreScroll 和 onPostScroll,此时的source = fling,所以我们需要注意的是onPreScroll 和 onPostScroll这里只消费NestedScrollSource.Drag就行了。另外需要注意的是这里滑动的距离都是用像素来代替的,真正做需求就需要用dp了。
小程序界面的上拉也是用嵌套滚动来实现的,代码和原理都大致相同就不贴了。
微信聊天界和小程序界面怎么用compose实现,请点这Jetpack Compose 实战之仿微信UI -实现首页小程序入口(六) - 掘金