1+ import androidx.compose.animation.core.*
2+ import androidx.compose.foundation.ExperimentalFoundationApi
3+ import androidx.compose.foundation.background
4+ import androidx.compose.foundation.border
5+ import androidx.compose.foundation.gestures.draggable2D
6+ import androidx.compose.foundation.gestures.rememberDraggable2DState
7+ import androidx.compose.foundation.layout.*
8+ import androidx.compose.foundation.shape.CircleShape
9+ import androidx.compose.runtime.Composable
10+ import androidx.compose.runtime.remember
11+ import androidx.compose.runtime.rememberCoroutineScope
12+ import androidx.compose.runtime.rememberUpdatedState
13+ import androidx.compose.ui.Modifier
14+ import androidx.compose.ui.draw.clip
15+ import androidx.compose.ui.draw.drawBehind
16+ import androidx.compose.ui.geometry.Offset
17+ import androidx.compose.ui.geometry.Size
18+ import androidx.compose.ui.graphics.Color
19+ import androidx.compose.ui.input.pointer.PointerIcon
20+ import androidx.compose.ui.input.pointer.pointerHoverIcon
21+ import androidx.compose.ui.platform.LocalDensity
22+ import androidx.compose.ui.unit.DpOffset
23+ import androidx.compose.ui.unit.dp
24+ import kotlinx.coroutines.launch
25+
26+
27+ @Composable
28+ fun rememberOffsetIndicatorState (
29+ onValueChange : (DpOffset ) -> Unit ,
30+ ): OffsetIndicatorState {
31+ val onValueChangeState = rememberUpdatedState(onValueChange)
32+ return remember {
33+ OffsetIndicatorState (
34+ onValueChange = { onValueChangeState.value.invoke(it) }
35+ )
36+ }
37+ }
38+
39+ class OffsetIndicatorState (
40+ val onValueChange : (DpOffset ) -> Unit ,
41+ ) {
42+ private val animatable = Animatable (DpOffset .Zero , DpOffset .VectorConverter )
43+ val targetValue get() = animatable.targetValue
44+
45+
46+ suspend fun snapTo (offset : DpOffset ) {
47+ animatable.snapTo(offset)
48+ onValueChange.invoke(offset)
49+ }
50+
51+ suspend fun animateTo (offset : DpOffset ) {
52+ onValueChange.invoke(offset)
53+ animatable.animateTo(offset, spring(stiffness = Spring .StiffnessHigh ))
54+ }
55+ }
56+
57+ private val DpOffset .VectorConverter : TwoWayConverter <Offset , AnimationVector2D >
58+ get() = TwoWayConverter (
59+ convertToVector = { AnimationVector2D (it.x, it.y) },
60+ convertFromVector = { Offset (it.v1, it.v2) }
61+ )
62+
63+
64+ @OptIn(ExperimentalFoundationApi ::class )
65+ @Composable
66+ fun OffsetIndicator (
67+ state : OffsetIndicatorState ,
68+ modifier : Modifier = Modifier
69+ ) {
70+ val density = LocalDensity .current
71+ val coroutineScope = rememberCoroutineScope()
72+ Box (modifier) {
73+ BoxWithConstraints (
74+ modifier = Modifier .fillMaxSize()
75+ .border(1 .dp, color = OFFSET_INDICATOR_LINE_COLOR )
76+ .drawBehind {
77+ drawLine(
78+ OFFSET_INDICATOR_LINE_COLOR ,
79+ strokeWidth = .5f ,
80+ start = size.TopCenterOffset ,
81+ end = size.BottomCenterOffset
82+ )
83+ drawLine(
84+ OFFSET_INDICATOR_LINE_COLOR ,
85+ strokeWidth = .5f ,
86+ start = size.CenterLeftOffset ,
87+ end = size.CenterRightOffset ,
88+ )
89+ }
90+ ) {
91+ val boxWidth = maxWidth.value.dp
92+
93+ val realOffset =
94+ DpOffset (
95+ state.targetValue.x + boxWidth / 2 - MOVABLE_POINTER_SIZE / 2 ,
96+ state.targetValue.y + boxWidth / 2 - MOVABLE_POINTER_SIZE / 2
97+ )
98+ val draggable2DState = rememberDraggable2DState {
99+ with (density) {
100+ DpOffset (
101+ (state.targetValue.x + it.x.toDp()).coerceIn(- boxWidth / 2 , boxWidth / 2 ),
102+ (state.targetValue.y + it.y.toDp()).coerceIn(- boxWidth / 2 , boxWidth / 2 ),
103+ ).let {
104+ println (" ${state.targetValue} result $it " )
105+ coroutineScope.launch { state.snapTo(it) }
106+ }
107+ }
108+ }
109+ Box (
110+ modifier = Modifier
111+ .offset(realOffset.x, realOffset.y)
112+ .size(MOVABLE_POINTER_SIZE )
113+ .clip(CircleShape )
114+ .pointerHoverIcon(PointerIcon .Hand )
115+ .background(Color .Blue )
116+ .draggable2D(draggable2DState)
117+ )
118+ }
119+ }
120+ }
121+
122+ private val OFFSET_INDICATOR_LINE_COLOR = Color .LightGray
123+ private val MOVABLE_POINTER_SIZE = 10 .dp
124+
125+ private val Size .TopLeftOffset : Offset
126+ get() = Offset .Zero
127+
128+ private val Size .TopCenterOffset : Offset
129+ get() = Offset (width / 2 , 0f )
130+
131+ private val Size .TopRightOffset : Offset
132+ get() = Offset (width, 0f )
133+
134+ private val Size .CenterLeftOffset : Offset
135+ get() = Offset (0f , height / 2 )
136+
137+ private val Size .CenterOffset : Offset
138+ get() = Offset (width / 2 , height / 2 )
139+
140+ private val Size .CenterRightOffset : Offset
141+ get() = Offset (width, height / 2 )
142+
143+ private val Size .BottomLeftOffset : Offset
144+ get() = Offset (0f , height)
145+
146+ private val Size .BottomCenterOffset : Offset
147+ get() = Offset (width / 2 , height)
148+
149+ private val Size .BottomRightOffset : Offset
150+ get() = Offset (width, height)
0 commit comments