Skip to content
This repository was archived by the owner on Jul 21, 2025. It is now read-only.

Commit e61a00c

Browse files
authored
feat: New sample app (#36)
1 parent 4fab980 commit e61a00c

File tree

8 files changed

+483
-204
lines changed

8 files changed

+483
-204
lines changed

.fleet/settings.json

Lines changed: 0 additions & 3 deletions
This file was deleted.

gradle/libs.versions.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ kotlin = "2.1.0"
1414
binaryCompatibilityValidator = "0.16.3"
1515
#compose
1616
composeMultiplatform = "1.7.1"
17+
colorpicker="1.1.2"
1718
#other
1819
dokka = "1.9.20"
1920
mavenPublish = "0.30.0"
@@ -22,6 +23,7 @@ mavenPublish = "0.30.0"
2223
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidxAppCompat" }
2324
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "androidxActivity" }
2425
androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "androidxAnnotation" }
26+
colorPicker = { module = "com.github.skydoves:colorpicker-compose", version.ref = "colorpicker" }
2527
[plugins]
2628
#android
2729
android-applcation = { id = "com.android.application", version.ref = "androidGradlePlugin" }

gradle/wrapper/gradle-wrapper.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
distributionBase=GRADLE_USER_HOME
22
distributionPath=wrapper/dists
3-
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
3+
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
44
networkTimeout=10000
55
validateDistributionUrl=true
66
zipStoreBase=GRADLE_USER_HOME

sample/compose-app/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,9 @@ kotlin {
6161
implementation(compose.ui)
6262
implementation(compose.uiUtil)
6363
implementation(compose.material3)
64-
implementation(compose.components.resources)
6564
implementation(compose.components.uiToolingPreview)
6665
implementation(project(":compose-shadow"))
66+
implementation(libs.colorPicker)
6767
}
6868

6969
desktopMain.dependencies {

sample/compose-app/src/androidMain/AndroidManifest.xml

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,31 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3-
xmlns:tools="http://schemas.android.com/tools">
3+
xmlns:tools="http://schemas.android.com/tools">
44

5-
<uses-permission android:name="android.permission.INTERNET"/>
6-
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
5+
<uses-permission android:name="android.permission.INTERNET" />
6+
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
77

88
<application
9-
android:name="com.adamglin.composeshadow.app.SampleApplication"
10-
android:allowBackup="true"
11-
android:enableOnBackInvokedCallback="true"
12-
android:hardwareAccelerated="true"
13-
android:icon="@mipmap/ic_launcher"
14-
android:label="@string/app_name"
15-
android:roundIcon="@mipmap/ic_launcher_round"
16-
android:supportsRtl="true"
17-
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
18-
android:usesCleartextTraffic="true"
19-
tools:targetApi="31">
9+
android:name="com.admaglin.composeshadow.app.SampleApplication"
10+
android:allowBackup="true"
11+
android:enableOnBackInvokedCallback="true"
12+
android:hardwareAccelerated="true"
13+
android:icon="@mipmap/ic_launcher"
14+
android:label="@string/app_name"
15+
android:roundIcon="@mipmap/ic_launcher_round"
16+
android:supportsRtl="true"
17+
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
18+
android:usesCleartextTraffic="true"
19+
tools:targetApi="31">
2020
<activity
21-
android:name=".app.MainActivity"
22-
android:exported="true"
23-
android:label="@string/title_activity_main"
24-
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
21+
android:name="com.admaglin.composeshadow.app.MainActivity"
22+
android:exported="true"
23+
android:label="@string/title_activity_main"
24+
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
2525
<intent-filter>
26-
<action android:name="android.intent.action.MAIN"/>
26+
<action android:name="android.intent.action.MAIN" />
2727

28-
<category android:name="android.intent.category.LAUNCHER"/>
28+
<category android:name="android.intent.category.LAUNCHER" />
2929
</intent-filter>
3030
</activity>
3131
</application>
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
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

Comments
 (0)