diff --git a/.gitignore b/.gitignore
index aa724b7..bbfdafb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,3 +13,15 @@
.externalNativeBuild
.cxx
local.properties
+/.idea/artifacts/*
+/.idea/.gitignore
+/.idea/.name
+/.idea/androidTestResultsUserPreferences.xml
+/.idea/compiler.xml
+/.idea/deploymentTargetDropDown.xml
+/.idea/gradle.xml
+/.idea/kotlinc.xml
+/.idea/migrations.xml
+/.idea/misc.xml
+/.idea/vcs.xml
+/convention-plugins/build/*
\ No newline at end of file
diff --git a/README.md b/README.md
index 8b9b61a..f2fac92 100644
--- a/README.md
+++ b/README.md
@@ -2,10 +2,12 @@
[](https://github.com/tahaak67/ShowcaseLayoutCompose/issues)
[](https://github.com/tahaak67/ShowcaseLayoutCompose/stargazers)
[](https://github.com/tahaak67/ShowcaseLayoutCompose/blob/main/LICENSE)
-[](https://github.com/JetBrains/compose-multiplatform)
+[](https://github.com/JetBrains/compose-multiplatform)



+
+
# Showcase Layout Compose
@@ -17,7 +19,9 @@ Create a beautiful animated showcase effect for your compose UIs easily !
-## Usage
+
.
+
+## Setup
Showcase Layout Compose can be used in **both** Jetpack Compose (native Android) or Compose Multiplatform (Kotlin Multiplatform) projects.
@@ -27,8 +31,9 @@ Showcase Layout Compose can be used in **both** Jetpack Compose (native Android)
Add the dependency to your module's `build.gradle` file like below
``` kotlin
-implementation("ly.com.tahaben:showcase-layout-compose:1.0.5-beta")
+implementation("ly.com.tahaben:showcase-layout-compose:1.0.5")
```
+## Usage
#### Step 1
@@ -67,7 +72,7 @@ text "ShowcaseLayout Test 1"
Text(
modifier = Modifier.showcase(
// should start with 1 and increment with 1 for each time you use Modifier.showcase()
- k = 1,
+ index = 1,
message =
),
text = "ShowcaseLayout Test 1"
@@ -79,9 +84,43 @@ you also use the old method by wrap the composables you want to showcase with `S
#### Step 3
-Start showcasing by making `isShowcasing = true`, and stop showcasing by making it false \
+You have 2 ways of showcasing, showcase everything subsequently or showcasing each item manually
+
+
+ Showcase all items subsequently
+Start showcasing by making isShowcasing = true
, and stop showcasing by making it false
above we stop showcasing after we showcase the last item using `onFinished` which is called whenever
all items are showcased,
+
+
+
+
+ Showcase a single item (1.0.5 and up)
+After you attach the index and showcase message to your components you can simply call showcaseItem(i)
where i is the index of the item you want to showcase
+
+```kotlin
+ val coroutineScope = rememberCoroutineScope()
+
+ coroutineScope.launch{
+ showcaseItem(1)
+ }
+```
+similarly you can show a greeting using showGreeting
and passing the message
+
+```kotlin
+ val coroutineScope = rememberCoroutineScope()
+
+ coroutineScope.launch{
+ showGreeting(
+ ShowcaseMsg(
+ text = "I like compose bro <3",
+ textStyle = TextStyle(color = Color.White)
+ )
+ )
+ }
+```
+
+
Done, our text is now showcased!, customize it further with Additional parameters
@@ -126,7 +165,7 @@ ShowcaseLayout(
-#### initKey
+#### initIndex
the initial value of the counter, set this to 1 if you don't want a greeting message before
showcasing targets.
@@ -210,20 +249,51 @@ arrow = Arrow(
)
```
-| Default Arrow | `curved = true` | `hasHead = false` |
-| :---------------: | :---------------: | :---------------: |
-|
|
|
|
+| Default Arrow | `curved = true` | `hasHead = false` |
+|:------------------------------------------------------------------------------------------------:|:------------------------------------------------------------------------------------------------:|:------------------------------------------------------------------------------------------------:|
+|
|
|
|
+
+#### Head style
+By default, an Arrow will have a triangle as the head to change this, set `head` in the arrow to one of these options
+
+| `TRIANGLE` | `CIRCLE` | `SQUARE` | `ROUND_SQUARE` |
+|:----------------------------------------------------------------------------------------------------------:|:-------------------------------------------------------------------------------------------------------:|:--------------------------------------------------------------------------------------------------------:|:--------------------------------------------------------------------------------------------------------------:|
+|
|
|
|
|
+
+You can also animate the arrow head and change the size, see the example below.
+```kotlin
+showcase(
+ index = 5,
+ message = ShowcaseMsg(
+ "A Circle !",
+ textStyle = TextStyle(
+ color = Color(0xFF827717),
+ fontSize = 18.sp
+ ),
+ msgBackground = MaterialTheme.colors.primary,
+ gravity = Gravity.Top,
+ arrow = Arrow(
+ color = MaterialTheme.colors.primary,
+ targetFrom = Side.Top,
+ head = Head.CIRCLE, // head style
+ headSize = 30f, // the size of the circle
+ animSize = true // animates the arrow head size
+ )
+ )
+)
+```
## Logging Events
In recent releases logs have been disabled by default, to print log statement of the current actions taken by compose layout register a listener in your ShowcaseLayout
```kotlin
registerEventListener(object: ShowcaseEventListener {
- override fun onEvent(event: String) {
- println(event)
+ override fun onEvent(level: Level, event: String) {
+ println("$level: $event")
}
})
```
+
## Complete Example
For a complete example check
@@ -240,4 +310,4 @@ Showcase Layout is used by:
- [Farhan](https://github.com/tahaak67/Farhan)
-Contact me if you used ShowcaseLayout in your app, and you want it added to this list
+Contact me on LinkedIn or open an issue if you used ShowcaseLayout in your app, and you want it added to this list
diff --git a/app/build.gradle b/app/build.gradle
index ec622b3..9344f8f 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -38,7 +38,7 @@ android {
buildConfig true
}
composeOptions {
- kotlinCompilerExtensionVersion '1.5.2'
+ kotlinCompilerExtensionVersion '1.5.11'
}
packagingOptions {
resources {
@@ -48,16 +48,16 @@ android {
}
dependencies {
- def composeBom = platform('androidx.compose:compose-bom:2023.08.00')
+ def composeBom = platform('androidx.compose:compose-bom:2024.02.02')
implementation composeBom
androidTestImplementation composeBom
- implementation 'androidx.core:core-ktx:1.10.1'
+ implementation 'androidx.core:core-ktx:1.12.0'
implementation "androidx.compose.ui:ui"
implementation "androidx.compose.material:material"
- implementation("androidx.navigation:navigation-compose:2.7.1")
+ implementation("androidx.navigation:navigation-compose:2.7.6")
implementation "androidx.compose.ui:ui-tooling-preview"
- implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
- implementation 'androidx.activity:activity-compose:1.7.2'
+ implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
+ implementation 'androidx.activity:activity-compose:1.8.2'
implementation project(':showcase-layout-compose')
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
diff --git a/app/src/androidTest/java/ly/com/tahaben/showcaselayoutcompose/ShowcaseE2E.kt b/app/src/androidTest/java/ly/com/tahaben/showcaselayoutcompose/ShowcaseE2E.kt
index f090f98..e78fbf5 100644
--- a/app/src/androidTest/java/ly/com/tahaben/showcaselayoutcompose/ShowcaseE2E.kt
+++ b/app/src/androidTest/java/ly/com/tahaben/showcaselayoutcompose/ShowcaseE2E.kt
@@ -3,6 +3,7 @@ package ly.com.tahaben.showcaselayoutcompose
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import ly.com.tahaben.showcaselayoutcompose.ui.MainScreen
import ly.com.tahaben.showcaselayoutcompose.ui.theme.ShowcaseLayoutComposeTheme
@@ -63,6 +64,39 @@ class ShowcaseE2E {
composeRule
.onNodeWithTag("canvas")
.performClick()
+ composeRule
+ .onNodeWithTag("canvas")
+ .performClick()
+ composeRule
+ .onNodeWithTag("canvas")
+ .performClick()
+ composeRule
+ .onNodeWithTag("canvas")
+ .performClick()
+ composeRule
+ .onNodeWithTag("canvas")
+ .assertDoesNotExist()
+ composeRule
+ .onNodeWithText("Usage")
+ .performClick()
+ composeRule
+ .onNodeWithTag("canvas")
+ .assertIsDisplayed()
+ composeRule
+ .onNodeWithTag("canvas")
+ .performClick()
+ composeRule
+ .onNodeWithTag("canvas")
+ .assertDoesNotExist()
+ composeRule
+ .onNodeWithText("Hello")
+ .performClick()
+ composeRule
+ .onNodeWithTag("canvas")
+ .assertIsDisplayed()
+ composeRule
+ .onNodeWithTag("canvas")
+ .performClick()
composeRule
.onNodeWithTag("canvas")
.assertDoesNotExist()
diff --git a/app/src/main/java/ly/com/tahaben/showcaselayoutcompose/ui/AboutScreen.kt b/app/src/main/java/ly/com/tahaben/showcaselayoutcompose/ui/AboutScreen.kt
index 0996931..939210b 100644
--- a/app/src/main/java/ly/com/tahaben/showcaselayoutcompose/ui/AboutScreen.kt
+++ b/app/src/main/java/ly/com/tahaben/showcaselayoutcompose/ui/AboutScreen.kt
@@ -18,7 +18,7 @@ import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.ArrowBack
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -57,7 +57,7 @@ fun AboutScreen(
IconButton(onClick = onNavigateUp) {
Icon(
modifier = Modifier,
- imageVector = Icons.Filled.ArrowBack,
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(id = R.string.back)
)
}
diff --git a/app/src/main/java/ly/com/tahaben/showcaselayoutcompose/ui/MainScreen.kt b/app/src/main/java/ly/com/tahaben/showcaselayoutcompose/ui/MainScreen.kt
index 2d07677..6bc21b9 100644
--- a/app/src/main/java/ly/com/tahaben/showcaselayoutcompose/ui/MainScreen.kt
+++ b/app/src/main/java/ly/com/tahaben/showcaselayoutcompose/ui/MainScreen.kt
@@ -2,6 +2,7 @@ package ly.com.tahaben.showcaselayoutcompose.ui
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -29,6 +30,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -43,9 +45,12 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import ly.com.tahaben.showcase_layout_compose.domain.Level
import ly.com.tahaben.showcase_layout_compose.domain.ShowcaseEventListener
import ly.com.tahaben.showcase_layout_compose.model.Arrow
import ly.com.tahaben.showcase_layout_compose.model.Gravity
+import ly.com.tahaben.showcase_layout_compose.model.Head
import ly.com.tahaben.showcase_layout_compose.model.MsgAnimation
import ly.com.tahaben.showcase_layout_compose.model.ShowcaseMsg
import ly.com.tahaben.showcase_layout_compose.model.Side
@@ -66,6 +71,7 @@ fun MainScreen(
var isShowcasing by remember {
mutableStateOf(false)
}
+ val coroutineScope = rememberCoroutineScope()
LaunchedEffect(key1 = true) {
delay(500)
isShowcasing = true
@@ -91,11 +97,16 @@ fun MainScreen(
textStyle = TextStyle(color = Color.Black, textAlign = TextAlign.Center),
msgBackground = Color.White,
roundedCorner = 15.dp
- )
+ ),
+ animationDuration = 100
) {
- registerEventListener(object: ShowcaseEventListener {
- override fun onEvent(event: String) {
- println(event)
+ registerEventListener(eventListener = object : ShowcaseEventListener {
+
+ override fun onEvent(level: Level, event: String) {
+ when (level) {
+ Level.DEBUG, Level.INFO, Level.WARNING -> println(event)
+ else -> Unit
+ }
}
})
Column(
@@ -113,7 +124,7 @@ fun MainScreen(
IconButton(onClick = { mDisplayMenu = !mDisplayMenu }) {
Icon(
modifier = Modifier.showcase(
- k = 3,
+ index = 3,
message = ShowcaseMsg(
buildAnnotatedString {
append("From the ")
@@ -176,6 +187,16 @@ fun MainScreen(
) {
Spacer(modifier = Modifier.height(spacing.spaceExtraLarge))
Text(
+ modifier = Modifier.clickable {
+ coroutineScope.launch {
+ showGreeting(
+ ShowcaseMsg(
+ text = "I love banana bro <3",
+ textStyle = TextStyle(color = Color.White)
+ )
+ )
+ }
+ },
text = stringResource(id = R.string.hello),
style = MaterialTheme.typography.h1,
color = MaterialTheme.colors.onPrimary
@@ -192,19 +213,20 @@ fun MainScreen(
Spacer(modifier = Modifier.width(spacing.spaceExtraSmall))
Text(
- modifier = Modifier.showcase(
- k = 4, message = ShowcaseMsg(
- "Useful tip tho :P",
- textStyle = TextStyle(color = Color.DarkGray),
- msgBackground = Color(0xFFE0F2F1),
- arrow = Arrow(
- targetFrom = Side.Top,
- hasHead = false,
- color = MaterialTheme.colors.primary
- ),
-
+ modifier = Modifier
+ .showcase(
+ index = 4, message = ShowcaseMsg(
+ "Useful tip tho :P",
+ textStyle = TextStyle(color = Color.DarkGray),
+ msgBackground = Color(0xFFE0F2F1),
+ arrow = Arrow(
+ targetFrom = Side.Top,
+ head = null,
+ color = MaterialTheme.colors.primary
+ )
)
- ),
+ )
+ .clickable { coroutineScope.launch { showcaseItem(4) } },
text = tip,
style = MaterialTheme.typography.h5,
color = MaterialTheme.colors.onPrimary
@@ -217,28 +239,53 @@ fun MainScreen(
horizontalArrangement = Arrangement.SpaceEvenly
) {
- MainScreenCard(
- modifier = Modifier.showcase(
- k = 1, message =
- ShowcaseMsg(
- "Track your phone usage from here",
- textStyle = TextStyle(
- color = Color(0xFF827717),
- fontSize = 18.sp
- ),
- msgBackground = MaterialTheme.colors.primary,
- gravity = Gravity.Bottom,
- arrow = Arrow(color = MaterialTheme.colors.primary),
- enterAnim = MsgAnimation.FadeInOut(),
- exitAnim = MsgAnimation.FadeInOut()
- )
+ MainScreenCard(
+ modifier = Modifier.showcase(
+ index = 1, message =
+ ShowcaseMsg(
+ "Track your phone usage from here",
+ textStyle = TextStyle(
+ color = Color(0xFF827717),
+ fontSize = 18.sp
+ ),
+ msgBackground = MaterialTheme.colors.primary,
+ gravity = Gravity.Bottom,
+ arrow = Arrow(color = MaterialTheme.colors.primary, animSize = false),
+ enterAnim = MsgAnimation.FadeInOut(),
+ exitAnim = MsgAnimation.FadeInOut()
+ )
+ ).showcase(index = 5, message =
+ ShowcaseMsg(
+ "From top !",
+ textStyle = TextStyle(
+ color = Color(0xFF827717),
+ fontSize = 18.sp
),
- text = stringResource(R.string.usage),
- iconId = R.drawable.ic_usage,
- status = ""
- ) { }
+ msgBackground = MaterialTheme.colors.primary,
+ gravity = Gravity.Top,
+ arrow = Arrow(color = MaterialTheme.colors.primary, targetFrom = Side.Top, headSize = 30f, head = Head.CIRCLE, animSize = true),
+ enterAnim = MsgAnimation.FadeInOut(),
+ exitAnim = MsgAnimation.FadeInOut()
+ )).showcase(index =6, message =
+ ShowcaseMsg(
+ "Right",
+ textStyle = TextStyle(
+ color = Color(0xFF827717),
+ fontSize = 18.sp
+ ),
+ msgBackground = MaterialTheme.colors.primary,
+ gravity = Gravity.Top,
+ arrow = Arrow(color = MaterialTheme.colors.primary, targetFrom = Side.Right, headSize = 35f, head = Head.ROUND_SQUARE, animSize = false),
+ enterAnim = MsgAnimation.FadeInOut(),
+ exitAnim = MsgAnimation.FadeInOut()
+ )),
+ text = stringResource(R.string.usage),
+ iconId = R.drawable.ic_usage,
+ status = ""
+ ) { coroutineScope.launch { showcaseItem(5) } }
MainScreenCard(
+ modifier = Modifier,
text = stringResource(R.string.notifications_filter),
iconId = R.drawable.ic_notification,
status = if (isNotificationFilterEnabled) stringResource(id = R.string.enabled) else stringResource(
@@ -258,14 +305,14 @@ fun MainScreen(
id = R.string.disabled
)
) { }
- MainScreenCard(
- modifier = Modifier.showcase(k = 2, message = null),
- text = stringResource(R.string.infinite_scrolling),
- iconId = R.drawable.ic_swipe_vertical_24,
- status = if (isInfiniteScrollBlockerEnabled) stringResource(id = R.string.enabled) else stringResource(
- id = R.string.disabled
- )
- ) { }
+ MainScreenCard(
+ modifier = Modifier.showcase(index = 2, message = null),
+ text = stringResource(R.string.infinite_scrolling),
+ iconId = R.drawable.ic_swipe_vertical_24,
+ status = if (isInfiniteScrollBlockerEnabled) stringResource(id = R.string.enabled) else stringResource(
+ id = R.string.disabled
+ )
+ ) { coroutineScope.launch { showcaseItem(2) } }
}
Spacer(modifier = Modifier.height(spacing.spaceMedium))
diff --git a/build.gradle b/build.gradle
index f00c75b..0cc73ff 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,19 +1,16 @@
buildscript {
repositories {
mavenCentral()
+ google()
}
}// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
- //id 'com.android.application' version '7.4.1' apply false
- id 'com.android.library' version '8.1.1' apply false
- id 'org.jetbrains.kotlin.android' version '1.9.0' apply false
- id 'org.jetbrains.kotlin.multiplatform' version '1.9.0' apply false
- id 'org.jetbrains.compose' version '1.5.0' apply false
+ id 'com.android.library' version '8.2.2' apply false
+ id 'org.jetbrains.kotlin.android' version '1.9.23' apply false
+ id 'org.jetbrains.kotlin.multiplatform' version '1.9.23' apply false
+ id 'org.jetbrains.compose' version '1.6.1' apply false
id("io.github.gradle-nexus.publish-plugin") version "1.3.0"
}
-task clean(type: Delete) {
- delete rootProject.buildDir
-}
apply from: "${rootDir}/scripts/publish-root.gradle"
\ No newline at end of file
diff --git a/convention-plugins/src/main/kotlin/convention.publication.gradle.kts b/convention-plugins/src/main/kotlin/convention.publication.gradle.kts
index 4ae409d..59333c2 100644
--- a/convention-plugins/src/main/kotlin/convention.publication.gradle.kts
+++ b/convention-plugins/src/main/kotlin/convention.publication.gradle.kts
@@ -14,7 +14,7 @@ ext["ossrhUsername"] = null
ext["ossrhPassword"] = null
val publishGroupId: String = "ly.com.tahaben"
-val publishVersion: String = "1.0.5-beta"
+val publishVersion: String = "1.0.5"
val publishArtifactId: String = "showcase-layout-compose"
@@ -42,7 +42,6 @@ val javadocJar by tasks.registering(Jar::class) {
fun getExtraString(name: String) = ext[name]?.toString()
-
group = publishGroupId
version = publishVersion
diff --git a/gradle.properties b/gradle.properties
index cddee97..3d63dcc 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -22,5 +22,5 @@ kotlin.code.style=official
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
-#Compose
-org.jetbrains.compose.experimental.uikit.enabled=true
\ No newline at end of file
+# Wasm
+org.jetbrains.compose.experimental.wasm.enabled=true
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 40bf9ba..4d3de01 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Tue Sep 05 13:36:22 EET 2023
+#Thu Feb 29 20:17:40 EET 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/metadata/screenshots/screenshot-10-square.png b/metadata/screenshots/screenshot-10-square.png
new file mode 100644
index 0000000..274b904
Binary files /dev/null and b/metadata/screenshots/screenshot-10-square.png differ
diff --git a/metadata/screenshots/screenshot-11-round-square.png b/metadata/screenshots/screenshot-11-round-square.png
new file mode 100644
index 0000000..f7a1f0e
Binary files /dev/null and b/metadata/screenshots/screenshot-11-round-square.png differ
diff --git a/metadata/screenshots/screenshot-12-triangle.png b/metadata/screenshots/screenshot-12-triangle.png
new file mode 100644
index 0000000..6b37fd2
Binary files /dev/null and b/metadata/screenshots/screenshot-12-triangle.png differ
diff --git a/metadata/screenshots/screenshot-13.png b/metadata/screenshots/screenshot-13.png
new file mode 100644
index 0000000..ca90bca
Binary files /dev/null and b/metadata/screenshots/screenshot-13.png differ
diff --git a/metadata/screenshots/screenshot-14.png b/metadata/screenshots/screenshot-14.png
new file mode 100644
index 0000000..579a72b
Binary files /dev/null and b/metadata/screenshots/screenshot-14.png differ
diff --git a/metadata/screenshots/screenshot-9-circle.png b/metadata/screenshots/screenshot-9-circle.png
new file mode 100644
index 0000000..20e51b3
Binary files /dev/null and b/metadata/screenshots/screenshot-9-circle.png differ
diff --git a/settings.gradle b/settings.gradle
index d0535b2..abe2b81 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,21 +1,21 @@
+
pluginManagement {
repositories {
- gradlePluginPortal()
google()
+ gradlePluginPortal()
mavenCentral()
}
}
plugins {
- id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0"
+ id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"
}
dependencyResolutionManagement {
- repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "Showcase Layout Compose"
-include ':app'
-include ':showcase-layout-compose'
+include (":app")
+include (":showcase-layout-compose")
includeBuild("convention-plugins")
diff --git a/showcase-layout-compose/build.gradle.kts b/showcase-layout-compose/build.gradle.kts
index bab7fa9..396131e 100644
--- a/showcase-layout-compose/build.gradle.kts
+++ b/showcase-layout-compose/build.gradle.kts
@@ -1,3 +1,6 @@
+import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
+import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig
+
plugins {
kotlin("multiplatform")
id("com.android.library")
@@ -15,7 +18,22 @@ kotlin {
iosArm64()
iosSimulatorArm64()
-
+ @OptIn(ExperimentalWasmDsl::class)
+ wasmJs {
+ moduleName = "composeApp"
+ browser {
+ commonWebpackConfig {
+ outputFileName = "composeApp.js"
+ devServer = (devServer ?: KotlinWebpackConfig.DevServer()).apply {
+ static = (static ?: mutableListOf()).apply {
+ // Serve sources to debug inside browser
+ add(project.projectDir.path)
+ }
+ }
+ }
+ }
+ binaries.executable()
+ }
sourceSets {
@@ -47,7 +65,7 @@ kotlin {
}
android {
- compileSdk = 33
+ compileSdk = 34
namespace = "ly.com.tahaben.showcaselayoutcompose"
compileOptions {
@@ -58,3 +76,8 @@ android {
jvmToolchain(17)
}
}
+
+
+compose.experimental {
+ web.application {}
+}
\ No newline at end of file
diff --git a/showcase-layout-compose/src/commonMain/kotlin/ly/com/tahaben/showcase_layout_compose/domain/Level.kt b/showcase-layout-compose/src/commonMain/kotlin/ly/com/tahaben/showcase_layout_compose/domain/Level.kt
new file mode 100644
index 0000000..983f480
--- /dev/null
+++ b/showcase-layout-compose/src/commonMain/kotlin/ly/com/tahaben/showcase_layout_compose/domain/Level.kt
@@ -0,0 +1,5 @@
+package ly.com.tahaben.showcase_layout_compose.domain
+
+enum class Level(val value: Int) {
+ VERBOSE(4),INFO(3),DEBUG(2),WARNING(1),ERROR(0)
+}
\ No newline at end of file
diff --git a/showcase-layout-compose/src/commonMain/kotlin/ly/com/tahaben/showcase_layout_compose/domain/ShowcaseEventListener.kt b/showcase-layout-compose/src/commonMain/kotlin/ly/com/tahaben/showcase_layout_compose/domain/ShowcaseEventListener.kt
index 7b59b5b..b1b44b8 100644
--- a/showcase-layout-compose/src/commonMain/kotlin/ly/com/tahaben/showcase_layout_compose/domain/ShowcaseEventListener.kt
+++ b/showcase-layout-compose/src/commonMain/kotlin/ly/com/tahaben/showcase_layout_compose/domain/ShowcaseEventListener.kt
@@ -1,5 +1,6 @@
package ly.com.tahaben.showcase_layout_compose.domain
+
interface ShowcaseEventListener {
- fun onEvent(event: String)
+ fun onEvent(level: Level, event: String)
}
diff --git a/showcase-layout-compose/src/commonMain/kotlin/ly/com/tahaben/showcase_layout_compose/model/Arrow.kt b/showcase-layout-compose/src/commonMain/kotlin/ly/com/tahaben/showcase_layout_compose/model/Arrow.kt
index 8ae93a0..71dda3b 100644
--- a/showcase-layout-compose/src/commonMain/kotlin/ly/com/tahaben/showcase_layout_compose/model/Arrow.kt
+++ b/showcase-layout-compose/src/commonMain/kotlin/ly/com/tahaben/showcase_layout_compose/model/Arrow.kt
@@ -25,13 +25,18 @@ import androidx.compose.ui.graphics.Color
* @param targetFrom the direction from where the arrow will point at the target, Ex: Side.Right.
* @param curved draw a curvy arrow from the middle of the screen to target, works best if the target is on the right/left edge of screen.
* @param animationDuration the time taken to animate the arrow in milliseconds.
+ * @param head The shape of the arrow head, if null only the line will be drawn without the arrow head.
+ * @param headSize size of the arrow head default is 25.
* @param color color of the arrow.
+ * @param animSize animate the head size from 0 to [headSize] when entering and reverse it on exit.
**/
data class Arrow(
val targetFrom: Side = Side.Bottom,
val curved: Boolean = false,
val animationDuration: Int = 1000,
- val hasHead: Boolean = true,
- val color: Color = Color.White
+ val head: Head? = Head.TRIANGLE,
+ val headSize: Float = 25f,
+ val color: Color = Color.White,
+ val animSize: Boolean = true
)
diff --git a/showcase-layout-compose/src/commonMain/kotlin/ly/com/tahaben/showcase_layout_compose/model/Head.kt b/showcase-layout-compose/src/commonMain/kotlin/ly/com/tahaben/showcase_layout_compose/model/Head.kt
new file mode 100644
index 0000000..a4adef2
--- /dev/null
+++ b/showcase-layout-compose/src/commonMain/kotlin/ly/com/tahaben/showcase_layout_compose/model/Head.kt
@@ -0,0 +1,7 @@
+package ly.com.tahaben.showcase_layout_compose.model
+/**
+ * @property ROUND_SQUARE a square with rounded corners
+ * */
+enum class Head {
+ CIRCLE, TRIANGLE, SQUARE, ROUND_SQUARE
+}
\ No newline at end of file
diff --git a/showcase-layout-compose/src/commonMain/kotlin/ly/com/tahaben/showcase_layout_compose/ui/ShowcaseLayout.kt b/showcase-layout-compose/src/commonMain/kotlin/ly/com/tahaben/showcase_layout_compose/ui/ShowcaseLayout.kt
index ed7186e..c0a0996 100644
--- a/showcase-layout-compose/src/commonMain/kotlin/ly/com/tahaben/showcase_layout_compose/ui/ShowcaseLayout.kt
+++ b/showcase-layout-compose/src/commonMain/kotlin/ly/com/tahaben/showcase_layout_compose/ui/ShowcaseLayout.kt
@@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
@@ -21,7 +22,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
@@ -41,13 +41,20 @@ import androidx.compose.ui.text.drawText
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.toSize
import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
+import ly.com.tahaben.showcase_layout_compose.domain.Level
import ly.com.tahaben.showcase_layout_compose.domain.ShowcaseEventListener
+import ly.com.tahaben.showcase_layout_compose.model.Arrow
import ly.com.tahaben.showcase_layout_compose.model.Gravity
+import ly.com.tahaben.showcase_layout_compose.model.Head
import ly.com.tahaben.showcase_layout_compose.model.MsgAnimation
import ly.com.tahaben.showcase_layout_compose.model.ShowcaseData
import ly.com.tahaben.showcase_layout_compose.model.ShowcaseMsg
@@ -72,48 +79,96 @@ import kotlin.math.atan2
* Created by Taha Ben Ashur (https://github.com/tahaak67) on 1,August,2022
*/
-private const val TAG = "ShowcaseLayout"
+private const val TAG = "ShowcaseLayout "
+private const val INDEX_RESET_DELAY = 250L
/**
* ShowcaseLayout
*
* @param isShowcasing to determine if showcase is starting or not.
* @param isDarkLayout if true the showcase view will be white instead of black.
- * @param initKey the initial value of counter, set this to 1 if you don't want a greeting screen before showcasing target.
+ * @param initIndex the initial value of counter, set this to 1 if you don't want a greeting screen before showcasing target.
* @param animationDuration total animation time taken when switching from current to next target in milliseconds.
* @param onFinish what happens when all items are showcased.
- * @param greeting greeting message to be shown before showcasing the first composable, leave initKey at 0 if you want to use this.
+ * @param greeting greeting message to be shown before showcasing the first composable, leave [initIndex] at 0 if you want to use this.
+ * @param lineThickness thickness of the arrow line in dp.
**/
@Composable
fun ShowcaseLayout(
isShowcasing: Boolean,
isDarkLayout: Boolean = false,
- initKey: Int = 0,
+ initIndex: Int = 0,
animationDuration: Int = 1000,
onFinish: () -> Unit,
greeting: ShowcaseMsg? = null,
+ lineThickness: Dp = 5.dp,
content: @Composable ShowcaseScope.() -> Unit
) {
- var currentKey by remember {
- mutableIntStateOf(initKey)
+ var currentIndex by remember {
+ mutableIntStateOf(initIndex)
}
+ val resetDelay by derivedStateOf { animationDuration.toLong() + INDEX_RESET_DELAY }
val scope = ShowcaseScopeImpl(greeting)
scope.content()
+
+ var showCasingItem by remember { mutableStateOf(false) }
+ var singleGreetingMsg by remember { mutableStateOf(null) }
+ var isSingleGreeting by remember { mutableStateOf(false) }
+ LaunchedEffect(key1 = isShowcasing) {
+ launch {
+ scope.showcaseActionFlow.collectLatest {
+ if (it != null) {
+ scope.showcaseEventListener?.onEvent(
+ Level.DEBUG,
+ TAG + "showcase single item index: $it"
+ )
+ currentIndex = it ?: initIndex
+ showCasingItem = true
+ } else {
+ showCasingItem = false
+ delay(resetDelay)
+ currentIndex = initIndex
+ }
+ }
+ }
+ launch {
+ scope.greetingActionFlow.collectLatest {
+ if (it != null) {
+ scope.showcaseEventListener?.onEvent(
+ Level.DEBUG,
+ TAG + "showcase single greeting: $it"
+ )
+ currentIndex = 0
+ isSingleGreeting = true
+ } else {
+ isSingleGreeting = false
+ delay(resetDelay)
+ currentIndex = initIndex
+ }
+ singleGreetingMsg = it
+ }
+ }
+ }
+
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
- AnimatedVisibility(isShowcasing, enter = fadeIn(), exit = fadeOut()) {
+ AnimatedVisibility(
+ isShowcasing || showCasingItem || isSingleGreeting,
+ enter = fadeIn(),
+ exit = fadeOut()
+ ) {
val offset by animateOffsetAsState(
- targetValue = scope.getPositionFor(currentKey),
+ targetValue = scope.getPositionFor(currentIndex),
animationSpec = tween(animationDuration),
label = "item offset anim"
)
val itemSize by animateSizeAsState(
- targetValue = scope.getSizeFor(currentKey),
+ targetValue = scope.getSizeFor(currentIndex),
animationSpec = tween(animationDuration),
label = "item size anim"
)
var message by remember {
- mutableStateOf(scope.getMessageFor(currentKey))
+ mutableStateOf(scope.getMessageFor(currentIndex))
}
val pathPortion = remember {
Animatable(initialValue = 0f)
@@ -128,10 +183,12 @@ fun ShowcaseLayout(
val animMsgTextAlpha = remember { Animatable(0f) }
val animMsgAlpha = remember { Animatable(0f) }
val animArrow = remember { Animatable(0f) }
+ val animArrowHead = remember { Animatable(0f) }
+
/** to animate current arrow line */
- LaunchedEffect(key1 = currentKey) {
- message = scope.getMessageFor(currentKey)
+ LaunchedEffect(key1 = currentIndex) {
+ message = scope.getMessageFor(currentIndex)
arrowAnimDuration = message?.arrow?.animationDuration
isArrowDelayOver = false
if (message?.arrow != null) {
@@ -149,8 +206,14 @@ fun ShowcaseLayout(
)
}
}
+ /* animate the arrow */
launch {
message?.arrow?.let { arrow ->
+ /** show the arrow if anim is false */
+ if (!arrow.animSize){
+ animArrowHead.snapTo(arrow.headSize)
+ }
+ /** move the arrow */
animArrow.animateTo(
1f,
tween(
@@ -158,16 +221,32 @@ fun ShowcaseLayout(
?: arrow.animationDuration
)
)
+ /** animate the size of the arrow */
+ if (arrow.animSize){
+ animArrowHead.animateTo(
+ arrow.headSize,
+ tween(
+ durationMillis = arrowAnimDuration
+ ?: arrow.animationDuration
+ )
+ )
+ }
}
}
}
- if (currentKey == 0) {
+ if (currentIndex == 0) {
+ if (isSingleGreeting) {
+ message = singleGreetingMsg
+ }
message?.let { msg ->
animMsgAlpha.animateTo(1f, tween(msg.enterAnim.duration))
animMsgTextAlpha.animateTo(1f, tween(msg.enterAnim.duration))
}
} else {
- scope.showcaseEventListener?.onEvent(TAG + "K:$currentKey enterAnim: ${message?.enterAnim}")
+ scope.showcaseEventListener?.onEvent(
+ Level.VERBOSE,
+ TAG + "index:$currentIndex enterAnim: ${message?.enterAnim}"
+ )
message?.let { msg ->
when (msg.enterAnim) {
is MsgAnimation.FadeInOut -> {
@@ -187,8 +266,10 @@ fun ShowcaseLayout(
}
val textMeasurer = rememberTextMeasurer()
-
- scope.showcaseEventListener?.onEvent(TAG + "offset :$offset")
+ scope.showcaseEventListener?.onEvent(
+ Level.VERBOSE,
+ TAG + "index: $currentIndex offset :$offset"
+ )
Canvas(
modifier = Modifier
.fillMaxSize()
@@ -200,6 +281,9 @@ fun ShowcaseLayout(
/** hide current arrow */
arrowAnimDuration?.let { duration ->
+ if (message?.arrow?.animSize == true){
+ animArrowHead.animateTo(0f, tween(duration/2))
+ }
launch {
animArrow.animateTo(0f, tween(duration / 2))
}
@@ -207,8 +291,14 @@ fun ShowcaseLayout(
pathPortion.animateTo(0f - Float.MIN_VALUE, tween(duration / 2))
}
message?.let { msg ->
- scope.showcaseEventListener?.onEvent(TAG + "K:$currentKey exitAnim: ${msg.exitAnim}")
- scope.showcaseEventListener?.onEvent(TAG + "K:$currentKey msg: $message")
+ scope.showcaseEventListener?.onEvent(
+ Level.VERBOSE,
+ TAG + "index:$currentIndex exitAnim: ${msg.exitAnim}"
+ )
+ scope.showcaseEventListener?.onEvent(
+ Level.VERBOSE,
+ TAG + "index:$currentIndex msg: ${message?.text}"
+ )
when (msg.exitAnim) {
is MsgAnimation.FadeInOut -> {
val duration = msg.enterAnim.duration
@@ -222,21 +312,37 @@ fun ShowcaseLayout(
}
}
}
-
- if (currentKey + 1 < scope.getHashMapSize()) {
- scope.showcaseEventListener?.onEvent(TAG + "current key +")
+ if (showCasingItem) {
+ scope.showcaseItemFinished()
+ return@launch
+ }
+ if (isSingleGreeting) {
+ scope.showGreetingFinished()
+ return@launch
+ }
+ if (currentIndex + 1 < scope.getHashMapSize()) {
+ scope.showcaseEventListener?.onEvent(
+ Level.INFO,
+ TAG + "moving to index ${currentIndex + 1}"
+ )
/** move to next item */
- currentKey++
+ currentIndex++
} else {
/** showcase finished */
- scope.showcaseEventListener?.onEvent(TAG + "finished")
+ scope.showcaseEventListener?.onEvent(
+ Level.INFO,
+ TAG + "finished"
+ )
onFinish()
- delay(animationDuration.toLong())
- currentKey = initKey
+ delay(resetDelay)
+ currentIndex = initIndex
}
isArrowDelayOver = false
}
- scope.showcaseEventListener?.onEvent(TAG + "tapped here $it")
+ scope.showcaseEventListener?.onEvent(
+ Level.VERBOSE,
+ TAG + "tapped here $it"
+ )
}
},
onDraw = {
@@ -269,9 +375,10 @@ fun ShowcaseLayout(
color = if (isDarkLayout) Color.White else Color.Black,
alpha = 0.80f,
)
- val hasArrowHead = message?.arrow?.hasHead == true
+ val hasArrowHead = message?.arrow?.head != null
+ val arrowHeadMargin = (message?.arrow?.headSize ?: Arrow().headSize) + 25
- if (currentKey > 0 && shouldDrawArrow) {
+ if (currentIndex > 0 && shouldDrawArrow) {
/** draw arrow line */
val arrowPath = Path().apply {
if (message?.arrow?.curved == true) {
@@ -299,7 +406,7 @@ fun ShowcaseLayout(
)
lineTo(
offset.x + (itemSize.width / 2),
- if (hasArrowHead) offset.y - 60 else offset.y
+ if (hasArrowHead) offset.y - arrowHeadMargin else offset.y
)
}
@@ -310,7 +417,7 @@ fun ShowcaseLayout(
)
lineTo(
offset.x + (itemSize.width / 2),
- if (hasArrowHead) offset.y + itemSize.height + 20 else offset.y + itemSize.height
+ if (hasArrowHead) offset.y + itemSize.height + arrowHeadMargin else offset.y + itemSize.height
)
}
@@ -320,7 +427,7 @@ fun ShowcaseLayout(
offset.y + (itemSize.height / 2)
)
lineTo(
- if (hasArrowHead) offset.x - 30 else offset.x,
+ if (hasArrowHead) offset.x - arrowHeadMargin else offset.x,
offset.y + (itemSize.height / 2)
)
}
@@ -331,7 +438,7 @@ fun ShowcaseLayout(
offset.y + (itemSize.height / 2)
)
lineTo(
- if (hasArrowHead) offset.x + itemSize.width + 30 else offset.x + itemSize.width,
+ if (hasArrowHead) offset.x + itemSize.width + arrowHeadMargin else offset.x + itemSize.width,
offset.y + (itemSize.height / 2)
)
}
@@ -355,32 +462,70 @@ fun ShowcaseLayout(
tan[0] = x
tan[1] = y
}
- scope.showcaseEventListener?.onEvent(TAG + "pos:${pos} tan:${tan}")
+ scope.showcaseEventListener?.onEvent(
+ Level.VERBOSE,
+ TAG + "pos:${pos} tan:${tan}"
+ )
}
drawPath(
path = outPath,
color = arrowColor,
- style = Stroke(width = 5.dp.toPx(), cap = StrokeCap.Round)
+ style = Stroke(width = lineThickness.toPx(), cap = StrokeCap.Round)
)
/** draw the arrow head (and rotate if needed) */
- if (message?.arrow?.hasHead == true) {
- val x = pos[0]
- val y = pos[1]
- val degrees = -atan2(tan[0], tan[1]) * (180f / PI.toFloat()) - 180f
- scope.showcaseEventListener?.onEvent(TAG + "max canvas: x:${size.width} y:${size.height}")
- rotate(degrees = degrees, pivot = Offset(x, y)) {
- drawPath(
- path = Path().apply {
- moveTo(x, y - 30f)
- lineTo(x - 30f, y + 60f)
- lineTo(x + 30f, y + 60f)
- close()
- },
+ val arrowSize = animArrowHead.value
+ val x = pos[0]
+ val y = pos[1]
+ val degrees = -atan2(tan[0], tan[1]) * (180f / PI.toFloat()) - 180f
+ scope.showcaseEventListener?.onEvent(
+ Level.VERBOSE,
+ TAG + "max canvas: x:${size.width} y:${size.height}"
+ )
+ when (message?.arrow?.head) {
+ Head.CIRCLE -> {
+ drawCircle(
+ center = Offset(x,y),
+ color = arrowColor,
+ alpha = animArrow.value,
+ radius = arrowSize
+ )
+
+ }
+ Head.TRIANGLE -> {
+ rotate(degrees = degrees, pivot = Offset(x, y)) {
+ drawPath(
+ path = Path().apply {
+ moveTo(x, y - arrowSize)
+ lineTo(x - arrowSize, y + arrowSize)
+ lineTo(x + arrowSize, y + arrowSize)
+ close()
+ },
+ color = arrowColor,
+ alpha = animArrow.value
+ )
+ }
+ }
+ Head.SQUARE -> {
+ drawRect(
+ topLeft = Offset(x - arrowSize.div(2),y - arrowSize.div(2)),
+ color = arrowColor,
+ alpha = animArrow.value,
+ size = Size(arrowSize,arrowSize)
+ )
+ }
+ Head.ROUND_SQUARE -> {
+ val radius = arrowSize.div(4)
+ drawRoundRect(
+ topLeft = Offset(x - arrowSize.div(2),y - arrowSize.div(2)),
color = arrowColor,
- alpha = animArrow.value
+ alpha = animArrow.value,
+ size = Size(arrowSize,arrowSize),
+ cornerRadius = CornerRadius(radius,radius)
)
}
+ null -> Unit
+
}
}
@@ -399,12 +544,12 @@ fun ShowcaseLayout(
/** Determine if message will be shown on top or below target */
val yOffset =
- if (currentKey == 0) (size.height / 2) else with(density) {
- val currentItemYPosition = scope.getPositionFor(currentKey).y
- val currentItemHeight = scope.getSizeFor(currentKey).height
+ if (currentIndex == 0) (size.height / 2) else with(density) {
+ val currentItemYPosition = scope.getPositionFor(currentIndex).y
+ val currentItemHeight = scope.getSizeFor(currentIndex).height
when (msg.gravity) {
Gravity.Top -> {
- currentItemYPosition + 230
+ currentItemYPosition - 230
}
Gravity.Bottom -> {
@@ -415,10 +560,16 @@ fun ShowcaseLayout(
val topPosition =
currentItemYPosition - 230
if (topPosition < 0) {
- scope.showcaseEventListener?.onEvent(TAG + "Not enough space on top show msg on bottom")
+ scope.showcaseEventListener?.onEvent(
+ Level.INFO,
+ TAG + "index: $currentIndex Not enough space on top show msg on bottom"
+ )
currentItemYPosition + currentItemHeight + 230
} else {
- scope.showcaseEventListener?.onEvent(TAG + "message can be shown on top")
+ scope.showcaseEventListener?.onEvent(
+ Level.INFO,
+ TAG + "index: $currentIndex message can be shown on top"
+ )
topPosition
}
}
@@ -433,16 +584,19 @@ fun ShowcaseLayout(
will get cut off, if that's the case we align the message Start or End to
the target Start or End as appropriate
*/
- val xOffset = if (currentKey == 0) {
+ val xOffset = if (currentIndex == 0 || msg.arrow?.curved == true) {
halfWidth - messageWidthHalf
} else {
- val currentItemXPosition = scope.getPositionFor(currentKey).x
- val currentItemWidth = scope.getSizeFor(currentKey).width
+ val currentItemXPosition = scope.getPositionFor(currentIndex).x
+ val currentItemWidth = scope.getSizeFor(currentIndex).width
val currentItemXMiddlePoint =
currentItemXPosition + (currentItemWidth / 2)
when {
(currentItemXMiddlePoint < halfWidth) -> {
- scope.showcaseEventListener?.onEvent(TAG + "layout on start half")
+ scope.showcaseEventListener?.onEvent(
+ Level.INFO,
+ TAG + "index: $currentIndex layout on start half"
+ )
if ((currentItemXMiddlePoint - messageWidthHalf) < 0) {
currentItemXPosition
} else {
@@ -451,12 +605,18 @@ fun ShowcaseLayout(
}
(currentItemXMiddlePoint == halfWidth) -> {
- scope.showcaseEventListener?.onEvent(TAG + "layout in middle")
+ scope.showcaseEventListener?.onEvent(
+ Level.INFO,
+ TAG + "index: $currentIndex layout in middle"
+ )
currentItemXMiddlePoint - messageWidthHalf
}
else -> {
- scope.showcaseEventListener?.onEvent(TAG + "layout on end half")
+ scope.showcaseEventListener?.onEvent(
+ Level.INFO,
+ TAG + "index: $currentIndex layout on end half"
+ )
if (currentItemXMiddlePoint + messageWidthHalf > size.width) {
currentItemXPosition + currentItemWidth - textResult.size.width
} else {
@@ -492,8 +652,10 @@ fun ShowcaseLayout(
}
}
)
-
- scope.showcaseEventListener?.onEvent(TAG + "calc: ${offset.y + itemSize.height - (maxHeight.value / 2)}")
+ scope.showcaseEventListener?.onEvent(
+ Level.VERBOSE,
+ TAG + "calc: ${offset.y + itemSize.height - (maxHeight.value / 2)}"
+ )
}
}
}
@@ -501,52 +663,102 @@ fun ShowcaseLayout(
class ShowcaseScopeImpl(greeting: ShowcaseMsg?) : ShowcaseScope {
private val showcaseDataHashMap = HashMap()
override var showcaseEventListener: ShowcaseEventListener? = null
+ private val _showcaseActionFlow = MutableStateFlow(null)
+ val showcaseActionFlow = _showcaseActionFlow.asStateFlow()
+ private val _greetingActionFlow = MutableStateFlow(null)
+ val greetingActionFlow = _greetingActionFlow.asStateFlow()
@Composable
override fun Showcase(
- k: Int,
+ index: Int,
message: ShowcaseMsg?,
itemContent: @Composable () -> Unit
) {
+ require(index >= 1) { "Index must be 1 or greater" }
Box(modifier = Modifier.onGloballyPositioned {
- showcaseEventListener?.onEvent(TAG + "key: $k")
- showcaseEventListener?.onEvent(TAG + "size: ${it.size} position: ${it.positionInRoot()}")
- showcaseDataHashMap[k] = ShowcaseData(it.size, it.positionInRoot(), message)
- showcaseEventListener?.onEvent(TAG + "showcase map: $showcaseDataHashMap")
+ showcaseDataHashMap[index] = ShowcaseData(it.size, it.positionInRoot(), message)
+
+ showcaseEventListener?.onEvent(Level.VERBOSE, TAG + "Index: $index")
+ showcaseEventListener?.onEvent(
+ Level.VERBOSE,
+ TAG + "size: ${it.size} position: ${it.positionInRoot()}"
+ )
+ showcaseEventListener?.onEvent(
+ Level.VERBOSE,
+ TAG + "showcase map: $showcaseDataHashMap"
+ )
+
}) {
itemContent()
}
}
- override fun Modifier.showcase(k: Int, message: ShowcaseMsg?): Modifier = composed {
- onGloballyPositioned {
- showcaseEventListener?.onEvent(TAG + "key: $k")
- showcaseEventListener?.onEvent(TAG + "size: ${it.size} position: ${it.positionInRoot()}")
- showcaseDataHashMap[k] = ShowcaseData(it.size, it.positionInRoot(), message)
- showcaseEventListener?.onEvent(TAG + "showcase map: $showcaseDataHashMap")
- }
+ override fun Modifier.showcase(index: Int, message: ShowcaseMsg?): Modifier {
+ require(index >= 1) { "Index must be 1 or greater" }
+ return this.then(
+ onGloballyPositioned {
+ if (it.isAttached) {
+ showcaseDataHashMap[index] = ShowcaseData(it.size, it.positionInRoot(), message)
+ showcaseEventListener?.onEvent(Level.VERBOSE, TAG + "Index: $index")
+ showcaseEventListener?.onEvent(
+ Level.VERBOSE,
+ TAG + "size: ${it.size} position: ${it.positionInRoot()}"
+ )
+ showcaseEventListener?.onEvent(
+ Level.VERBOSE,
+ TAG + "showcase map: $showcaseDataHashMap"
+ )
+ }
+ }
+ )
}
override fun registerEventListener(eventListener: ShowcaseEventListener) {
this.showcaseEventListener = eventListener
}
+ override suspend fun showcaseItem(index: Int) {
+ showcaseEventListener?.onEvent(Level.DEBUG, TAG + "showcase item $index")
+ _showcaseActionFlow.emit(index)
+ }
+
+ suspend fun showcaseItemFinished() {
+ showcaseEventListener?.onEvent(Level.DEBUG, TAG + "showcase item finished")
+ _showcaseActionFlow.emit(null)
+ }
+
+ override suspend fun showGreeting(message: ShowcaseMsg) {
+ _greetingActionFlow.emit(message)
+ showcaseEventListener?.onEvent(
+ Level.DEBUG,
+ TAG + "greeting ${message.text.substring(0..10)}"
+ )
+ }
+
+ suspend fun showGreetingFinished() {
+ _greetingActionFlow.emit(null)
+ showcaseEventListener?.onEvent(
+ Level.DEBUG,
+ TAG + "greeting show finished"
+ )
+ }
+
init {
showcaseDataHashMap[0] = ShowcaseData(IntSize(0, 0), Offset(0f, 0f), greeting)
}
- fun getSizeFor(k: Int): Size {
- val size = showcaseDataHashMap[k]?.size?.toSize() ?: Size(0f, 0f)
- showcaseEventListener?.onEvent(TAG + "showcase map size: $size")
+ fun getSizeFor(index: Int): Size {
+ val size = showcaseDataHashMap[index]?.size?.toSize() ?: Size(0f, 0f)
+ showcaseEventListener?.onEvent(Level.VERBOSE, TAG + "showcase map size: $size")
return size
}
- fun getPositionFor(k: Int): Offset {
- if (k == 0) {
+ fun getPositionFor(index: Int): Offset {
+ if (index == 0) {
return showcaseDataHashMap[1]?.position ?: Offset(0f, 0f)
}
- val p = showcaseDataHashMap[k]?.position ?: Offset(0f, 0f)
+ val p = showcaseDataHashMap[index]?.position ?: Offset(0f, 0f)
return p
}
@@ -554,7 +766,8 @@ class ShowcaseScopeImpl(greeting: ShowcaseMsg?) : ShowcaseScope {
return showcaseDataHashMap.size
}
- fun getMessageFor(currentKey: Int): ShowcaseMsg? {
- return showcaseDataHashMap[currentKey]?.message
+ fun getMessageFor(currentIndex: Int): ShowcaseMsg? {
+ return showcaseDataHashMap[currentIndex]?.message
}
+
}
diff --git a/showcase-layout-compose/src/commonMain/kotlin/ly/com/tahaben/showcase_layout_compose/ui/ShowcaseScope.kt b/showcase-layout-compose/src/commonMain/kotlin/ly/com/tahaben/showcase_layout_compose/ui/ShowcaseScope.kt
index 9a2bfcc..f9b1a1c 100644
--- a/showcase-layout-compose/src/commonMain/kotlin/ly/com/tahaben/showcase_layout_compose/ui/ShowcaseScope.kt
+++ b/showcase-layout-compose/src/commonMain/kotlin/ly/com/tahaben/showcase_layout_compose/ui/ShowcaseScope.kt
@@ -27,19 +27,19 @@ interface ShowcaseScope {
/**
* Showcase ShowcaseScope
*
- * @param k key of current item MUST start at 1 and increment by 1 for each composable inside this ShowcaseLayout.
+ * @param index key of current item MUST start at 1 and increment by 1 for each composable inside this ShowcaseLayout.
* @param message a message to display when showcasing this composable.
**/
@Composable
- fun Showcase(k: Int, message: ShowcaseMsg?, itemContent: @Composable () -> Unit)
+ fun Showcase(index: Int, message: ShowcaseMsg?, itemContent: @Composable () -> Unit)
/**
* Showcase ShowcaseScope
*
- * @param k key of current item MUST start at 1 and increment by 1 for each composable inside this ShowcaseLayout.
+ * @param index key of current item **MUST** start at 1 and increment by 1 for each composable inside this ShowcaseLayout.
* @param message a message to display when showcasing this composable.
**/
- fun Modifier.showcase(k: Int, message: ShowcaseMsg?): Modifier
+ fun Modifier.showcase(index: Int, message: ShowcaseMsg?): Modifier
var showcaseEventListener: ShowcaseEventListener?
@@ -50,4 +50,9 @@ interface ShowcaseScope {
* @param eventListener the [ShowcaseEventListener] to use.
**/
fun registerEventListener(eventListener: ShowcaseEventListener)
+
+ suspend fun showcaseItem(index: Int)
+// suspend fun showcaseItemFinished()
+ suspend fun showGreeting(message: ShowcaseMsg)
+// suspend fun showGreetingFinished()
}
\ No newline at end of file