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
, который позволяет переопределить метод рисования.
Доступные типы:
Узел | Использование | Пример ссылки |
| ||
| ||
Реализация этого интерфейса позволяет вашему | ||
| ||
| ||
| ||
| ||
| ||
| ||
Это может быть полезно для объединения нескольких реализаций узлов в одну. | ||
Позволяет классам |
Узлы автоматически становятся недействительными при вызове обновления для соответствующего элемента. Поскольку наш пример — это 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
должны переопределить следующие методы:
-
create
: это функция, которая создает экземпляр вашего узла модификатора. Это вызывается для создания узла при первом применении вашего модификатора. Обычно это сводится к созданию узла и его настройке с использованием параметров, переданных в фабрику модификаторов. -
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
.
Однако экземпляры узла-модификатора не отслеживают автоматически изменения состояния. Чтобы автоматически реагировать на локальное изменение композиции, вы можете прочитать ее текущее значение внутри области:
-
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 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
. Этот модификатор делает недействительным только то, что требуется, и пропускает любую недействительность, которая не требуется:
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) } } }