Cómo crear modificadores personalizados

Compose proporciona muchos modificadores para comportamientos comunes desde el primer momento. pero también puedes crear tus propios modificadores personalizados.

Los modificadores tienen varias partes:

  • Una fábrica de modificadores
    • Esta es una función de extensión de Modifier, que proporciona una API idiomática para tu modificador y permite que los modificadores se encadenan fácilmente entre sí. El produce los elementos modificadores que usa Compose para modificar tu IU.
  • Un elemento modificador
    • Aquí es donde puedes implementar el comportamiento de tu modificador.

Hay varias formas de implementar un modificador personalizado según el las funciones necesarias. A menudo, la forma más fácil de implementar un modificador personalizado es para implementar una fábrica de modificador personalizado que combina otros de modificador de variable en conjunto. Si necesitas un comportamiento más personalizado, implementa con las APIs de Modifier.Node, que son de un nivel inferior, pero proporcionar más flexibilidad.

Encadena los modificadores existentes

A menudo, es posible crear modificadores personalizados solo usando modificadores. Por ejemplo, Modifier.clip() se implementa con el elemento graphicsLayer. Esta estrategia usa elementos modificadores existentes proporciona tu propia fábrica de modificadores personalizado.

Antes de implementar su propio modificador personalizado, compruebe si puede usar el mismo de administración de amenazas.

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

O, si descubres que estás repitiendo el mismo grupo de modificadores con frecuencia, puedes unirlos en tu propio modificador:

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

Cómo crear un modificador personalizado con una fábrica de modificadores componible

También puedes crear un modificador personalizado con una función de componibilidad para pasar valores en un modificador existente. Esto se conoce como fábrica de modificadores componibles.

El uso de una fábrica de modificadores componible para crear un modificador también permite usar APIs de Compose de nivel superior, como animate*AsState y otras APIs de Compose las APIs de Animation con copia de estado. Por ejemplo, el siguiente fragmento muestra un modificador que anima un cambio alfa cuando está habilitado o inhabilitado:

@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 }
}

Si tu modificador personalizado es un método conveniente para proporcionar valores predeterminados desde un CompositionLocal, la forma más fácil de implementar esto es usar un elemento componible modificador de fábrica:

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

Este enfoque tiene algunas advertencias que se detallan a continuación.

Los valores de CompositionLocal se resuelven en el sitio de llamadas de la fábrica del modificador

Cuando se crea un modificador personalizado con una fábrica de modificadores componibles, la composición los lugareños toman el valor del árbol de composición del lugar donde se crean, no que se usan. Esto puede generar resultados inesperados. Por ejemplo, tomemos la composición de modificador local del ejemplo anterior, implementado de forma ligeramente diferente mediante un función de componibilidad:

@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)
        }
    }
}

Si no es como esperas que funcione tu modificador, usa un Modifier.Node en su lugar, ya que se mostrarán las configuraciones locales de composición correctamente en el sitio de uso y que se puedan elevar de manera segura.

Los modificadores de funciones de componibilidad nunca se omiten

Los modificadores de fábrica que admiten composición nunca se omiten porque estas funciones que tienen valores de retorno no se pueden omitir. Esto significa que tu función modificadora se llamará en cada recomposición, lo que puede ser costoso si se recompone con frecuencia.

Los modificadores de funciones de componibilidad se deben llamar dentro de una función del mismo tipo

Como todas las funciones de componibilidad, se debe llamar a un modificador de fábrica componible dentro de la composición. Esto limita a dónde se puede elevar un modificador, ya que nunca se elevará fuera de la composición. En cambio, los modificadores que no admiten composición las fábricas se puedan elevar fuera de las funciones de componibilidad para facilitar la reutilización y mejorar el rendimiento:

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
}

Implementa el comportamiento del modificador personalizado con Modifier.Node

Modifier.Node es una API de nivel inferior para crear modificadores en Compose. Integra es la misma API en la que Compose implementa sus propios modificadores y es la más y eficaz para crear modificadores personalizados.

Cómo implementar un modificador personalizado con Modifier.Node

Existen tres partes para implementar un modificador personalizado con Modifier.Node:

  • Una implementación de Modifier.Node que contiene la lógica y estado de tu modificador.
  • Un ModifierNodeElement que crea y actualiza un modificador las instancias de nodo.
  • Una fábrica de modificador opcional, como se detalla más arriba

Las clases ModifierNodeElement no tienen estado y se asignan instancias nuevas cada una de recomposición, mientras que las clases Modifier.Node pueden ser con estado y sobrevivirán entre varias recomposiciones y que pueden reutilizarse.

En la siguiente sección, se describe cada parte y se muestra un ejemplo de creación de un modificador personalizado para dibujar un círculo.

Modifier.Node

La implementación de Modifier.Node (en este ejemplo, CircleNode) implementa el del modificador personalizado.

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

En este ejemplo, dibuja el círculo con el color que se pasó al modificador .

Un nodo implementa Modifier.Node, así como cero o más tipos de nodos. Existen diferentes tipos de nodos según la funcionalidad que requiera tu modificador. El del ejemplo anterior debe poder dibujar, por lo que implementa DrawModifierNode, que le permite anular el método de dibujo.

Los tipos disponibles son los siguientes:

Nodo

Uso

Vínculo de muestra

LayoutModifierNode

Es un objeto Modifier.Node que cambia la forma en que se mide y presenta su contenido unido.

Ejemplo

DrawModifierNode

Un objeto Modifier.Node que se dibuja en el espacio del diseño

Ejemplo

CompositionLocalConsumerModifierNode

Si implementas esta interfaz, tu Modifier.Node podrá leer los elementos locales de composición.

Ejemplo

SemanticsModifierNode

Un Modifier.Node que agrega un par clave-valor de semántica para usar en pruebas, accesibilidad y casos de uso similares

Ejemplo

PointerInputModifierNode

Un Modifier.Node que reciba PointerInputChanges.

Ejemplo

ParentDataModifierNode

Es un Modifier.Node que proporciona datos al diseño de nivel superior.

Ejemplo

LayoutAwareModifierNode

Un Modifier.Node que recibe devoluciones de llamada de onMeasured y onPlaced.

Ejemplo

GlobalPositionAwareModifierNode

Un Modifier.Node que recibe una devolución de llamada onGloballyPositioned con el LayoutCoordinates final del diseño cuando es posible que haya cambiado la posición global del contenido.

Ejemplo

ObserverModifierNode

Los elementos Modifier.Node que implementan ObserverNode pueden proporcionar su propia implementación de onObservedReadsChanged, a la que se llamará en respuesta a los cambios en los objetos de instantáneas leídos dentro de un bloque observeReads.

Ejemplo

DelegatingNode

Un Modifier.Node que puede delegar trabajo a otras instancias de Modifier.Node

Esto puede ser útil para integrar varias implementaciones de nodos en una.

Ejemplo

TraversableNode

Permite que las clases Modifier.Node se desplacen hacia arriba y hacia abajo en el árbol de nodos para clases del mismo tipo o para una clave en particular.

Ejemplo

Los nodos se invalidan automáticamente cuando se llama a la actualización en su respectivo . Como nuestro ejemplo es un DrawModifierNode, cada vez que se llama a una actualización elemento, el nodo activa un nuevo diseño y su color se actualiza correctamente. Sí Puedes inhabilitar la invalidación automática, como se detalla a continuación.

ModifierNodeElement

Un ModifierNodeElement es una clase inmutable que contiene los datos para crear o actualiza tu modificador personalizado:

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

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

Las implementaciones de ModifierNodeElement deben anular los siguientes métodos:

  1. create: Esta es la función que crea una instancia de tu nodo modificador. Esto se vuelve que se llama para crear el nodo cuando se aplica el modificador por primera vez. Por lo general, este equivale a construir el nodo y configurarlo con los parámetros que se pasaron a la fábrica del modificador.
  2. update: Se llama a esta función cada vez que se proporciona este modificador en el en el mismo lugar en el que ya existe este nodo, pero cambió una propiedad. Este es determinados por el método equals de la clase. El nodo modificador que se creado con anterioridad se envía como parámetro a la llamada update. En este punto, deberías actualizar los nodos para que se correspondan con la parámetros. La capacidad de que los nodos se reutilicen de esta manera es clave para mejoras en el rendimiento que ofrece Modifier.Node; por lo que debes actualizar nodo existente, en lugar de crear uno nuevo en el método update. En nuestra ejemplo de círculo, se actualiza el color del nodo.

Además, las implementaciones de ModifierNodeElement también deben implementar equals y hashCode. Solo se llamará a update si una comparación es igual a la el elemento anterior muestra un valor falso.

En el ejemplo anterior, se usa una clase de datos para lograrlo. Estos métodos se usan para verificar si un nodo necesita actualizarse o no. Si el elemento tiene propiedades que no contribuyan a si un nodo debe actualizarse, o si desea evitar por motivos de compatibilidad binaria, puedes implementar equals de forma manual. y hashCode, p.ej., el elemento modificador de padding.

Fábrica de modificadores

Esta es la superficie de la API pública de tu modificador. En la mayoría de las implementaciones Crea el elemento modificador y agrégalo a la cadena de modificadores:

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

Ejemplo completo

Estas tres partes se unen para crear el modificador personalizado para dibujar un círculo con las APIs de 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)
    }
}

Situaciones comunes con Modifier.Node

Cuando creas modificadores personalizados con Modifier.Node, estas son algunas situaciones comunes en las que encuentran.

Cero parámetros

Si tu modificador no tiene parámetros, nunca necesita actualizarse. además, no necesita ser una clase de datos. Este es un ejemplo de implementación de un modificador que aplica una cantidad fija de padding a un elemento componible:

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)
        }
    }
}

Haz referencia a los elementos locales de composición

Los modificadores Modifier.Node no observan automáticamente los cambios en el estado de Compose. objetos, como CompositionLocal. La ventaja de los modificadores Modifier.Node que tienen más de modificadores que se acaban de crear con una fábrica componible es que pueden leer El valor de la composición local desde el que se usa el modificador en tu IU árbol, no donde se asigna el modificador, con currentValueOf.

Sin embargo, las instancias de nodo modificador no observan automáticamente los cambios de estado. Para reaccionar automáticamente a un cambio local de composición, puedes leer su dentro de un alcance:

En este ejemplo, se observa el valor de LocalContentColor para dibujar un fondo basado en en su color. Como ContentDrawScope observa cambios en las instantáneas, esta vuelve a dibujar automáticamente cuando cambia el valor de LocalContentColor:

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

Para reaccionar a los cambios de estado fuera de un permiso y actualizar automáticamente tu usa un ObserverModifierNode.

Por ejemplo, Modifier.scrollable usa esta técnica para observar los cambios en LocalDensity. A continuación, se muestra un ejemplo simplificado:

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)
    }
}

Modificador de animación

Las implementaciones de Modifier.Node tienen acceso a un coroutineScope. Esto permite que Usar las APIs de Compose Animatable Por ejemplo, este fragmento modifica CircleNode desde arriba para aplicar el fundido de entrada y salida de forma repetida:

class CircleNode(var color: Color) : Modifier.Node(), DrawModifierNode {
    private val alpha = Animatable(1f)

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

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

Comparte el estado entre modificadores por medio de la delegación

Los modificadores Modifier.Node pueden delegar a otros nodos. Hay muchos casos de uso para esto, como extraer implementaciones comunes en diferentes modificadores pero también se puede usar para compartir un estado común entre los modificadores.

Por ejemplo, una implementación básica de un nodo modificador en el que se puede hacer clic que comparte Datos de interacción:

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

Inhabilita la invalidación automática de nodos

Modifier.Node nodo se invalida automáticamente cuando su correspondiente Se actualizaron las llamadas de ModifierNodeElement. A veces, en un modificador más complejo, puedes desean inhabilitar este comportamiento para tener un control más detallado sobre cuándo tu modificador invalida fases.

Esto puede ser especialmente útil si tu modificador personalizado modifica el diseño y dibujar. Desactivar la invalidación automática te permite invalidar el dibujo cuando Solo las propiedades relacionadas con el dibujo, como color, cambian y no invalidan el diseño. Esto puede mejorar el rendimiento de tu modificador.

A continuación, se muestra un ejemplo hipotético con un modificador que tiene un color, size y la lambda onClick como propiedades. Este modificador solo invalida lo que es obligatorio y omite cualquier invalidación que no cumpla con los siguientes requisitos:

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)
        }
    }
}