맞춤 수정자 만들기

Compose는 일반적인 동작을 위한 많은 수정자를 즉시 제공합니다. 맞춤 수정자를 직접 만들 수도 있습니다.

수정자는 여러 부분으로 구성됩니다.

  • 수정자 팩토리 <ph type="x-smartling-placeholder">
      </ph>
    • Modifier의 확장 함수로, 관용적인 API를 제공합니다. 를 사용하면 수정자를 쉽게 연결할 수 있습니다. 이 수정자 팩토리는 Compose에서 수정하는 데 사용하는 수정자 요소를 생성합니다. 있습니다.
  • 수정자 요소 <ph type="x-smartling-placeholder">
      </ph>
    • 여기에서 수정자의 동작을 구현할 수 있습니다.

맞춤 수정자를 구현하는 방법에는 필요한 기능을 제공합니다 맞춤 수정자를 구현하는 가장 쉬운 방법은 그냥 이미 정의된 다른 수정자 팩토리를 결합하는 맞춤 수정자 팩토리를 구현하기 위해 팩토리를 함께 사용합니다. 맞춤 동작이 더 필요한 경우 하위 수준이지만 Modifier.Node API를 사용하여 수정자 요소를 더 많은 유연성을 제공합니다

기존 수정자를 함께 체이닝

맞춤 수정자를 만드는 것이 가능한 경우가 많습니다. 특수키를 사용합니다. 예를 들어 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)

구성 가능한 수정자 팩토리를 사용하여 맞춤 수정자 만들기

구성 가능한 함수를 사용하여 맞춤 수정자를 만들어 값을 전달할 수도 있습니다. 추가할 수 있습니다. 이를 구성 가능한 수정자 팩토리라고 합니다.

구성 가능한 수정자 팩토리를 사용하여 수정자를 만들면 상위 수준 Compose API(예: animate*AsState 및 기타 Compose) 상태 지원 애니메이션 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는 Compose에서 수정자를 만들기 위한 하위 수준 API입니다. 그것은 은 Compose가 자체 수정자를 구현하는 것과 동일한 API이며 맞춤 수정자를 만드는 효율적인 방법입니다.

Modifier.Node를 사용하여 맞춤 수정자 구현

Modifier.Node를 사용하여 맞춤 수정자를 구현하는 것은 세 부분으로 구성됩니다.

  • 로직을 보유하는 Modifier.Node 구현 상태입니다.
  • 수정자를 만들고 업데이트하는 ModifierNodeElement 노드 인스턴스입니다
  • 위에 설명된 대로 선택적 수정자 팩토리입니다.

ModifierNodeElement 클래스는 스테이트리스(Stateless) 클래스이며 각각 새 인스턴스가 할당됨 리컴포지션: Modifier.Node 클래스는 스테이트풀(Stateful)이고 여러 리컴포지션에 걸쳐 사용할 수 있으며 재사용도 가능합니다.

다음 섹션에서는 각 부분을 설명하고 맞춤 수정자를 사용하여 원을 그리는 것입니다.

Modifier.Node

Modifier.Node 구현 (이 예에서는 CircleNode)은 맞춤 수정자의 기능입니다.

// Modifier.Node
private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() {
    override fun ContentDrawScope.draw() {
        drawCircle(color)
    }
}

이 예에서는 수정자에 전달된 색상으로 원을 그립니다. 함수를 사용하세요.

노드는 Modifier.Node 외에도 0개 이상의 노드 유형을 구현합니다. 현재 수정자에 필요한 기능에 따라 다양한 노드 유형을 지원합니다. 이 위의 예는 그릴 수 있어야 하므로 DrawModifierNode를 구현합니다. 그리기 메서드를 재정의할 수 있습니다.

사용 가능한 유형은 다음과 같습니다.

노드

사용 정보

샘플 링크

LayoutModifierNode

래핑된 콘텐츠가 측정되고 배치되는 방식을 변경하는 Modifier.Node입니다.

샘플

DrawModifierNode

레이아웃 공간에 그리는 Modifier.Node

샘플

CompositionLocalConsumerModifierNode

이 인터페이스를 구현하면 Modifier.Node가 컴포지션 로컬을 읽을 수 있습니다.

샘플

SemanticsModifierNode

테스트, 접근성, 유사한 사용 사례에 사용할 시맨틱 키/값을 추가하는 Modifier.Node입니다.

샘플

PointerInputModifierNode

PointerInputChanges.를 수신하는 Modifier.Node

샘플

ParentDataModifierNode

상위 요소 레이아웃에 데이터를 제공하는 Modifier.Node

샘플

LayoutAwareModifierNode

onMeasuredonPlaced 콜백을 수신하는 Modifier.Node

샘플

GlobalPositionAwareModifierNode

콘텐츠의 전역 위치가 변경되었을 수 있을 때 레이아웃의 마지막 LayoutCoordinates가 있는 onGloballyPositioned 콜백을 수신하는 Modifier.Node입니다.

샘플

ObserverModifierNode

ObserverNode를 구현하는 Modifier.NodeobserveReads 블록 내에서 읽은 스냅샷 객체의 변경에 응답하여 호출되는 자체 onObservedReadsChanged 구현을 제공할 수 있습니다.

샘플

DelegatingNode

다른 Modifier.Node 인스턴스에 작업을 위임할 수 있는 Modifier.Node

이는 여러 노드 구현을 하나로 구성하는 데 유용할 수 있습니다.

샘플

TraversableNode

Modifier.Node 클래스가 동일한 유형의 클래스 또는 특정 키의 노드 트리를 위/아래로 순회할 수 있도록 합니다.

샘플

노드는 해당 노드에서 업데이트가 호출될 때 자동으로 무효화됩니다. 요소가 포함됩니다. 이 예는 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 구현은 다음 메서드를 재정의해야 합니다.

  1. create: 수정자 노드를 인스턴스화하는 함수입니다. 이 경우 다음 결과가 발생합니다. 수정자가 처음 적용될 때 노드를 만들기 위해 호출됩니다. 일반적으로 노드를 구성하고 이를 위해 필요한 파라미터를 사용하여 수정자 팩토리로 전달되었습니다.
  2. update: 이 함수는 이 수정자가 제공될 때마다 호출됩니다. 동일한 스팟이지만 속성이 변경되었습니다 이것은 클래스의 equals 메서드에 의해 결정됩니다. 이전에 만든 수정자 노드 update 호출에 매개변수로 전송됩니다. 이 시점에서 노드의 기본 포드를 업데이트된 매개변수입니다. 이러한 방식으로 노드를 재사용할 수 있는 기능은 Modifier.Node가 제공하는 성능 향상 따라서 기존 노드의 update 메서드에서 새 노드를 만듭니다. Google의 노드의 색이 업데이트됩니다.

또한 ModifierNodeElement 구현은 equals도 구현해야 합니다. 및 hashCode update는 이전 요소가 false를 반환합니다.

위의 예에서는 데이터 클래스를 사용하여 이를 수행합니다. 이러한 메서드는 노드 업데이트가 필요한지 여부를 확인할 수 있습니다 요소에 노드 업데이트가 필요한지 여부에 영향을 미치지 않거나, 노드의 데이터를 바이너리 호환성을 위해 클래스를 사용하는 경우 equals를 수동으로 구현할 수 있습니다. 및 hashCode(예: 패딩 수정자 요소를 참고하세요.

수정자 팩토리

이는 수정자의 공개 API 노출 영역입니다. 대부분의 구현은 단순히 수정자 요소를 만들어 수정자 체인에 추가합니다.

// Modifier factory
fun Modifier.circle(color: Color) = this then CircleElement(color)

전체 예

이 세 부분이 결합하여 원을 그리는 맞춤 수정자를 만듭니다. Modifier.Node API 사용:

// 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로 맞춤 수정자를 만들 때 발생할 수 있는 몇 가지 일반적인 상황은 다음과 같습니다. 발생할 수 있습니다.

매개변수 0개

수정자에 매개변수가 없으면 업데이트할 필요가 없습니다. 게다가 데이터 클래스가 아니어도 됩니다. 다음은 샘플 구현입니다. 컴포저블에 고정된 양의 패딩을 적용하는 수정자의 예:

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 수정자의 이점은 다음과 같습니다. 구성 가능한 팩토리로 방금 만든 수정자는 UI에서 수정자가 사용된 컴포지션 로컬의 값 트리(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에 액세스할 수 있습니다. 이렇게 하면 Compose Animatable API 사용 예를 들어 이 스니펫은 위에서부터 반복적으로 페이드 인 및 페이드 아웃을 반복하는 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, sizeonClick 람다를 속성으로 사용합니다. 이 수정자는 필요하며, 다음에 해당하지 않는 무효화는 건너뜁니다.

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