diff --git a/example/composeApp/src/commonMain/kotlin/zone/ien/taptargetcmp/example/App.kt b/example/composeApp/src/commonMain/kotlin/zone/ien/taptargetcmp/example/App.kt index bd01ffa..fe126ef 100644 --- a/example/composeApp/src/commonMain/kotlin/zone/ien/taptargetcmp/example/App.kt +++ b/example/composeApp/src/commonMain/kotlin/zone/ien/taptargetcmp/example/App.kt @@ -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() } @@ -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", ) @@ -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!", ) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9184c14..fd9b727 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,13 +1,13 @@ [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" @@ -15,7 +15,7 @@ compose-navigation3 = "1.1.1" activity-compose = "1.13.0" # functionality -androidx-core = "1.18.0" +androidx-core = "1.19.0" # backend vanniktechMavenPublish = "0.36.0" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 61bcd03..d14b7a8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/taptarget/src/commonMain/kotlin/zone/ien/taptargetcmp/TapTarget.kt b/taptarget/src/commonMain/kotlin/zone/ien/taptargetcmp/TapTarget.kt index ac22306..137a65a 100644 --- a/taptarget/src/commonMain/kotlin/zone/ien/taptargetcmp/TapTarget.kt +++ b/taptarget/src/commonMain/kotlin/zone/ien/taptargetcmp/TapTarget.kt @@ -3,6 +3,9 @@ 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 @@ -10,6 +13,7 @@ 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 @@ -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() + } + } + ) + } + } } - ) } - } } - } } 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 { 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() - internal var currentTargetIndex by mutableIntStateOf(0) - - val currentTarget get() = tapTargets[currentTargetIndex] + internal val tapTargets = mutableStateMapOf() + internal var currentTargetIndex by mutableIntStateOf(0) + val currentTarget: TapTarget? + get() = tapTargets.keys.sorted().getOrNull(currentTargetIndex)?.let { tapTargets[it] } } 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() + } } \ No newline at end of file