Jetpack Compose花里胡哨 - 40+行实现左右侧滑返回、带动画、有细节的Activity

2,583 阅读3分钟

参数还需要调优,速度计算也只是不考虑最终抬手速度的粗略版本

此处代码仅作为demo,实际上线还需要优化参数和速度计算逻辑

功能简述

  • 左右侧滑都可以让Activity返回
  • 滑动带动Activity时,可看到其下面的内容
  • 滑动不超过指定范围取消此次滑动返回
  • 滑动超过某范围后松手,则Activity自动滑出
  • 滑动速度过大时,即便滑动范围不够,也让Activity自动滑出
  • 滑动被取消时,Activity再滑回原位

先看最终效果

滑动返回最终效果.gif

原理

说来简单,对设置好window透明的Activity的decorView做跟随手势的x轴偏移,再加一点点动画效果和速度计算即可


跟着我,一步步来~

实现一个透明的Activity

我们需要它被滑动的时候能看到底下的情况,设置其主题即可

<style name="Theme.diy" parent="Theme.TestApp">
    <item name="android:windowBackground">@android:color/transparent</item>
    <item name="android:windowIsTranslucent">true</item>
</style> 

设置拖曳监听

此处需求,用Modifier.draggable()就够了。

Modifier.draggable(
        rememberDraggableState { delta ->
            //+=delta即可得到最终滑动值
        },
        Orientation.Horizontal,//横向滑动
        onDragStarted = {
           //在滑动开始时监听
        },
        onDragStopped = {
            //滑动结束时计算最终速度、并决定是让Activity位置重置还是退出
        }
)

最终代码

思路一目了然了,直接上代码分析一波——

假定以下内容发生在Activity的setContent{}行为内部,

所以有一个this@Activity去获得当前Activity

val screenXMax = remember { Resources.getSystem().displayMetrics.widthPixels.toFloat() }//屏幕宽px,转成float方便后续使用
val closePerc = remember { 0.55f }//百分比
val maxSpeed = remember { 4.5f }//最大速度,超过这个速度的滑动会导致Activity无视滑动距离直接关闭
var speed by remember { mutableStateOf(0f) }//计算出的速度

var targetOffsetX by remember { mutableStateOf(0f) }//Activity的最终偏移目标
val activityOffset = remember { Animatable(0f) }//Activity的偏移,动画

var dragging by remember { mutableStateOf(false) }//是否正在被滑动
var timeToClose by remember { mutableStateOf(false) }//是时候关闭了?

//监听最终目标的改变和滑动状态的改变
LaunchedEffect(targetOffsetX, dragging) {
    //当已经判定为“是时候关闭Activity了”
    if (timeToClose) {
        activityOffset.animateTo(
        //最终目标为正,右滑出去,否则左滑出去
        if (targetOffsetX > 0) screenXMax else -screenXMax, getTween(300, easing = LinearOutSlowInEasing)) {
            //动画的每一帧都让decorView的偏移和动画的值对齐,实现decorView的动画
            this@Activity.window.decorView.x = this.value
        }
        //关闭Activity
        finish()
    } else if (dragging) {
        //滑动过程中让Activity偏移和目标值实时对齐
        activityOffset.snapTo(targetOffsetX)
        this@Activity.window.decorView.x = targetOffsetX
    } else activityOffset.animateTo(targetOffsetX, getTween(300, easing = FastOutLinearInEasing)) {
        //进入这一块意味着已经用户松手了,让decorView动画到Activity的目标值(此时targetOffsetX=0)
        this@Activity.window.decorView.x = this.value
    }
}
Box(
        Modifier.draggable(
                rememberDraggableState { delta ->
                    //跟踪滑动偏移
                    targetOffsetX += delta
                },
                Orientation.Horizontal,//横向滑动
                onDragStarted = {
                    dragging = true
                    Timer.setTag("closeAct")//这是我自定义的一个Timer函数,它会把当前时间戳记在传入的tag中
                },
                onDragStopped = {
                    dragging = false
                    //速度等于路程除时间
                    speed = abs(targeOffsetX) / Timer.getTimeInterval("closeAct")//自定义的一个Timer函数,它会返回当前时间与上次调用Timer.setTag(tag)的时间差
                    if (abs(targeOffsetX) > closePerc * screenXMax || speed > maxSpeed) {
                        timeToClose = true
                        return@draggable
                    } else {
                        timeToClose = false
                    }
                    targeOffsetX = 0f
                }
        )
            .background(Color.Red)//给个红色背景好区分此Activity
            .fillMaxSize()//这个box没内容,于是让它用红色占满整个Activity
)

可改进点

很显而易见的,速度计算采用的时间是从滑动开始到结束的时间,这不合理。

速度计算采用的路程是像素,这也不合理。

应当指定为:滑动的最后50ms所经过的物理距离(如:多少英寸)。

这个100ms也是我随便说的,但是应该越小越好,且需要考虑到时间戳在不同设备上可能有不同的生成间隔,所以不能太小,只有这样才能尽可能精确地计算出用户最后抬手瞬间的速度

另外scroll动作的惯性是怎么实现的,计算速度这里或许可以参考下,我没研究,我不知道,哈哈哈

说到这里你应该有关于速度的优化方案了,我就懒得进一步完善了。hhh