В 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)
Как создать собственный модификатор с помощью фабрики composable-модификаторов
Вы также можете создать собственный модификатор, используя composable-функцию, чтобы передавать значения в существующий модификатор. Это называется фабрикой модификаторов composable-функций.
Использование фабрики composable-модификаторов для создания модификатора также позволяет использовать 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-функций:
@Composable fun Modifier.fadedBackground(): Modifier { val color = LocalContentColor.current return this then Modifier.background(color.copy(alpha = 0.5f)) }
У этого подхода есть некоторые особенности, которые подробно описаны в следующих разделах.
Значения CompositionLocal разрешаются на сайте вызова фабрики модификаторов.
При создании пользовательского модификатора с помощью фабрики составных модификаторов локальные переменные композиции принимают значение из дерева композиции, в котором они созданы, а не используются. Это может привести к неожиданным результатам. Например, рассмотрим пример модификатора CompositionLocal, упомянутый ранее, реализованный немного иначе с использованием composable-функции:
@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. В этом случае локальные переменные композиции будут правильно разрешены на сайте использования и могут быть безопасно подняты.
Модификаторы composable-функций никогда не пропускаются.
Модификаторы фабрики composable-функций никогда не пропускаются, потому что composable-функции, которые возвращают значения, не могут быть пропущены. Это означает, что ваша функция-модификатор будет вызываться при каждой рекомпозиции, что может быть дорого, если она рекомпонуется часто.
Модификаторы composable-функций должны вызываться внутри composable-функции.
Как и все composable-функции, фабричный модификатор должен вызываться из композиции. Это ограничивает область, в которую можно поднять модификатор, поскольку его нельзя поднять за пределы композиции. Для сравнения: фабрики некомпонуемых модификаторов можно вынести за пределы компонуемых функций, чтобы упростить повторное использование и повысить производительность.
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
There are three parts to implementing a custom modifier using 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.
Доступны следующие типы:
Узел |
Использование |
Пример ссылки |
|
||
|
||
Реализация этого интерфейса позволяет вашему |
||
|
||
|
||
|
||
Объект |
||
|
||
|
||
Это может быть полезно для объединения нескольких реализаций узлов в одну. |
||
Позволяет классам |
Узлы автоматически становятся недействительными, когда для соответствующего элемента вызывается обновление. Поскольку в нашем примере используется 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 будет вызываться, только если сравнение с предыдущим элементом возвращает 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.
Ноль параметров
Если у вашего модификатора нет параметров, то его никогда не нужно обновлять и, кроме того, он не должен быть классом данных. Ниже приведен пример реализации модификатора, который применяет фиксированный отступ к composable-функции.
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 перед модификаторами, созданными с помощью фабрики composable-функций, заключается в том, что они могут считывать значение локальной композиции из того места, где модификатор используется в дереве пользовательского интерфейса, а не там, где он выделен, с помощью currentValueOf.
Однако экземпляры узлов-модификаторов не отслеживают изменения состояния автоматически. Чтобы автоматически реагировать на изменение локальной композиции, вы можете прочитать ее текущее значение в области видимости:
DrawModifierNode:ContentDrawScopeLayoutModifierNode:MeasureScopeиIntrinsicMeasureScopeSemanticsModifierNode: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) } } }