Создание собственных модификаторов

Compose предоставляет множество модификаторов для распространенных вариантов поведения прямо из коробки, но вы также можете создавать свои собственные модификаторы.

Модификаторы состоят из нескольких частей:

  • Фабрика модификаторов
    • Это функция расширения Modifier , которая предоставляет идиоматический API для вашего модификатора и позволяет легко связывать модификаторы вместе. Фабрика модификаторов производит элементы-модификаторы, используемые Compose для изменения вашего пользовательского интерфейса.
  • Элемент-модификатор
    • Здесь вы можете реализовать поведение вашего модификатора.

Существует несколько способов реализации пользовательского модификатора в зависимости от необходимой функциональности. Часто самый простой способ реализовать собственный модификатор — это просто реализовать собственную фабрику модификаторов, которая объединяет вместе другие уже определенные фабрики модификаторов. Если вам нужно более индивидуальное поведение, реализуйте элемент-модификатор с помощью API-интерфейсов Modifier.Node , которые находятся на более низком уровне, но обеспечивают большую гибкость.

Соедините существующие модификаторы вместе

Часто можно создавать собственные модификаторы, просто используя существующие модификаторы. Например, Modifier.clip() реализован с использованием модификатора graphicsLayer . В этой стратегии используются существующие элементы модификаторов, и вы предоставляете свою собственную фабрику модификаторов.

Прежде чем реализовывать собственный модификатор, посмотрите, сможете ли вы использовать ту же стратегию.

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

Или, если вы обнаружите, что часто повторяете одну и ту же группу модификаторов, вы можете обернуть их в свой собственный модификатор:

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

Создайте собственный модификатор, используя составную фабрику модификаторов.

Вы также можете создать собственный модификатор, используя составную функцию для передачи значений существующему модификатору. Это известно как фабрика составных модификаторов.

Использование фабрики составных модификаторов для создания модификатора также позволяет использовать API-интерфейсы компоновки более высокого уровня, такие как animate*AsState и другие API-интерфейсы анимации с поддержкой состояния Compose . Например, в следующем фрагменте показан модификатор, который анимирует изменение альфа-канала при включении/отключении:

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

Если ваш пользовательский модификатор представляет собой удобный метод предоставления значений по умолчанию из CompositionLocal , самый простой способ реализовать это — использовать фабрику составных модификаторов:

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

Этот подход имеет некоторые предостережения, подробно описанные ниже.

Значения CompositionLocal разрешаются на месте вызова фабрики модификаторов.

При создании пользовательского модификатора с использованием фабрики составных модификаторов локальные элементы композиции берут значение из дерева композиции, в котором они созданы, а не используются. Это может привести к неожиданным результатам. Например, возьмем приведенный выше пример локального модификатора композиции, реализованный немного по-другому с использованием составной функции:

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

Если вы ожидаете, что ваш модификатор будет работать не так, используйте вместо него собственный Modifier.Node , поскольку локальные элементы композиции будут правильно разрешены на сайте использования и их можно будет безопасно поднять.

Модификаторы составных функций никогда не пропускаются.

Модификаторы составной фабрики никогда не пропускаются , поскольку составные функции, имеющие возвращаемые значения, не могут быть пропущены. Это означает, что ваша функция-модификатор будет вызываться при каждой рекомпозиции, что может оказаться дорогостоящим, если она будет часто перекомпоновываться.

Модификаторы составных функций должны вызываться внутри составной функции.

Как и все составные функции, модификатор составной фабрики должен вызываться из композиции. Это ограничивает возможности поднятия модификатора, поскольку его никогда нельзя вывести из композиции. Для сравнения, некомпонуемые фабрики модификаторов можно вывести из компонуемых функций, чтобы упростить повторное использование и повысить производительность:

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
}

Реализация пользовательского поведения модификатора с помощью Modifier.Node

Modifier.Node — это API более низкого уровня для создания модификаторов в Compose. Это тот же API, в котором Compose реализует свои собственные модификаторы, и это наиболее эффективный способ создания пользовательских модификаторов.

Реализуйте собственный модификатор с помощью Modifier.Node

Реализация пользовательского модификатора с помощью Modifier.Node состоит из трех частей:

  • Реализация Modifier.Node , которая содержит логику и состояние вашего модификатора.
  • ModifierNodeElement , который создает и обновляет экземпляры узла модификатора.
  • Дополнительная фабрика модификаторов, как описано выше.

Классы ModifierNodeElement не сохраняют состояние, и при каждой рекомпозиции выделяются новые экземпляры, тогда как классы Modifier.Node могут иметь состояние и сохраняться при нескольких рекомпозициях и даже могут использоваться повторно.

В следующем разделе описывается каждая часть и показан пример создания пользовательского модификатора для рисования круга.

Modifier.Node

Реализация Modifier.Node (в данном примере CircleNode ) реализует функциональность вашего пользовательского модификатора.

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

В этом примере он рисует круг с цветом, переданным функции-модификатору.

Узел реализует Modifier.Node а также ноль или более типов узлов. Существуют разные типы узлов в зависимости от функциональности, которая требуется вашему модификатору. В приведенном выше примере необходима возможность рисования, поэтому он реализует DrawModifierNode , который позволяет переопределить метод рисования.

Доступные типы:

Узел

Использование

Пример ссылки

LayoutModifierNode

Modifier.Node , который изменяет способ измерения и размещения упакованного содержимого.

Образец

DrawModifierNode

Modifier.Node , который рисует пространство макета.

Образец

CompositionLocalConsumerModifierNode

Реализация этого интерфейса позволяет вашему Modifier.Node читать локальные значения композиции.

Образец

SemanticsModifierNode

Modifier.Node , который добавляет семантический ключ/значение для использования в тестировании, доступности и подобных случаях использования.

Образец

PointerInputModifierNode

Modifier.Node , который получает PointerInputChanges.

Образец

ParentDataModifierNode

Modifier.Node , который предоставляет данные родительскому макету.

Образец

LayoutAwareModifierNode

Modifier.Node , который получает обратные вызовы onMeasured и onPlaced .

Образец

GlobalPositionAwareModifierNode

Modifier.Node , который получает обратный вызов onGloballyPositioned с конечными LayoutCoordinates макета, когда глобальное положение содержимого могло измениться.

Образец

ObserverModifierNode

Modifier.Node , реализующие ObserverNode могут предоставить собственную реализацию onObservedReadsChanged , которая будет вызываться в ответ на изменения в объектах моментальных снимков, считанных в блоке observeReads .

Образец

DelegatingNode

Modifier.Node , который может делегировать работу другим экземплярам Modifier.Node .

Это может быть полезно для объединения нескольких реализаций узлов в одну.

Образец

TraversableNode

Позволяет классам Modifier.Node перемещаться вверх/вниз по дереву узлов для классов одного и того же типа или для определенного ключа.

Образец

Узлы автоматически становятся недействительными при вызове обновления для соответствующего элемента. Поскольку наш пример — это DrawModifierNode , при каждом вызове обновления элемента узел запускает перерисовку, и его цвет корректно обновляется. От автоматической аннулирования можно отказаться, как описано ниже .

ModifierNodeElement

ModifierNodeElement — это неизменяемый класс, который содержит данные для создания или обновления вашего пользовательского модификатора:

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

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

Реализации ModifierNodeElement должны переопределить следующие методы:

  1. create : это функция, которая создает экземпляр вашего узла модификатора. Это вызывается для создания узла при первом применении вашего модификатора. Обычно это сводится к созданию узла и его настройке с использованием параметров, переданных в фабрику модификаторов.
  2. update : эта функция вызывается всякий раз, когда этот модификатор указан в том же месте, где этот узел уже существует, но свойство изменилось. Это определяется методом equals класса. Узел модификатора, который был создан ранее, отправляется в качестве параметра при вызове update . На этом этапе вам следует обновить свойства узлов, чтобы они соответствовали обновленным параметрам. Возможность повторного использования узлов таким образом является ключом к повышению производительности, которую дает Modifier.Node ; поэтому вам необходимо обновить существующий узел, а не создавать новый в методе update . В нашем примере с кругом обновляется цвет узла.

Кроме того, реализации ModifierNodeElement также должны реализовывать equals и hashCode . update будет вызываться только в том случае, если сравнение равенства с предыдущим элементом возвращает false.

В приведенном выше примере для достижения этой цели используется класс данных. Эти методы используются для проверки того, нуждается ли узел в обновлении или нет. Если у вашего элемента есть свойства, которые не влияют на необходимость обновления узла, или вы хотите избежать классов данных по соображениям двоичной совместимости, тогда вы можете вручную реализовать equals и hashCode например элемент модификатора заполнения .

Фабрика модификаторов

Это общедоступная поверхность API вашего модификатора. Большинство реализаций просто создают элемент-модификатор и добавляют его в цепочку модификаторов:

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

Полный пример

Эти три части объединяются для создания пользовательского модификатора для рисования круга с помощью 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)
    }
}

Распространенные ситуации с использованием Modifier.Node

При создании пользовательских модификаторов с помощью Modifier.Node вы можете столкнуться с некоторыми распространенными ситуациями.

Нулевые параметры

Если ваш модификатор не имеет параметров, то он никогда не нуждается в обновлении и, более того, не обязательно должен быть классом данных. Вот пример реализации модификатора, который применяет фиксированное количество дополнений к составному объекту:

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

Ссылки на местные жители композиции

Модификаторы Modifier.Node не отслеживают автоматически изменения в объектах состояния Compose, например CompositionLocal . Преимущество модификаторов Modifier.Node перед модификаторами, которые только что созданы с помощью составной фабрики, заключается в том, что они могут читать локальное значение композиции из того места, где модификатор используется в вашем дереве пользовательского интерфейса, а не там, где модификатор выделен, используя currentValueOf .

Однако экземпляры узла-модификатора не отслеживают автоматически изменения состояния. Чтобы автоматически реагировать на локальное изменение композиции, вы можете прочитать ее текущее значение внутри области:

В этом примере рассматривается значение LocalContentColor для рисования фона на основе его цвета. Поскольку ContentDrawScope отслеживает изменения снимка, он автоматически перерисовывается при изменении значения LocalContentColor :

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

Чтобы реагировать на изменения состояния за пределами области и автоматически обновлять модификатор, используйте ObserverModifierNode .

Например, Modifier.scrollable использует этот метод для наблюдения за изменениями LocalDensity . Упрощенный пример показан ниже:

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

Модификатор анимации

Реализации Modifier.Node имеют доступ к coroutineScope . Это позволяет использовать API Compose Animatable . Например, этот фрагмент изменяет CircleNode сверху, чтобы он неоднократно появлялся и исчезал:

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

Совместное использование состояния между модификаторами с помощью делегирования

Модификаторы Modifier.Node могут делегировать полномочия другим узлам. Для этого существует множество вариантов использования, например, для извлечения общих реализаций из разных модификаторов, но его также можно использовать для совместного использования общего состояния между модификаторами.

Например, базовая реализация кликабельного узла-модификатора, который совместно использует данные взаимодействия:

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

Отказ от автоматической аннулирования узла

Узлы Modifier.Node автоматически становятся недействительными, когда соответствующий им ModifierNodeElement вызывает update. Иногда в более сложных модификаторах вы можете отказаться от этого поведения, чтобы иметь более детальный контроль над тем, когда ваш модификатор делает фазы недействительными.

Это может быть особенно полезно, если ваш пользовательский модификатор изменяет как макет, так и отрисовку. Отказ от автоматической отмены позволяет вам просто сделать недействительным отрисовку только для свойств, связанных с отрисовкой, таких как color , изменение, но не делать недействительным макет. Это может улучшить производительность вашего модификатора.

Гипотетический пример этого показан ниже с модификатором, который имеет в качестве свойств color , size и лямбда onClick . Этот модификатор делает недействительным только то, что требуется, и пропускает любую недействительность, которая не требуется:

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