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

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 перемещаться вверх/вниз по дереву узлов для классов того же типа или для определенного ключа.

Образец

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

ModifierNodeElement

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

// ModifierNodeElement
private data class CircleElement(val color: Color) : ModifierNodeElement() {
    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() {
    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() {
    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 обновляются. Иногда, в более сложном модификаторе, вы можете захотеть отказаться от этого поведения, чтобы иметь более тонкий контроль над тем, когда ваш модификатор делает недействительными фазы.

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

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

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