Skip to content
Merged
Show file tree
Hide file tree
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 @@ -38,7 +38,15 @@ import zone.ien.taptargetcmp.example.ui.theme.AppTheme
@Composable
fun App() {
AppTheme {
TapTargetCoordinator(showTapTargets = true, onComplete = {}) {
TapTargetCoordinator(
showTapTargets = true,
onTargetChanged = { precedence ->
println("this tap target $precedence")
},
onComplete = {
println("this tap target end")
}
) {
Surface(modifier = Modifier.fillMaxSize()) {
Content()
}
Expand All @@ -50,13 +58,14 @@ fun App() {
@Composable
private fun TapTargetScope.Content() {
val toolbarTapTarget = getStandardTapTargetDefinition(
precedence = 1,
precedence = 350,
title = "Toolbar tap target",
description = "This is a toolbar, tap it!",
)


val tab2TapTarget = getStandardTapTargetDefinition(
precedence = 2,
precedence = 200,
title = "Tab2 tap target",
description = "A moving target",
)
Expand All @@ -76,7 +85,7 @@ private fun TapTargetScope.Content() {
text = { Text(text = "Click here") },
modifier = Modifier.tapTarget(
getStandardTapTargetDefinition(
precedence = 0,
precedence = 150,
title = "Button tap target",
description = "This is a button, tap it!",
)
Expand Down
10 changes: 5 additions & 5 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
[versions]
agp = "9.0.0"
kotlin = "2.3.21"
agp = "9.1.0"
kotlin = "2.4.0"
android-minSdk = "29"
android-compileSdk = "36"
android-targetSdk = "36"
lib-version-name = "1.2.1"
lib-version-name = "1.3.0"

# plugin
compose-plugin = "1.11.0-rc01"
compose-plugin = "1.11.1"
compose-plugin-experimental = "1.11.0-alpha07"
compose-navigation3 = "1.1.1"

# ui
activity-compose = "1.13.0"

# functionality
androidx-core = "1.18.0"
androidx-core = "1.19.0"

# backend
vanniktechMavenPublish = "0.36.0"
Expand Down
4 changes: 2 additions & 2 deletions gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#Thu May 07 22:10:16 KST 2026
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=a17ddd85a26b6a7f5ddb71ff8b05fc5104c0202c6e64782429790c933686c806
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
distributionSha256Sum=bafc141b619ad6350fd975fc903156dd5c151998cc8b058e8c1044ab5f7b031f
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
224 changes: 123 additions & 101 deletions taptarget/src/commonMain/kotlin/zone/ien/taptargetcmp/TapTarget.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@ package zone.ien.taptargetcmp
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.onGloballyPositioned
Expand All @@ -23,135 +27,153 @@ import androidx.compose.ui.unit.TextUnit

@Composable
fun TapTargetCoordinator(
showTapTargets: Boolean,
modifier: Modifier = Modifier,
onComplete: () -> Unit = { },
state: TapTargetCoordinatorState = remember { TapTargetCoordinatorState() },
contentAlignment: Alignment = Alignment.Center,
content: @Composable TapTargetScope.() -> Unit,
showTapTargets: Boolean,
modifier: Modifier = Modifier,
onComplete: () -> Unit = { },
onTargetChanged: (Int) -> Unit = {},
state: TapTargetCoordinatorState = remember { TapTargetCoordinatorState() },
contentAlignment: Alignment = Alignment.Center,
content: @Composable TapTargetScope.() -> Unit,
) {
val scope = remember(state) { TapTargetScope(state) }
val scope = remember(state) { TapTargetScope(state) }

Box(
contentAlignment = contentAlignment,
modifier = modifier
) {
scope.content()
LaunchedEffect(state.currentTargetIndex) {
state.currentTarget?.let { onTargetChanged(it.precedence) }
}

CompositionLocalProvider(LocalTapTargetScope provides scope) {
Box(
contentAlignment = contentAlignment,
modifier = modifier
) {
scope.content()

if (showTapTargets) {
val currentTapTarget = state.currentTarget
if (currentTapTarget != null) {
Box(modifier = Modifier.fillMaxSize()) {
TapTarget(
tapTarget = currentTapTarget,
onComplete = {
state.currentTargetIndex++
if (state.currentTargetIndex >= state.tapTargets.size) {
onComplete()
}
if (showTapTargets) {
val currentTapTarget = state.currentTarget
if (currentTapTarget != null) {
Box(modifier = Modifier.fillMaxSize()) {
TapTarget(
tapTarget = currentTapTarget,
onComplete = {
state.currentTargetIndex++
if (state.currentTargetIndex >= state.tapTargets.size) {
onComplete()
}
}
Comment thread
ienground marked this conversation as resolved.
)
}
}
}
)
}
}
}
}
}

class TapTargetScope internal constructor(private val state: TapTargetCoordinatorState) {

fun Modifier.tapTarget(
title: TextDefinition,
description: TextDefinition,
precedence: Int,
tapTargetStyle: TapTargetStyle = TapTargetStyle.Default,
onTargetClick: () -> Unit = { },
onTargetCancel: () -> Unit = { },
): Modifier {
return onGloballyPositioned { layoutCoordinates ->
state.tapTargets[precedence] = TapTarget(
precedence = precedence,
coordinates = layoutCoordinates,
title = title,
description = description,
style = tapTargetStyle,
onTargetClick = onTargetClick,
onTargetCancel = onTargetCancel,
)
fun Modifier.tapTarget(
title: TextDefinition,
description: TextDefinition,
precedence: Int,
tapTargetStyle: TapTargetStyle = TapTargetStyle.Default,
onTargetClick: () -> Unit = { },
onTargetCancel: () -> Unit = { },
): Modifier {
return onGloballyPositioned { layoutCoordinates ->
state.tapTargets[precedence] = TapTarget(
precedence = precedence,
coordinates = layoutCoordinates,
title = title,
description = description,
style = tapTargetStyle,
onTargetClick = onTargetClick,
onTargetCancel = onTargetCancel,
)
}
}
}

fun Modifier.tapTarget(tapTargetDefinition: TapTargetDefinition): Modifier {
return tapTarget(
tapTargetDefinition.title,
tapTargetDefinition.description,
tapTargetDefinition.precedence,
tapTargetDefinition.tapTargetStyle,
tapTargetDefinition.onTargetClick,
tapTargetDefinition.onTargetCancel
)
}
fun Modifier.tapTarget(tapTargetDefinition: TapTargetDefinition): Modifier {
return tapTarget(
tapTargetDefinition.title,
tapTargetDefinition.description,
tapTargetDefinition.precedence,
tapTargetDefinition.tapTargetStyle,
tapTargetDefinition.onTargetClick,
tapTargetDefinition.onTargetCancel
)
}
}

val LocalTapTargetScope = staticCompositionLocalOf<TapTargetScope?> { null }

fun Modifier.ifTapTarget(definition: TapTargetDefinition?): Modifier = composed {
val scope = LocalTapTargetScope.current
if (scope != null && definition != null) {
with(scope) { this@composed.tapTarget(definition) }
} else {
this
}
}

data class TapTargetDefinition(
val title: TextDefinition,
val description: TextDefinition,
val precedence: Int,
val tapTargetStyle: TapTargetStyle = TapTargetStyle.Default,
val onTargetClick: () -> Unit = { },
val onTargetCancel: () -> Unit = { },
val title: TextDefinition,
val description: TextDefinition,
val precedence: Int,
val tapTargetStyle: TapTargetStyle = TapTargetStyle.Default,
val onTargetClick: () -> Unit = { },
val onTargetCancel: () -> Unit = { },
)

class TapTargetCoordinatorState internal constructor() {
internal val tapTargets = mutableStateMapOf<Int, TapTarget>()
internal var currentTargetIndex by mutableIntStateOf(0)

val currentTarget get() = tapTargets[currentTargetIndex]
internal val tapTargets = mutableStateMapOf<Int, TapTarget>()
internal var currentTargetIndex by mutableIntStateOf(0)
val currentTarget: TapTarget?
get() = tapTargets.keys.sorted().getOrNull(currentTargetIndex)?.let { tapTargets[it] }
}
Comment thread
ienground marked this conversation as resolved.

class TapTarget internal constructor(
val precedence: Int,
val title: TextDefinition,
val description: TextDefinition,
val coordinates: LayoutCoordinates,
val style: TapTargetStyle = TapTargetStyle.Default,
val onTargetClick: () -> Unit,
val onTargetCancel: () -> Unit,
val precedence: Int,
val title: TextDefinition,
val description: TextDefinition,
val coordinates: LayoutCoordinates,
val style: TapTargetStyle = TapTargetStyle.Default,
val onTargetClick: () -> Unit,
val onTargetCancel: () -> Unit,
)

data class TextDefinition(
val text: String,
internal val textStyle: TextStyle = TextStyle.Default,
internal val color: Color = Color.Unspecified,
internal val fontSize: TextUnit = TextUnit.Unspecified,
internal val fontStyle: FontStyle? = null,
internal val fontWeight: FontWeight? = null,
internal val fontFamily: FontFamily? = null,
internal val letterSpacing: TextUnit = TextUnit.Unspecified,
internal val textDecoration: TextDecoration? = null,
internal val textAlign: TextAlign? = null,
internal val lineHeight: TextUnit = TextUnit.Unspecified,
val text: String,
internal val textStyle: TextStyle = TextStyle.Default,
internal val color: Color = Color.Unspecified,
internal val fontSize: TextUnit = TextUnit.Unspecified,
internal val fontStyle: FontStyle? = null,
internal val fontWeight: FontWeight? = null,
internal val fontFamily: FontFamily? = null,
internal val letterSpacing: TextUnit = TextUnit.Unspecified,
internal val textDecoration: TextDecoration? = null,
internal val textAlign: TextAlign? = null,
internal val lineHeight: TextUnit = TextUnit.Unspecified,
) {
val style = textStyle.merge(
TextStyle(
color = color,
fontSize = fontSize,
fontWeight = fontWeight,
textAlign = textAlign ?: TextAlign.Unspecified,
lineHeight = lineHeight,
fontFamily = fontFamily,
textDecoration = textDecoration,
fontStyle = fontStyle,
letterSpacing = letterSpacing
val style = textStyle.merge(
TextStyle(
color = color,
fontSize = fontSize,
fontWeight = fontWeight,
textAlign = textAlign ?: TextAlign.Unspecified,
lineHeight = lineHeight,
fontFamily = fontFamily,
textDecoration = textDecoration,
fontStyle = fontStyle,
letterSpacing = letterSpacing
)
)
)
}

data class TapTargetStyle(
val backgroundColor: Color = Color.Blue,
val backgroundAlpha: Float = 1f,
val tapTargetHighlightColor: Color = Color.White,
val backgroundColor: Color = Color.Blue,
val backgroundAlpha: Float = 1f,
val tapTargetHighlightColor: Color = Color.White,
) {
companion object {
val Default = TapTargetStyle()
}
companion object {
val Default = TapTargetStyle()
}
}
Loading