1+ package top.yukonga.miuix.kmp.utils
2+
3+ import androidx.compose.animation.core.Animatable
4+ import androidx.compose.animation.core.tween
5+ import androidx.compose.foundation.Indication
6+ import androidx.compose.foundation.IndicationNodeFactory
7+ import androidx.compose.foundation.interaction.FocusInteraction
8+ import androidx.compose.foundation.interaction.HoverInteraction
9+ import androidx.compose.foundation.interaction.Interaction
10+ import androidx.compose.foundation.interaction.InteractionSource
11+ import androidx.compose.foundation.interaction.MutableInteractionSource
12+ import androidx.compose.foundation.interaction.PressInteraction
13+ import androidx.compose.runtime.Composable
14+ import androidx.compose.runtime.LaunchedEffect
15+ import androidx.compose.runtime.State
16+ import androidx.compose.runtime.mutableStateOf
17+ import androidx.compose.runtime.remember
18+ import androidx.compose.ui.Modifier
19+ import androidx.compose.ui.graphics.Color
20+ import androidx.compose.ui.graphics.drawscope.ContentDrawScope
21+ import androidx.compose.ui.node.DelegatableNode
22+ import androidx.compose.ui.node.DrawModifierNode
23+ import kotlinx.coroutines.Job
24+ import kotlinx.coroutines.launch
25+
26+ /* *
27+ * Miuix default [Indication] that draws a rectangular overlay when pressed.
28+ */
29+ class MiuixIndication (
30+ private val backgroundColor : Color = Color .Black
31+ ) : IndicationNodeFactory {
32+ override fun create (interactionSource : InteractionSource ): DelegatableNode =
33+ MiuixIndicationInstance (interactionSource, backgroundColor)
34+
35+ override fun hashCode (): Int = - 1
36+
37+ override fun equals (other : Any? ) = other == = this
38+
39+ private class MiuixIndicationInstance (
40+ private val interactionSource : InteractionSource ,
41+ private val backgroundColor : Color
42+ ) : Modifier.Node(), DrawModifierNode {
43+ private var isPressed = false
44+ private var isHovered = false
45+ private var isFocused = false
46+ private val animatedAlpha = Animatable (0f )
47+ private var pressedAnimation: Job ? = null
48+ private var restingAnimation: Job ? = null
49+
50+ private suspend fun updateStates () {
51+ animatedAlpha.stop()
52+ var targetAlpha = 0.0f
53+ if (isHovered) targetAlpha + = 0.06f
54+ if (isFocused) targetAlpha + = 0.08f
55+ if (isPressed) targetAlpha + = 0.1f
56+ if (targetAlpha == 0.0f ) {
57+ restingAnimation = coroutineScope.launch {
58+ pressedAnimation?.join()
59+ animatedAlpha.animateTo(0f , tween(150 ))
60+ }
61+ } else {
62+ restingAnimation?.cancel()
63+ pressedAnimation?.cancel()
64+ pressedAnimation = coroutineScope.launch {
65+ animatedAlpha.animateTo(targetAlpha, tween(150 ))
66+ }
67+ }
68+ }
69+
70+ override fun onAttach () {
71+ coroutineScope.launch {
72+ var pressed = false
73+ var hovered = false
74+ var focused = false
75+ var held = false
76+ interactionSource.interactions.collect { interaction ->
77+ when (interaction) {
78+ is PressInteraction .Press -> pressed = true
79+ is PressInteraction .Release , is PressInteraction .Cancel -> pressed = false
80+ is HoverInteraction .Enter -> hovered = true
81+ is HoverInteraction .Exit -> hovered = false
82+ is FocusInteraction .Focus -> focused = true
83+ is FocusInteraction .Unfocus -> focused = false
84+ is HoldDownInteraction .Hold -> held = true
85+ is HoldDownInteraction .Release -> held = false
86+ else -> return @collect
87+ }
88+ var invalidateNeeded = false
89+ if (isPressed != (pressed || held)) {
90+ isPressed = (pressed || held)
91+ invalidateNeeded = true
92+ }
93+ if (isHovered != hovered) {
94+ isHovered = hovered
95+ invalidateNeeded = true
96+ }
97+ if (isFocused != focused) {
98+ isFocused = focused
99+ invalidateNeeded = true
100+ }
101+ if (invalidateNeeded) {
102+ updateStates()
103+ }
104+ }
105+ }
106+ }
107+
108+ override fun ContentDrawScope.draw () {
109+ // Draw content
110+ drawContent()
111+ // Draw foreground
112+ drawRect(color = backgroundColor.copy(alpha = animatedAlpha.value), size = size)
113+ }
114+ }
115+ }
116+
117+ /* *
118+ * An interaction related to hold down events.
119+ *
120+ * @see Hold
121+ * @see Release
122+ */
123+ interface HoldDownInteraction : Interaction {
124+ /* *
125+ * An interaction representing a hold down event on a component.
126+ *
127+ * @see Release
128+ */
129+ class Hold : HoldDownInteraction
130+
131+ /* *
132+ * An interaction representing a [Hold] event being released on a component.
133+ *
134+ * @property hold the source [Hold] interaction that is being released
135+ *
136+ * @see Hold
137+ */
138+ class Release (val hold : Hold ) : HoldDownInteraction
139+ }
140+
141+ /* *
142+ * Subscribes to this [MutableInteractionSource] and returns a [State] representing whether this
143+ * component is selected or not.
144+ *
145+ * @return [State] representing whether this component is being focused or not
146+ */
147+ @Composable
148+ fun InteractionSource.collectIsHeldDownAsState (): State <Boolean > {
149+ val isHeldDown = remember { mutableStateOf(false ) }
150+ LaunchedEffect (this ) {
151+ val holdInteraction = mutableListOf<HoldDownInteraction .Hold >()
152+ interactions.collect { interaction ->
153+ when (interaction) {
154+ is HoldDownInteraction .Hold -> holdInteraction.add(interaction)
155+ is HoldDownInteraction .Release -> holdInteraction.remove(interaction.hold)
156+ }
157+ isHeldDown.value = holdInteraction.isNotEmpty()
158+ }
159+ }
160+ return isHeldDown
161+ }
0 commit comments