Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,54 +3,109 @@ package com.ninecraft.booket.core.common.extensions
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.material3.ripple
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.layer.GraphicsLayer
import androidx.compose.ui.graphics.layer.drawLayer
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.node.DelegatingNode
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.node.SemanticsModifierNode
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.SemanticsPropertyReceiver
import androidx.compose.ui.semantics.disabled
import androidx.compose.ui.semantics.onClick
import androidx.compose.ui.semantics.role
import com.ninecraft.booket.core.common.utils.MultipleEventsCutter
import com.ninecraft.booket.core.common.utils.get

// https://stackoverflow.com/questions/66703448/how-to-disable-ripple-effect-when-clicking-in-jetpack-compose
inline fun Modifier.noRippleClickable(crossinline onClick: () -> Unit): Modifier = composed {
clickable(
fun Modifier.noRippleClickable(onClick: () -> Unit): Modifier =
this.clickable(
interactionSource = null,
indication = null,
interactionSource = remember { MutableInteractionSource() },
) {
onClick()
}
}
onClick = onClick,
)

fun Modifier.clickableSingle(
enabled: Boolean = true,
onClickLabel: String? = null,
role: Role? = null,
onClick: () -> Unit,
) = composed(
inspectorInfo = debugInspectorInfo {
name = "clickable"
properties["enabled"] = enabled
properties["onClickLabel"] = onClickLabel
properties["role"] = role
properties["onClick"] = onClick
},
) {
val multipleEventsCutter = remember { MultipleEventsCutter.get() }
Modifier.clickable(
enabled = enabled,
onClickLabel = onClickLabel,
onClick = { multipleEventsCutter.processEvent { onClick() } },
role = role,
indication = ripple(),
interactionSource = remember { MutableInteractionSource() },
): Modifier = this then ClickableSingleElement(enabled, onClickLabel, role, onClick)

private data class ClickableSingleElement(
private val enabled: Boolean,
private val onClickLabel: String?,
private val role: Role?,
private val onClick: () -> Unit,
) : ModifierNodeElement<ClickableSingleNode>() {
override fun create() = ClickableSingleNode(enabled, onClickLabel, role, onClick)
override fun update(node: ClickableSingleNode) {
node.update(enabled, onClickLabel, role, onClick)
}
}

private class ClickableSingleNode(
private var enabled: Boolean,
private var onClickLabel: String?,
private var role: Role?,
private var onClick: () -> Unit,
) : DelegatingNode(), SemanticsModifierNode {
private val multipleEventsCutter = MultipleEventsCutter.get()
private val interactionSource = MutableInteractionSource()

@Suppress("unused")
private val indicationNode = delegate(
ripple().create(interactionSource),
)

@Suppress("unused")
private val pointerInputNode = delegate(
SuspendingPointerInputModifierNode {
if (!enabled) return@SuspendingPointerInputModifierNode
detectTapGestures(
onPress = { offset ->
val press = PressInteraction.Press(offset)
interactionSource.emit(press)
val released = tryAwaitRelease()
if (released) {
interactionSource.emit(PressInteraction.Release(press))
} else {
interactionSource.emit(PressInteraction.Cancel(press))
}
},
onTap = {
multipleEventsCutter.processEvent { onClick() }
},
)
},
)
Comment on lines +69 to +89
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

enabled 상태 변경 시 포인터 입력 핸들러가 재시작되지 않습니다.

SuspendingPointerInputModifierNode의 코루틴은 한 번 시작되면 enabled 값이 변경되어도 자동으로 재시작되지 않습니다. enabledtrue에서 false로 변경되어도 이미 실행 중인 detectTapGestures 코루틴이 계속 탭을 처리합니다.

update() 메서드에서 enabled 상태 변경 시 resetPointerInputHandler()를 호출해야 합니다.

🩹 수정 제안
-    `@Suppress`("unused")
     private val pointerInputNode = delegate(
         SuspendingPointerInputModifierNode {
             if (!enabled) return@SuspendingPointerInputModifierNode
             detectTapGestures(
                 onPress = { offset ->
                     val press = PressInteraction.Press(offset)
                     interactionSource.emit(press)
                     val released = tryAwaitRelease()
                     if (released) {
                         interactionSource.emit(PressInteraction.Release(press))
                     } else {
                         interactionSource.emit(PressInteraction.Cancel(press))
                     }
                 },
                 onTap = {
                     multipleEventsCutter.processEvent { onClick() }
                 },
             )
         },
     )

     fun update(enabled: Boolean, onClickLabel: String?, role: Role?, onClick: () -> Unit) {
+        val enabledChanged = this.enabled != enabled
         this.enabled = enabled
         this.onClickLabel = onClickLabel
         this.role = role
         this.onClick = onClick
+        if (enabledChanged) {
+            pointerInputNode.resetPointerInputHandler()
+        }
     }

Also applies to: 103-108

🤖 Prompt for AI Agents
In
`@core/common/src/main/kotlin/com/ninecraft/booket/core/common/extensions/Modifier.kt`
around lines 69 - 89, The pointer input coroutine started by pointerInputNode's
SuspendingPointerInputModifierNode doesn't restart when enabled toggles; modify
the update() implementation to detect changes to the enabled property and call
resetPointerInputHandler() when enabled value differs from previous state so the
detectTapGestures coroutine is restarted; locate pointerInputNode, the
SuspendingPointerInputModifierNode block, and the update() method and ensure you
compare the old and new enabled values and invoke resetPointerInputHandler() on
change.


override fun SemanticsPropertyReceiver.applySemantics() {
onClick(label = onClickLabel) {
if (!enabled) return@onClick false
multipleEventsCutter.processEvent { this@ClickableSingleNode.onClick() }
true
}
this@ClickableSingleNode.role?.let { this.role = it }
if (!enabled) {
disabled()
}
}

fun update(enabled: Boolean, onClickLabel: String?, role: Role?, onClick: () -> Unit) {
this.enabled = enabled
this.onClickLabel = onClickLabel
this.role = role
this.onClick = onClick
}
}

fun Modifier.captureToGraphicsLayer(graphicsLayer: GraphicsLayer) =
Expand Down
Loading