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

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 анимации, основанные на состоянии компоновки . Например, в следующем фрагменте показан модификатор, который анимирует изменение альфа-канала при включении/отключении:

@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 будет вызван только в том случае, если сравнение equals с предыдущим элементом вернет false.

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

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

Это публичная 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 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)
            ) {
            }
        }
    }
}

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

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

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

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

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

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

Это может быть особенно полезно, если ваш пользовательский модификатор изменяет и макет, и отрисовку. Отказ от автоматической аннулирования позволяет аннулировать только свойства, связанные с отрисовкой, например, 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)
        }
    }
}