Di chuyển sang API Indication và Ripple

Để cải thiện hiệu suất kết hợp của các thành phần tương tác sử dụng Modifier.clickable, chúng tôi đã ra mắt các API mới. Các API này cho phép triển khai Indication hiệu quả hơn, chẳng hạn như hiệu ứng gợn sóng.

androidx.compose.foundation:foundation:1.7.0+androidx.compose.material:material-ripple:1.7.0+ bao gồm những thay đổi sau đây về API:

Không dùng nữa

Thay thế

Indication#rememberUpdatedInstance

IndicationNodeFactory

rememberRipple()

Thay vào đó, các API ripple() mới được cung cấp trong các thư viện Material.

Lưu ý: Trong ngữ cảnh này, "thư viện Material" đề cập đến androidx.compose.material:material, androidx.compose.material3:material3, androidx.wear.compose:compose-materialandroidx.wear.compose:compose-material3.

RippleTheme

Hãy thực hiện một trong hai thao tác sau:

  • Dùng các API RippleConfiguration của thư viện Material, hoặc
  • Xây dựng chế độ triển khai hiệu ứng gợn sóng cho hệ thống thiết kế của riêng bạn

Trang này mô tả tác động của thay đổi về hành vi và hướng dẫn di chuyển sang các API mới.

Thay đổi về hành vi

Các phiên bản thư viện sau đây có thay đổi về hành vi gợn sóng:

  • androidx.compose.material:material:1.7.0+
  • androidx.compose.material3:material3:1.3.0+
  • androidx.wear.compose:compose-material:1.4.0+

Các phiên bản thư viện Material này không còn sử dụng rememberRipple() nữa mà sử dụng các API hiệu ứng gợn sóng mới. Do đó, chúng không truy vấn LocalRippleTheme. Do đó, nếu bạn đặt LocalRippleTheme trong ứng dụng, các thành phần Material sẽ không sử dụng các giá trị này.

Các phần sau đây mô tả cách di chuyển sang các API mới.

Di chuyển từ rememberRipple sang ripple

Sử dụng thư viện Material

Nếu bạn đang sử dụng một thư viện Material, hãy thay thế trực tiếp rememberRipple() bằng một lệnh gọi đến ripple() trong thư viện tương ứng. API này tạo hiệu ứng gợn sóng bằng các giá trị bắt nguồn từ API giao diện Material. Sau đó, hãy truyền đối tượng được trả về đến Modifier.clickable và/hoặc các thành phần khác.

Ví dụ: đoạn mã sau đây sử dụng các API không dùng nữa:

Box(
    Modifier.clickable(
        onClick = {},
        interactionSource = remember { MutableInteractionSource() },
        indication = rememberRipple()
    )
) {
    // ...
}

Bạn nên sửa đổi đoạn mã trên thành:

@Composable
private fun RippleExample() {
    Box(
        Modifier.clickable(
            onClick = {},
            interactionSource = remember { MutableInteractionSource() },
            indication = ripple()
        )
    ) {
        // ...
    }
}

Xin lưu ý rằng ripple() không còn là một hàm có khả năng kết hợp và không cần phải được ghi nhớ. Bạn cũng có thể sử dụng lại trên nhiều thành phần, tương tự như các đối tượng sửa đổi. Vì vậy, hãy cân nhắc việc trích xuất quá trình tạo hiệu ứng gợn sóng thành một giá trị cấp cao nhất để lưu các lượt phân bổ.

Triển khai hệ thống thiết kế tuỳ chỉnh

Nếu đang triển khai hệ thống thiết kế của riêng mình và trước đây bạn đã sử dụng rememberRipple() cùng với RippleTheme tuỳ chỉnh để định cấu hình hiệu ứng gợn sóng, thì thay vào đó, bạn nên cung cấp API gợn sóng của riêng mình để uỷ quyền cho các API nút gợn sóng được hiển thị trong material-ripple. Sau đó, các thành phần của bạn có thể sử dụng hiệu ứng gợn sóng của riêng bạn, hiệu ứng này sẽ sử dụng trực tiếp các giá trị giao diện. Để biết thêm thông tin, hãy xem phần Di chuyển từ RippleTheme.

Di chuyển từ RippleTheme

Tạm thời chọn không sử dụng thay đổi về hành vi

Thư viện Material có CompositionLocal, LocalUseFallbackRippleImplementation tạm thời mà bạn có thể dùng để định cấu hình tất cả các thành phần Material nhằm quay lại sử dụng rememberRipple. Bằng cách này, rememberRipple sẽ tiếp tục truy vấn LocalRippleTheme.

Đoạn mã sau đây minh hoạ cách sử dụng API LocalUseFallbackRippleImplementation CompositionLocal:

CompositionLocalProvider(LocalUseFallbackRippleImplementation provides true) {
    MaterialTheme {
        App()
    }
}

Nếu đang sử dụng một giao diện ứng dụng tuỳ chỉnh được tạo dựa trên Material, bạn có thể cung cấp thành phần cục bộ một cách an toàn như một phần của giao diện ứng dụng:

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyAppTheme(content: @Composable () -> Unit) {
    CompositionLocalProvider(LocalUseFallbackRippleImplementation provides true) {
        MaterialTheme(content = content)
    }
}

Để biết thêm thông tin, hãy xem phần Nâng cấp phiên bản thư viện Material mà không cần di chuyển.

Sử dụng RippleTheme để tắt hiệu ứng gợn sóng cho một thành phần nhất định

Các thư viện materialmaterial3 hiển thị RippleConfigurationLocalRippleConfiguration, cho phép bạn định cấu hình giao diện của hiệu ứng gợn sóng trong một cây con. Xin lưu ý rằng RippleConfigurationLocalRippleConfiguration là các thuộc tính thử nghiệm và chỉ dành cho việc tuỳ chỉnh theo từng thành phần. Các API này không hỗ trợ hoạt động tuỳ chỉnh trên toàn cầu/toàn bộ giao diện; hãy xem phần Sử dụng RippleTheme để thay đổi toàn bộ hiệu ứng gợn sóng trong một ứng dụng để biết thêm thông tin về trường hợp sử dụng đó.

Ví dụ: đoạn mã sau đây sử dụng các API không dùng nữa:

private object DisabledRippleTheme : RippleTheme {

    @Composable
    override fun defaultColor(): Color = Color.Transparent

    @Composable
    override fun rippleAlpha(): RippleAlpha = RippleAlpha(0f, 0f, 0f, 0f)
}

// ...
    CompositionLocalProvider(LocalRippleTheme provides DisabledRippleTheme) {
        Button {
            // ...
        }
    }

Bạn nên sửa đổi đoạn mã trên thành:

CompositionLocalProvider(LocalRippleConfiguration provides null) {
    Button {
        // ...
    }
}

Sử dụng RippleTheme để thay đổi màu/alpha của hiệu ứng gợn sóng cho một thành phần nhất định

Như mô tả trong phần trước, RippleConfigurationLocalRippleConfiguration là các API thử nghiệm và chỉ dành cho hoạt động tuỳ chỉnh theo thành phần.

Ví dụ: đoạn mã sau đây sử dụng các API không dùng nữa:

private object DisabledRippleThemeColorAndAlpha : RippleTheme {

    @Composable
    override fun defaultColor(): Color = Color.Red

    @Composable
    override fun rippleAlpha(): RippleAlpha = MyRippleAlpha
}

// ...
    CompositionLocalProvider(LocalRippleTheme provides DisabledRippleThemeColorAndAlpha) {
        Button {
            // ...
        }
    }

Bạn nên sửa đổi đoạn mã trên thành:

@OptIn(ExperimentalMaterialApi::class)
private val MyRippleConfiguration =
    RippleConfiguration(color = Color.Red, rippleAlpha = MyRippleAlpha)

// ...
    CompositionLocalProvider(LocalRippleConfiguration provides MyRippleConfiguration) {
        Button {
            // ...
        }
    }

Sử dụng RippleTheme để thay đổi toàn bộ hiệu ứng gợn sóng trong một ứng dụng

Trước đây, bạn có thể dùng LocalRippleTheme để xác định hành vi gợn sóng ở cấp toàn bộ giao diện. Về cơ bản, đây là điểm tích hợp giữa thành phần hệ thống thiết kế tuỳ chỉnh cục bộ và hiệu ứng gợn sóng. Thay vì hiển thị một thành phần tạo giao diện chung, material-ripple hiện hiển thị một hàm createRippleModifierNode(). Hàm này cho phép các thư viện hệ thống thiết kế tạo chế độ triển khai wrapper bậc cao hơn, truy vấn các giá trị giao diện của chúng rồi uỷ quyền triển khai hiệu ứng gợn sóng cho nút do hàm này tạo.

Điều này cho phép các hệ thống thiết kế trực tiếp truy vấn những gì chúng cần và hiển thị mọi lớp giao diện theo chủ đề mà người dùng có thể định cấu hình theo yêu cầu ở trên cùng mà không cần tuân theo những gì được cung cấp ở lớp material-ripple. Thay đổi này cũng giúp xác định rõ hơn chủ đề/quy cách mà hiệu ứng gợn sóng tuân thủ, vì chính API gợn sóng xác định hợp đồng đó, thay vì được suy ra ngầm từ chủ đề.

Để được hướng dẫn, hãy xem việc triển khai API hiệu ứng gợn sóng trong các thư viện Material và thay thế các lệnh gọi đến thành phần cục bộ Material khi cần cho hệ thống thiết kế của riêng bạn.

Di chuyển từ Indication sang IndicationNodeFactory

Đang truyền bóng cho Indication

Nếu chỉ tạo một Indication để truyền đi, chẳng hạn như tạo một hiệu ứng gợn sóng để truyền đến Modifier.clickable hoặc Modifier.indication, thì bạn không cần thực hiện bất kỳ thay đổi nào. IndicationNodeFactory kế thừa từ Indication, vì vậy mọi thứ sẽ tiếp tục biên dịch và hoạt động.

Đang tạo Indication

Nếu bạn đang tạo chế độ triển khai Indication của riêng mình, thì quá trình di chuyển sẽ diễn ra đơn giản trong hầu hết các trường hợp. Ví dụ: hãy xem xét một Indication áp dụng hiệu ứng thu phóng khi nhấn:

object ScaleIndication : Indication {
    @Composable
    override fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance {
        // key the remember against interactionSource, so if it changes we create a new instance
        val instance = remember(interactionSource) { ScaleIndicationInstance() }

        LaunchedEffect(interactionSource) {
            interactionSource.interactions.collectLatest { interaction ->
                when (interaction) {
                    is PressInteraction.Press -> instance.animateToPressed(interaction.pressPosition)
                    is PressInteraction.Release -> instance.animateToResting()
                    is PressInteraction.Cancel -> instance.animateToResting()
                }
            }
        }

        return instance
    }
}

private class ScaleIndicationInstance : IndicationInstance {
    var currentPressPosition: Offset = Offset.Zero
    val animatedScalePercent = Animatable(1f)

    suspend fun animateToPressed(pressPosition: Offset) {
        currentPressPosition = pressPosition
        animatedScalePercent.animateTo(0.9f, spring())
    }

    suspend fun animateToResting() {
        animatedScalePercent.animateTo(1f, spring())
    }

    override fun ContentDrawScope.drawIndication() {
        scale(
            scale = animatedScalePercent.value,
            pivot = currentPressPosition
        ) {
            this@drawIndication.drawContent()
        }
    }
}

Bạn có thể di chuyển dữ liệu này theo 2 bước:

  1. Di chuyển ScaleIndicationInstance thành DrawModifierNode. Nền tảng API cho DrawModifierNode rất giống với IndicationInstance: nền tảng này hiển thị một hàm ContentDrawScope#draw() có chức năng tương đương với IndicationInstance#drawContent(). Bạn cần thay đổi hàm đó, sau đó triển khai logic collectLatest ngay bên trong nút, thay vì Indication.

    Ví dụ: đoạn mã sau đây sử dụng các API không dùng nữa:

    private class ScaleIndicationInstance : IndicationInstance {
        var currentPressPosition: Offset = Offset.Zero
        val animatedScalePercent = Animatable(1f)
    
        suspend fun animateToPressed(pressPosition: Offset) {
            currentPressPosition = pressPosition
            animatedScalePercent.animateTo(0.9f, spring())
        }
    
        suspend fun animateToResting() {
            animatedScalePercent.animateTo(1f, spring())
        }
    
        override fun ContentDrawScope.drawIndication() {
            scale(
                scale = animatedScalePercent.value,
                pivot = currentPressPosition
            ) {
                this@drawIndication.drawContent()
            }
        }
    }

    Bạn nên sửa đổi đoạn mã trên thành:

    private class ScaleIndicationNode(
        private val interactionSource: InteractionSource
    ) : Modifier.Node(), DrawModifierNode {
        var currentPressPosition: Offset = Offset.Zero
        val animatedScalePercent = Animatable(1f)
    
        private suspend fun animateToPressed(pressPosition: Offset) {
            currentPressPosition = pressPosition
            animatedScalePercent.animateTo(0.9f, spring())
        }
    
        private suspend fun animateToResting() {
            animatedScalePercent.animateTo(1f, spring())
        }
    
        override fun onAttach() {
            coroutineScope.launch {
                interactionSource.interactions.collectLatest { interaction ->
                    when (interaction) {
                        is PressInteraction.Press -> animateToPressed(interaction.pressPosition)
                        is PressInteraction.Release -> animateToResting()
                        is PressInteraction.Cancel -> animateToResting()
                    }
                }
            }
        }
    
        override fun ContentDrawScope.draw() {
            scale(
                scale = animatedScalePercent.value,
                pivot = currentPressPosition
            ) {
                this@draw.drawContent()
            }
        }
    }

  2. Di chuyển ScaleIndication để triển khai IndicationNodeFactory. Vì logic của bộ sưu tập hiện đã được chuyển vào nút, nên đây là một đối tượng nhà máy rất đơn giản, có trách nhiệm duy nhất là tạo một thực thể nút.

    Ví dụ: đoạn mã sau đây sử dụng các API không dùng nữa:

    object ScaleIndication : Indication {
        @Composable
        override fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance {
            // key the remember against interactionSource, so if it changes we create a new instance
            val instance = remember(interactionSource) { ScaleIndicationInstance() }
    
            LaunchedEffect(interactionSource) {
                interactionSource.interactions.collectLatest { interaction ->
                    when (interaction) {
                        is PressInteraction.Press -> instance.animateToPressed(interaction.pressPosition)
                        is PressInteraction.Release -> instance.animateToResting()
                        is PressInteraction.Cancel -> instance.animateToResting()
                    }
                }
            }
    
            return instance
        }
    }

    Bạn nên sửa đổi đoạn mã trên thành:

    object ScaleIndicationNodeFactory : IndicationNodeFactory {
        override fun create(interactionSource: InteractionSource): DelegatableNode {
            return ScaleIndicationNode(interactionSource)
        }
    
        override fun hashCode(): Int = -1
    
        override fun equals(other: Any?) = other === this
    }

Dùng Indication để tạo IndicationInstance

Trong hầu hết các trường hợp, bạn nên dùng Modifier.indication để hiển thị Indication cho một thành phần. Tuy nhiên, trong trường hợp hiếm gặp là bạn đang tạo IndicationInstance theo cách thủ công bằng rememberUpdatedInstance, bạn cần cập nhật việc triển khai để kiểm tra xem Indication có phải là IndicationNodeFactory hay không để có thể sử dụng một cách triển khai nhẹ hơn. Ví dụ: Modifier.indication sẽ uỷ quyền nội bộ cho nút đã tạo nếu đó là IndicationNodeFactory. Nếu không, nó sẽ dùng Modifier.composed để gọi rememberUpdatedInstance.