Jetpack Compose

Intro showcase view in Jetpack Compose 2022

Intro showcase view in Jetpack Compose
494views

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
)

 

Intro showcase view in Jetpack Compose

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