카테고리 없음

[Jetpack Compose] clickable Ripple Effect Custom (MutableInteractionSource & Indication)

빨주노초잠만보 2025. 1. 3. 14:05

 

Modifier의 clickable 속성을 사용하면 기본으로 ripple 효과가 발생합게 되는데 이를 사용하다 보면 종종 ripple 효과를 제거하거나 적절히 커스텀해야 하는 상황이 발생합니다. 오늘은 ripple 효과를 적절히 커스텀하는 방법을 알아보고 이 과정에서 필요한 구성요소에 대해 분석해 보겠습니다.

1. Ripple 효과 제거하기

 

ripple 효과를 제거하는 가장 쉬운 방법은 interactionSourceindication을 인자로 받는 clickable을 사용해

indication을 null로, interactionSource에 remember { MutableInteractionSource() }을 전달하면 됩니다. 그렇다면 indication과 interactionSource은 무엇일까요?

Modifier.clickable(
    interactionSource = remember { MutableInteractionSource() },
    indication = null
)

2. interactionSource

interaction의 사전적 의미는 "상호 작용" 즉, 클릭 이벤트 같은 유저와의 상호작용을 의미합니다.

 

Interaction은 Interface로 사용자와의 상호작용으로 인해 발생한 이벤트가 실제로 실행되기 전 잠깐동안 가지는 UI 상태를 나타냅니다.

 

예를 들어 버튼이 눌렸다 때 질 때 onClick() 함수가 호출된다고 가정했을 때 onClick 이벤트가 수행되기 전 잠시동안 버튼이 눌린 상태(Ex. Ripple)를 잠시 보여주기 위해 표현하기 위해 사용됩니다.

InteractionSource는 이러한 유저와의 상호 작용 상태를 데이터 스트림(Flow)로 구현하며 이 상호 작용을 방출하는 MutableInteractionSource로 이루어져 있습니다.

@Stable
interface InteractionSource {
    val interactions: Flow<Interaction>
}

@Stable
interface MutableInteractionSource : InteractionSource {
    suspend fun emit(interaction: Interaction)
    fun tryEmit(interaction: Interaction): Boolean
}

 

그리고 MutableInteractionSource는 이를 SharedFlow로 구현해 사용자에게 지속적으로 값을 방출합니다. SharedFlow의 추가 옵션으로 extraBufferCapacity에 16개의 버퍼를 추가로 생성하였으며 BufferOverflow.DROP_OLDEST를 사용해 버퍼가 가득 찼을 때 가장 오래된 값부터 제거합니다.

fun MutableInteractionSource(): MutableInteractionSource = MutableInteractionSourceImpl()

@Stable
private class MutableInteractionSourceImpl : MutableInteractionSource {

    override val interactions = MutableSharedFlow<Interaction>(
        extraBufferCapacity = 16,
        onBufferOverflow = BufferOverflow.DROP_OLDEST,
    )

    override suspend fun emit(interaction: Interaction) {
        interactions.emit(interaction)
    }

    override fun tryEmit(interaction: Interaction): Boolean {
        return interactions.tryEmit(interaction)
    }
}

2. Indication

 

Indicate의 사전적 의미는 "나타내다" 즉, 유저와의 상호작용으로 발생하는 리플 같은 시각적 효과를 제어하는 역할을 합니다. ripple 효과가 바로 Indication의 기본 구현으로 Indication을 커스터마이징하면 사용자 인터페이스(UI)에서 발생하는 시각적 효과를 조절할 수 있습니다.

Indication의 주요 특징

  • Ripple Effect: 기본적으로 Indication은 리플 효과를 제공합니다. 사용자가 클릭할 때 터치한 위치에서 원 모양으로 퍼져 나가는 애니메이션을 제공합니다.
  • Customizable: Indication은 리플의 색상, 크기, 지속 시간 등을 커스터마이징 할 수 있습니다.

Indication은 하나의 컴포저블 함수를 가지고 있습니다. rememberUpdatedInstance는 앞서 살펴본 interractionSource을 인자로 받습니다. 즉, 클릭 같은 상호작용이 발생할 때마다 상태를 업데이트하고 상호작용에 맞는 IndicationInstance를 반환합니다. IndicationInstance는 Indication의 구현체로 화면에 표시될 시각적 상호작용의 구체적인 상태를 나타냅니다.

@Stable
interface Indication {
    @Composable
    fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance =
        NoIndicationInstance
}

 

rememberUpdatedInstance는 시각적 효과가 없거나 리플 효과를 사용하지 않으려는 경우에 사용될 수 있는 기본 구현체로 이 객체는 시각적 효과를 비활성화하며 기본적으로 컴포넌트 자체만 그리는 역할을 합니다.

 

3. IndicationInstance

IndicationInstance는 drawIndication함수를 호출해 상호작용이 발생한 컴포넌트에 실제로 시각적 효과를 그리는 역할을 합니다. Indication.rememberUpdatedInstance에 의해 변경된 상태를 읽어옴으로써 IndicationInstance는 상태를 변경하지 않고 시각적 효과를 그리는 역할만을 수행합니다.

interface IndicationInstance {
    fun ContentDrawScope.drawIndication()
}

4. ripple

Ripple은 Indication Interaface의 구현체로 rememberUpdateInstance를 구현해 앞서 살펴봤던 interactionSource의 sharedFlow로 선언되어 있는 interactions를 구독해 상태가 변경될 때 이에 따른 리플 효과를 발생시킵니다. 

collect 내부 프로세스

  • PressInteraction.Press
    • 사용자가 화면을 눌렀을 때 발생하는 이벤트
    • instance.addRipple(interaction, this)가 호출되어 새로운 리플 애니메이션을 추가합니다.
  • PressInteraction.Release
    • 사용자가 화면을 눌렀다가 떼었을 때 발생하는 이벤트입니다.
    • instance.removeRipple(interaction.press)가 호출되어 리플 애니메이션을 제거합니다.
  • PressInteraction.Cancel
    • 사용자가 화면을 눌렀다가 취소되었을 때 발생하는 이벤트 리플을 취소하고 제거
    • instance.removeRipple(interaction.press)가 호출됩니다.
  • else
    • 다른 종류의 상호작용에 대해 instance.updateStateLayer(interaction, this)를 호출
    • 이 메서드는 상호작용에 맞춰 상태 레이어를 업데이트하는 작업을 수행

5. 리플 효과 커스텀

리플 효과를 제거하거나 커스텀할 수 있는 함수를 만들어보겠습니다.

 

Modifier의 확장 함수로 리플 효과를 커스텀하게 합니다. 리플 효과의 기본값을 리플 효과를 사용하지 않도록 선언하고 해당 함수는 인라이닝하여 함수 호출 시 오버헤드를 감소시켰습니다.

sealed interface ClickEffect {
    data object RemoveRippleEffect : ClickEffect
    data class UseRippleEffect(val ripple: Indication) : ClickEffect
}

inline fun Modifier.clickableWithCustomRippleEffect(
    clickEffect: ClickEffect = ClickEffect.RemoveRippleEffect,
    crossinline onClick: () -> Unit
): Modifier = composed {
    val indication = when (clickEffect) {
        is ClickEffect.RemoveRippleEffect -> null
        is ClickEffect.UseRippleEffect -> clickEffect.ripple
    }
    clickable(
        indication = indication,
        interactionSource = remember { MutableInteractionSource() }) {
        onClick()
    }
}

6. 결과

 

앞선 내용을 정리해보겠습니다. 

 

1. clickable이 기본적으로 제공하는 리플 효과를 제거 하려면 interactionSource에 MutableInteractionSource를 전달하고 indication을 null로 설정해야 합니다.

Modifier.clickable(
    interactionSource = remember { MutableInteractionSource() },
    indication = null
)

 

2. MutableInteractionSource는 클릭 이벤트 같은 사용자와의 상호작용 상태를 SharedFlow 형태로 구독하며, 상호작용 상태가 변화할 때마다 Interaction을 방출합니다.

 

3. Interaction은 클릭 이벤트 같은 사용자와의 상호 작용시 실제 이벤트 발생 이전의 UI 상태를 잠시동안 UI에 표현하기 위해 사용됩니다.(Ex. Ripple)

 

4. 이 상태 변화에 따라 시각적 효과를 발생시키는 역할을 하는 것이 IndicationInstance입니다.

 

5. IndicationInstance는 상호작용에 따라 화면에 시각적 효과를 그리게 됩니다.