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 bạn cách triển khai Indication hiệu quả, chẳng hạn như Ripples.

androidx.compose.foundation:foundation:1.7.0+androidx.compose.material:material-ripple:1.7.0+ bao gồm API sau các thay đổi:

Không dùng nữa

Thay thế

Indication#rememberUpdatedInstance

IndicationNodeFactory

rememberRipple()

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 hoạt động triển khai Ripples 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(); 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 đặt LocalRippleTheme trong ứng dụng, Material thành phần 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 hoạt động cũ không cần di chuyển; tuy nhiên, bạn nên chuyển sang các API mới. Cho hướng dẫn di chuyển, hãy xem 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 hàm LocalUseFallbackRippleImplementation CompositionLocal API để định cấu hình Các thành phần Material sẽ quay lại hành vi cũ:

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

Đảm bảo cung cấp phần này bên ngoài MaterialTheme để các Ripples cũ có thể được cung cấp 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 một gọi đến ripple() từ thư viện tương ứng. API này tạo ra một 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 đó, chuyển giá trị được trả về đối tượng với 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()
        )
    ) {
        // ...
    }
}

Lưu ý rằng ripple() không còn là hàm có khả năng kết hợp và không cần phải là đã nhớ. Kết quả này cũng có thể được sử dụng lại 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 việc trích xuất hoạt động tạo hiệu ứng gợn sóng thành giá trị cấp cao nhất để lưu phân bổ.

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

Nếu bạn đang triển khai hệ thống thiết kế của riêng mình và trước đó bạn đã sử dụng rememberRipple() cùng với một RippleTheme tuỳ chỉnh để định cấu hình hiệu ứng gợn sóng, 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 nút gợn sóng API được 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 của bạ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 tham gia thay đổi về hành vi

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

@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 bài viết Nâng cấp phiên bản thư viện Material mà không có di chuyển.

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

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

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

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 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ộ tất cả Ripples 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 tại toàn chủ đề. Về cơ bản, đây là một điểm tích hợp giữa thiết kế cấu trúc cục bộ và Ripples. Thay vì đưa ra thông điệp chung chung gốc chủ đề, material-ripple hiện sẽ hiển thị createRippleModifierNode() . Hàm này cho phép các thư viện hệ thống thiết kế tạo ra yêu cầu triển khai wrapper, truy vấn các giá trị giao diện của chúng rồi uỷ quyền phương thức 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ị bất kỳ các lớp giao diện bắt buộc mà người dùng có thể định cấu hình ở trên cùng mà không phải tuân theo dữ liệu được cung cấp ở lớp material-ripple. Thay đổi này cũng giúp nêu rõ chủ đề/thông số kỹ thuật mà hiệu ứng gợn sóng tuân thủ, vì đây là chính Ripples API xác định hợp đồng đó thay vì được ngầm ẩn bắt nguồn từ giao diện.

Để biết hướng dẫn, hãy xem bài viết triển khai API gợn sóng trong Material các thư viện và thay thế các lệnh gọi đến thành phần kết hợp cục bộ 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 xung quanh Indication

Nếu bạn chỉ tạo Indication để truyền, chẳng hạn như tạo một gợn sóng để chuyển đến Modifier.clickable hoặc Modifier.indication, bạn không không cần thực hiện thay đổi nào. IndicationNodeFactory kế thừa từ Indication, để 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 phương thức triển khai Indication của riêng mình, thì quá trình di chuyển sẽ đơn giản trong hầu hết các trường hợp. Ví dụ: hãy xem xét Indication áp dụng một mức độ tác động đến báo chí:

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 qua 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ó 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 chức năng đó, sau đó triển khai trực tiếp logic collectLatest 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 thu thập hiện được chuyển vào nút, đây là một nhà máy rất đơn giản mà chỉ có trách nhiệm 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 một IndicationInstance

Trong hầu hết các 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 thủ công IndicationInstance đang sử dụng rememberUpdatedInstance, bạn cần cập nhật để kiểm tra xem Indication có phải là IndicationNodeFactory hay không. có thể sử dụng 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ẽ sử dụng Modifier.composed để gọi rememberUpdatedInstance.