diff --git a/.github/android_example.gif b/.github/android_example.gif
new file mode 100644
index 0000000..64109bb
Binary files /dev/null and b/.github/android_example.gif differ
diff --git a/.github/green-stash.gif b/.github/green-stash.gif
deleted file mode 100644
index 02e01ae..0000000
Binary files a/.github/green-stash.gif and /dev/null differ
diff --git a/.github/ios_example.gif b/.github/ios_example.gif
new file mode 100644
index 0000000..8536a62
Binary files /dev/null and b/.github/ios_example.gif differ
diff --git a/.github/tap-target-image.gif b/.github/tap-target-image.gif
deleted file mode 100644
index d69f7ab..0000000
Binary files a/.github/tap-target-image.gif and /dev/null differ
diff --git a/.gitignore b/.gitignore
index d533403..ea6934c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,7 +6,7 @@ local.properties
.idea/*
!.idea/copyright
.DS_Store
-/build
+build
/captures
.externalNativeBuild
ktlint
\ No newline at end of file
diff --git a/README.md b/README.md
index d510cf2..05c948b 100644
--- a/README.md
+++ b/README.md
@@ -1,37 +1,31 @@
-#### \
-
-#### [:mega: Use AI to manage your app's reviews on Google Play :arrow_right:](https://playstorereply.com)
-Increase your apps ratings. Engage with all users, no more unanswered reviews.
-Save time and increase customer satisfaction.
-
-#### \
-
-# tap-target-compose
-tap-target-compose is a Jetpack Compose implementation of the [Material Design tap targets](https://m1.material.io/growth-communications/feature-discovery.html#feature-discovery-design), used for feature discovery.
+# tap-target-cmp
+tap-target-compose is a Compose Multiplatform implementation of the [Material Design tap targets](https://m1.material.io/growth-communications/feature-discovery.html#feature-discovery-design), used for feature discovery.
This library was inspired by its View counterpart, [TapTargetView](https://github.com/KeepSafe/TapTargetView).
-| | |
-| --- | --- |
-|  | [](https://github.com/Pool-Of-Tears/GreenStash) |
+| | |
+|-----------------------------------|-------------------------------|
+|  |  |
# Sample app
This library comes with a sample app that shows examples of how to use it.
-* [Click here to see the source code of the sample app](./sample-app/).
+* [Click here to see the source code of the example](./example/).
-:eyes: If you want to know when a new release of the library is published: [watch this repository on GitHub](https://github.com/PierfrancescoSoffritti/tap-target-compose/watchers).
+:eyes: If you want to know when a new release of the library is published: [watch this repository on GitHub](https://github.com/ienground/tap-target-cmp/watchers).
# Download
-The Gradle dependency is available via [MavenCentral](https://repo1.maven.org/maven2/com/pierfrancescosoffritti/).
-
-The minimum API level supported by this library is API 13.
+The minimum API level supported by this library is API 29.
Add this to your module level `build.gradle` file to start using the library.
+```toml
+taptarget = { group = "zone.ien.taptargetcmp", name = "taptarget", version = "1.2.1" }
+```
+
```gradle
dependencies {
- implementation "com.pierfrancescosoffritti.taptargetcompose:core:1.2.1"
+ implementation(libs.taptarget)
}
```
@@ -108,4 +102,4 @@ The library supports chaining of multiple tap targets, but you can also show onl
---
-For any question feel free to [open an issue on the GitHub repository](https://github.com/PierfrancescoSoffritti/tap-target-compose/issues).
\ No newline at end of file
+For any question feel free to [open an issue on the GitHub repository](https://github.com/ienground/tap-target-cmp/issues).
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index 8753165..d784a54 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,11 +1,57 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
- alias(libs.plugins.android.application) apply false
- alias(libs.plugins.jetbrains.kotlin.android) apply false
- alias(libs.plugins.android.library) apply false
- alias(libs.plugins.gradleNexus.publish)
- alias(libs.plugins.jetbrains.dokka)
+ alias(libs.plugins.android.kotlin.multiplatform.library) apply false
+ alias(libs.plugins.kotlin.multiplatform) apply false
+ alias(libs.plugins.serialization) apply false
+ alias(libs.plugins.vanniktech.mavenPublish) apply false
+ alias(libs.plugins.android.lint) apply false
alias(libs.plugins.compose.compiler) apply false
}
+val isReleaseVersion = !version.toString().endsWith("SNAPSHOT")
-apply(from = "./scripts/publish-root.gradle")
\ No newline at end of file
+subprojects {
+ plugins.withId("com.vanniktech.maven.publish") {
+ configure {
+ publishToMavenCentral(automaticRelease = true)
+
+ // Artifact ID만 각 프로젝트의 이름으로 자동 설정
+ group = "zone.ien.taptargetcmp"
+ version = libs.versions.lib.version.name.get()
+ println("${group} ${project.name} ${version}")
+
+ coordinates(group.toString(), project.name, version.toString())
+
+ pom {
+ name = project.name
+ description = "A Compose Multiplatform implementation of Material Design tap targets, for feature discovery."
+ inceptionYear = "2026"
+ url = "https://github.com/ienground/tap-target-cmp"
+ licenses {
+ license {
+ name = "Apache-2.0"
+ url = "https://www.apache.org/licenses/LICENSE-2.0"
+ }
+ }
+ developers {
+ developer {
+ id = "ienground"
+ name = "Ericano Rhee"
+ url = "https://www.ien.zone"
+ }
+ }
+ scm {
+ url = "https://github.com/ienground/tap-target-cmp.git"
+ connection = "scm:git:https://github.com/ienground/tap-target-cmp.git"
+ developerConnection = "scm:git:https://github.com/ienground/tap-target-cmp.git"
+ }
+ }
+
+ val isPublishingToMavenLocal = gradle.startParameter.taskNames.any { it.contains("publishToMavenLocal", ignoreCase = true) }
+ val isSnapshot = version.toString().endsWith("SNAPSHOT")
+
+ if (!isSnapshot && !isPublishingToMavenLocal) {
+ signAllPublications()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
deleted file mode 100644
index f55cb6b..0000000
--- a/buildSrc/build.gradle.kts
+++ /dev/null
@@ -1,7 +0,0 @@
-plugins {
- `kotlin-dsl`
-}
-
-repositories {
- mavenCentral()
-}
\ No newline at end of file
diff --git a/buildSrc/src/main/java/com/psoffritti/taptargetcompose/Configuration.kt b/buildSrc/src/main/java/com/psoffritti/taptargetcompose/Configuration.kt
deleted file mode 100644
index f0d9a71..0000000
--- a/buildSrc/src/main/java/com/psoffritti/taptargetcompose/Configuration.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.psoffritti.taptargetcompose
-
-object Configuration {
- const val compileSdk = 35
- const val targetSdk = 35
- const val minSdk = 13
- const val minSdkSampleApp = 21
- private const val majorVersion = 1
- private const val minorVersion = 2
- private const val patchVersion = 1
- const val versionCode = 4
- const val versionName = "$majorVersion.$minorVersion.$patchVersion"
- const val snapshotVersionName = "$majorVersion.$minorVersion.${patchVersion + 1}-SNAPSHOT"
-}
\ No newline at end of file
diff --git a/example/androidApp/build.gradle.kts b/example/androidApp/build.gradle.kts
new file mode 100644
index 0000000..42e75c4
--- /dev/null
+++ b/example/androidApp/build.gradle.kts
@@ -0,0 +1,49 @@
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
+plugins {
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.compose.multiplatform)
+ alias(libs.plugins.compose.compiler)
+}
+
+android {
+ namespace = "zone.ien.taptargetcmp.example"
+ compileSdk = libs.versions.android.compileSdk.get().toInt()
+
+ defaultConfig {
+ applicationId = "zone.ien.taptargetcmp.example"
+ minSdk = libs.versions.android.minSdk.get().toInt()
+ targetSdk = libs.versions.android.targetSdk.get().toInt()
+ versionCode = 1
+ versionName = libs.versions.lib.version.name.get()
+ }
+ packaging {
+ resources {
+ excludes += "/META-INF/{AL2.0,LGPL2.1}"
+ }
+ }
+ buildTypes {
+ getByName("release") {
+ isMinifyEnabled = false
+ proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_21
+ targetCompatibility = JavaVersion.VERSION_21
+ }
+}
+
+kotlin {
+ target {
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_21)
+ }
+ }
+
+ dependencies {
+ implementation(projects.example.composeApp)
+ implementation(libs.activity.compose)
+ implementation(libs.compose.ui.tooling)
+ }
+}
\ No newline at end of file
diff --git a/sample-app/proguard-rules.pro b/example/androidApp/proguard-rules.pro
similarity index 100%
rename from sample-app/proguard-rules.pro
rename to example/androidApp/proguard-rules.pro
diff --git a/sample-app/src/main/AndroidManifest.xml b/example/androidApp/src/main/AndroidManifest.xml
similarity index 100%
rename from sample-app/src/main/AndroidManifest.xml
rename to example/androidApp/src/main/AndroidManifest.xml
diff --git a/example/androidApp/src/main/kotlin/zone/ien/taptargetcmp/example/MainActivity.kt b/example/androidApp/src/main/kotlin/zone/ien/taptargetcmp/example/MainActivity.kt
new file mode 100644
index 0000000..e2184da
--- /dev/null
+++ b/example/androidApp/src/main/kotlin/zone/ien/taptargetcmp/example/MainActivity.kt
@@ -0,0 +1,24 @@
+package zone.ien.taptargetcmp.example
+
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.tooling.preview.Preview
+
+class MainActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: android.os.Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setContent {
+ enableEdgeToEdge()
+ App()
+ }
+ }
+}
+
+@Preview
+@Composable
+fun AppAndroidPreview() {
+ App()
+}
\ No newline at end of file
diff --git a/sample-app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/example/androidApp/src/main/res/drawable-v24/ic_launcher_foreground.xml
similarity index 100%
rename from sample-app/src/main/res/drawable-v24/ic_launcher_foreground.xml
rename to example/androidApp/src/main/res/drawable-v24/ic_launcher_foreground.xml
diff --git a/sample-app/src/main/res/drawable/ic_launcher_background.xml b/example/androidApp/src/main/res/drawable/ic_launcher_background.xml
similarity index 100%
rename from sample-app/src/main/res/drawable/ic_launcher_background.xml
rename to example/androidApp/src/main/res/drawable/ic_launcher_background.xml
diff --git a/sample-app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/example/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
similarity index 100%
rename from sample-app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
rename to example/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
diff --git a/sample-app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/example/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
similarity index 100%
rename from sample-app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
rename to example/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
diff --git a/sample-app/src/main/res/mipmap-hdpi/ic_launcher.webp b/example/androidApp/src/main/res/mipmap-hdpi/ic_launcher.webp
similarity index 100%
rename from sample-app/src/main/res/mipmap-hdpi/ic_launcher.webp
rename to example/androidApp/src/main/res/mipmap-hdpi/ic_launcher.webp
diff --git a/sample-app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/example/androidApp/src/main/res/mipmap-hdpi/ic_launcher_round.webp
similarity index 100%
rename from sample-app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
rename to example/androidApp/src/main/res/mipmap-hdpi/ic_launcher_round.webp
diff --git a/sample-app/src/main/res/mipmap-mdpi/ic_launcher.webp b/example/androidApp/src/main/res/mipmap-mdpi/ic_launcher.webp
similarity index 100%
rename from sample-app/src/main/res/mipmap-mdpi/ic_launcher.webp
rename to example/androidApp/src/main/res/mipmap-mdpi/ic_launcher.webp
diff --git a/sample-app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/example/androidApp/src/main/res/mipmap-mdpi/ic_launcher_round.webp
similarity index 100%
rename from sample-app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
rename to example/androidApp/src/main/res/mipmap-mdpi/ic_launcher_round.webp
diff --git a/sample-app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/example/androidApp/src/main/res/mipmap-xhdpi/ic_launcher.webp
similarity index 100%
rename from sample-app/src/main/res/mipmap-xhdpi/ic_launcher.webp
rename to example/androidApp/src/main/res/mipmap-xhdpi/ic_launcher.webp
diff --git a/sample-app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/example/androidApp/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
similarity index 100%
rename from sample-app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
rename to example/androidApp/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
diff --git a/sample-app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/example/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher.webp
similarity index 100%
rename from sample-app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
rename to example/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher.webp
diff --git a/sample-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/example/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
similarity index 100%
rename from sample-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
rename to example/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
diff --git a/sample-app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/example/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
similarity index 100%
rename from sample-app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
rename to example/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
diff --git a/sample-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/example/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
similarity index 100%
rename from sample-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
rename to example/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
diff --git a/sample-app/src/main/res/values/strings.xml b/example/androidApp/src/main/res/values/strings.xml
similarity index 100%
rename from sample-app/src/main/res/values/strings.xml
rename to example/androidApp/src/main/res/values/strings.xml
diff --git a/sample-app/src/main/res/values/themes.xml b/example/androidApp/src/main/res/values/themes.xml
similarity index 100%
rename from sample-app/src/main/res/values/themes.xml
rename to example/androidApp/src/main/res/values/themes.xml
diff --git a/example/composeApp/build.gradle.kts b/example/composeApp/build.gradle.kts
new file mode 100644
index 0000000..7682fab
--- /dev/null
+++ b/example/composeApp/build.gradle.kts
@@ -0,0 +1,50 @@
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
+plugins {
+ alias(libs.plugins.kotlin.multiplatform)
+ alias(libs.plugins.android.kotlin.multiplatform.library)
+ alias(libs.plugins.serialization)
+ alias(libs.plugins.compose.multiplatform)
+ alias(libs.plugins.compose.compiler)
+}
+
+kotlin {
+ androidLibrary {
+ namespace = "zone.ien.taptargetcmp.example.lib"
+ compileSdk = libs.versions.android.compileSdk.get().toInt()
+ minSdk = libs.versions.android.minSdk.get().toInt()
+
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_21)
+ }
+
+ androidResources {
+ enable = true
+ }
+ }
+
+ listOf(
+ iosArm64(),
+ iosSimulatorArm64()
+ ).forEach { iosTarget ->
+ iosTarget.binaries.framework {
+ baseName = "ComposeApp"
+ isStatic = true
+ }
+ }
+
+ sourceSets {
+ commonMain.dependencies {
+ implementation(libs.compose.material3)
+ implementation(libs.compose.preview)
+ implementation(projects.taptarget)
+ }
+ commonTest.dependencies {
+ implementation(libs.kotlin.test)
+ }
+ }
+}
+
+dependencies {
+ androidRuntimeClasspath(libs.compose.ui.tooling)
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..bd01ffa
--- /dev/null
+++ b/example/composeApp/src/commonMain/kotlin/zone/ien/taptargetcmp/example/App.kt
@@ -0,0 +1,171 @@
+package zone.ien.taptargetcmp.example
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExtendedFloatingActionButton
+import androidx.compose.material3.FabPosition
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Tab
+import androidx.compose.material3.TabRow
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.launch
+import zone.ien.taptargetcmp.TapTargetCoordinator
+import zone.ien.taptargetcmp.TapTargetDefinition
+import zone.ien.taptargetcmp.TapTargetScope
+import zone.ien.taptargetcmp.TapTargetStyle
+import zone.ien.taptargetcmp.TextDefinition
+import zone.ien.taptargetcmp.example.icon.Add
+import zone.ien.taptargetcmp.example.icon.MaterialIcons
+import zone.ien.taptargetcmp.example.ui.theme.AppTheme
+
+@Composable
+fun App() {
+ AppTheme {
+ TapTargetCoordinator(showTapTargets = true, onComplete = {}) {
+ Surface(modifier = Modifier.fillMaxSize()) {
+ Content()
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun TapTargetScope.Content() {
+ val toolbarTapTarget = getStandardTapTargetDefinition(
+ precedence = 1,
+ title = "Toolbar tap target",
+ description = "This is a toolbar, tap it!",
+ )
+
+ val tab2TapTarget = getStandardTapTargetDefinition(
+ precedence = 2,
+ title = "Tab2 tap target",
+ description = "A moving target",
+ )
+
+ Scaffold(
+ modifier = Modifier.fillMaxSize(),
+ containerColor = MaterialTheme.colorScheme.surface,
+ topBar = {
+ TopAppBar(title = {
+ Text(text = "Tap Target", modifier = Modifier.tapTarget(toolbarTapTarget))
+ })
+ },
+ floatingActionButton = {
+ ExtendedFloatingActionButton(
+ onClick = { },
+ icon = { Icon(MaterialIcons.Add, contentDescription = null) },
+ text = { Text(text = "Click here") },
+ modifier = Modifier.tapTarget(
+ getStandardTapTargetDefinition(
+ precedence = 0,
+ title = "Button tap target",
+ description = "This is a button, tap it!",
+ )
+ ),
+ )
+ },
+ floatingActionButtonPosition = FabPosition.End,
+ ) { innerPadding ->
+ Column(modifier = Modifier.padding(innerPadding)) {
+ TabbedControls(onTabSelected = {}) { tab ->
+ when (tab) {
+ Tab.TAB1 -> Text("Tab 1 content")
+ Tab.TAB2 -> Text("Tab 2 content", modifier = Modifier.tapTarget(tab2TapTarget))
+ }
+ }
+ }
+ }
+}
+
+private enum class Tab {
+ TAB1, TAB2;
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun TabbedControls(
+ onTabSelected: (Tab) -> Unit,
+ modifier: Modifier = Modifier,
+ contentProvider: @Composable (Tab) -> Unit
+) {
+ val scope = rememberCoroutineScope()
+
+ val pagerState = rememberPagerState(pageCount = { Tab.entries.size })
+ val tabTitles = Tab.entries.map {
+ when (it) {
+ Tab.TAB1 -> "Tab1"
+ Tab.TAB2 -> "Tab2"
+ }
+ }
+
+ Column(modifier = modifier) {
+ TabRow(selectedTabIndex = pagerState.currentPage) {
+ tabTitles.forEachIndexed { index, title ->
+ Tab(
+ selected = index == pagerState.currentPage,
+ onClick = { scope.launch { pagerState.animateScrollToPage(index) } },
+ text = { Text(title) }
+ )
+ }
+ }
+ Spacer(modifier = Modifier.height(8.dp))
+ HorizontalPager(state = pagerState, modifier = Modifier.height(128.dp)) { pageIndex ->
+ contentProvider(Tab.entries[pageIndex])
+ }
+
+ LaunchedEffect(pagerState.settledPage) {
+ // Trigger the callback each time settledPage changes.
+ onTabSelected(Tab.entries[pagerState.settledPage])
+ }
+ }
+}
+
+@Composable
+private fun getStandardTapTargetDefinition(
+ precedence: Int,
+ title: String,
+ description: String,
+ onTargetCLick: () -> Unit = {},
+ onTargetCancel: () -> Unit = {},
+): TapTargetDefinition {
+ return TapTargetDefinition(
+ precedence = precedence,
+ title = TextDefinition(
+ text = title,
+ textStyle = MaterialTheme.typography.titleLarge,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.onSecondaryContainer
+ ),
+ description = TextDefinition(
+ text = description,
+ textStyle = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSecondaryContainer
+ ),
+ tapTargetStyle = TapTargetStyle(
+ backgroundColor = MaterialTheme.colorScheme.secondaryContainer,
+ tapTargetHighlightColor = MaterialTheme.colorScheme.onSecondaryContainer,
+ backgroundAlpha = 1f,
+ ),
+ onTargetClick = onTargetCLick,
+ onTargetCancel = onTargetCancel,
+ )
+}
\ No newline at end of file
diff --git a/example/composeApp/src/commonMain/kotlin/zone/ien/taptargetcmp/example/icon/Add.kt b/example/composeApp/src/commonMain/kotlin/zone/ien/taptargetcmp/example/icon/Add.kt
new file mode 100644
index 0000000..6120b13
--- /dev/null
+++ b/example/composeApp/src/commonMain/kotlin/zone/ien/taptargetcmp/example/icon/Add.kt
@@ -0,0 +1,55 @@
+package zone.ien.taptargetcmp.example.icon
+
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.graphics.vector.path
+import androidx.compose.ui.unit.dp
+
+val MaterialIcons.Add: ImageVector
+ get() {
+ if (_Add != null) {
+ return _Add!!
+ }
+ _Add = ImageVector.Builder(
+ name = "Add",
+ defaultWidth = 24.dp,
+ defaultHeight = 24.dp,
+ viewportWidth = 960f,
+ viewportHeight = 960f
+ ).apply {
+ path(fill = SolidColor(Color(0xFF1F1F1F))) {
+ moveTo(440f, 520f)
+ lineTo(240f, 520f)
+ quadToRelative(-17f, 0f, -28.5f, -11.5f)
+ reflectiveQuadTo(200f, 480f)
+ quadToRelative(0f, -17f, 11.5f, -28.5f)
+ reflectiveQuadTo(240f, 440f)
+ horizontalLineToRelative(200f)
+ verticalLineToRelative(-200f)
+ quadToRelative(0f, -17f, 11.5f, -28.5f)
+ reflectiveQuadTo(480f, 200f)
+ quadToRelative(17f, 0f, 28.5f, 11.5f)
+ reflectiveQuadTo(520f, 240f)
+ verticalLineToRelative(200f)
+ horizontalLineToRelative(200f)
+ quadToRelative(17f, 0f, 28.5f, 11.5f)
+ reflectiveQuadTo(760f, 480f)
+ quadToRelative(0f, 17f, -11.5f, 28.5f)
+ reflectiveQuadTo(720f, 520f)
+ lineTo(520f, 520f)
+ verticalLineToRelative(200f)
+ quadToRelative(0f, 17f, -11.5f, 28.5f)
+ reflectiveQuadTo(480f, 760f)
+ quadToRelative(-17f, 0f, -28.5f, -11.5f)
+ reflectiveQuadTo(440f, 720f)
+ verticalLineToRelative(-200f)
+ close()
+ }
+ }.build()
+
+ return _Add!!
+ }
+
+@Suppress("ObjectPropertyName")
+private var _Add: ImageVector? = null
diff --git a/example/composeApp/src/commonMain/kotlin/zone/ien/taptargetcmp/example/icon/MaterialIcons.kt b/example/composeApp/src/commonMain/kotlin/zone/ien/taptargetcmp/example/icon/MaterialIcons.kt
new file mode 100644
index 0000000..5cd2d9d
--- /dev/null
+++ b/example/composeApp/src/commonMain/kotlin/zone/ien/taptargetcmp/example/icon/MaterialIcons.kt
@@ -0,0 +1,3 @@
+package zone.ien.taptargetcmp.example.icon
+
+object MaterialIcons
diff --git a/sample-app/src/main/java/com/psoffritti/taptargetcompose/ui/theme/Color.kt b/example/composeApp/src/commonMain/kotlin/zone/ien/taptargetcmp/example/ui/theme/Color.kt
similarity index 98%
rename from sample-app/src/main/java/com/psoffritti/taptargetcompose/ui/theme/Color.kt
rename to example/composeApp/src/commonMain/kotlin/zone/ien/taptargetcmp/example/ui/theme/Color.kt
index 1561045..5e5cb12 100644
--- a/sample-app/src/main/java/com/psoffritti/taptargetcompose/ui/theme/Color.kt
+++ b/example/composeApp/src/commonMain/kotlin/zone/ien/taptargetcmp/example/ui/theme/Color.kt
@@ -1,4 +1,4 @@
-package com.psoffritti.taptargetcompose.ui.theme
+package zone.ien.taptargetcmp.example.ui.theme
import androidx.compose.ui.graphics.Color
diff --git a/sample-app/src/main/java/com/psoffritti/taptargetcompose/ui/theme/Theme.kt b/example/composeApp/src/commonMain/kotlin/zone/ien/taptargetcmp/example/ui/theme/Theme.kt
similarity index 98%
rename from sample-app/src/main/java/com/psoffritti/taptargetcompose/ui/theme/Theme.kt
rename to example/composeApp/src/commonMain/kotlin/zone/ien/taptargetcmp/example/ui/theme/Theme.kt
index 9a68d7e..dca9dd4 100644
--- a/sample-app/src/main/java/com/psoffritti/taptargetcompose/ui/theme/Theme.kt
+++ b/example/composeApp/src/commonMain/kotlin/zone/ien/taptargetcmp/example/ui/theme/Theme.kt
@@ -1,4 +1,4 @@
-package com.psoffritti.taptargetcompose.ui.theme
+package zone.ien.taptargetcmp.example.ui.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
diff --git a/example/composeApp/src/iosMain/kotlin/zone/ien/taptargetcmp/example/MainViewController.kt b/example/composeApp/src/iosMain/kotlin/zone/ien/taptargetcmp/example/MainViewController.kt
new file mode 100644
index 0000000..7f45e35
--- /dev/null
+++ b/example/composeApp/src/iosMain/kotlin/zone/ien/taptargetcmp/example/MainViewController.kt
@@ -0,0 +1,5 @@
+package zone.ien.taptargetcmp.example
+
+import androidx.compose.ui.window.ComposeUIViewController
+
+fun MainViewController() = ComposeUIViewController { App() }
\ No newline at end of file
diff --git a/example/iosApp/Configuration/Config.xcconfig b/example/iosApp/Configuration/Config.xcconfig
new file mode 100644
index 0000000..7f4ce0b
--- /dev/null
+++ b/example/iosApp/Configuration/Config.xcconfig
@@ -0,0 +1,7 @@
+TEAM_ID=
+
+PRODUCT_NAME=Taptarget
+PRODUCT_BUNDLE_IDENTIFIER=zone.ien.taptargetcmp.example.Example$(TEAM_ID)
+
+CURRENT_PROJECT_VERSION=1
+MARKETING_VERSION=1.0
\ No newline at end of file
diff --git a/example/iosApp/iosApp.xcodeproj/project.pbxproj b/example/iosApp/iosApp.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..7e9110a
--- /dev/null
+++ b/example/iosApp/iosApp.xcodeproj/project.pbxproj
@@ -0,0 +1,375 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 77;
+ objects = {
+
+/* Begin PBXFileReference section */
+ 3A70B7FFD2EF9475B39878DC /* Taptarget.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Taptarget.app; sourceTree = BUILT_PRODUCTS_DIR; };
+/* End PBXFileReference section */
+
+/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
+ BF60BC51A4FC830ECF66E9A8 /* Exceptions for "iosApp" folder in "iosApp" target */ = {
+ isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
+ membershipExceptions = (
+ Info.plist,
+ );
+ target = 451901D00D1CFF1EE0CD9FA3 /* iosApp */;
+ };
+/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
+
+/* Begin PBXFileSystemSynchronizedRootGroup section */
+ 618D6E17DA4269CB0EBD4332 /* iosApp */ = {
+ isa = PBXFileSystemSynchronizedRootGroup;
+ exceptions = (
+ BF60BC51A4FC830ECF66E9A8 /* Exceptions for "iosApp" folder in "iosApp" target */,
+ );
+ path = iosApp;
+ sourceTree = "";
+ };
+ 7F32DC8847A2528DD5BA3D3C /* Configuration */ = {
+ isa = PBXFileSystemSynchronizedRootGroup;
+ path = Configuration;
+ sourceTree = "";
+ };
+/* End PBXFileSystemSynchronizedRootGroup section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 4E041684C0A28B3842B9CDB1 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 9A4064F9F00428CC8A74D355 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 3A70B7FFD2EF9475B39878DC /* Taptarget.app */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ E0D5208EC197DC2AAFCBD5F8 = {
+ isa = PBXGroup;
+ children = (
+ 7F32DC8847A2528DD5BA3D3C /* Configuration */,
+ 618D6E17DA4269CB0EBD4332 /* iosApp */,
+ 9A4064F9F00428CC8A74D355 /* Products */,
+ );
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 451901D00D1CFF1EE0CD9FA3 /* iosApp */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = EDDB7722E65351F3212F695A /* Build configuration list for PBXNativeTarget "iosApp" */;
+ buildPhases = (
+ 413695130B836538C0F1B79E /* Compile Kotlin Framework */,
+ 7182ADBE3A18A5ED0EABA306 /* Sources */,
+ 4E041684C0A28B3842B9CDB1 /* Frameworks */,
+ ADE552C2797EC60E023FB327 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ fileSystemSynchronizedGroups = (
+ 618D6E17DA4269CB0EBD4332 /* iosApp */,
+ );
+ name = iosApp;
+ packageProductDependencies = (
+ );
+ productName = iosApp;
+ productReference = 3A70B7FFD2EF9475B39878DC /* Taptarget.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ A8E4552D1C27948C03A84D99 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ BuildIndependentTargetsInParallel = 1;
+ LastSwiftUpdateCheck = 1620;
+ LastUpgradeCheck = 1620;
+ TargetAttributes = {
+ 451901D00D1CFF1EE0CD9FA3 = {
+ CreatedOnToolsVersion = 16.2;
+ };
+ };
+ };
+ buildConfigurationList = FCC04410EA9A0AD585F4BD99 /* Build configuration list for PBXProject "iosApp" */;
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = E0D5208EC197DC2AAFCBD5F8;
+ minimizedProjectReferenceProxies = 1;
+ preferredProjectObjectVersion = 77;
+ productRefGroup = 9A4064F9F00428CC8A74D355 /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 451901D00D1CFF1EE0CD9FA3 /* iosApp */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ ADE552C2797EC60E023FB327 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ 413695130B836538C0F1B79E /* Compile Kotlin Framework */ = {
+ isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ );
+ name = "Compile Kotlin Framework";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "if [ \"YES\" = \"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \\\"YES\\\"\"\n exit 0\nfi\ncd \"$SRCROOT/..\"\n../gradlew :example:composeApp:embedAndSignAppleFrameworkForXcode\n";
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 7182ADBE3A18A5ED0EABA306 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin XCBuildConfiguration section */
+ 1BC302D312A25FFCB2FC8457 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ARCHS = arm64;
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
+ DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
+ DEVELOPMENT_TEAM = M8ZHHMB2AV;
+ ENABLE_PREVIEWS = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = iosApp/Info.plist;
+ INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
+ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+ INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = zone.ien.taptargetcmp.example.Example;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
+ 5540229B7DD869650AE56760 /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReferenceAnchor = 7F32DC8847A2528DD5BA3D3C /* Configuration */;
+ baseConfigurationReferenceRelativePath = Config.xcconfig;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 18.2;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ MTL_FAST_MATH = YES;
+ SDKROOT = iphoneos;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ 6D56266BAC5382354FF6611D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ARCHS = arm64;
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
+ DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
+ DEVELOPMENT_TEAM = M8ZHHMB2AV;
+ ENABLE_PREVIEWS = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = iosApp/Info.plist;
+ INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
+ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+ INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = zone.ien.taptargetcmp.example.Example;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ B703D85C6CA065A3541FC1D5 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReferenceAnchor = 7F32DC8847A2528DD5BA3D3C /* Configuration */;
+ baseConfigurationReferenceRelativePath = Config.xcconfig;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 18.2;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ };
+ name = Debug;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ EDDB7722E65351F3212F695A /* Build configuration list for PBXNativeTarget "iosApp" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 6D56266BAC5382354FF6611D /* Debug */,
+ 1BC302D312A25FFCB2FC8457 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ FCC04410EA9A0AD585F4BD99 /* Build configuration list for PBXProject "iosApp" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ B703D85C6CA065A3541FC1D5 /* Debug */,
+ 5540229B7DD869650AE56760 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = A8E4552D1C27948C03A84D99 /* Project object */;
+}
diff --git a/example/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/example/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..919434a
--- /dev/null
+++ b/example/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/example/iosApp/iosApp.xcodeproj/project.xcworkspace/xcuserdata/ienground.xcuserdatad/UserInterfaceState.xcuserstate b/example/iosApp/iosApp.xcodeproj/project.xcworkspace/xcuserdata/ienground.xcuserdatad/UserInterfaceState.xcuserstate
new file mode 100644
index 0000000..a1fd271
Binary files /dev/null and b/example/iosApp/iosApp.xcodeproj/project.xcworkspace/xcuserdata/ienground.xcuserdatad/UserInterfaceState.xcuserstate differ
diff --git a/example/iosApp/iosApp.xcodeproj/project.xcworkspace/xcuserdata/ienground.xcuserdatad/xcschemes/xcschememanagement.plist b/example/iosApp/iosApp.xcodeproj/project.xcworkspace/xcuserdata/ienground.xcuserdatad/xcschemes/xcschememanagement.plist
new file mode 100644
index 0000000..ee3458d
--- /dev/null
+++ b/example/iosApp/iosApp.xcodeproj/project.xcworkspace/xcuserdata/ienground.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/example/iosApp/iosApp.xcodeproj/xcuserdata/ienground.xcuserdatad/xcschemes/iosApp.xcscheme b/example/iosApp/iosApp.xcodeproj/xcuserdata/ienground.xcuserdatad/xcschemes/iosApp.xcscheme
new file mode 100644
index 0000000..743ecb8
--- /dev/null
+++ b/example/iosApp/iosApp.xcodeproj/xcuserdata/ienground.xcuserdatad/xcschemes/iosApp.xcscheme
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/iosApp/iosApp.xcodeproj/xcuserdata/ienground.xcuserdatad/xcschemes/xcschememanagement.plist b/example/iosApp/iosApp.xcodeproj/xcuserdata/ienground.xcuserdatad/xcschemes/xcschememanagement.plist
new file mode 100644
index 0000000..fa59f97
--- /dev/null
+++ b/example/iosApp/iosApp.xcodeproj/xcuserdata/ienground.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -0,0 +1,14 @@
+
+
+
+
+ SchemeUserState
+
+ iosApp.xcscheme
+
+ orderHint
+ 0
+
+
+
+
diff --git a/example/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json b/example/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json
new file mode 100644
index 0000000..eb87897
--- /dev/null
+++ b/example/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "colors" : [
+ {
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/example/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/example/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..4e8d485
--- /dev/null
+++ b/example/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,36 @@
+{
+ "images" : [
+ {
+ "filename" : "app-icon-1024.png",
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "tinted"
+ }
+ ],
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/example/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png b/example/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png
new file mode 100644
index 0000000..53fc536
Binary files /dev/null and b/example/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png differ
diff --git a/example/iosApp/iosApp/Assets.xcassets/Contents.json b/example/iosApp/iosApp/Assets.xcassets/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/example/iosApp/iosApp/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/example/iosApp/iosApp/ContentView.swift b/example/iosApp/iosApp/ContentView.swift
new file mode 100644
index 0000000..c765ff2
--- /dev/null
+++ b/example/iosApp/iosApp/ContentView.swift
@@ -0,0 +1,21 @@
+import UIKit
+import SwiftUI
+import ComposeApp
+
+struct ComposeView: UIViewControllerRepresentable {
+ func makeUIViewController(context: Context) -> UIViewController {
+ MainViewControllerKt.MainViewController()
+ }
+
+ func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
+}
+
+struct ContentView: View {
+ var body: some View {
+ ComposeView()
+ .ignoresSafeArea()
+ }
+}
+
+
+
diff --git a/example/iosApp/iosApp/Info.plist b/example/iosApp/iosApp/Info.plist
new file mode 100644
index 0000000..11845e1
--- /dev/null
+++ b/example/iosApp/iosApp/Info.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ CADisableMinimumFrameDurationOnPhone
+
+
+
diff --git a/example/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json b/example/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/example/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/example/iosApp/iosApp/iOSApp.swift b/example/iosApp/iosApp/iOSApp.swift
new file mode 100644
index 0000000..b56461c
--- /dev/null
+++ b/example/iosApp/iosApp/iOSApp.swift
@@ -0,0 +1,15 @@
+import SwiftUI
+import ComposeApp
+
+@main
+struct iOSApp: App {
+ init() {
+
+ }
+
+ var body: some Scene {
+ WindowGroup {
+ ContentView()
+ }
+ }
+}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 51a9535..9184c14 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,35 +1,53 @@
[versions]
-compose-bom = "2024.10.00"
-activity-compose = "1.9.3"
-core-ktx = "1.13.1"
-appcompat = "1.7.0"
-material = "1.12.0"
+agp = "9.0.0"
+kotlin = "2.3.21"
+android-minSdk = "29"
+android-compileSdk = "36"
+android-targetSdk = "36"
+lib-version-name = "1.2.1"
-androidGradlePlugin = "8.7.1"
-androidLibrary = "8.7.1"
-kotlinAndroid = "2.0.20"
+# plugin
+compose-plugin = "1.11.0-rc01"
+compose-plugin-experimental = "1.11.0-alpha07"
+compose-navigation3 = "1.1.1"
-gradleNexus = "1.3.0"
-dokka = "1.8.10"
+# ui
+activity-compose = "1.13.0"
+
+# functionality
+androidx-core = "1.18.0"
+
+# backend
+vanniktechMavenPublish = "0.36.0"
+
+# test
+runner = "1.7.0"
+coreVersion = "1.7.0"
+junit = "1.3.0"
[libraries]
-androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" }
-androidx-compose-material3 = { module = "androidx.compose.material3:material3" }
-androidx-compose-material-icons = { module = "androidx.compose.material:material-icons-extended" }
-androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
-androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
-androidx-compose-animation = { module = "androidx.compose.animation:animation" }
-androidx-compose-foundation = { module = "androidx.compose.foundation:foundation" }
-
-androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity-compose" }
-androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "core-ktx" }
-androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
-material = { module = "com.google.android.material:material", version.ref = "material" }
+# ui
+activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity-compose" }
+compose-material3 = { group = "org.jetbrains.compose.material3", name = "material3", version.ref = "compose-plugin-experimental"}
+compose-preview = { group = "org.jetbrains.compose.ui", name = "ui-tooling-preview", version.ref = "compose-plugin" }
+compose-ui-tooling = { group = "org.jetbrains.compose.ui", name = "ui-tooling", version.ref = "compose-plugin" }
+compose-resources = { group = "org.jetbrains.compose.components", name = "components-resources", version.ref = "compose-plugin" }
+
+# functionality
+androidx-core = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core"} # android only
+
+# test
+kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
+runner = { group = "androidx.test", name = "runner", version.ref = "runner" }
+core = { group = "androidx.test", name = "core", version.ref = "coreVersion" }
+junit = { group = "androidx.test.ext", name = "junit", version.ref = "junit" }
[plugins]
-android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
-android-library = { id = "com.android.library", version.ref = "androidLibrary" }
-jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlinAndroid" }
-gradleNexus-publish = { id = "io.github.gradle-nexus.publish-plugin", version.ref = "gradleNexus" }
-jetbrains-dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }
-compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlinAndroid" }
\ No newline at end of file
+android-kotlin-multiplatform-library = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" }
+android-application = { id = "com.android.application" }
+compose-multiplatform = { id = "org.jetbrains.compose", version.ref = "compose-plugin" }
+compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
+kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
+serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin"}
+vanniktech-mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "vanniktechMavenPublish" }
+android-lint = { id = "com.android.lint", version.ref = "agp" }
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index cd59683..61bcd03 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,7 @@
-#Fri Dec 08 17:09:33 GMT 2023
+#Thu May 07 22:10:16 KST 2026
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
+distributionSha256Sum=a17ddd85a26b6a7f5ddb71ff8b05fc5104c0202c6e64782429790c933686c806
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/sample-app/.gitignore b/sample-app/.gitignore
deleted file mode 100644
index 42afabf..0000000
--- a/sample-app/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/build
\ No newline at end of file
diff --git a/sample-app/build.gradle.kts b/sample-app/build.gradle.kts
deleted file mode 100644
index 7539b48..0000000
--- a/sample-app/build.gradle.kts
+++ /dev/null
@@ -1,58 +0,0 @@
-import com.psoffritti.taptargetcompose.Configuration
-import org.gradle.kotlin.dsl.android
-
-plugins {
- alias(libs.plugins.android.application)
- alias(libs.plugins.jetbrains.kotlin.android)
- alias(libs.plugins.compose.compiler)
-}
-
-android {
- namespace = "com.psoffritti.taptargetcompose"
- compileSdk = Configuration.compileSdk
-
- defaultConfig {
- applicationId = "com.psoffritti.taptargetcompose"
- minSdk = Configuration.minSdkSampleApp
- targetSdk = Configuration.targetSdk
- versionCode = Configuration.versionCode
- versionName = Configuration.versionName
-
- testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
- }
-
- buildTypes {
- release {
- isMinifyEnabled = false
- proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
- }
- }
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_1_8
- targetCompatibility = JavaVersion.VERSION_1_8
- }
- kotlinOptions {
- jvmTarget = "1.8"
- }
- buildFeatures {
- compose = true
- }
-}
-
-dependencies {
- implementation(project(":tap-target-compose"))
-
- implementation(libs.androidx.core.ktx)
- implementation(libs.androidx.appcompat)
- implementation(libs.material)
-
- val composeBom = platform(libs.androidx.compose.bom)
- implementation(composeBom)
- androidTestImplementation(composeBom)
- implementation(libs.androidx.compose.material3)
- implementation(libs.androidx.compose.material.icons)
- implementation(libs.androidx.activity.compose)
-
- implementation(libs.androidx.compose.ui.tooling.preview)
- implementation(libs.androidx.compose.ui.tooling)
-}
\ No newline at end of file
diff --git a/sample-app/src/main/java/com/psoffritti/taptargetcompose/MainActivity.kt b/sample-app/src/main/java/com/psoffritti/taptargetcompose/MainActivity.kt
deleted file mode 100644
index 29dfb1e..0000000
--- a/sample-app/src/main/java/com/psoffritti/taptargetcompose/MainActivity.kt
+++ /dev/null
@@ -1,175 +0,0 @@
-package com.psoffritti.taptargetcompose
-
-import android.os.Bundle
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.pager.HorizontalPager
-import androidx.compose.foundation.pager.rememberPagerState
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Add
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.ExtendedFloatingActionButton
-import androidx.compose.material3.FabPosition
-import androidx.compose.material3.Icon
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Surface
-import androidx.compose.material3.Tab
-import androidx.compose.material3.TabRow
-import androidx.compose.material3.Text
-import androidx.compose.material3.TopAppBar
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.dp
-import com.psoffritti.taptargetcompose.ui.theme.AppTheme
-import kotlinx.coroutines.launch
-
-class MainActivity : ComponentActivity() {
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- setContent {
- AppTheme {
- TapTargetCoordinator(showTapTargets = true, onComplete = {}) {
- Surface(modifier = Modifier.fillMaxSize()) {
- Content()
- }
- }
- }
- }
- }
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-private fun TapTargetScope.Content() {
- val toolbarTapTarget = getStandardTapTargetDefinition(
- precedence = 1,
- title = "Toolbar tap target",
- description = "This is a toolbar, tap it!",
- )
-
- val tab2TapTarget = getStandardTapTargetDefinition(
- precedence = 2,
- title = "Tab2 tap target",
- description = "A moving target",
- )
-
- Scaffold(
- modifier = Modifier.fillMaxSize(),
- containerColor = MaterialTheme.colorScheme.surface,
- topBar = {
- TopAppBar(title = {
- Text(text = "Tap Target", modifier = Modifier.tapTarget(toolbarTapTarget))
- })
- },
- floatingActionButton = {
- ExtendedFloatingActionButton(
- onClick = { },
- icon = { Icon(Icons.Filled.Add, contentDescription = null) },
- text = { Text(text = "Click here") },
- modifier = Modifier.tapTarget(
- getStandardTapTargetDefinition(
- precedence = 0,
- title = "Button tap target",
- description = "This is a button, tap it!",
- )
- ),
- )
- },
- floatingActionButtonPosition = FabPosition.End,
- ) { innerPadding ->
- Column(modifier = Modifier.padding(innerPadding)) {
- TabbedControls(onTabSelected = {}) { tab ->
- when (tab) {
- Tab.TAB1 -> Text("Tab 1 content")
- Tab.TAB2 -> Text("Tab 2 content", modifier = Modifier.tapTarget(tab2TapTarget))
- }
- }
- }
- }
-}
-
-private enum class Tab {
- TAB1, TAB2;
-}
-
-@OptIn(ExperimentalFoundationApi::class)
-@Composable
-private fun TabbedControls(
- onTabSelected: (Tab) -> Unit,
- modifier: Modifier = Modifier,
- contentProvider: @Composable (Tab) -> Unit
-) {
- val scope = rememberCoroutineScope()
-
- val pagerState = rememberPagerState(pageCount = { Tab.entries.size })
- val tabTitles = Tab.entries.map {
- when (it) {
- Tab.TAB1 -> "Tab1"
- Tab.TAB2 -> "Tab2"
- }
- }
-
- Column(modifier = modifier) {
- TabRow(selectedTabIndex = pagerState.currentPage) {
- tabTitles.forEachIndexed { index, title ->
- Tab(
- selected = index == pagerState.currentPage,
- onClick = { scope.launch { pagerState.animateScrollToPage(index) } },
- text = { Text(title) }
- )
- }
- }
- Spacer(modifier = Modifier.height(8.dp))
- HorizontalPager(state = pagerState, modifier = Modifier.height(128.dp)) { pageIndex ->
- contentProvider(Tab.entries[pageIndex])
- }
-
- LaunchedEffect(pagerState.settledPage) {
- // Trigger the callback each time settledPage changes.
- onTabSelected(Tab.entries[pagerState.settledPage])
- }
- }
-}
-
-@Composable
-private fun getStandardTapTargetDefinition(
- precedence: Int,
- title: String,
- description: String,
- onTargetCLick: () -> Unit = {},
- onTargetCancel: () -> Unit = {},
-): TapTargetDefinition {
- return TapTargetDefinition(
- precedence = precedence,
- title = TextDefinition(
- text = title,
- textStyle = MaterialTheme.typography.titleLarge,
- fontWeight = FontWeight.Bold,
- color = MaterialTheme.colorScheme.onSecondaryContainer
- ),
- description = TextDefinition(
- text = description,
- textStyle = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.onSecondaryContainer
- ),
- tapTargetStyle = TapTargetStyle(
- backgroundColor = MaterialTheme.colorScheme.secondaryContainer,
- tapTargetHighlightColor = MaterialTheme.colorScheme.onSecondaryContainer,
- backgroundAlpha = 1f,
- ),
- onTargetClick = onTargetCLick,
- onTargetCancel = onTargetCancel,
- )
-}
\ No newline at end of file
diff --git a/scripts/publish-module.gradle b/scripts/publish-module.gradle
deleted file mode 100644
index 7cf3ecd..0000000
--- a/scripts/publish-module.gradle
+++ /dev/null
@@ -1,84 +0,0 @@
-apply plugin: 'maven-publish'
-apply plugin: 'signing'
-apply plugin: 'org.jetbrains.dokka'
-
-tasks.register('androidSourcesJar', Jar) {
- archiveClassifier.set('sources')
- if (project.plugins.findPlugin("com.android.library")) {
- from android.sourceSets.main.kotlin.srcDirs
- } else {
- from sourceSets.main.kotlin.srcDirs
- }
-}
-
-tasks.withType(dokkaHtmlPartial.getClass()).configureEach {
- pluginsMapConfiguration.set(
- ["org.jetbrains.dokka.base.DokkaBase": """{ "separateInheritedMembers": true}"""]
- )
-}
-
-task javadocJar(type: Jar, dependsOn: dokkaJavadoc) {
- archiveClassifier.set('javadoc')
- from dokkaJavadoc.outputDirectory
-}
-
-artifacts {
- archives androidSourcesJar
- archives javadocJar
-}
-
-group = PUBLISH_GROUP_ID
-version = PUBLISH_VERSION
-
-afterEvaluate {
- publishing {
- publications {
- release(MavenPublication) {
- tasks.named("generateMetadataFileForReleasePublication").configure { dependsOn("androidSourcesJar") }
- groupId PUBLISH_GROUP_ID
- artifactId PUBLISH_ARTIFACT_ID
- version PUBLISH_VERSION
- if (project.plugins.findPlugin("com.android.library")) {
- from components.release
- } else {
- from components.java
- }
-
- artifact javadocJar
-
- pom {
- name = PUBLISH_ARTIFACT_ID
- description = PUBLISH_DESCRIPTION
- url = 'https://github.com/PierfrancescoSoffritti/tap-target-compose/'
- licenses {
- license {
- name = 'MIT License'
- url = 'https://github.com/PierfrancescoSoffritti/tap-target-compose/blob/main/LICENSE'
- }
- }
- developers {
- developer {
- id = 'psoffritti'
- name = 'Pierfrancesco Soffritti'
- email = 'soffritti.pierfrancesco@gmail.com'
- }
- }
- scm {
- connection = 'scm:git:github.com/PierfrancescoSoffritti/tap-target-compose.git'
- developerConnection = 'scm:git:ssh://github.com/PierfrancescoSoffritti/tap-target-compose.git'
- url = 'https://github.com/PierfrancescoSoffritti/tap-target-compose.git'
- }
- }
- }
- }
- }
-}
-
-signing {
- useInMemoryPgpKeys(
- rootProject.ext["signing.keyId"],
- rootProject.ext["signing.key"],
- rootProject.ext["signing.password"],
- )
- sign publishing.publications
-}
\ No newline at end of file
diff --git a/scripts/publish-root.gradle b/scripts/publish-root.gradle
deleted file mode 100644
index 7a8012c..0000000
--- a/scripts/publish-root.gradle
+++ /dev/null
@@ -1,48 +0,0 @@
-import com.psoffritti.taptargetcompose.Configuration
-
-// Create variables with empty default values
-ext["ossrhUsername"] = ''
-ext["ossrhPassword"] = ''
-ext["sonatypeStagingProfileId"] = ''
-ext["signing.keyId"] = ''
-ext["signing.password"] = ''
-ext["signing.key"] = ''
-ext["snapshot"] = ''
-
-File secretPropsFile = project.rootProject.file('local.properties')
-if (secretPropsFile.exists()) {
- // Read local.properties file first if it exists
- Properties p = new Properties()
- new FileInputStream(secretPropsFile).withCloseable { is -> p.load(is) }
- p.each { name, value -> ext[name] = value }
-} else {
- // Use system environment variables
- ext["ossrhUsername"] = System.getenv('OSSRH_USERNAME')
- ext["ossrhPassword"] = System.getenv('OSSRH_PASSWORD')
- ext["sonatypeStagingProfileId"] = System.getenv('SONATYPE_STAGING_PROFILE_ID')
- ext["signing.keyId"] = System.getenv('SIGNING_KEY_ID')
- ext["signing.password"] = System.getenv('SIGNING_PASSWORD')
- ext["signing.key"] = System.getenv('SIGNING_KEY')
- ext["snapshot"] = System.getenv('SNAPSHOT')
-}
-
-if (snapshot) {
- ext["rootVersionName"] = Configuration.snapshotVersionName
-} else {
- ext["rootVersionName"] = Configuration.versionName
-}
-
-// Set up Sonatype repository
-nexusPublishing {
- repositories {
- sonatype {
- nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/"))
- snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/"))
-
- stagingProfileId = sonatypeStagingProfileId
- username = ossrhUsername
- password = ossrhPassword
- version = rootVersionName
- }
- }
-}
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 17a4486..2c5d623 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -1,3 +1,5 @@
+enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
+
pluginManagement {
repositories {
google()
@@ -13,6 +15,9 @@ dependencyResolutionManagement {
}
}
-rootProject.name = "tap-target-compose"
-include(":sample-app")
-include(":tap-target-compose")
+rootProject.name = "tap-target-cmp"
+include(":example")
+include(":example:composeApp")
+include(":example:androidApp")
+include(":example:iosApp")
+include(":taptarget")
diff --git a/tap-target-compose/.gitignore b/tap-target-compose/.gitignore
deleted file mode 100644
index 42afabf..0000000
--- a/tap-target-compose/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/build
\ No newline at end of file
diff --git a/tap-target-compose/build.gradle.kts b/tap-target-compose/build.gradle.kts
deleted file mode 100644
index cd0adf7..0000000
--- a/tap-target-compose/build.gradle.kts
+++ /dev/null
@@ -1,50 +0,0 @@
-import com.psoffritti.taptargetcompose.Configuration
-import org.gradle.kotlin.dsl.android
-
-plugins {
- alias(libs.plugins.android.library)
- alias(libs.plugins.jetbrains.kotlin.android)
- alias(libs.plugins.compose.compiler)
-}
-
-android {
- namespace = "com.psoffritti.taptargetcompose"
- compileSdk = Configuration.compileSdk
-
- defaultConfig {
- minSdk = Configuration.minSdk
-
- testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
- consumerProguardFiles("consumer-rules.pro")
- }
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_1_8
- targetCompatibility = JavaVersion.VERSION_1_8
- }
- kotlinOptions {
- jvmTarget = "1.8"
- }
- buildFeatures {
- compose = true
- }
-}
-
-dependencies {
- val composeBom = platform(libs.androidx.compose.bom)
- implementation(composeBom)
- androidTestImplementation(composeBom)
- implementation(libs.androidx.compose.animation)
- implementation(libs.androidx.compose.foundation)
-
- implementation(libs.androidx.compose.ui.tooling.preview)
- implementation(libs.androidx.compose.ui.tooling)
-}
-
-extra.apply {
- set("PUBLISH_GROUP_ID", "com.pierfrancescosoffritti.taptargetcompose")
- set("PUBLISH_ARTIFACT_ID", "core")
- set("PUBLISH_VERSION", Configuration.versionName)
- set("PUBLISH_DESCRIPTION", "A compose implementation of Material Design tap targets, for feature discovery.")
-}
-
-apply(from = "../scripts/publish-module.gradle")
\ No newline at end of file
diff --git a/tap-target-compose/consumer-rules.pro b/tap-target-compose/consumer-rules.pro
deleted file mode 100644
index e69de29..0000000
diff --git a/tap-target-compose/proguard-rules.pro b/tap-target-compose/proguard-rules.pro
deleted file mode 100644
index 481bb43..0000000
--- a/tap-target-compose/proguard-rules.pro
+++ /dev/null
@@ -1,21 +0,0 @@
-# Add project specific ProGuard rules here.
-# You can control the set of applied configuration files using the
-# proguardFiles setting in build.gradle.
-#
-# For more details, see
-# http://developer.android.com/guide/developing/tools/proguard.html
-
-# If your project uses WebView with JS, uncomment the following
-# and specify the fully qualified class name to the JavaScript interface
-# class:
-#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
-# public *;
-#}
-
-# Uncomment this to preserve the line number information for
-# debugging stack traces.
-#-keepattributes SourceFile,LineNumberTable
-
-# If you keep the line number information, uncomment this to
-# hide the original source file name.
-#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/tap-target-compose/src/main/AndroidManifest.xml b/tap-target-compose/src/main/AndroidManifest.xml
deleted file mode 100644
index a5918e6..0000000
--- a/tap-target-compose/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/tap-target-compose/src/main/java/com/psoffritti/taptargetcompose/Rendering.kt b/tap-target-compose/src/main/java/com/psoffritti/taptargetcompose/Rendering.kt
deleted file mode 100644
index f099e27..0000000
--- a/tap-target-compose/src/main/java/com/psoffritti/taptargetcompose/Rendering.kt
+++ /dev/null
@@ -1,561 +0,0 @@
-package com.psoffritti.taptargetcompose
-
-import android.app.Activity
-import android.content.Context
-import android.content.ContextWrapper
-import android.util.Log
-import android.view.ViewGroup
-import androidx.compose.animation.core.Animatable
-import androidx.compose.animation.core.AnimationVector1D
-import androidx.compose.animation.core.FastOutLinearInEasing
-import androidx.compose.animation.core.FastOutSlowInEasing
-import androidx.compose.animation.core.RepeatMode
-import androidx.compose.animation.core.infiniteRepeatable
-import androidx.compose.animation.core.tween
-import androidx.compose.foundation.Canvas
-import androidx.compose.foundation.gestures.detectTapGestures
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableFloatStateOf
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.graphics.BlendMode
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.graphicsLayer
-import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.layout.boundsInWindow
-import androidx.compose.ui.platform.ComposeView
-import androidx.compose.ui.platform.LocalConfiguration
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.text.AnnotatedString
-import androidx.compose.ui.text.TextLayoutResult
-import androidx.compose.ui.text.TextMeasurer
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.drawText
-import androidx.compose.ui.text.rememberTextMeasurer
-import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-import kotlinx.coroutines.launch
-import kotlin.math.max
-import kotlin.math.min
-import kotlin.math.pow
-
-private const val DEBUG = false
-
-/** Padding between the tap target and the highlight circle. */
-private val TARGET_PADDING = 20.dp
-
-/** The horizontal margin for the text block */
-private val TEXT_HORIZONTAL_MARGIN = 40.dp
-/** The vertical margin for the text block */
-private val TEXT_VERTICAL_MARGIN = 40.dp
-
-/** The margin between the outer circle and the text. */
-private val OUTER_CIRCLE_INTERNAL_MARGIN = 40.dp
-
-/** The maximum width of the text. */
-private val MAX_TEXT_WIDTH = 360f.dp
-
-/** The margin between the title and description text. */
-private val TEXT_SPACING = 2.dp
-
-private fun Dp.toPx(density: Density) = with(density) { toPx() }
-
-private const val LOG_TAG = "TapTarget"
-
-/** Composable responsible for drawing the tap target. */
-@Composable
-internal fun TapTarget(tapTarget: TapTarget, onComplete: () -> Unit) {
- val density = LocalDensity.current
- val screenSizePx = Size(
- LocalConfiguration.current.screenWidthDp * density.density,
- LocalConfiguration.current.screenHeightDp * density.density
- )
-
- var lastTargetCenter by remember { mutableStateOf(Offset.Zero) }
- // For moving targets, the coordinates can change, use a function
- // to always get the latest.
- val getTargetCenterPx = {
- if (tapTarget.coordinates.isAttached) {
- val center = tapTarget.coordinates.boundsInWindow().center
- lastTargetCenter = center
- center
- }
- else {
- lastTargetCenter
- }
- }
-
- val targetMaxDimensionPx = remember(tapTarget) {
- max(tapTarget.coordinates.size.width, tapTarget.coordinates.size.height)
- }
- val targetRadiusPx = targetMaxDimensionPx / 2 + TARGET_PADDING.toPx(density)
-
- // The radius of the outer circle of the tap target.
- var outerCircleRadiusPx by remember { mutableFloatStateOf(0f) }
- // The radius of the highlight circle shown around the target.
- var highlightCircleRadiusPx by remember { mutableFloatStateOf(0f) }
- // Whether we are animating in or out.
- var animateIn by remember { mutableStateOf(true) }
- // Whether the user clicked the target.
- var targetClicked by remember { mutableStateOf(false) }
- // Whether the user clicked outside the target.
- var targetCancelled by remember { mutableStateOf(false) }
-
- val outerCircleScaleAnimatable = remember { Animatable(0f) }
- val highlightCircleScaleAnimatable = remember { Animatable(0f) }
- val tapTargetCircleScaleAnimatable = remember { Animatable(0f) }
- val textAlphaScaleAnimatable = remember { Animatable(0f) }
-
- if (targetClicked) {
- // The user tapped the target, notify the target.
- tapTarget.onTargetClick()
- targetClicked = false
- }
-
- if (
- // We have completed the current target, and are now animating out.
- !animateIn &&
- // The outer circle has finished collapsing.
- outerCircleScaleAnimatable.value == 0f &&
- // The highlight circle has finished collapsing.
- highlightCircleScaleAnimatable.value == 0f
- ) {
- // Animate out is complete. We are now switching to the next target and should animate in.
- animateIn = true
-
- if (targetCancelled) {
- // The user tapped outside the target, notify the target.
- tapTarget.onTargetCancel()
- targetCancelled = false
- }
-
- // The animation is complete, notify that we are ready for the next target.
- onComplete()
- }
-
- if (animateIn) {
- AnimateIn(
- key1 = tapTarget,
- outerCircleAnimatable = outerCircleScaleAnimatable,
- highlightCircleAnimatable = highlightCircleScaleAnimatable,
- tapTargetCircleAnimatable = tapTargetCircleScaleAnimatable,
- textAlphaAnimatable = textAlphaScaleAnimatable
- )
- }
- else {
- AnimateOut(
- key1 = tapTarget,
- outerCircleAnimatable = outerCircleScaleAnimatable,
- highlightCircleAnimatable = highlightCircleScaleAnimatable,
- textAlphaAnimatable= textAlphaScaleAnimatable
- )
- }
-
- val maxTextWidthPx = MAX_TEXT_WIDTH.toPx(density)
- val textHorizontalMarginPx = TEXT_HORIZONTAL_MARGIN.toPx(density)
- val textVerticalMarginPx = TEXT_VERTICAL_MARGIN.toPx(density)
-
- val textWidthPx = min(screenSizePx.width, maxTextWidthPx) - textHorizontalMarginPx * 2
-
- val constraints = Constraints.fixedWidth(textWidthPx.toInt())
- val textMeasurer = rememberTextMeasurer()
- val titleMeasure = tapTarget.title.rememberMeasure(textMeasurer, constraints)
- val descriptionMeasure = tapTarget.description.rememberMeasure(textMeasurer, constraints)
-
- val textBlockHeightPx = titleMeasure.size.height +
- descriptionMeasure.size.height +
- TEXT_SPACING.toPx(density)
-
- val textBlockTopLeft = getTextBlockOffset(
- Size(textWidthPx, textBlockHeightPx),
- screenSizePx,
- getTargetCenterPx(),
- targetRadiusPx,
- textHorizontalMarginPx,
- textVerticalMarginPx
- )
- val textBlockRect = Rect(
- textBlockTopLeft.x,
- textBlockTopLeft.y,
- textBlockTopLeft.x + textWidthPx,
- textBlockTopLeft.y + textBlockHeightPx
- )
-
- val topLeftRadius = textBlockRect.topLeft.distanceTo(getTargetCenterPx())
- val topRightRadius = textBlockRect.topRight.distanceTo(getTargetCenterPx())
- val bottomLeftRadius = textBlockRect.bottomLeft.distanceTo(getTargetCenterPx())
- val bottomRightRadius = textBlockRect.bottomRight.distanceTo(getTargetCenterPx())
- val maxRadius = max(
- topLeftRadius,
- topRightRadius,
- bottomLeftRadius,
- bottomRightRadius
- )
-
- outerCircleRadiusPx = maxRadius + OUTER_CIRCLE_INTERNAL_MARGIN.toPx(density)
- highlightCircleRadiusPx = min(targetRadiusPx*2, outerCircleRadiusPx)
-
- TapTargetRenderer(
- tapTarget,
- onTargetCancel = {
- targetCancelled = true
- animateIn = false
- },
- onTargetClick = {
- targetClicked = true
- animateIn = false
- },
- outerCircleScaleProvider = { outerCircleScaleAnimatable.value },
- highlightCircleScaleProvider = { highlightCircleScaleAnimatable.value },
- tapTargetCircleScaleProvider = { tapTargetCircleScaleAnimatable.value },
- textAlphaProvider = { textAlphaScaleAnimatable.value },
- getTargetCenter = getTargetCenterPx,
- targetRadius = targetRadiusPx,
- outerCircleRadius = outerCircleRadiusPx,
- highlightCircleRadius = highlightCircleRadiusPx,
- textBlockTopLeft = textBlockTopLeft,
- titleMeasure = titleMeasure,
- descriptionMeasure = descriptionMeasure,
- textBlockRect = textBlockRect
- )
-}
-
-/** Component that draws the tap target. */
-@Composable
-private fun TapTargetRenderer(
- tapTarget: TapTarget,
- onTargetClick: () -> Unit,
- onTargetCancel: () -> Unit,
- getTargetCenter: () -> Offset,
- outerCircleScaleProvider: () -> Float,
- highlightCircleScaleProvider: () -> Float,
- tapTargetCircleScaleProvider: () -> Float,
- textAlphaProvider: () -> Float,
- targetRadius: Float,
- outerCircleRadius: Float,
- highlightCircleRadius: Float,
- textBlockTopLeft: Offset,
- titleMeasure: TextLayoutResult,
- descriptionMeasure: TextLayoutResult,
- textBlockRect: Rect
-) {
- Canvas(
- modifier = Modifier
- .fillMaxSize()
- .pointerInput(tapTarget) {
- detectTapGestures { tapOffset ->
- when {
- tapOffset.isOutsideCircle(getTargetCenter(), outerCircleRadius) -> {
- // The user clicked outside the target
- onTargetCancel()
- }
-
- tapOffset.isInsideCircle(getTargetCenter(), targetRadius) -> {
- // The user clicked the target
- onTargetClick()
- }
- }
- }
- } // Add transparency to the entire canvas, so we can show what's below the tap target area.
- .graphicsLayer(alpha = 0.99f)
- ) {
- // Don't draw the circles if they are smaller than the targetRadius.
- // Otherwise we would draw above the tap target during animation.
- if (outerCircleRadius * outerCircleScaleProvider() < targetRadius) {
- return@Canvas
- }
-
- // Draw outer circle
- drawCircle(
- center = getTargetCenter(),
- radius = outerCircleRadius * outerCircleScaleProvider(),
- color = tapTarget.style.backgroundColor,
- alpha = tapTarget.style.backgroundAlpha
- )
-
- // Draw highlight circle
- drawCircle(
- center = getTargetCenter(),
- radius = highlightCircleRadius * highlightCircleScaleProvider(),
- color = tapTarget.style.tapTargetHighlightColor,
- alpha = 1 - highlightCircleScaleProvider().pow(4)
- )
-
- // XOR circle used to reveal the tap target, since we are drawing the other circles above it.
- drawCircle(
- center = getTargetCenter(),
- radius = targetRadius + ((targetRadius / 10) * tapTargetCircleScaleProvider()),
- color = tapTarget.style.tapTargetHighlightColor,
- blendMode = BlendMode.Xor
- )
-
- drawText(
- textLayoutResult = titleMeasure,
- topLeft = textBlockTopLeft,
- alpha = textAlphaProvider().pow(2)
- )
- drawText(
- textLayoutResult = descriptionMeasure,
- topLeft = textBlockTopLeft.plus(
- Offset(x = 0f, y = titleMeasure.size.height + TEXT_SPACING.toPx())
- ),
- alpha = textAlphaProvider().pow(2)
- )
-
- if (DEBUG) {
- // Draw the text block rect to see text bounds.
- drawRect(
- color = Color.Black.copy(alpha = 0.4f),
- topLeft = textBlockRect.topLeft,
- size = Size(textBlockRect.width, textBlockRect.height)
- )
- }
- }
-}
-
-/**
- * Calculates and returns the top left coordinates of the text block.
- * @param textBlockSize The size of the text block.
- * @param targetCenter The center of the target.
- * @param targetRadius The radius of the target.
- * @param horizontalMargin The horizontal margin between the text block and the screen edge.
- * @param verticalMargin The vertical margin between the text block and the screen edge.
- */
-// TODO(issue#3) the entire screen size is used to position the text block,
-// therefore it might overlap the status bar.
-private fun getTextBlockOffset(
- textBlockSize: Size,
- screenSize: Size,
- targetCenter: Offset,
- targetRadius: Float,
- horizontalMargin: Float,
- verticalMargin: Float
-): Offset {
- // The X coordinate of the text block, if positioned to the left of the target.
- val xLeft = max(targetCenter.x - textBlockSize.width, horizontalMargin)
- // The X coordinate of the text block, if positioned to the right of the target.
- val xRightTemp = targetCenter.x
- val xRight = if (xRightTemp + textBlockSize.width > screenSize.width - horizontalMargin) {
- // The text block would end outside of the margin. Make sure it doesn't.
- screenSize.width - horizontalMargin - textBlockSize.width
- }
- else {
- xRightTemp
- }
-
- val xOffset = if (xLeft > 0) {
- xLeft
- }
- else {
- xRight
- }
-
- // The Y coordinate of the text block, if positioned above the target.
- val yTop = targetCenter.y - targetRadius - textBlockSize.height - verticalMargin
- // The Y coordinate of the text block, if positioned below the target.
- val yBottom = targetCenter.y + targetRadius + verticalMargin
-
- val yOffset = if (yTop > 0) {
- yTop
- } else {
- yBottom
- }
-
- return Offset(xOffset, yOffset)
-}
-
-@Composable
-private fun AnimateIn(
- key1: Any?,
- outerCircleAnimatable: Animatable,
- highlightCircleAnimatable: Animatable,
- tapTargetCircleAnimatable: Animatable,
- textAlphaAnimatable: Animatable
-) {
- LaunchedEffect(key1) {
- // Outer circle
- launch {
- outerCircleAnimatable.animateTo(
- targetValue = 1f,
- animationSpec = tween(
- durationMillis = 300,
- easing = FastOutSlowInEasing,
- ),
- )
- }
-
- // Highlight circle
- launch {
- highlightCircleAnimatable.animateTo(
- targetValue = 1f,
- animationSpec = infiniteRepeatable(
- animation = tween(1000, easing = FastOutSlowInEasing),
- repeatMode = RepeatMode.Restart,
- )
- )
- }
-
- // Tap target circle
- launch {
- tapTargetCircleAnimatable.animateTo(
- targetValue = 1f,
- animationSpec = infiniteRepeatable(
- animation = tween(500, easing = FastOutLinearInEasing),
- repeatMode = RepeatMode.Reverse,
- )
- )
- }
-
- // Text alpha
- launch {
- textAlphaAnimatable.animateTo(
- targetValue = 1f,
- animationSpec = tween(
- delayMillis = 200,
- durationMillis = 300,
- easing = FastOutSlowInEasing,
- ),
- )
- }
- }
-}
-
-@Composable
-private fun AnimateOut(
- key1: Any?,
- outerCircleAnimatable: Animatable,
- highlightCircleAnimatable: Animatable,
- textAlphaAnimatable: Animatable
-) {
- LaunchedEffect(key1) {
- // Outer circle
- launch {
- outerCircleAnimatable.animateTo(
- targetValue = 0f,
- animationSpec = tween(
- durationMillis = 300,
- easing = FastOutSlowInEasing,
- ),
- )
- }
-
- // Highlight circle
- launch {
- highlightCircleAnimatable.animateTo(
- targetValue = 0f,
- animationSpec = tween(
- durationMillis = 300,
- easing = FastOutSlowInEasing,
- )
- )
- }
-
- // Text alpha
- launch {
- textAlphaAnimatable.animateTo(
- targetValue = 0f,
- animationSpec = tween(
- durationMillis = 300,
- easing = FastOutSlowInEasing,
- ),
- )
- }
- }
-}
-
-/**
- * A composable that is drawn as an overlay over the entire screen.
- * @param key The key used to know when to remove the overlay.
- * @param content The content of the overlay.
- */
-@Composable
-internal fun Overlay(
- key: Any?,
- content: @Composable () -> Unit
-) {
- val context = LocalContext.current
- val activity = context.getActivity()
- if (activity == null) {
- Log.d(LOG_TAG, "Can't show overlay, activity is null")
- return
- }
-
- val decor = activity.window.decorView as? ViewGroup
- if (decor == null) {
- Log.d(LOG_TAG, "Can't show overlay, decor is null")
- return
- }
-
- val layoutParams = ViewGroup.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.MATCH_PARENT
- )
-
- // We want the overlay to be always rendered above the entire content of the app.
- // To do this we need to access the decor view of the activity and add our overlay to it.
- // Otherwise the tap target rendering would be limited to the size of the [TapTargetCoordinator].
- DisposableEffect(key) {
- val composeView = ComposeView(context).apply {
- setContent { content() }
- }
- decor.addView(composeView, layoutParams)
-
- onDispose {
- decor.removeView(composeView)
- }
- }
-}
-
-private fun Context.getActivity(): Activity? = when (this) {
- is Activity -> this
- is ContextWrapper -> baseContext.getActivity()
- else -> null
-}
-
-@Composable
-private fun TextDefinition.rememberMeasure(
- textMeasurer: TextMeasurer,
- constraints: Constraints
-): TextLayoutResult {
- return remember(
- text,
- constraints,
- style.color,
- style.fontSize,
- style.fontWeight,
- style.textAlign,
- style.lineHeight,
- style.fontFamily,
- style.textDecoration,
- style.fontStyle,
- style.letterSpacing
- ) {
- textMeasurer.measure(
- AnnotatedString(text),
- constraints = constraints,
- style = TextStyle(
- color = style.color,
- fontSize = style.fontSize,
- fontWeight = style.fontWeight,
- textAlign = style.textAlign,
- lineHeight = style.lineHeight,
- fontFamily = style.fontFamily,
- textDecoration = style.textDecoration,
- fontStyle = style.fontStyle,
- letterSpacing = style.letterSpacing
- )
- )
- }
-}
\ No newline at end of file
diff --git a/taptarget/build.gradle.kts b/taptarget/build.gradle.kts
new file mode 100644
index 0000000..12f9fc5
--- /dev/null
+++ b/taptarget/build.gradle.kts
@@ -0,0 +1,64 @@
+plugins {
+ alias(libs.plugins.kotlin.multiplatform)
+ alias(libs.plugins.android.kotlin.multiplatform.library)
+ alias(libs.plugins.compose.multiplatform)
+ alias(libs.plugins.android.lint)
+ alias(libs.plugins.compose.compiler)
+ alias(libs.plugins.vanniktech.mavenPublish)
+}
+
+kotlin {
+ androidLibrary {
+ namespace = "zone.ien.taptargetcmp"
+ compileSdk = libs.versions.android.compileSdk.get().toInt()
+ minSdk = libs.versions.android.minSdk.get().toInt()
+ androidResources {
+ enable = true
+ }
+
+ withHostTestBuilder {
+ }
+
+ withDeviceTestBuilder {
+ sourceSetTreeName = "test"
+ }.configure {
+ instrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+ }
+ iosArm64()
+ iosSimulatorArm64()
+
+ sourceSets {
+ commonMain.dependencies {
+ implementation(libs.compose.material3)
+ implementation(libs.compose.preview)
+ implementation(libs.compose.resources)
+ }
+
+ commonTest.dependencies {
+ implementation(libs.kotlin.test)
+ }
+
+ androidMain.dependencies {
+ implementation(libs.activity.compose)
+ }
+
+ getByName("androidDeviceTest").dependencies {
+ implementation(libs.runner)
+ implementation(libs.core)
+ implementation(libs.junit)
+ }
+
+ iosMain.dependencies {
+ }
+ }
+}
+
+dependencies {
+ androidRuntimeClasspath(libs.compose.ui.tooling)
+}
+
+compose.resources {
+ publicResClass = true
+ generateResClass = auto
+}
diff --git a/tap-target-compose/src/main/java/com/psoffritti/taptargetcompose/Math.kt b/taptarget/src/commonMain/kotlin/zone/ien/taptargetcmp/Math.kt
similarity index 94%
rename from tap-target-compose/src/main/java/com/psoffritti/taptargetcompose/Math.kt
rename to taptarget/src/commonMain/kotlin/zone/ien/taptargetcmp/Math.kt
index 13b31f9..70bd9f7 100644
--- a/tap-target-compose/src/main/java/com/psoffritti/taptargetcompose/Math.kt
+++ b/taptarget/src/commonMain/kotlin/zone/ien/taptargetcmp/Math.kt
@@ -1,4 +1,4 @@
-package com.psoffritti.taptargetcompose
+package zone.ien.taptargetcmp
import androidx.compose.ui.geometry.Offset
import kotlin.math.pow
diff --git a/taptarget/src/commonMain/kotlin/zone/ien/taptargetcmp/Rendering.kt b/taptarget/src/commonMain/kotlin/zone/ien/taptargetcmp/Rendering.kt
new file mode 100644
index 0000000..8dc96cc
--- /dev/null
+++ b/taptarget/src/commonMain/kotlin/zone/ien/taptargetcmp/Rendering.kt
@@ -0,0 +1,530 @@
+package zone.ien.taptargetcmp
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.AnimationVector1D
+import androidx.compose.animation.core.FastOutLinearInEasing
+import androidx.compose.animation.core.FastOutSlowInEasing
+import androidx.compose.animation.core.RepeatMode
+import androidx.compose.animation.core.infiniteRepeatable
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.BlendMode
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.layout.boundsInWindow
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalWindowInfo
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextMeasurer
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.drawText
+import androidx.compose.ui.text.rememberTextMeasurer
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Popup
+import androidx.compose.ui.window.PopupProperties
+import kotlinx.coroutines.launch
+import kotlin.math.max
+import kotlin.math.min
+import kotlin.math.pow
+
+private const val DEBUG = false
+
+/** Padding between the tap target and the highlight circle. */
+private val TARGET_PADDING = 20.dp
+
+/** The horizontal margin for the text block */
+private val TEXT_HORIZONTAL_MARGIN = 40.dp
+
+/** The vertical margin for the text block */
+private val TEXT_VERTICAL_MARGIN = 40.dp
+
+/** The margin between the outer circle and the text. */
+private val OUTER_CIRCLE_INTERNAL_MARGIN = 40.dp
+
+/** The maximum width of the text. */
+private val MAX_TEXT_WIDTH = 360f.dp
+
+/** The margin between the title and description text. */
+private val TEXT_SPACING = 2.dp
+
+private fun Dp.toPx(density: Density) = with(density) { toPx() }
+
+private const val LOG_TAG = "TapTarget"
+
+/** Composable responsible for drawing the tap target. */
+@Composable
+internal fun TapTarget(tapTarget: TapTarget, onComplete: () -> Unit) {
+ val density = LocalDensity.current
+ val containerSize = LocalWindowInfo.current.containerSize
+ val screenSizePx = Size(
+ containerSize.width.toFloat(),
+ containerSize.height.toFloat()
+ )
+
+ var lastTargetCenter by remember { mutableStateOf(Offset.Zero) }
+ // For moving targets, the coordinates can change, use a function
+ // to always get the latest.
+ val getTargetCenterPx = {
+ if (tapTarget.coordinates.isAttached) {
+ val center = tapTarget.coordinates.boundsInWindow().center
+ lastTargetCenter = center
+ center
+ } else {
+ lastTargetCenter
+ }
+ }
+
+ val targetMaxDimensionPx = remember(tapTarget) {
+ max(tapTarget.coordinates.size.width, tapTarget.coordinates.size.height)
+ }
+ val targetRadiusPx = targetMaxDimensionPx / 2 + TARGET_PADDING.toPx(density)
+
+ // The radius of the outer circle of the tap target.
+ var outerCircleRadiusPx by remember { mutableFloatStateOf(0f) }
+ // The radius of the highlight circle shown around the target.
+ var highlightCircleRadiusPx by remember { mutableFloatStateOf(0f) }
+ // Whether we are animating in or out.
+ var animateIn by remember { mutableStateOf(true) }
+ // Whether the user clicked the target.
+ var targetClicked by remember { mutableStateOf(false) }
+ // Whether the user clicked outside the target.
+ var targetCancelled by remember { mutableStateOf(false) }
+
+ val outerCircleScaleAnimatable = remember { Animatable(0f) }
+ val highlightCircleScaleAnimatable = remember { Animatable(0f) }
+ val tapTargetCircleScaleAnimatable = remember { Animatable(0f) }
+ val textAlphaScaleAnimatable = remember { Animatable(0f) }
+
+ if (targetClicked) {
+ // The user tapped the target, notify the target.
+ tapTarget.onTargetClick()
+ targetClicked = false
+ }
+
+ if (
+ // We have completed the current target, and are now animating out.
+ !animateIn &&
+ // The outer circle has finished collapsing.
+ outerCircleScaleAnimatable.value == 0f &&
+ // The highlight circle has finished collapsing.
+ highlightCircleScaleAnimatable.value == 0f
+ ) {
+ // Animate out is complete. We are now switching to the next target and should animate in.
+ animateIn = true
+
+ if (targetCancelled) {
+ // The user tapped outside the target, notify the target.
+ tapTarget.onTargetCancel()
+ targetCancelled = false
+ }
+
+ // The animation is complete, notify that we are ready for the next target.
+ onComplete()
+ }
+
+ if (animateIn) {
+ AnimateIn(
+ key1 = tapTarget,
+ outerCircleAnimatable = outerCircleScaleAnimatable,
+ highlightCircleAnimatable = highlightCircleScaleAnimatable,
+ tapTargetCircleAnimatable = tapTargetCircleScaleAnimatable,
+ textAlphaAnimatable = textAlphaScaleAnimatable
+ )
+ } else {
+ AnimateOut(
+ key1 = tapTarget,
+ outerCircleAnimatable = outerCircleScaleAnimatable,
+ highlightCircleAnimatable = highlightCircleScaleAnimatable,
+ textAlphaAnimatable = textAlphaScaleAnimatable
+ )
+ }
+
+ val maxTextWidthPx = MAX_TEXT_WIDTH.toPx(density)
+ val textHorizontalMarginPx = TEXT_HORIZONTAL_MARGIN.toPx(density)
+ val textVerticalMarginPx = TEXT_VERTICAL_MARGIN.toPx(density)
+
+ val textWidthPx = min(screenSizePx.width, maxTextWidthPx) - textHorizontalMarginPx * 2
+
+ val constraints = Constraints.fixedWidth(textWidthPx.toInt())
+ val textMeasurer = rememberTextMeasurer()
+ val titleMeasure = tapTarget.title.rememberMeasure(textMeasurer, constraints)
+ val descriptionMeasure = tapTarget.description.rememberMeasure(textMeasurer, constraints)
+
+ val textBlockHeightPx = titleMeasure.size.height +
+ descriptionMeasure.size.height +
+ TEXT_SPACING.toPx(density)
+
+ val textBlockTopLeft = getTextBlockOffset(
+ Size(textWidthPx, textBlockHeightPx),
+ screenSizePx,
+ getTargetCenterPx(),
+ targetRadiusPx,
+ textHorizontalMarginPx,
+ textVerticalMarginPx
+ )
+ val textBlockRect = Rect(
+ textBlockTopLeft.x,
+ textBlockTopLeft.y,
+ textBlockTopLeft.x + textWidthPx,
+ textBlockTopLeft.y + textBlockHeightPx
+ )
+
+ val topLeftRadius = textBlockRect.topLeft.distanceTo(getTargetCenterPx())
+ val topRightRadius = textBlockRect.topRight.distanceTo(getTargetCenterPx())
+ val bottomLeftRadius = textBlockRect.bottomLeft.distanceTo(getTargetCenterPx())
+ val bottomRightRadius = textBlockRect.bottomRight.distanceTo(getTargetCenterPx())
+ val maxRadius = max(
+ topLeftRadius,
+ topRightRadius,
+ bottomLeftRadius,
+ bottomRightRadius
+ )
+
+ outerCircleRadiusPx = maxRadius + OUTER_CIRCLE_INTERNAL_MARGIN.toPx(density)
+ highlightCircleRadiusPx = min(targetRadiusPx * 2, outerCircleRadiusPx)
+
+ TapTargetRenderer(
+ tapTarget,
+ onTargetCancel = {
+ targetCancelled = true
+ animateIn = false
+ },
+ onTargetClick = {
+ targetClicked = true
+ animateIn = false
+ },
+ outerCircleScaleProvider = { outerCircleScaleAnimatable.value },
+ highlightCircleScaleProvider = { highlightCircleScaleAnimatable.value },
+ tapTargetCircleScaleProvider = { tapTargetCircleScaleAnimatable.value },
+ textAlphaProvider = { textAlphaScaleAnimatable.value },
+ getTargetCenter = getTargetCenterPx,
+ targetRadius = targetRadiusPx,
+ outerCircleRadius = outerCircleRadiusPx,
+ highlightCircleRadius = highlightCircleRadiusPx,
+ textBlockTopLeft = textBlockTopLeft,
+ titleMeasure = titleMeasure,
+ descriptionMeasure = descriptionMeasure,
+ textBlockRect = textBlockRect
+ )
+}
+
+/** Component that draws the tap target. */
+@Composable
+private fun TapTargetRenderer(
+ tapTarget: TapTarget,
+ onTargetClick: () -> Unit,
+ onTargetCancel: () -> Unit,
+ getTargetCenter: () -> Offset,
+ outerCircleScaleProvider: () -> Float,
+ highlightCircleScaleProvider: () -> Float,
+ tapTargetCircleScaleProvider: () -> Float,
+ textAlphaProvider: () -> Float,
+ targetRadius: Float,
+ outerCircleRadius: Float,
+ highlightCircleRadius: Float,
+ textBlockTopLeft: Offset,
+ titleMeasure: TextLayoutResult,
+ descriptionMeasure: TextLayoutResult,
+ textBlockRect: Rect
+) {
+ Canvas(
+ modifier = Modifier
+ .fillMaxSize()
+ .pointerInput(tapTarget) {
+ detectTapGestures { tapOffset ->
+ when {
+ tapOffset.isOutsideCircle(getTargetCenter(), outerCircleRadius) -> {
+ // The user clicked outside the target
+ onTargetCancel()
+ }
+
+ tapOffset.isInsideCircle(getTargetCenter(), targetRadius) -> {
+ // The user clicked the target
+ onTargetClick()
+ }
+ }
+ }
+ } // Add transparency to the entire canvas, so we can show what's below the tap target area.
+ .graphicsLayer(alpha = 0.99f)
+ ) {
+ // Don't draw the circles if they are smaller than the targetRadius.
+ // Otherwise we would draw above the tap target during animation.
+ if (outerCircleRadius * outerCircleScaleProvider() < targetRadius) {
+ return@Canvas
+ }
+
+ // Draw outer circle
+ drawCircle(
+ center = getTargetCenter(),
+ radius = outerCircleRadius * outerCircleScaleProvider(),
+ color = tapTarget.style.backgroundColor,
+ alpha = tapTarget.style.backgroundAlpha
+ )
+
+ // Draw highlight circle
+ drawCircle(
+ center = getTargetCenter(),
+ radius = highlightCircleRadius * highlightCircleScaleProvider(),
+ color = tapTarget.style.tapTargetHighlightColor,
+ alpha = 1 - highlightCircleScaleProvider().pow(4)
+ )
+
+ // XOR circle used to reveal the tap target, since we are drawing the other circles above it.
+ drawCircle(
+ center = getTargetCenter(),
+ radius = targetRadius + ((targetRadius / 10) * tapTargetCircleScaleProvider()),
+ color = tapTarget.style.tapTargetHighlightColor,
+ blendMode = BlendMode.Xor
+ )
+
+ drawText(
+ textLayoutResult = titleMeasure,
+ topLeft = textBlockTopLeft,
+ alpha = textAlphaProvider().pow(2)
+ )
+ drawText(
+ textLayoutResult = descriptionMeasure,
+ topLeft = textBlockTopLeft.plus(
+ Offset(x = 0f, y = titleMeasure.size.height + TEXT_SPACING.toPx())
+ ),
+ alpha = textAlphaProvider().pow(2)
+ )
+
+ if (DEBUG) {
+ // Draw the text block rect to see text bounds.
+ drawRect(
+ color = Color.Black.copy(alpha = 0.4f),
+ topLeft = textBlockRect.topLeft,
+ size = Size(textBlockRect.width, textBlockRect.height)
+ )
+ }
+ }
+}
+
+/**
+ * Calculates and returns the top left coordinates of the text block.
+ * @param textBlockSize The size of the text block.
+ * @param targetCenter The center of the target.
+ * @param targetRadius The radius of the target.
+ * @param horizontalMargin The horizontal margin between the text block and the screen edge.
+ * @param verticalMargin The vertical margin between the text block and the screen edge.
+ */
+// TODO(issue#3) the entire screen size is used to position the text block,
+// therefore it might overlap the status bar.
+private fun getTextBlockOffset(
+ textBlockSize: Size,
+ screenSize: Size,
+ targetCenter: Offset,
+ targetRadius: Float,
+ horizontalMargin: Float,
+ verticalMargin: Float
+): Offset {
+ // The X coordinate of the text block, if positioned to the left of the target.
+ val xLeft = max(targetCenter.x - textBlockSize.width, horizontalMargin)
+ // The X coordinate of the text block, if positioned to the right of the target.
+ val xRightTemp = targetCenter.x
+ val xRight = if (xRightTemp + textBlockSize.width > screenSize.width - horizontalMargin) {
+ // The text block would end outside of the margin. Make sure it doesn't.
+ screenSize.width - horizontalMargin - textBlockSize.width
+ } else {
+ xRightTemp
+ }
+
+ val xOffset = if (xLeft > 0) {
+ xLeft
+ } else {
+ xRight
+ }
+
+ // The Y coordinate of the text block, if positioned above the target.
+ val yTop = targetCenter.y - targetRadius - textBlockSize.height - verticalMargin
+ // The Y coordinate of the text block, if positioned below the target.
+ val yBottom = targetCenter.y + targetRadius + verticalMargin
+
+ val yOffset = if (yTop > 0) {
+ yTop
+ } else {
+ yBottom
+ }
+
+ return Offset(xOffset, yOffset)
+}
+
+@Composable
+private fun AnimateIn(
+ key1: Any?,
+ outerCircleAnimatable: Animatable,
+ highlightCircleAnimatable: Animatable,
+ tapTargetCircleAnimatable: Animatable,
+ textAlphaAnimatable: Animatable
+) {
+ LaunchedEffect(key1) {
+ // Outer circle
+ launch {
+ outerCircleAnimatable.animateTo(
+ targetValue = 1f,
+ animationSpec = tween(
+ durationMillis = 300,
+ easing = FastOutSlowInEasing,
+ ),
+ )
+ }
+
+ // Highlight circle
+ launch {
+ highlightCircleAnimatable.animateTo(
+ targetValue = 1f,
+ animationSpec = infiniteRepeatable(
+ animation = tween(1000, easing = FastOutSlowInEasing),
+ repeatMode = RepeatMode.Restart,
+ )
+ )
+ }
+
+ // Tap target circle
+ launch {
+ tapTargetCircleAnimatable.animateTo(
+ targetValue = 1f,
+ animationSpec = infiniteRepeatable(
+ animation = tween(500, easing = FastOutLinearInEasing),
+ repeatMode = RepeatMode.Reverse,
+ )
+ )
+ }
+
+ // Text alpha
+ launch {
+ textAlphaAnimatable.animateTo(
+ targetValue = 1f,
+ animationSpec = tween(
+ delayMillis = 200,
+ durationMillis = 300,
+ easing = FastOutSlowInEasing,
+ ),
+ )
+ }
+ }
+}
+
+@Composable
+private fun AnimateOut(
+ key1: Any?,
+ outerCircleAnimatable: Animatable,
+ highlightCircleAnimatable: Animatable,
+ textAlphaAnimatable: Animatable
+) {
+ LaunchedEffect(key1) {
+ // Outer circle
+ launch {
+ outerCircleAnimatable.animateTo(
+ targetValue = 0f,
+ animationSpec = tween(
+ durationMillis = 300,
+ easing = FastOutSlowInEasing,
+ ),
+ )
+ }
+
+ // Highlight circle
+ launch {
+ highlightCircleAnimatable.animateTo(
+ targetValue = 0f,
+ animationSpec = tween(
+ durationMillis = 300,
+ easing = FastOutSlowInEasing,
+ )
+ )
+ }
+
+ // Text alpha
+ launch {
+ textAlphaAnimatable.animateTo(
+ targetValue = 0f,
+ animationSpec = tween(
+ durationMillis = 300,
+ easing = FastOutSlowInEasing,
+ ),
+ )
+ }
+ }
+}
+
+/**
+ * A composable that is drawn as an overlay over the entire screen.
+ * @param key The key used to know when to remove the overlay.
+ * @param content The content of the overlay.
+ */
+
+@Composable
+internal fun Overlay(
+ key: Any?,
+ content: @Composable () -> Unit
+) {
+ // key가 변경되면 Popup이 재구성됨
+ key(key) {
+ Popup(
+ properties = PopupProperties(
+ focusable = true,
+ dismissOnBackPress = false,
+ dismissOnClickOutside = false,
+ )
+ ) {
+ content()
+ }
+ }
+}
+
+@Composable
+private fun TextDefinition.rememberMeasure(
+ textMeasurer: TextMeasurer,
+ constraints: Constraints
+): TextLayoutResult {
+ return remember(
+ text,
+ constraints,
+ style.color,
+ style.fontSize,
+ style.fontWeight,
+ style.textAlign,
+ style.lineHeight,
+ style.fontFamily,
+ style.textDecoration,
+ style.fontStyle,
+ style.letterSpacing
+ ) {
+ textMeasurer.measure(
+ AnnotatedString(text),
+ constraints = constraints,
+ style = TextStyle(
+ color = style.color,
+ fontSize = style.fontSize,
+ fontWeight = style.fontWeight,
+ textAlign = style.textAlign,
+ lineHeight = style.lineHeight,
+ fontFamily = style.fontFamily,
+ textDecoration = style.textDecoration,
+ fontStyle = style.fontStyle,
+ letterSpacing = style.letterSpacing
+ )
+ )
+ }
+}
\ No newline at end of file
diff --git a/tap-target-compose/src/main/java/com/psoffritti/taptargetcompose/TapTarget.kt b/taptarget/src/commonMain/kotlin/zone/ien/taptargetcmp/TapTarget.kt
similarity index 66%
rename from tap-target-compose/src/main/java/com/psoffritti/taptargetcompose/TapTarget.kt
rename to taptarget/src/commonMain/kotlin/zone/ien/taptargetcmp/TapTarget.kt
index cffedfd..ac22306 100644
--- a/tap-target-compose/src/main/java/com/psoffritti/taptargetcompose/TapTarget.kt
+++ b/taptarget/src/commonMain/kotlin/zone/ien/taptargetcmp/TapTarget.kt
@@ -1,7 +1,7 @@
-package com.psoffritti.taptargetcompose
+package zone.ien.taptargetcmp
-import androidx.annotation.FloatRange
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
@@ -21,21 +21,11 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.TextUnit
-/**
- * A composable that can shows tap targets in succession.
- * @param showTapTargets Whether to show the tap targets or not.
- * @param modifier The modifier to apply to this layout.
- * @param onComplete Called when all tap targets have been shown.
- * @param state The state of [TapTargetCoordinator],
- * can be used to know which tap target is being shown.
- * @param content The main content. Each composable in the [content] can be marked as a tap target.
- */
@Composable
fun TapTargetCoordinator(
showTapTargets: Boolean,
modifier: Modifier = Modifier,
onComplete: () -> Unit = { },
- // TODO(issue#1) use rememberSavable
state: TapTargetCoordinatorState = remember { TapTargetCoordinatorState() },
contentAlignment: Alignment = Alignment.Center,
content: @Composable TapTargetScope.() -> Unit,
@@ -46,20 +36,17 @@ fun TapTargetCoordinator(
contentAlignment = contentAlignment,
modifier = modifier
) {
- // Always render the content.
scope.content()
+
if (showTapTargets) {
val currentTapTarget = state.currentTarget
if (currentTapTarget != null) {
- // Render the TapTarget in an overlay.
- // The overlay is removed when the composable leaves the composition.
- Overlay(key = Unit) {
+ Box(modifier = Modifier.fillMaxSize()) {
TapTarget(
tapTarget = currentTapTarget,
onComplete = {
state.currentTargetIndex++
if (state.currentTargetIndex >= state.tapTargets.size) {
- // There are no TapTargets left to render.
onComplete()
}
}
@@ -70,22 +57,13 @@ fun TapTargetCoordinator(
}
}
-/** The scope from which a tap target can be created, through [Modifier.tapTarget]. */
class TapTargetScope internal constructor(private val state: TapTargetCoordinatorState) {
- /**
- * Modifier used to mark a [Composable] as a tap target.
- * @param precedence The precedence of the target. Targets are shown in order of precedence.
- * @param tapTargetStyle The style of the tap target.
- * @param onTargetClick Called when the target is clicked.
- * @param onTargetCancel Called when the target is cancelled, by clicking outside the target.
- */
fun Modifier.tapTarget(
title: TextDefinition,
description: TextDefinition,
precedence: Int,
tapTargetStyle: TapTargetStyle = TapTargetStyle.Default,
- // TODO(issue#2) can we avoid intercepting the click instead of defining a callback?
onTargetClick: () -> Unit = { },
onTargetCancel: () -> Unit = { },
): Modifier {
@@ -102,10 +80,6 @@ class TapTargetScope internal constructor(private val state: TapTargetCoordinato
}
}
- /**
- * The same as [Modifier.tapTarget], but takes a [TapTargetDefinition].
- * @see Modifier.tapTarget
- */
fun Modifier.tapTarget(tapTargetDefinition: TapTargetDefinition): Modifier {
return tapTarget(
tapTargetDefinition.title,
@@ -118,7 +92,6 @@ class TapTargetScope internal constructor(private val state: TapTargetCoordinato
}
}
-/** The content of a tap target. */
data class TapTargetDefinition(
val title: TextDefinition,
val description: TextDefinition,
@@ -128,7 +101,6 @@ data class TapTargetDefinition(
val onTargetCancel: () -> Unit = { },
)
-/** Keeps track of which tap target is currently being shown. */
class TapTargetCoordinatorState internal constructor() {
internal val tapTargets = mutableStateMapOf()
internal var currentTargetIndex by mutableIntStateOf(0)
@@ -136,16 +108,6 @@ class TapTargetCoordinatorState internal constructor() {
val currentTarget get() = tapTargets[currentTargetIndex]
}
-/**
- * Defines a tap target.
- * @param precedence The precedence of the target. Tap Targets are shown in order of precedence.
- * @param title The title of the tap target.
- * @param description The description of the tap target.
- * @param coordinates The [LayoutCoordinates] of the target.
- * @param style The style of the tap target.
- * @param onTargetClick Called when the target is clicked.
- * @param onTargetCancel Called when the target is cancelled, by clicking outside the target.
- */
class TapTarget internal constructor(
val precedence: Int,
val title: TextDefinition,
@@ -156,7 +118,6 @@ class TapTarget internal constructor(
val onTargetCancel: () -> Unit,
)
-/** Defines a text block shown in the tap target. */
data class TextDefinition(
val text: String,
internal val textStyle: TextStyle = TextStyle.Default,
@@ -185,15 +146,8 @@ data class TextDefinition(
)
}
-/**
- * Defines the look and feel of a tap target.
- * @param backgroundColor The background color of the main circle.
- * @param backgroundAlpha The alpha of the main circle.
- * @param tapTargetHighlightColor The color of the highlight circle.
- */
data class TapTargetStyle(
val backgroundColor: Color = Color.Blue,
- @FloatRange(from = 0.0, to = 1.0)
val backgroundAlpha: Float = 1f,
val tapTargetHighlightColor: Color = Color.White,
) {