From 880e9b6e9f006c6c64a7a34c6e181831d1ca2069 Mon Sep 17 00:00:00 2001 From: Tahaak67 Date: Tue, 15 Jul 2025 14:12:10 +0200 Subject: [PATCH 1/6] Start at index 1 if no greeting is provided --- .../domain/usecase/ValidateInitIndex.kt | 8 ++ .../ui/ShowcaseLayout.kt | 94 +++++++++++-------- .../ui/TargetShowcaseLayout.kt | 14 +-- 3 files changed, 70 insertions(+), 46 deletions(-) create mode 100644 showcase-layout-compose/src/commonMain/kotlin/ly/com/tahaben/showcase_layout_compose/domain/usecase/ValidateInitIndex.kt diff --git a/showcase-layout-compose/src/commonMain/kotlin/ly/com/tahaben/showcase_layout_compose/domain/usecase/ValidateInitIndex.kt b/showcase-layout-compose/src/commonMain/kotlin/ly/com/tahaben/showcase_layout_compose/domain/usecase/ValidateInitIndex.kt new file mode 100644 index 0000000..c59f3ff --- /dev/null +++ b/showcase-layout-compose/src/commonMain/kotlin/ly/com/tahaben/showcase_layout_compose/domain/usecase/ValidateInitIndex.kt @@ -0,0 +1,8 @@ +package ly.com.tahaben.showcase_layout_compose.domain.usecase + +import ly.com.tahaben.showcase_layout_compose.model.ShowcaseMsg + + +fun validateInitIndex(initIndex: Int, greeting: ShowcaseMsg?): Int { + return if (greeting == null) initIndex.coerceAtLeast(1) else initIndex +} 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 186aeb5..9acbea0 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 @@ -1,12 +1,6 @@ package ly.com.tahaben.showcase_layout_compose.ui -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.Animatable -import androidx.compose.animation.core.animateOffsetAsState -import androidx.compose.animation.core.animateSizeAsState -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut +import androidx.compose.animation.core.* import androidx.compose.foundation.Canvas import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Box @@ -40,6 +34,7 @@ import kotlinx.coroutines.flow.asStateFlow 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.domain.usecase.validateInitIndex import ly.com.tahaben.showcase_layout_compose.model.* import kotlin.math.PI import kotlin.math.atan2 @@ -92,8 +87,9 @@ fun ShowcaseLayout( cornerRadius: Dp = 16.dp, content: @Composable ShowcaseScope.() -> Unit ) { + val validatedInitIndex = remember(initIndex, greeting) { validateInitIndex(initIndex, greeting) } var currentIndex by remember { - mutableIntStateOf(initIndex) + mutableIntStateOf(validatedInitIndex) } val currentContent by rememberUpdatedState(content) val resetDelay by derivedStateOf { animationDuration.toLong() + INDEX_RESET_DELAY } @@ -109,7 +105,7 @@ fun ShowcaseLayout( Level.DEBUG, TAG + "showcase single item index: ${showcaseItem.value}" ) - currentIndex = showcaseItem.value ?: initIndex + currentIndex = showcaseItem.value ?: validatedInitIndex true } else { false @@ -125,7 +121,7 @@ fun ShowcaseLayout( TAG + "showcase single greeting: ${singleGreeting.value?.text}" ) singleGreetingMsg = singleGreeting.value - currentIndex = 0 + currentIndex = validatedInitIndex true } else { false @@ -135,11 +131,7 @@ fun ShowcaseLayout( BoxWithConstraints(modifier = Modifier.fillMaxSize()) { val coroutineScope = rememberCoroutineScope() - AnimatedVisibility( - isShowcasing || showCasingItem || isSingleGreeting, - enter = fadeIn(), - exit = fadeOut() - ) { + if (isShowcasing || showCasingItem || isSingleGreeting) { val offset by animateOffsetAsState( targetValue = scope.getPositionFor(currentIndex), animationSpec = tween(animationDuration), @@ -166,11 +158,16 @@ fun ShowcaseLayout( val animMsgAlpha = remember { Animatable(0f) } val animArrow = remember { Animatable(0f) } val animArrowHead = remember { Animatable(0f) } + val canvasAlpha = remember { Animatable(0f) } - - /** to animate current arrow line */ + /** to animate canvas alpha and current arrow line */ LaunchedEffect(key1 = currentIndex) { - + if (currentIndex == validatedInitIndex || showCasingItem || isSingleGreeting) { + canvasAlpha.animateTo( + 1f, + animationSpec = tween(durationMillis = animationDuration / 2, easing = FastOutSlowInEasing) + ) + } message = scope.getMessageFor(currentIndex) arrowAnimDuration = message?.arrow?.animationDuration isArrowDelayOver = false @@ -217,11 +214,12 @@ fun ShowcaseLayout( } } } - if (currentIndex == 0) { + if (currentIndex == validatedInitIndex) { if (isSingleGreeting) { message = singleGreetingMsg } message?.let { msg -> + if (msg.msgBackground != null) animMsgAlpha.animateTo(1f, tween(msg.enterAnim.duration)) animMsgTextAlpha.animateTo(1f, tween(msg.enterAnim.duration)) } @@ -234,6 +232,7 @@ fun ShowcaseLayout( when (msg.enterAnim) { is MsgAnimation.FadeInOut -> { val duration = msg.enterAnim.duration + if (msg.msgBackground != null) animMsgAlpha.animateTo(1f, tween(duration)) animMsgTextAlpha.animateTo(1f, tween(duration)) } @@ -289,6 +288,7 @@ fun ShowcaseLayout( is MsgAnimation.FadeInOut -> { val duration = msg.enterAnim.duration animMsgTextAlpha.animateTo(0f, tween(duration)) + if (msg.msgBackground != null) animMsgAlpha.animateTo(0f, tween(duration)) } @@ -299,15 +299,21 @@ fun ShowcaseLayout( } } if (showCasingItem) { + canvasAlpha.animateTo( + 0f, + animationSpec = tween(durationMillis = animationDuration / 2, easing = FastOutSlowInEasing) + ) scope.showcaseItemFinished() - delay(resetDelay) - currentIndex = initIndex + currentIndex = validatedInitIndex return@launch } if (isSingleGreeting) { + canvasAlpha.animateTo( + 0f, + animationSpec = tween(durationMillis = animationDuration / 2, easing = FastOutSlowInEasing) + ) scope.showGreetingFinished() - delay(resetDelay) - currentIndex = initIndex + currentIndex = validatedInitIndex return@launch } if (currentIndex + 1 < scope.getHashMapSize()) { @@ -323,9 +329,13 @@ fun ShowcaseLayout( Level.INFO, TAG + "finished" ) + canvasAlpha.animateTo( + 0f, + animationSpec = tween(durationMillis = animationDuration / 2, easing = FastOutSlowInEasing) + ) onFinish() delay(resetDelay) - currentIndex = initIndex + currentIndex = validatedInitIndex } isArrowDelayOver = false } @@ -341,8 +351,7 @@ fun ShowcaseLayout( if (currentIndex == 0 || isSingleGreeting) { // Draw a full canvas without any cutout for greeting or index 0 drawRect( - color = if (isDarkLayout) Color.White else Color.Black, - alpha = 0.80f, + color = if (isDarkLayout) Color.White.copy(alpha = 0.9f * canvasAlpha.value) else Color.Black.copy(alpha = 0.9f * canvasAlpha.value), size = size ) } else { @@ -373,10 +382,10 @@ fun ShowcaseLayout( /** draw the showcasePath */ drawPath( path = showcasePath, - color = if (isDarkLayout) Color.White else Color.Black, - alpha = 0.80f, + color = if (isDarkLayout) Color.White.copy(alpha = 0.9f * canvasAlpha.value) else Color.Black.copy(alpha = 0.9f * canvasAlpha.value), ) } + TargetShape.CIRCLE -> { // Calculate the center and radius of the circle val centerX = offset.x + itemSize.width / 2 @@ -391,12 +400,14 @@ fun ShowcaseLayout( // Create a path for the target area (circle) val targetPath = Path().apply { - addOval(Rect( - centerX - radius, - centerY - radius, - centerX + radius, - centerY + radius - )) + addOval( + Rect( + centerX - radius, + centerY - radius, + centerX + radius, + centerY + radius + ) + ) } // Create a combined path with a hole @@ -408,10 +419,10 @@ fun ShowcaseLayout( // Draw the path drawPath( path = showcasePath, - color = if (isDarkLayout) Color.White else Color.Black, - alpha = 0.80f, + color = if (isDarkLayout) Color.White.copy(alpha = 0.9f * canvasAlpha.value) else Color.Black.copy(alpha = 0.9f * canvasAlpha.value), ) } + TargetShape.ROUNDED_RECTANGLE -> { // Create paths for the outer and inner areas val outerPath = Path().apply { @@ -492,8 +503,7 @@ fun ShowcaseLayout( // Draw the path drawPath( path = showcasePath, - color = if (isDarkLayout) Color.White else Color.Black, - alpha = 0.80f, + color = if (isDarkLayout) Color.White.copy(alpha = 0.9f * canvasAlpha.value) else Color.Black.copy(alpha = 0.9f * canvasAlpha.value), ) } } @@ -519,7 +529,7 @@ fun ShowcaseLayout( /** Determine if message will be shown on top or below target */ val yOffset = - if (currentIndex == 0) (size.height / 2) else with(density) { + if (currentIndex == 0 || isSingleGreeting) (size.height / 2) else with(density) { val currentItemYPosition = scope.getPositionFor(currentIndex).y val currentItemHeight = scope.getSizeFor(currentIndex).height @@ -573,7 +583,7 @@ 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 (currentIndex == 0 || msg.arrow?.curved == true) { + val xOffset = if (currentIndex == 0 || isSingleGreeting || msg.arrow?.curved == true) { halfWidth - messageWidthHalf } else { val currentItemXPosition = scope.getPositionFor(currentIndex).x @@ -683,6 +693,7 @@ fun ShowcaseLayout( cardOffset.y + cardSize.height ) } + Side.Bottom -> { // Start from top center of the message card moveTo( @@ -690,6 +701,7 @@ fun ShowcaseLayout( cardOffset.y ) } + Side.Left -> { // Start from right center of the message card moveTo( @@ -697,6 +709,7 @@ fun ShowcaseLayout( cardCenterY ) } + Side.Right -> { // Start from left center of the message card moveTo( @@ -704,6 +717,7 @@ fun ShowcaseLayout( cardCenterY ) } + else -> { // Default to bottom center if targetFrom is not specified moveTo( diff --git a/showcase-layout-compose/src/commonMain/kotlin/ly/com/tahaben/showcase_layout_compose/ui/TargetShowcaseLayout.kt b/showcase-layout-compose/src/commonMain/kotlin/ly/com/tahaben/showcase_layout_compose/ui/TargetShowcaseLayout.kt index 57efaf2..f9b8f9a 100644 --- a/showcase-layout-compose/src/commonMain/kotlin/ly/com/tahaben/showcase_layout_compose/ui/TargetShowcaseLayout.kt +++ b/showcase-layout-compose/src/commonMain/kotlin/ly/com/tahaben/showcase_layout_compose/ui/TargetShowcaseLayout.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.unit.dp 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.usecase.validateInitIndex 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.TargetShape @@ -83,8 +84,9 @@ fun TargetShowcaseLayout( animateToNextTarget: Boolean = true, content: @Composable ShowcaseScope.() -> Unit ) { + val validatedInitIndex = remember(initIndex, greeting) { validateInitIndex(initIndex, greeting) } var currentIndex by remember { - mutableIntStateOf(initIndex) + mutableIntStateOf(validatedInitIndex) } val currentContent by rememberUpdatedState(content) val scope = ShowcaseScopeImpl(greeting) @@ -99,7 +101,7 @@ fun TargetShowcaseLayout( Level.DEBUG, TAG + "showcase single item index: ${showcaseItem.value}" ) - currentIndex = showcaseItem.value ?: initIndex + currentIndex = showcaseItem.value ?: validatedInitIndex true } else { false @@ -195,7 +197,7 @@ fun TargetShowcaseLayout( ) } // If this is the first showcase or we're resetting, snap to initial values - if (currentIndex == 0 || currentIndex == initIndex) { + if (currentIndex == 0 || currentIndex == validatedInitIndex) { delay(animationDuration.toLong()) handleMessageEnterAnimation(message, messageTextAlpha, animationDuration) } else { @@ -334,7 +336,7 @@ fun TargetShowcaseLayout( // Finish showcasing the single item scope.showcaseItemFinished() - currentIndex = initIndex + currentIndex = validatedInitIndex } return@detectTapGestures } else if (isSingleGreeting) { @@ -358,7 +360,7 @@ fun TargetShowcaseLayout( // Finish showcasing the greeting scope.showGreetingFinished() - currentIndex = initIndex + currentIndex = validatedInitIndex } return@detectTapGestures } else if (currentIndex + 1 < scope.getHashMapSize()) { @@ -434,7 +436,7 @@ fun TargetShowcaseLayout( delay(shrinkDuration.toLong()) // Reset index and call onFinish - currentIndex = initIndex + currentIndex = validatedInitIndex onFinish() } From 28baa6ba829f64e990f2eda315c6c110d46931c3 Mon Sep 17 00:00:00 2001 From: Tahaak67 Date: Tue, 15 Jul 2025 14:23:50 +0200 Subject: [PATCH 2/6] Update README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 789b5a0..9238c56 100644 --- a/README.md +++ b/README.md @@ -271,8 +271,7 @@ ShowcaseLayout( #### initIndex -the initial value of the counter, set this to 1 if you don't want a greeting message before -showcasing targets. +the initial value of what index will showcase first. #### animationDuration From 93fd188b3020b6a999c4cbb8fc0b31d8320f1fdd Mon Sep 17 00:00:00 2001 From: Tahaak67 Date: Tue, 15 Jul 2025 14:44:41 +0200 Subject: [PATCH 3/6] Fix TargetShowcaseLayout not showing first target when initIndex is > 0 --- .../ui/TargetShowcaseLayout.kt | 42 ++++++++++++++----- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/showcase-layout-compose/src/commonMain/kotlin/ly/com/tahaben/showcase_layout_compose/ui/TargetShowcaseLayout.kt b/showcase-layout-compose/src/commonMain/kotlin/ly/com/tahaben/showcase_layout_compose/ui/TargetShowcaseLayout.kt index f9b8f9a..beb7eef 100644 --- a/showcase-layout-compose/src/commonMain/kotlin/ly/com/tahaben/showcase_layout_compose/ui/TargetShowcaseLayout.kt +++ b/showcase-layout-compose/src/commonMain/kotlin/ly/com/tahaben/showcase_layout_compose/ui/TargetShowcaseLayout.kt @@ -117,7 +117,6 @@ fun TargetShowcaseLayout( TAG + "showcase single greeting: ${singleGreeting.value?.text}" ) singleGreetingMsg = singleGreeting.value - currentIndex = 0 true } else { false @@ -128,8 +127,8 @@ fun TargetShowcaseLayout( BoxWithConstraints(modifier = Modifier.fillMaxSize()) { val coroutineScope = rememberCoroutineScope() if (isShowcasing || showCasingItem || isSingleGreeting) { - val itemSize = scope.getSizeFor(currentIndex) - val offset = scope.getPositionFor(currentIndex) + var itemSize = scope.getSizeFor(currentIndex) + var offset = scope.getPositionFor(currentIndex) val animatedWidth = remember { Animatable(itemSize.width) } val animatedHeight = remember { Animatable(itemSize.height) } @@ -140,10 +139,10 @@ fun TargetShowcaseLayout( val outerAnimatable = remember { Animatable(0.0f) } - val message = if (isSingleGreeting) singleGreetingMsg else scope.getMessageFor(currentIndex) + var message = if (isSingleGreeting) singleGreetingMsg else scope.getMessageFor(currentIndex) val textMeasurer = rememberTextMeasurer() val messageTextAlpha = remember { Animatable(0f) } - val canvasColor = message?.msgBackground ?: Color.Black.copy(alpha = 0.9f) + var canvasColor = message?.msgBackground ?: Color.Black.copy(alpha = 0.9f) val canvasColorAnimated by animateColorAsState( canvasColor, animationSpec = tween(durationMillis = animationDuration, easing = FastOutSlowInEasing) @@ -198,8 +197,23 @@ fun TargetShowcaseLayout( } // If this is the first showcase or we're resetting, snap to initial values if (currentIndex == 0 || currentIndex == validatedInitIndex) { + + canvasAlpha.snapTo(1f) + itemSize = scope.getSizeFor(currentIndex) + offset = scope.getPositionFor(currentIndex) + animatedX.snapTo(offset.x) + animatedY.snapTo(offset.y) + message = scope.getMessageFor(currentIndex) + canvasColor = message?.msgBackground ?: Color.Black.copy(alpha = 0.9f) + println("canvas color: $canvasColor") delay(animationDuration.toLong()) handleMessageEnterAnimation(message, messageTextAlpha, animationDuration) + outerAnimatable.animateTo( + 1f, + animationSpec = tween( + durationMillis = animationDuration, + easing = FastOutSlowInEasing + )) } else { if (animateToNextTarget && !showCasingItem) { outerAnimatable.snapTo(1f) @@ -723,11 +737,19 @@ fun TargetShowcaseLayout( // Draw the donut path with the dimensions that have been adjusted for text // Apply canvasAlpha to make the circle completely disappear during transitions - drawPath( - path = donutPath, - color = canvasColorAnimated.copy(alpha = 0.9f * canvasAlpha.value), - style = Fill // Fill the donut shape - ) + if (currentIndex == validatedInitIndex){ + drawPath( + path = donutPath, + color = canvasColor.copy(alpha = 0.9f * canvasAlpha.value), + style = Fill // Fill the donut shape + ) + }else{ + drawPath( + path = donutPath, + color = canvasColorAnimated.copy(alpha = 0.9f * canvasAlpha.value), + style = Fill // Fill the donut shape + ) + } // Draw the pulsing ring (outside the punch) val pulsePath = Path().apply { From 33344cbe2f093cddc31390a275dfe74fb9778dbe Mon Sep 17 00:00:00 2001 From: Tahaak67 Date: Tue, 15 Jul 2025 14:51:42 +0200 Subject: [PATCH 4/6] Bump library version to 1.0.9 --- .../src/main/kotlin/convention.publication.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/convention-plugins/src/main/kotlin/convention.publication.gradle.kts b/convention-plugins/src/main/kotlin/convention.publication.gradle.kts index 03ca2eb..5d9a9e2 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.8" +val publishVersion: String = "1.0.9" val publishArtifactId: String = "showcase-layout-compose" From 5dda2753efd3d9c5a1da09a46c48552fc9970bab Mon Sep 17 00:00:00 2001 From: Tahaak67 Date: Tue, 15 Jul 2025 14:54:41 +0200 Subject: [PATCH 5/6] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9238c56..05f8182 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![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.6.1-blue)](https://github.com/JetBrains/compose-multiplatform) +[![Compose Multiplatform](https://img.shields.io/badge/Compose%20Multiplatform-v1.8.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) @@ -43,7 +43,7 @@ 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.8") +implementation("ly.com.tahaben:showcase-layout-compose:1.0.9") ``` ## Usage From 0f7fbf6ec30b2c47424bfb6105eb7ede57797c39 Mon Sep 17 00:00:00 2001 From: Tahaak67 Date: Tue, 15 Jul 2025 14:58:03 +0200 Subject: [PATCH 6/6] Update publish.yml --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 7aa3c4e..496241e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -39,7 +39,7 @@ jobs: - name: Publish to MavenCentral - run: ./gradlew publishToSonatype --max-workers 1 closeAndReleaseSonatypeStagingRepository + run: ./gradlew publishAllPublicationsToSonatype2Repository --max-workers 1 env: