
Hi, Android developer in this Intro showcase view in Jetpack Compose example we make an example to show the App feature. As you can see most Android applications add a showcase for the First time App open user. When the user opens see the App menu with a help text. In the Showcase, you can see the title of the menu and the description of the menu.
In this android jetpack example, you can get the full source code of the example. An easy step for adding this flow to your android app. In this, example we add help text on the FlottingAction button, SearchView, Back Button, and profile image View on the screen.
Intro showcase view in Jetpack Compose
So let’s start to make the example follow the below steps.
Step 1:- Start your Android Studio and Create a jetpack to compose the project.
Start your android studio and create a project to select the jetpack activity and finis. after creating a project add the below dependency in your App base Gradle build and syn project.
Dependencies
dependencies { implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.appcompat:appcompat:1.4.1' implementation 'com.google.android.material:material:1.5.0' implementation "androidx.compose.ui:ui:$compose_version" implementation "androidx.compose.material:material:$compose_version" implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1' implementation 'androidx.activity:activity-compose:1.4.0' testImplementation 'junit:junit:4.+' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" }
Step 2: Create a New Kotlin File with the name ShowCaseView.kt
After syn, your project creates a new Kotlin file with the name ShowcaseView to make the UI of Help text Showcase. Follow the below source code to make showcase masque UI.
import androidx.compose.animation.core.* import androidx.compose.foundation.Canvas import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.* import androidx.compose.runtime.snapshots.SnapshotStateMap import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.layout.boundsInRoot import androidx.compose.ui.layout.positionInRoot import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp import kotlinx.coroutines.delay import kotlin.math.absoluteValue import kotlin.math.max @Composable fun ShowCaseView( targets: SnapshotStateMap<String, ShowCaseProperty>, backgroundColor: Color = Color.Blue, onShowCaseCompleted: () -> Unit ) { val uniqueTargets = targets.values.sortedBy { it.index } var currentTargetIndex by remember { mutableStateOf(0) } val currentTarget = if (uniqueTargets.isNotEmpty() && currentTargetIndex < uniqueTargets.size) uniqueTargets[currentTargetIndex] else null currentTarget?.let { TargetContent(target = it, backgroundColor = backgroundColor) { if (++currentTargetIndex >= uniqueTargets.size) { onShowCaseCompleted() } } } } @Composable fun TargetContent( target: ShowCaseProperty, backgroundColor: Color, onShowCaseCompleted: () -> Unit ) { val screenHeight = LocalConfiguration.current.screenHeightDp val targetCords = target.coordinate val topArea = 88.dp val targetRect = targetCords.boundsInRoot() var textCoordinate: LayoutCoordinates? by remember { mutableStateOf(null) } val yOffset = with(LocalDensity.current) { targetCords.positionInRoot().y.toDp() } val maxDimension = max(targetCords.size.width.absoluteValue, targetCords.size.height.absoluteValue) val targetRadius = maxDimension / 2f + 40f val animationSpec = infiniteRepeatable<Float>( animation = tween(2000, easing = FastOutLinearInEasing), repeatMode = RepeatMode.Restart ) var outerOffset by remember { mutableStateOf(Offset(0f, 0f)) } var outerRadius by remember { mutableStateOf(0f) } textCoordinate?.let { textCoordinates -> val textRect = textCoordinates.boundsInRoot() val textHeight = textCoordinates.size.height val isInGutter = topArea > yOffset || yOffset > screenHeight.dp.minus(topArea) outerOffset = getOutCircleCenter( targetRect, textRect, targetRadius, textHeight, isInGutter ) outerRadius = getOuterRadius(textRect, targetRect) + targetRadius } val outerAnimatable = remember { Animatable(0.6f) } val animatables = listOf( remember { Animatable(0f) }, remember { Animatable(0f) } ) LaunchedEffect(target) { outerAnimatable.snapTo(0.6f) outerAnimatable.animateTo( targetValue = 1f, animationSpec = tween( durationMillis = 500, easing = FastOutSlowInEasing ) ) } animatables.forEachIndexed { index, animatable -> LaunchedEffect(animatable) { delay(index + 1000L) animatable.animateTo( targetValue = 1f, animationSpec = animationSpec ) } } val dys = animatables.map { it.value } Box { Canvas( modifier = Modifier .fillMaxSize() .pointerInput(target) { detectTapGestures { tapOffset -> if (targetRect.contains(tapOffset)) { onShowCaseCompleted() } } } .graphicsLayer(alpha = 0.99f) ) { drawCircle( color = backgroundColor, center = outerOffset, radius = outerRadius * outerAnimatable.value, alpha = 0.9f ) dys.forEach { dy -> drawCircle( color = Color.White, radius = maxDimension * dy * 2f, center = targetRect.center, alpha = 1 - dy ) } drawCircle( color = Color.Transparent, radius = targetRadius, center = targetRect.center, blendMode = BlendMode.Clear ) } ShowCaseText( currentTarget = target, boundsInParent = targetRect, targetRadius = targetRadius ) { textCoordinate = it } } }
Step 3: Create a ShowCaseText Kotlin file.
make a new Kotlin file with the name ShowCaseText and design the showing text on the screen.
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import kotlin.math.max import kotlin.math.min import kotlin.math.pow import kotlin.math.sqrt @Composable fun ShowCaseText( currentTarget: ShowCaseProperty, boundsInParent: Rect, targetRadius: Float, onGloballyPositioned: (LayoutCoordinates) -> Unit ) { var txtOffsetY by remember { mutableStateOf(0f) } Column( modifier = Modifier .offset(y = with(LocalDensity.current) { txtOffsetY.toDp() }) .onGloballyPositioned { onGloballyPositioned(it) val textHeight = it.size.height val possibleTop = boundsInParent.center.y - targetRadius - textHeight txtOffsetY = if (possibleTop > 0) { possibleTop } else { boundsInParent.center.y + targetRadius } } .padding(16.dp) ) { Text( text = currentTarget.title, fontSize = 24.sp, color = currentTarget.titleColor, fontWeight = FontWeight.Bold ) Text( text = currentTarget.subTitle, fontSize = 16.sp, color = currentTarget.subTitleColor, fontWeight = FontWeight.Normal ) } } fun getOutCircleCenter( targetBound: Rect, textBound: Rect, targetRadius: Float, textHeight: Int, isInGutter: Boolean ): Offset { val outerCenterX: Float var outerCenterY: Float val onTop = targetBound.center.y - targetRadius - textHeight > 0 val left = min( textBound.left, targetBound.left - targetRadius ) val right = max( textBound.right, targetBound.right + targetRadius ) val centerY = if (onTop) targetBound.center.y - targetRadius - textHeight else targetBound.center.y + targetRadius + textHeight outerCenterY = centerY outerCenterX = (left + right) / 2 if (isInGutter) { outerCenterY = targetBound.center.y } return Offset(outerCenterX, outerCenterY) } fun getOuterRadius( textRect: Rect, targetRect: Rect ) : Float { val topLeftX = min(textRect.topLeft.x, targetRect.topLeft.x) val topLeftY = min(textRect.topLeft.y, targetRect.topLeft.y) val bottomRightX = max(textRect.bottomRight.x, targetRect.bottomRight.x) val bottomRightY = max(textRect.bottomRight.y, targetRect.bottomRight.y) val expandedBounds = Rect(topLeftX, topLeftY, bottomRightX, bottomRightY) val d = sqrt(expandedBounds.height.toDouble().pow(2.0) + expandedBounds.width.toDouble().pow(2.0)).toFloat() return (d / 2f) }
Step 4: Create a new Kotlin file for Property with the Name ShowCaseProperty
data class ShowCaseProperty( val index: Int, val coordinate: LayoutCoordinates, val title: String, val subTitle: String, val titleColor: Color = Color.White, val subTitleColor: Color = Color.White )
Step 5: Final Step to Make UI On MainActivity.kt
import android.os.Bundle import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.Image import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.Email import androidx.compose.material.icons.filled.Search import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.jetpack.showcaseview.ui.theme.ShowCaseViewTheme import com.jetpack.showcaseview.ui.theme.ThemeColor class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { ShowCaseViewTheme { Surface(color = MaterialTheme.colors.background) { MainContent() } } } } } @Composable fun MainContent() { val context = LocalContext.current val targets = remember { mutableStateMapOf<String, ShowCaseProperty>() } Box { Scaffold( modifier = Modifier.fillMaxSize(), topBar = { TopAppBar( title = { }, backgroundColor = Color.Transparent, elevation = 0.dp, navigationIcon = { IconButton( onClick = { /*TODO*/ }, modifier = Modifier .onGloballyPositioned { coordinates -> targets["back"] = ShowCaseProperty( 4, coordinates, "Go back!", "Clicking here to go previse screen!!! " ) } ) { Icon( imageVector = Icons.Filled.ArrowBack, contentDescription = "Back" ) } }, actions = { IconButton( onClick = { /*TODO*/ }, modifier = Modifier .onGloballyPositioned { coordinates -> targets["search"] = ShowCaseProperty( 3, coordinates, "Search View", "clicking here for search anything in App " ) } ) { Icon( imageVector = Icons.Filled.Search, contentDescription = "Search" ) } } ) }, floatingActionButton = { FloatingActionButton( onClick = { /*TODO*/ }, modifier = Modifier .onGloballyPositioned { coordinates -> targets["email"] = ShowCaseProperty( 1, coordinates, "Email", "Click here to send a email" ) }, backgroundColor = ThemeColor, contentColor = Color.White, elevation = FloatingActionButtonDefaults.elevation(6.dp) ) { Icon( imageVector = Icons.Filled.Email, contentDescription = "Email" ) } } ) { Box( modifier = Modifier .fillMaxSize() ) { Box( modifier = Modifier .fillMaxHeight(0.3f) ) { Column( modifier = Modifier .align(Alignment.BottomStart) .fillMaxWidth() .padding(16.dp) .height(90.dp), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { Text( modifier = Modifier.padding(15.dp), text = "Codeplayon", fontWeight = FontWeight.Bold, fontSize = 24.sp, color = ThemeColor ) Text( text = "Jetpack Compose intro ShowCase", fontWeight = FontWeight.Normal, fontSize = 20.sp, color = Color.Black, textAlign = TextAlign.Center ) } Image( painter = painterResource(id = R.drawable.ic_profile_foreground), contentDescription = "Profile", modifier = Modifier .align(Alignment.TopCenter) .clip(CircleShape) .onGloballyPositioned { coordinates -> targets["profile"] = ShowCaseProperty( 0, coordinates, "User Profile", "You can click here to update profile image" ) } ) } } } ShowCaseView(targets = targets) { Toast.makeText(context, "App Intro finished!", Toast.LENGTH_SHORT).show() } } }
Now Finally complete all the steps and run your project and see the Output. Intro showcase view in jetpack compose with example source code.
Read More Tutorial