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