Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:

- name: Publish to MavenCentral

run: ./gradlew publishToSonatype --max-workers 1 closeAndReleaseSonatypeStagingRepository
run: ./gradlew publishAllPublicationsToSonatype2Repository --max-workers 1


env:
Expand Down
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"


Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 }
Expand All @@ -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
Expand All @@ -125,7 +121,7 @@ fun ShowcaseLayout(
TAG + "showcase single greeting: ${singleGreeting.value?.text}"
)
singleGreetingMsg = singleGreeting.value
currentIndex = 0
currentIndex = validatedInitIndex
true
} else {
false
Expand All @@ -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),
Expand All @@ -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
Expand Down Expand Up @@ -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))
}
Expand All @@ -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))
}
Expand Down Expand Up @@ -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))
}

Expand All @@ -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()) {
Expand All @@ -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
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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),
)
}
}
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -683,27 +693,31 @@ fun ShowcaseLayout(
cardOffset.y + cardSize.height
)
}

Side.Bottom -> {
// Start from top center of the message card
moveTo(
cardCenterX,
cardOffset.y
)
}

Side.Left -> {
// Start from right center of the message card
moveTo(
cardOffset.x + cardSize.width,
cardCenterY
)
}

Side.Right -> {
// Start from left center of the message card
moveTo(
cardOffset.x,
cardCenterY
)
}

else -> {
// Default to bottom center if targetFrom is not specified
moveTo(
Expand Down
Loading