Skip to content

Commit d6dddc8

Browse files
committed
Add single target showcase
Fix emoji not showing on web target
1 parent 8a42e2b commit d6dddc8

4 files changed

Lines changed: 187 additions & 13 deletions

File tree

Binary file not shown.

composeApp/src/commonMain/kotlin/App.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import androidx.compose.ui.text.style.TextAlign
2222
import androidx.compose.ui.unit.dp
2323
import kotlinx.coroutines.delay
2424
import kotlinx.coroutines.launch
25+
import ly.com.tahaben.showcase_layout_compose.domain.Level
26+
import ly.com.tahaben.showcase_layout_compose.domain.ShowcaseEventListener
2527
import ly.com.tahaben.showcase_layout_compose.model.*
2628
import ly.com.tahaben.showcase_layout_compose.ui.TargetShowcaseLayout
2729
import org.jetbrains.compose.resources.ExperimentalResourceApi
@@ -95,7 +97,7 @@ fun App(openUrl: (String) -> Boolean, onWebLoadFinish: () -> Unit = {}) {
9597
lineThickness = lineThinckness.dp,
9698
animationDuration = animationDuration,
9799
// circleMode = true
98-
targetShape = TargetShape.ROUNDED_RECTANGLE
100+
targetShape = TargetShape.CIRCLE
99101
) {
100102
Column(modifier = Modifier.fillMaxSize().verticalScroll(scrollState)) {
101103
TopAppBar(title = {
@@ -557,6 +559,11 @@ fun App(openUrl: (String) -> Boolean, onWebLoadFinish: () -> Unit = {}) {
557559
}
558560
finishedSubsequentShowcase = false
559561
}
562+
registerEventListener(object: ShowcaseEventListener {
563+
override fun onEvent(level: Level, event: String) {
564+
println("$level: $event")
565+
}
566+
})
560567
}
561568
}
562569
}
Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,21 @@
1+
import androidx.compose.runtime.LaunchedEffect
12
import androidx.compose.ui.ExperimentalComposeUiApi
3+
import androidx.compose.ui.platform.LocalFontFamilyResolver
4+
import androidx.compose.ui.text.font.FontFamily
5+
import androidx.compose.ui.text.platform.Font
26
import androidx.compose.ui.window.CanvasBasedWindow
7+
import showcase_layout_compose_kmp.composeapp.generated.resources.Res
38

49
@OptIn(ExperimentalComposeUiApi::class)
510
fun main() {
6-
CanvasBasedWindow(canvasElementId = "ComposeTarget") { App(openUrl = UrlLauncherWeb()::openUrl , onWebLoadFinish = ::onLoadFinished) }
11+
CanvasBasedWindow(canvasElementId = "ComposeTarget") {
12+
val fontFamilyResolver = LocalFontFamilyResolver.current
13+
14+
LaunchedEffect(Unit) {
15+
val notoEmojisBytes = Res.readBytes("/font/noto_color_emoji_regular.ttf")
16+
val fontFamily = FontFamily(listOf(Font("NotoColorEmoji", notoEmojisBytes)))
17+
fontFamilyResolver.preload(fontFamily)
18+
}
19+
App(openUrl = UrlLauncherWeb()::openUrl , onWebLoadFinish = ::onLoadFinished)
20+
}
721
}

showcase-layout-compose/src/commonMain/kotlin/ly/com/tahaben/showcase_layout_compose/ui/TargetShowcaseLayout.kt

Lines changed: 164 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,24 @@ fun TargetShowcaseLayout(
8888
}
8989
}
9090
}
91+
val singleGreeting = scope.greetingActionFlow.collectAsState()
92+
val isSingleGreeting by remember {
93+
derivedStateOf {
94+
if (singleGreeting.value != null) {
95+
scope.showcaseEventListener?.onEvent(
96+
Level.DEBUG,
97+
TAG + "showcase single greeting: ${singleGreeting.value?.text}"
98+
)
99+
singleGreetingMsg = singleGreeting.value
100+
currentIndex = 0
101+
true
102+
} else {
103+
false
104+
}
105+
}
106+
}
91107
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
92-
if (isShowcasing) {
108+
if (isShowcasing || showCasingItem || isSingleGreeting) {
93109
val itemSize = scope.getSizeFor(currentIndex)
94110
val offset = scope.getPositionFor(currentIndex)
95111
val coroutineScope = rememberCoroutineScope()
@@ -106,10 +122,10 @@ fun TargetShowcaseLayout(
106122
val outerAlphaAnimatable = remember(currentIndex) { Animatable(0f) }
107123

108124
// Animation for message text opacity to create smooth transitions
109-
val messageTextAlpha = remember { Animatable(1f) }
125+
val messageTextAlpha = remember { Animatable(0f) }
110126

111127
// Animation for overall canvas alpha to make the circle completely disappear
112-
val canvasAlpha = remember { Animatable(1f) }
128+
val canvasAlpha = remember { Animatable(0f) }
113129

114130
LaunchedEffect(currentIndex) {
115131
outerAnimatable.snapTo(0.6f)
@@ -193,9 +209,12 @@ fun TargetShowcaseLayout(
193209
val prevY = animatedY.value
194210
val prevWidth = animatedWidth.value
195211
val prevHeight = animatedHeight.value
196-
212+
if(currentIndex == 0 || isSingleGreeting){
213+
//canvasAlpha.snapTo(0f)
214+
canvasAlpha.animateTo(1f, animationSpec = tween(durationMillis = animationDuration / 2, easing = FastOutSlowInEasing))
215+
}
197216
// If this is the first showcase or we're resetting, snap to initial values
198-
if (currentIndex == 1 || currentIndex == initIndex) {
217+
if (currentIndex == 0 || currentIndex == initIndex) {
199218
animatedX.snapTo(offset.x)
200219
animatedY.snapTo(offset.y)
201220
animatedHeight.snapTo(0f)
@@ -207,7 +226,16 @@ fun TargetShowcaseLayout(
207226
launch {
208227
animatedWidth.animateTo(itemSize.width)
209228
}
210-
} else if(currentIndex == scope.getHashMapSize()){
229+
delay(animationDuration.toLong())
230+
messageTextAlpha.animateTo(
231+
1f,
232+
animationSpec = tween(
233+
durationMillis = animationDuration / 2,
234+
easing = FastOutSlowInEasing
235+
)
236+
)
237+
}
238+
else if(currentIndex == scope.getHashMapSize()){
211239
// last index
212240
messageTextAlpha.animateTo(0f, animationSpec = tween(durationMillis = animationDuration / 2, easing = FastOutSlowInEasing))
213241
canvasAlpha.animateTo(0f)
@@ -351,13 +379,12 @@ fun TargetShowcaseLayout(
351379
}
352380
}
353381
LaunchedEffect(isShowcasing){
354-
if (isShowcasing && currentIndex == 0){
355-
currentIndex = 1
382+
if (isShowcasing && currentIndex != 0 && !isSingleGreeting){
356383
pulseAlpha.snapTo(0.6f)
357384
pulseRadius.snapTo(0f)
358385
}
359386
}
360-
val message = scope.getMessageFor(currentIndex)
387+
val message = if(isSingleGreeting) singleGreetingMsg else scope.getMessageFor(currentIndex)
361388
val textMeasurer = rememberTextMeasurer()
362389

363390
Canvas(
@@ -369,7 +396,72 @@ fun TargetShowcaseLayout(
369396
Level.VERBOSE,
370397
TAG + "tapped here $it"
371398
)
372-
if (currentIndex + 1 < scope.getHashMapSize()) {
399+
if (showCasingItem) {
400+
coroutineScope.launch {
401+
// Fade out the message text
402+
messageTextAlpha.animateTo(
403+
0f,
404+
animationSpec = tween(
405+
durationMillis = animationDuration / 3,
406+
easing = FastOutSlowInEasing
407+
)
408+
)
409+
410+
// Fade out the entire canvas to make the circle completely disappear
411+
launch {
412+
canvasAlpha.animateTo(
413+
0f,
414+
animationSpec = tween(
415+
durationMillis = animationDuration / 3,
416+
easing = FastOutSlowInEasing
417+
)
418+
)
419+
}
420+
421+
// Wait for animations to complete
422+
delay((animationDuration / 3).toLong())
423+
424+
// Finish showcasing the single item
425+
scope.showcaseItemFinished()
426+
427+
currentIndex = initIndex
428+
println("Showcase index reset to $currentIndex")
429+
}
430+
return@detectTapGestures
431+
}
432+
else if (isSingleGreeting) {
433+
coroutineScope.launch {
434+
// Fade out the message text
435+
messageTextAlpha.animateTo(
436+
0f,
437+
animationSpec = tween(
438+
durationMillis = animationDuration / 3,
439+
easing = FastOutSlowInEasing
440+
)
441+
)
442+
443+
// Fade out the entire canvas to make the circle completely disappear
444+
launch {
445+
canvasAlpha.animateTo(
446+
0f,
447+
animationSpec = tween(
448+
durationMillis = animationDuration / 3,
449+
easing = FastOutSlowInEasing
450+
)
451+
)
452+
}
453+
454+
// Wait for animations to complete
455+
delay((animationDuration / 3).toLong())
456+
457+
// Finish showcasing the greeting
458+
scope.showGreetingFinished()
459+
460+
currentIndex = initIndex
461+
}
462+
return@detectTapGestures
463+
}
464+
else if (currentIndex + 1 < scope.getHashMapSize()) {
373465
if (!animateToNextTarget){
374466
// Shrink at current location, then move to new location, then expand
375467
// Step 1: Shrink at current location
@@ -535,6 +627,67 @@ fun TargetShowcaseLayout(
535627
}
536628
}
537629
) {
630+
if (isSingleGreeting || currentIndex == 0){
631+
// For greeting, fill the entire screen with a solid color
632+
drawRect(
633+
color = if (isDarkLayout) Color.White.copy(alpha = 0.9f) else Color.Black.copy(alpha = 0.9f),
634+
size = size,
635+
alpha = canvasAlpha.value
636+
)
637+
638+
// Display the greeting message in the middle of the screen
639+
message?.let { msg ->
640+
// Measure text with appropriate constraints to ensure it wraps if needed
641+
val maxTextWidth = max(1, (size.width * 0.8f).toInt()) // Use 80% of screen width
642+
val textResult = textMeasurer.measure(
643+
msg.text,
644+
style = msg.textStyle,
645+
overflow = TextOverflow.Visible,
646+
constraints = Constraints(0, maxTextWidth)
647+
)
648+
649+
// Center the text on the screen
650+
val textX = (size.width - textResult.size.width) / 2
651+
val textY = (size.height - textResult.size.height) / 2
652+
653+
// Draw a background for the text with padding
654+
val bgPadding = 40f
655+
val bgRect = Rect(
656+
left = textX - bgPadding,
657+
top = textY - bgPadding,
658+
right = textX + textResult.size.width + bgPadding,
659+
bottom = textY + textResult.size.height + bgPadding
660+
)
661+
662+
// Draw the text background
663+
if (msg.roundedCorner == 0.dp) {
664+
drawRect(
665+
color = msg.msgBackground ?: Color.Transparent,
666+
topLeft = Offset(bgRect.left, bgRect.top),
667+
size = Size(bgRect.width, bgRect.height),
668+
alpha = messageTextAlpha.value
669+
)
670+
} else {
671+
drawRoundRect(
672+
color = msg.msgBackground ?: Color.Transparent,
673+
topLeft = Offset(bgRect.left, bgRect.top),
674+
size = Size(bgRect.width, bgRect.height),
675+
cornerRadius = CornerRadius(msg.roundedCorner.value),
676+
alpha = messageTextAlpha.value
677+
)
678+
}
679+
680+
// Draw the text
681+
drawText(
682+
textResult,
683+
topLeft = Offset(textX, textY),
684+
alpha = messageTextAlpha.value
685+
)
686+
}
687+
688+
// Return early to avoid drawing the target shape
689+
return@Canvas
690+
}
538691
// Calculate the radius for the target shape
539692
// For a circle, this is the actual radius
540693
// For a rectangle, we'll use the actual width and height
@@ -801,7 +954,7 @@ fun TargetShowcaseLayout(
801954
style = Fill // Fill the donut shape
802955
)
803956

804-
// Draw the pulsing ring (outside the hole)
957+
// Draw the pulsing ring (outside the punch)
805958
val pulsePath = Path().apply {
806959
op(
807960
Path().apply {

0 commit comments

Comments
 (0)