美文网首页
实战!如何在 Jetpack Compose 中拥有一个与众不同

实战!如何在 Jetpack Compose 中拥有一个与众不同

作者: 蜗牛是不是牛 | 来源:发表于2023-06-14 17:38 被阅读0次

    前言

    前不久,在Stack Overflow上用自己半瓢水的动画知识,帮助提问者解决了一个问题 的同时,看到很多眼生的方法,比如composed,于是顺便学习了一波自定义 Modifier。回过头来总结时,发现解决此问题的过程,非常适合作为一个案例去由浅入深的掌握自定义Modifier~

    步入正题!

    相信大家既然已经在学习Compose了,那想必也非常熟悉如何使用 Modifer 了,由于Compose 被Android 团推设计的非常容易上手,所以有不了解如何使用的朋友可以去看看 文档 ,即可轻松掌握基础的使用!

    拥有一个与众不同的Modifier,其实就是实现一个特别功能的Modifier,然后使用它去修饰我们的Composable可组合函数,来实现我们的特殊需求。

    下面,我们就通过代码一步一步来实战一个特别功能的 Modifier , 相信如果跟着过一遍的话,基本上也就掌握了自定义 Modifier的知识。

    1.1 需求,给 Composeable 添加虚线边框

    image.png

    既然是添加边框,想当然直接用 Modifier.border

    fun Modifier.border(width: Dp, brush: Brush, shape: Shape): Modifier
    
    

    然而,自带的border()提供了边框宽度,边框色彩,边框形状,但并没有一个设置 “虚线” 的参数给我们,没办法要么等待官方猴年马月之后更新支持,要么自己动手,丰衣足食 DIY 一个来用,岂不美哉!

    1.2 绘制虚线

    边框并不属于Composable内容部分提供的,所以我们要把它绘制出来,然后依附在Composable内容的边上。我们仿照Modifier.border,使用Modifier.drawXxx来实现一个。

    关于Compose draw可以看这里使用 Jetpack Compose 完成自定义绘制

    在 View 中,需要绘制虚线时,我们会用到DashPathEffect来实现各式各样的虚线,同样,在ComposePathEffect.dashPathEffect 用法基本保持一致

    @Composable
    fun ShowCard() {
            Box(
                contentAlignment = Alignment.Center,
                modifier = Modifier
                    .width(160.dp)
                    .height(50.dp)
                    .padding(2.dp)
                    .drawBehind {
                        // 绘制圆角矩形,可以满足圆角边框需求
                        drawRoundRect(
                                color = Color.Black,
                                style = Stroke(
                                    width = 1f,
                                    pathEffect = PathEffect.dashPathEffect(
                                    intervals = floatArrayOf(20f, 20f),
                                    phase = 0f
                                 )
                                )
                            )
    
                        }
            ) {
                Text(text = "看看四周的框框")
            }
        }
    
    
    

    intervalsphase,分别用来控制虚线的间隔,以及偏移量。

    到这里,如果只是为了某一个 Composable 添加虚线边框的话,已经初步满足目的。但是,我们还想要把这个效果独立出来

    1.3 抽取为自定义 Modifier

    其实Android提供的自带modifer ,比如 size() padding() 等等,都是通过拓展函数的方式来实现链式调用。

    创建一个拓展函数,同时我们提高一下可配置性,将一些属性作为方法参数,并提供圆角大小,丰富一下功能:

    fun Modifier.dashBorder(、
       color: Color = Color.Black,
       width: Dp = 1.dp,
       cornerRadiusDp: Dp = 0.dp,
       dashLength:Dp,
    ) = drawBehind {
       drawRoundRect(
           color = color,
            style = Stroke(
               width = width.toPx(),
               pathEffect = PathEffect.dashPathEffect(
                   // 简单起见,让空白和线段的长度相同
                   intervals = floatArrayOf(dashLength.toPx(),dashLength.toPx()),
                   phase = 0f
               )
           ),
           cornerRadius = CornerRadius(cornerRadiusDp.toPx())
       )
    
    }
    // 使用
       Box(
            contentAlignment = Alignment.Center,
               modifier = Modifier
                   .width(160.dp)
                   .height(50.dp)
                   .padding(2.dp)
                   .dashBorder(
                       width = 1.dp,
                       intervals = 5.dp,
                       cornerRadiusDp = 5.dp
                   )
           ) {
               Text(text = "看看四周的框框")
           }
    
    
    

    Compose中我们通常使用Dp作为屏幕显示单位,所以我们暴露方法参数最好使用Dp,在绘制时,使用dp.toPx() 即可, 另外,建议提供默认参数值,让代码更简洁。 到此,我们的自定义Modifier已经实现了

    • 可设置宽度的边框
    • 可设置虚线的长度
    • 可添加圆角 完美!

    2.1 如何让边框动起来?

    其实,如果是针对需求的话,我们的Modifier已经实现,但是,为了更好的学习,我们更进一步——让我们的虚线边框转动起来!

    image.png

    分析,如果想要边框动转起来,我们应该找到能够引发边框沿着我们的四个边转动的角色,有自定义view经验的朋友估计已经知道了,那就是 dashPathEffectphase

    phase,虚线的偏移量,说白了就是虚线开端偏移起点的距离。

    动画原理:让虚线偏移一个完整虚线长度(包括线段和空白),然后restart,这样在视觉上看,就是一个无限延伸的线啦!

    image.png

    文字有点抽象,我们结合示意图来分析,我们用实线代表虚线中的线,用虚线代表虚线中的空白,当线段从A偏移到B时,我们让动画 Restart,这样又会从A'偏移到B',如此不断Restart,实现无限转动效果。

    2.2 使用 ComposedModifier 给边框添加动画

    Compose中,动画给状态的改变提供丝滑的过渡效果,不可避免的,在 Modifier 中如果需要使用状态 api (remember),要用到 ComposedModifier 来为我们提供一个带有状态的 Modifier

    fun Modifier.composed(
        inspectorInfo: InspectorInfo.() -> Unit = NoInspectorInfo,
        factory: @Composable Modifier.() -> Modifier
    ): Modifier = this.then(ComposedModifier(inspectorInfo, factory))
    
    

    使用拓展函数composed即可拿到一个ComposedModifier

    fun Modifier.dashBorder(
        color: Color = Color.Black,
        width: Dp = 1.dp,
        dashLength:Dp,
        cornerRadiusDp: Dp = 0.dp,
    ) = composed {
        // 不在drawScope 中,无法直接使用 dp.toPx()
        val density = LocalDensity.current
        val dashLengthPx = density.run { dashLength.toPx() }
        // 声明一个无限循环动画
        val infinite = rememberInfiniteTransition()
        val anim by infinite.animateFloat(initialValue = 0f,
            targetValue = dashLengthPx*2,//偏移一个完整长度
            animationSpec = infiniteRepeatable(
                animation = tween(1000, easing = LinearEasing),
                repeatMode = RepeatMode.Restart // 动画循环模式为 restart
            ) )
    
        drawBehind {
            drawRoundRect(
                color = color,
                style = Stroke(
                    width = width.toPx(),
                    pathEffect = PathEffect.dashPathEffect(
                        intervals = floatArrayOf(dashLength.toPx(),dashLength.toPx()),
                        phase = anim // 动画应用
                    )
                ),
                cornerRadius = CornerRadius(cornerRadiusDp.toPx())
            )
        }
    }
    
    
    

    看看效果,至此,我们成功的让边框动起来啦😀:

    image.png

    加餐 🍔

    让动画更加完美,增加动画的暂停与继续

    思考:有时候,我们并不希望动画一直在那里播放,那么如何控制动画的暂停与恢复呢?

    其实这里的已经偏向于动画啦,感兴趣的可以继续阅读

    根据我的学习,很不幸,compose 并没有提供直接控制动画暂停与继续的api , 但是我们可以开动一下思维,变通一下,实现暂停与继续的等价效果。

    • 首先,我们需要使用更底层的api -- Animatable,来设置每次动画的起始值。
    • 然后我们增加一个参数animate来控制动画是否播放, 新增一个状态lastAnimValue来记录上次动画的结束值,并且动画的目标值也需要额外加上lastAnimValue,保证动画每次偏移是一个完整长度。
    • 再次开始动画时,将lastAnimValue 作为本次动画的起始值。
    fun Modifier.dashsBorder(
        color: Color = Color.Black,
        width: Dp = 1.dp,
        dashLength:Dp,
        cornerRadiusDp: Dp = 0.dp,
        animate: Boolean = true
    ) = composed {
    
        var lastAnimValue by remember { mutableStateOf(0f) }
        val anim = remember(animate) { Animatable(lastAnimValue) }
    
        val density = LocalDensity.current
        val dashLengthPx = density.run { dashLength.toPx() }
    
        LaunchedEffect(animate) {
            if (animate) {
                anim.animateTo(
                 (dashLengthPx * 2 + lastAnimValue),
                    animationSpec =
                    infiniteRepeatable(
                        animation = tween(1000, easing = LinearEasing),
                        repeatMode = RepeatMode.Restart,
                    )
                ) {
                    lastAnimValue = value // store the anim value
                }
            }
        }
    
        drawBehind {
            drawRoundRect(
                color = color,
                style = Stroke(
                    width = width.toPx(),
                    pathEffect = PathEffect.dashPathEffect(
                        intervals = floatArrayOf(dashLength.toPx(),dashLength.toPx()),
                        phase = anim.value
                    )
                ),
                cornerRadius = CornerRadius(cornerRadiusDp.toPx())
            )
        }
    }
    
    

    看看效果,完美😎:

    image.png

    总结

    自定义Modifier,在Compose中是一个很有用的知识点,不少前辈大佬们利用它来实现了许许多多实用的功能,比如骨架屏,比如自由滚动。希望大家能掌握它,然后创造分享出更多的便利代码库✈️,这样,就可以更早下班啦💃~

    参考资料

    android - How to have dashed border in Jetpack Compose? - Stack Overflow

    android - How to pause/resume a jetpack compose infinite transition animation - Stack Overflow

    相关文章

      网友评论

          本文标题:实战!如何在 Jetpack Compose 中拥有一个与众不同

          本文链接:https://www.haomeiwen.com/subject/wfkkydtx.html