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+ có 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 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

Either:

  • Dùng các API RippleConfiguration của thư viện Material hoặc
  • Tạo phương thức triển khai hiệu ứng gợn sóng hệ thống thiết kế của riêng bạn

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

Thay đổi về hành vi

Các phiên bản thư viện sau đây có sự 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(); thay vào đó, chúng sử dụng các API gợn sóng mới. Do đó, chúng không truy vấn LocalRippleTheme. Do đó, nếu bạn thiết lập LocalRippleTheme trong ứng dụng, các thành phần Material sẽ không sử dụng các giá trị này.

Phần sau đây mô tả cách tạm thời quay lại hành vi cũ mà không di chuyển; tuy nhiên, bạn nên chuyển sang các API mới. Để biết hướng dẫn di chuyển, hãy xem phần Di chuyển từ rememberRipple sang ripple và các phần tiếp theo.

Nâng cấp phiên bản thư viện Material mà không cần di chuyển

Để bỏ chặn việc nâng cấp các phiên bản thư viện, bạn có thể sử dụng API LocalUseFallbackRippleImplementation CompositionLocal tạm thời để định cấu hình các thành phần Material nhằm quay lại hành vi cũ:

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

Hãy nhớ cung cấp phương thức này bên ngoài MaterialTheme để có thể cung cấp các hiệu ứng gợn sóng cũ thông qua LocalIndication.

Các phần sau đây mô tả cách chuyển sang 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 thư viện Material, hãy trực tiếp thay thế rememberRipple() bằng lệnh gọi đến ripple() từ thư viện tương ứng. API này tạo hiệu ứng gợn sóng bằng cách sử dụ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 để:

@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 được ghi nhớ. Bạn cũng có thể sử dụng lại phương thức này trên nhiều thành phần, tương tự như đối tượng sửa đổi. Vì vậy, hãy cân nhắc trích xuất hoạt động tạo hiệu ứng gợn sóng cho một giá trị cấp cao nhất để lưu mức 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 đó đã dùng rememberRipple() cùng với RippleTheme tuỳ chỉnh để định cấu hình hiệu ứng gợn sóng, thì bạn nên cung cấp API gợn sóng của riêng mình để uỷ quyền các API nút gợn sóng hiển thị trong material-ripple. Sau đó, các thành phần có thể sử dụng hiệu ứng gợn sóng của riêng bạn để 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 khỏiRippleTheme.

Di chuyển từ RippleTheme

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

Thư viện Material có một CompositionLocal tạm thời là LocalUseFallbackRippleImplementation mà bạn có thể sử dụng để định cấu hình tất 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 xây dựng dựa trên Material, thì bạn có thể cung cấp cấu trúc cục bộ một cách an toàn dưới dạng một phần giao diện của ứng dụng:

@OptIn(ExperimentalMaterialApi::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 cụ thể

Thư viện materialmaterial3 hiển thị RippleConfigurationLocalRippleConfiguration, cho phép bạn định cấu hình giao diện của các hiệu ứng gợn sóng trong cây con. Xin lưu ý rằng RippleConfigurationLocalRippleConfiguration đang trong giai đoạn thử nghiệm và chỉ dành cho mục đích tuỳ chỉnh theo từng thành phần. Các API này không hỗ trợ tuỳ chỉnh toàn cục/giao diện; hãy xem bài viết 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 để:

@OptIn(ExperimentalMaterialApi::class)
private val DisabledRippleConfiguration =
    RippleConfiguration(isEnabled = false)

// ...
    CompositionLocalProvider(LocalRippleConfiguration provides DisabledRippleConfiguration) {
        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 cụ thể

Như mô tả trong phần trước, RippleConfigurationLocalRippleConfiguration là các API thử nghiệm và chỉ dành để tuỳ chỉnh theo từng 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 để:

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

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

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

Trước đây, bạn có thể sử dụng LocalRippleTheme để xác định hành vi gợn sóng ở cấp độ toàn 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ì để lộ giao diện nguyên gốc chung, material-ripple giờ đây 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 cách triển khai wrapper có thứ tự cao hơn, truy vấn các giá trị giao diện của các thư viện đó 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ế truy vấn trực tiếp những gì chúng cần và hiển thị mọi lớp giao diện cần thiết mà người dùng có thể định cấu hình ở trên cùng mà không cần tuân theo nội dung được cung cấp ở lớp material-ripple. Thay đổi này cũng làm rõ hơn giao diện/thông số kỹ thuật mà hiệu ứng gợn sóng đang tuân theo, vì chính API gợn sóng xác định hợp đồng đó, thay vì được ngầm nguồn gốc từ giao diện.

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

Di chuyển từ Indication sang IndicationNodeFactory

Đi qua Indication

Nếu chỉ tạo Indication để truyền, 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ự triển khai Indication, thì trong hầu hết trường hợp, quá trình di chuyển sẽ đơn giản. Ví dụ: hãy xem xét một Indication áp dụng hiệu ứng tỷ lệ 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 thẻ 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ị 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 trực tiếp 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 để:

    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 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 và trách nhiệm duy nhất của nó 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 để:

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

Sử dụng Indication để tạo IndicationInstance

Trong hầu hết trường hợp, bạn nên sử 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 hoi là bạn đang tạo IndicationInstance theo cách thủ công bằng rememberUpdatedInstance, bạn cần cập nhật phương thức triển khai để kiểm tra xem Indication có phải là IndicationNodeFactory hay không để có thể sử dụng phương thức triển khai đơn giản 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, lệnh này sẽ sử dụng Modifier.composed để gọi rememberUpdatedInstance.