實戰!如何在 Jetpack Compose 中擁有一個與眾不同的 Modifier

前言

前不久,在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

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容