Tạo đối tượng sửa đổi tuỳ chỉnh

Compose cung cấp nhiều đối tượng sửa đổi cho các hành vi phổ biến ngay từ đầu, nhưng bạn cũng có thể tạo đối tượng sửa đổi tuỳ chỉnh của riêng mình.

Đối tượng sửa đổi có nhiều phần:

  • Nhà máy đối tượng sửa đổi
    • Đây là một hàm mở rộng trên Modifier, cung cấp một API thành ngữ cho đối tượng sửa đổi và cho phép liên kết các đối tượng sửa đổi với nhau. Nhà máy đối tượng sửa đổi tạo ra các phần tử đối tượng sửa đổi mà Compose sử dụng để sửa đổi giao diện người dùng.
  • Phần tử đối tượng sửa đổi
    • Đây là nơi bạn có thể triển khai hành vi của đối tượng sửa đổi.

Có nhiều cách để triển khai đối tượng sửa đổi tuỳ chỉnh, tuỳ thuộc vào chức năng cần thiết. Thông thường, cách đơn giản nhất để triển khai đối tượng sửa đổi tuỳ chỉnh là triển khai một nhà máy đối tượng sửa đổi tuỳ chỉnh kết hợp các nhà máy đối tượng sửa đổi khác đã được xác định. Nếu cần thêm hành vi tuỳ chỉnh, hãy triển khai phần tử đối tượng sửa đổi bằng API Modifier.Node. API này ở cấp thấp hơn nhưng mang lại tính linh hoạt cao hơn.

Liên kết các đối tượng sửa đổi hiện có với nhau

Bạn thường có thể tạo đối tượng sửa đổi tuỳ chỉnh bằng cách sử dụng các đối tượng sửa đổi hiện có. Ví dụ: Modifier.clip() được triển khai bằng đối tượng sửa đổi graphicsLayer. Chiến lược này sử dụng các phần tử đối tượng sửa đổi hiện có và bạn cung cấp nhà máy đối tượng sửa đổi tuỳ chỉnh của riêng mình.

Trước khi triển khai đối tượng sửa đổi tuỳ chỉnh của riêng bạn, hãy xem liệu bạn có thể sử dụng cùng một chiến lược hay không.

fun Modifier.clip(shape: Shape) = graphicsLayer(shape = shape, clip = true)

Hoặc nếu thấy thường xuyên lặp lại cùng một nhóm đối tượng sửa đổi, bạn có thể gói các đối tượng sửa đổi đó vào đối tượng sửa đổi của riêng mình:

fun Modifier.myBackground(color: Color) = padding(16.dp)
    .clip(RoundedCornerShape(8.dp))
    .background(color)

Tạo đối tượng sửa đổi tuỳ chỉnh bằng nhà máy đối tượng sửa đổi có khả năng kết hợp

Bạn cũng có thể tạo đối tượng sửa đổi tuỳ chỉnh bằng hàm composable để truyền các giá trị đến một đối tượng sửa đổi hiện có. Đây được gọi là nhà máy đối tượng sửa đổi có khả năng kết hợp.

Việc sử dụng nhà máy đối tượng sửa đổi có khả năng kết hợp để tạo đối tượng sửa đổi cũng cho phép bạn sử dụng các API Compose cấp cao hơn, chẳng hạn như animate*AsState và các API ảnh động khác được hỗ trợ bởi trạng thái Compose. Ví dụ: đoạn mã sau đây cho thấy một đối tượng sửa đổi tạo ảnh động cho sự thay đổi alpha khi được bật/tắt:

@Composable
fun Modifier.fade(enable: Boolean): Modifier {
    val alpha by animateFloatAsState(if (enable) 0.5f else 1.0f)
    return this then Modifier.graphicsLayer { this.alpha = alpha }
}

Nếu đối tượng sửa đổi tuỳ chỉnh của bạn là một phương thức tiện lợi để cung cấp các giá trị mặc định từ CompositionLocal, thì cách dễ nhất để triển khai là sử dụng nhà máy đối tượng sửa đổi có khả năng kết hợp:

@Composable
fun Modifier.fadedBackground(): Modifier {
    val color = LocalContentColor.current
    return this then Modifier.background(color.copy(alpha = 0.5f))
}

Phương pháp này có một số lưu ý, được trình bày chi tiết trong các phần sau.

Giá trị CompositionLocal được phân giải tại vị trí gọi của factory đối tượng sửa đổi

Khi tạo đối tượng sửa đổi tuỳ chỉnh bằng nhà máy đối tượng sửa đổi có khả năng kết hợp, các thành phần kết hợp cục bộ sẽ lấy giá trị từ cây thành phần kết hợp nơi chúng được tạo, chứ không phải nơi chúng được sử dụng. Điều này có thể dẫn đến kết quả không mong muốn. Ví dụ: hãy xem xét ví dụ về đối tượng sửa đổi thành phần kết hợp cục bộ đã đề cập trước đó, được triển khai hơi khác một chút bằng hàm composable:

@Composable
fun Modifier.myBackground(): Modifier {
    val color = LocalContentColor.current
    return this then Modifier.background(color.copy(alpha = 0.5f))
}

@Composable
fun MyScreen() {
    CompositionLocalProvider(LocalContentColor provides Color.Green) {
        // Background modifier created with green background
        val backgroundModifier = Modifier.myBackground()

        // LocalContentColor updated to red
        CompositionLocalProvider(LocalContentColor provides Color.Red) {

            // Box will have green background, not red as expected.
            Box(modifier = backgroundModifier)
        }
    }
}

Nếu đây không phải là cách bạn muốn đối tượng sửa đổi hoạt động, hãy sử dụng tuỳ chỉnh Modifier.Node thay vì các thành phần kết hợp cục bộ sẽ được phân giải chính xác tại vị trí sử dụng và có thể được nâng cấp một cách an toàn.

Không bao giờ bỏ qua đối tượng sửa đổi hàm composable

Không bao giờ bỏ qua đối tượng sửa đổi nhà máy có khả năng kết hợp vì không thể bỏ qua các hàm có khả năng kết hợp có giá trị trả về. Điều này có nghĩa là hàm đối tượng sửa đổi sẽ được gọi trên mọi quá trình kết hợp lại, có thể tốn kém nếu quá trình kết hợp lại diễn ra thường xuyên.

Phải gọi đối tượng sửa đổi hàm composable trong một hàm composable

Giống như tất cả các hàm có khả năng kết hợp, đối tượng sửa đổi nhà máy có khả năng kết hợp phải được gọi từ bên trong thành phần kết hợp. Điều này giới hạn vị trí mà đối tượng sửa đổi có thể được nâng cấp, vì đối tượng sửa đổi không bao giờ có thể được nâng cấp ra khỏi thành phần kết hợp. Ngược lại, các factory đối tượng sửa đổi không có khả năng kết hợp có thể được nâng cấp ra khỏi các hàm composable để cho phép sử dụng lại dễ dàng hơn và cải thiện hiệu suất:

val extractedModifier = Modifier.background(Color.Red) // Hoisted to save allocations

@Composable
fun Modifier.composableModifier(): Modifier {
    val color = LocalContentColor.current.copy(alpha = 0.5f)
    return this then Modifier.background(color)
}

@Composable
fun MyComposable() {
    val composedModifier = Modifier.composableModifier() // Cannot be extracted any higher
}

Triển khai hành vi đối tượng sửa đổi tuỳ chỉnh bằng Modifier.Node

Modifier.Node là một API cấp thấp hơn để tạo đối tượng sửa đổi trong Compose. Đây là cùng một API mà Compose triển khai các đối tượng sửa đổi của riêng mình và là cách hiệu quả nhất để tạo đối tượng sửa đổi tuỳ chỉnh.

Triển khai đối tượng sửa đổi tuỳ chỉnh bằng Modifier.Node

Có 3 phần để triển khai đối tượng sửa đổi tuỳ chỉnh bằng Modifier.Node:

  • Quá trình triển khai Modifier.Node chứa logic và trạng thái của đối tượng sửa đổi.
  • Một ModifierNodeElement tạo và cập nhật các thực thể nút đối tượng sửa đổi.
  • Nhà máy đối tượng sửa đổi không bắt buộc, như đã trình bày chi tiết trước đó.

Các lớp ModifierNodeElement không có trạng thái và các thực thể mới được phân bổ cho mỗi lần kết hợp lại, trong khi các lớp Modifier.Node có thể có trạng thái và sẽ tồn tại qua nhiều lần kết hợp lại, thậm chí có thể được sử dụng lại.

Phần sau đây mô tả từng phần và cho thấy ví dụ về cách tạo đối tượng sửa đổi tuỳ chỉnh để vẽ một vòng tròn.

Modifier.Node

Quá trình triển khai Modifier.Node (trong ví dụ này là CircleNode) triển khai chức năng của đối tượng sửa đổi tuỳ chỉnh.

// Modifier.Node
private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() {
    override fun ContentDrawScope.draw() {
        drawCircle(color)
    }
}

Trong ví dụ này, nó vẽ vòng tròn với màu được truyền vào hàm đối tượng sửa đổi.

Một nút triển khai Modifier.Node cũng như không hoặc nhiều loại nút. Có nhiều loại nút dựa trên chức năng mà đối tượng sửa đổi của bạn yêu cầu. Ví dụ trước cần có khả năng vẽ, vì vậy, nó triển khai DrawModifierNode, cho phép ghi đè phương thức vẽ.

Các loại có sẵn như sau:

Nút

Tác dụng

Đường liên kết đến mẫu

LayoutModifierNode

Modifier.Node thay đổi cách đo lường và sắp xếp bố cục nội dung được bao bọc.

Mẫu

DrawModifierNode

Modifier.Node vẽ vào không gian của bố cục.

Mẫu

CompositionLocalConsumerModifierNode

Việc triển khai giao diện này cho phép Modifier.Node đọc các thành phần kết hợp cục bộ.

Mẫu

SemanticsModifierNode

Modifier.Node thêm cặp giá trị/khoá ngữ nghĩa để sử dụng trong kiểm thử, hỗ trợ tiếp cận và các trường hợp sử dụng tương tự.

Mẫu

PointerInputModifierNode

Một Modifier.Node nhận PointerInputChanges.

Mẫu

ParentDataModifierNode

Modifier.Node cung cấp dữ liệu cho bố cục mẹ.

Mẫu

LayoutAwareModifierNode

Modifier.Node nhận các lệnh gọi lại onMeasuredonPlaced.

Mẫu

GlobalPositionAwareModifierNode

Modifier.Node nhận lệnh gọi lại onGloballyPositioned với LayoutCoordinates cuối cùng của bố cục khi vị trí toàn cầu của nội dung có thể đã thay đổi.

Mẫu

ObserverModifierNode

Modifier.Node triển khai ObserverNode có thể cung cấp quá trình triển khai onObservedReadsChanged của riêng chúng. Quá trình triển khai này sẽ được gọi để phản hồi các thay đổi đối với các đối tượng ảnh chụp nhanh được đọc trong khối observeReads.

Mẫu

DelegatingNode

Modifier.Node có thể uỷ thác công việc cho các thực thể Modifier.Node khác.

Điều này có thể hữu ích để kết hợp nhiều quá trình triển khai nút thành một.

Mẫu

TraversableNode

Cho phép các lớp Modifier.Node di chuyển lên/xuống cây nút cho các lớp cùng loại hoặc cho một khoá cụ thể.

Mẫu

Các nút sẽ tự động bị vô hiệu hoá khi lệnh gọi cập nhật được thực hiện trên phần tử tương ứng. Vì ví dụ của chúng ta là DrawModifierNode, nên mỗi khi lệnh gọi cập nhật được thực hiện trên phần tử, nút sẽ kích hoạt quá trình vẽ lại và màu của nút sẽ cập nhật chính xác. Bạn có thể chọn không tự động vô hiệu hoá, như trình bày chi tiết trong phần Chọn không tự động vô hiệu hoá nút.

ModifierNodeElement

ModifierNodeElement là một lớp bất biến chứa dữ liệu để tạo hoặc cập nhật đối tượng sửa đổi tuỳ chỉnh:

// ModifierNodeElement
private data class CircleElement(val color: Color) : ModifierNodeElement<CircleNode>() {
    override fun create() = CircleNode(color)

    override fun update(node: CircleNode) {
        node.color = color
    }
}

Quá trình triển khai ModifierNodeElement cần ghi đè các phương thức sau:

  1. create: Đây là hàm tạo thực thể cho nút đối tượng sửa đổi. Hàm này được gọi để tạo nút khi đối tượng sửa đổi của bạn được áp dụng lần đầu tiên. Thông thường, điều này tương đương với việc tạo nút và định cấu hình nút bằng các tham số đã được truyền vào nhà máy đối tượng sửa đổi.
  2. update: Hàm này được gọi bất cứ khi nào đối tượng sửa đổi này được cung cấp ở cùng một vị trí mà nút này đã tồn tại, nhưng một thuộc tính đã thay đổi. Điều này được xác định bằng phương thức equals của lớp. Nút đối tượng sửa đổi đã được tạo trước đó được gửi dưới dạng tham số đến lệnh gọi update. Tại thời điểm này, bạn nên cập nhật các thuộc tính của nút để tương ứng với các tham số đã cập nhật. Khả năng các nút được sử dụng lại theo cách này là chìa khoá để đạt được hiệu suất mà Modifier.Node mang lại; do đó, bạn phải cập nhật nút hiện có thay vì tạo một nút mới trong phương thức update. Trong ví dụ về vòng tròn của chúng ta, màu của nút được cập nhật.

Ngoài ra, quá trình triển khai ModifierNodeElement cũng cần triển khai equalshashCode. update sẽ chỉ được gọi nếu so sánh bằng với phần tử trước đó trả về giá trị false.

Ví dụ trước sử dụng một lớp dữ liệu để thực hiện việc này. Các phương thức này được dùng để kiểm tra xem có cần cập nhật nút hay không. Nếu phần tử của bạn có các thuộc tính mà không đóng góp vào việc có cần cập nhật nút hay không hoặc bạn muốn tránh các lớp dữ liệu vì lý do tương thích nhị phân, thì bạn có thể triển khai theo cách thủ công equalshashCode, ví dụ: phần tử đối tượng sửa đổi phần đệm.

Nhà máy đối tượng sửa đổi

Đây là bề mặt API công khai của đối tượng sửa đổi. Hầu hết các quá trình triển khai đều tạo phần tử đối tượng sửa đổi và thêm phần tử đó vào chuỗi đối tượng sửa đổi:

// Modifier factory
fun Modifier.circle(color: Color) = this then CircleElement(color)

Ví dụ đầy đủ

Ba phần này kết hợp với nhau để tạo đối tượng sửa đổi tuỳ chỉnh nhằm vẽ một vòng tròn bằng API Modifier.Node:

// Modifier factory
fun Modifier.circle(color: Color) = this then CircleElement(color)

// ModifierNodeElement
private data class CircleElement(val color: Color) : ModifierNodeElement<CircleNode>() {
    override fun create() = CircleNode(color)

    override fun update(node: CircleNode) {
        node.color = color
    }
}

// Modifier.Node
private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() {
    override fun ContentDrawScope.draw() {
        drawCircle(color)
    }
}

Các tình huống phổ biến khi sử dụng Modifier.Node

Khi tạo đối tượng sửa đổi tuỳ chỉnh bằng Modifier.Node, sau đây là một số tình huống phổ biến mà bạn có thể gặp phải.

Không có tham số

Nếu đối tượng sửa đổi của bạn không có tham số, thì đối tượng sửa đổi đó không bao giờ cần cập nhật và hơn nữa, không cần phải là một lớp dữ liệu. Sau đây là quá trình triển khai mẫu của một đối tượng sửa đổi áp dụng một lượng phần đệm cố định cho một thành phần kết hợp:

fun Modifier.fixedPadding() = this then FixedPaddingElement

data object FixedPaddingElement : ModifierNodeElement<FixedPaddingNode>() {
    override fun create() = FixedPaddingNode()
    override fun update(node: FixedPaddingNode) {}
}

class FixedPaddingNode : LayoutModifierNode, Modifier.Node() {
    private val PADDING = 16.dp

    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {
        val paddingPx = PADDING.roundToPx()
        val horizontal = paddingPx * 2
        val vertical = paddingPx * 2

        val placeable = measurable.measure(constraints.offset(-horizontal, -vertical))

        val width = constraints.constrainWidth(placeable.width + horizontal)
        val height = constraints.constrainHeight(placeable.height + vertical)
        return layout(width, height) {
            placeable.place(paddingPx, paddingPx)
        }
    }
}

Tham chiếu đến các thành phần kết hợp cục bộ

Đối tượng sửa đổi Modifier.Node không tự động quan sát các thay đổi đối với các đối tượng trạng thái Compose, chẳng hạn như CompositionLocal. Ưu điểm mà đối tượng sửa đổi Modifier.Node có so với các đối tượng sửa đổi chỉ được tạo bằng factory có khả năng kết hợp là chúng có thể đọc giá trị của thành phần kết hợp cục bộ từ nơi đối tượng sửa đổi được sử dụng trong cây giao diện người dùng, chứ không phải nơi đối tượng sửa đổi được phân bổ, bằng cách sử dụng currentValueOf.

Tuy nhiên, các thực thể nút đối tượng sửa đổi không tự động quan sát các thay đổi về trạng thái. Để tự động phản ứng với việc thay đổi thành phần kết hợp cục bộ, bạn có thể đọc giá trị hiện tại của thành phần đó trong một phạm vi:

Ví dụ này quan sát giá trị của LocalContentColor để vẽ nền dựa trên màu của giá trị đó. Vì ContentDrawScope quan sát các thay đổi về ảnh chụp nhanh, nên điều này sẽ tự động vẽ lại khi giá trị của LocalContentColor thay đổi:

class BackgroundColorConsumerNode :
    Modifier.Node(),
    DrawModifierNode,
    CompositionLocalConsumerModifierNode {
    override fun ContentDrawScope.draw() {
        val currentColor = currentValueOf(LocalContentColor)
        drawRect(color = currentColor)
        drawContent()
    }
}

Để phản ứng với các thay đổi về trạng thái bên ngoài phạm vi và tự động cập nhật đối tượng sửa đổi, hãy sử dụng ObserverModifierNode.

Ví dụ: Modifier.scrollable sử dụng kỹ thuật này để quan sát các thay đổi trong LocalDensity. Ví dụ đơn giản được minh hoạ trong ví dụ sau:

class ScrollableNode :
    Modifier.Node(),
    ObserverModifierNode,
    CompositionLocalConsumerModifierNode {

    // Place holder fling behavior, we'll initialize it when the density is available.
    val defaultFlingBehavior = DefaultFlingBehavior(splineBasedDecay(UnityDensity))

    override fun onAttach() {
        updateDefaultFlingBehavior()
        observeReads { currentValueOf(LocalDensity) } // monitor change in Density
    }

    override fun onObservedReadsChanged() {
        // if density changes, update the default fling behavior.
        updateDefaultFlingBehavior()
    }

    private fun updateDefaultFlingBehavior() {
        val density = currentValueOf(LocalDensity)
        defaultFlingBehavior.flingDecay = splineBasedDecay(density)
    }
}

Tạo ảnh động cho đối tượng sửa đổi

Quá trình triển khai Modifier.Node có quyền truy cập vào coroutineScope. Điều này cho phép sử dụng các API có khả năng tạo ảnh động trong Compose. Ví dụ: đoạn mã này sửa đổi CircleNode đã hiển thị trước đó để mờ dần và lặp lại nhiều lần:

class CircleNode(var color: Color) : Modifier.Node(), DrawModifierNode {
    private lateinit var alpha: Animatable<Float, AnimationVector1D>

    override fun ContentDrawScope.draw() {
        drawCircle(color = color, alpha = alpha.value)
        drawContent()
    }

    override fun onAttach() {
        alpha = Animatable(1f)
        coroutineScope.launch {
            alpha.animateTo(
                0f,
                infiniteRepeatable(tween(1000), RepeatMode.Reverse)
            ) {
            }
        }
    }
}

Chia sẻ trạng thái giữa các đối tượng sửa đổi bằng cách uỷ thác

Đối tượng sửa đổi Modifier.Node có thể uỷ thác cho các nút khác. Có nhiều trường hợp sử dụng cho việc này, chẳng hạn như trích xuất các quá trình triển khai phổ biến trên nhiều đối tượng sửa đổi, nhưng cũng có thể dùng để chia sẻ trạng thái phổ biến trên các đối tượng sửa đổi.

Ví dụ: quá trình triển khai cơ bản của một nút đối tượng sửa đổi có thể nhấp chia sẻ dữ liệu tương tác:

class ClickableNode : DelegatingNode() {
    val interactionData = InteractionData()
    val focusableNode = delegate(
        FocusableNode(interactionData)
    )
    val indicationNode = delegate(
        IndicationNode(interactionData)
    )
}

Chọn không tự động vô hiệu hoá nút

Các nút Modifier.Node tự động bị vô hiệu hoá khi ModifierNodeElement tương ứng gọi lệnh cập nhật. Đối với các đối tượng sửa đổi phức tạp, bạn có thể muốn chọn không sử dụng hành vi này để kiểm soát chi tiết hơn thời điểm đối tượng sửa đổi của bạn vô hiệu hoá các giai đoạn.

Điều này đặc biệt hữu ích nếu đối tượng sửa đổi tuỳ chỉnh của bạn sửa đổi cả bố cục và bản vẽ. Việc chọn không tự động vô hiệu hoá cho phép bạn chỉ vô hiệu hoá bản vẽ khi chỉ các thuộc tính liên quan đến bản vẽ, chẳng hạn như color, thay đổi. Điều này giúp tránh vô hiệu hoá bố cục và có thể cải thiện hiệu suất của đối tượng sửa đổi.

Ví dụ giả định về điều này được minh hoạ trong ví dụ sau với một đối tượng sửa đổi có hàm lambda color, sizeonClick làm thuộc tính. Đối tượng sửa đổi này chỉ vô hiệu hoá những gì cần thiết, bỏ qua mọi quá trình vô hiệu hoá không cần thiết:

class SampleInvalidatingNode(
    var color: Color,
    var size: IntSize,
    var onClick: () -> Unit
) : DelegatingNode(), LayoutModifierNode, DrawModifierNode {
    override val shouldAutoInvalidate: Boolean
        get() = false

    private val clickableNode = delegate(
        ClickablePointerInputNode(onClick)
    )

    fun update(color: Color, size: IntSize, onClick: () -> Unit) {
        if (this.color != color) {
            this.color = color
            // Only invalidate draw when color changes
            invalidateDraw()
        }

        if (this.size != size) {
            this.size = size
            // Only invalidate layout when size changes
            invalidateMeasurement()
        }

        // If only onClick changes, we don't need to invalidate anything
        clickableNode.update(onClick)
    }

    override fun ContentDrawScope.draw() {
        drawRect(color)
    }

    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {
        val size = constraints.constrain(size)
        val placeable = measurable.measure(constraints)
        return layout(size.width, size.height) {
            placeable.place(0, 0)
        }
    }
}