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 @@ [![GitHub issues](https://img.shields.io/github/issues/tahaak67/ShowcaseLayoutCompose)](https://github.com/tahaak67/ShowcaseLayoutCompose/issues) [![GitHub stars](https://img.shields.io/github/stars/tahaak67/ShowcaseLayoutCompose)](https://github.com/tahaak67/ShowcaseLayoutCompose/stargazers) [![GitHub license](https://img.shields.io/github/license/tahaak67/ShowcaseLayoutCompose)](https://github.com/tahaak67/ShowcaseLayoutCompose/blob/main/LICENSE) -[![Compose Multiplatform](https://img.shields.io/badge/Compose%20Multiplatform-v1.5.0-blue)](https://github.com/JetBrains/compose-multiplatform) +[![Compose Multiplatform](https://img.shields.io/badge/Compose%20Multiplatform-v1.6.1-blue)](https://github.com/JetBrains/compose-multiplatform) ![badge-android](http://img.shields.io/badge/platform-android-3DDC84.svg) ![badge-ios](http://img.shields.io/badge/platform-ios-CDCDCD.svg) ![badge-desktop](http://img.shields.io/badge/platform-desktop-DB413D.svg) +![badge-web](https://img.shields.io/badge/platform-web-orange?logoColor=gray) + # Showcase Layout Compose @@ -17,7 +19,9 @@ Create a beautiful animated showcase effect for your compose UIs easily ! Library demo GIF -## Usage +Library demo GIF.Library demo GIF + +## 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( Greeting msg example

-#### 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` | -| :---------------: | :---------------: | :---------------: | -|Screenshot | Screenshot| Screenshot| +| Default Arrow | `curved = true` | `hasHead = false` | +|:------------------------------------------------------------------------------------------------:|:------------------------------------------------------------------------------------------------:|:------------------------------------------------------------------------------------------------:| +| Screenshot | Screenshot | Screenshot | + +#### 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` | +|:----------------------------------------------------------------------------------------------------------:|:-------------------------------------------------------------------------------------------------------:|:--------------------------------------------------------------------------------------------------------:|:--------------------------------------------------------------------------------------------------------------:| +| Screenshot | Screenshot | Screenshot | Screenshot | + +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