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 Compose более высокого уровня, такие как 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 , поскольку локальные переменные композиции будут корректно разрешаться в месте использования и могут быть безопасно перемещены (hoisted).
Модификаторы составных функций никогда не пропускаются.
Модификаторы компонуемых фабрик никогда не пропускаются , поскольку компонуемые функции, имеющие возвращаемые значения, не могут быть пропущены. Это означает, что ваша функция-модификатор будет вызываться при каждой рекомпозиции, что может быть затратным, если рекомпозиция происходит часто.
Модификаторы составных функций должны вызываться внутри составной функции.
Как и все компонуемые функции, компонуемый фабричный модификатор должен вызываться изнутри композиции. Это ограничивает возможности перемещения модификатора, поскольку его никогда нельзя вынести за пределы композиции. В сравнении с этим, некомпозуемые фабричные модификаторы могут быть вынесены за пределы компонуемых функций, что упрощает их повторное использование и повышает производительность:
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 , что позволяет переопределять метод draw.
Доступные типы следующие:
Узел | Использование | Пример ссылки |
| ||
| ||
Реализация этого интерфейса позволяет вашему | ||
| ||
| ||
| ||
| ||
| ||
Модификаторы | ||
Это может быть полезно для объединения нескольких реализаций Node.js в одну. | ||
Позволяет классам |
Узлы автоматически аннулируются при вызове метода `update` для соответствующего им элемента. Поскольку в нашем примере используется узел DrawModifierNode , каждый раз, когда вызывается метод `update` для элемента, узел запускает перерисовку, и его цвет корректно обновляется. Можно отключить автоматическое аннулирование, как подробно описано в разделе «Отключение автоматического аннулирования узлов» .
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 необходимо переопределить следующие методы:
-
create: Эта функция создает экземпляр вашего узла-модификатора. Она вызывается для создания узла при первом применении модификатора. Обычно это сводится к построению узла и его настройке с параметрами, переданными в фабрику модификаторов. - Функция
updateвызывается всякий раз, когда этот модификатор указывается в том же месте, где уже существует этот узел, но изменяется его свойство. Это определяется методомequalsкласса. Ранее созданный узел-модификатор передается в качестве параметра вызовуupdate. На этом этапе следует обновить свойства узлов в соответствии с обновленными параметрами. Возможность повторного использования узлов таким образом является ключевым фактором повышения производительности, которое обеспечиваетModifier.Node; поэтому необходимо обновлять существующий узел, а не создавать новый в методеupdate. В нашем примере с кругом обновляется цвет узла.
Кроме того, реализации ModifierNodeElement также должны реализовывать equals и hashCode . update будет вызываться только в том случае, если сравнение с предыдущим элементом методом equals вернет false.
В приведенном выше примере для этого используется класс данных. Эти методы используются для проверки необходимости обновления узла. Если ваш элемент имеет свойства, которые не влияют на необходимость обновления узла, или вы хотите избежать использования классов данных по соображениям бинарной совместимости, вы можете вручную реализовать equals и hashCode , например, в элементе-модификаторе padding .
Фабрика модификаторов
Это публичный 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 .
Однако экземпляры узлов-модификаторов не отслеживают изменения состояния автоматически. Чтобы автоматически реагировать на изменение локального элемента композиции, можно считать его текущее значение внутри области видимости:
-
DrawModifierNode:ContentDrawScope -
LayoutModifierNode:MeasureScope&IntrinsicMeasureScope -
SemanticsModifierNode:SemanticsPropertyReceiver
В этом примере отслеживается значение параметра 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) } } }