In this bonus part, we can manage our shimmer loading effect animation according to our actions. I will add two buttons for:

  1. Start/Stop shimmer animation
  2. Display light/dark modes

First of all, I would like to start with updating our “.shimmerLoadingAnimation()” extension function. We need to have two parameters; one is for start/stop, another is for light/dark modes:

import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color

fun Modifier.shimmerLoadingAnimation(
isLoadingCompleted: Boolean = true, // <-- New parameter for start/stop.
isLightModeActive: Boolean = true, // <-- New parameter for display modes.
widthOfShadowBrush: Int = 500,
angleOfAxisY: Float = 270f,
durationMillis: Int = 1000,
): Modifier {
if (isLoadingCompleted) { // <-- Step 1.
return this
}
else {
return composed {

// Step 2.
val shimmerColors = ShimmerAnimationData(isLightMode = isLightModeActive).getColours()

val transition = rememberInfiniteTransition(label = "")

val translateAnimation = transition.animateFloat(
initialValue = 0f,
targetValue = (durationMillis + widthOfShadowBrush).toFloat(),
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = durationMillis,
easing = LinearEasing,
),
repeatMode = RepeatMode.Restart,
),
label = "Shimmer loading animation",
)

this.background(
brush = Brush.linearGradient(
colors = shimmerColors,
start = Offset(x = translateAnimation.value - widthOfShadowBrush, y = 0.0f),
end = Offset(x = translateAnimation.value, y = angleOfAxisY),
),
)
}
}
}

data class ShimmerAnimationData(
private val isLightMode: Boolean
) {
fun getColours(): List<Color> {
return if (isLightMode) {
val color = Color.White

listOf(
color.copy(alpha = 0.3f),
color.copy(alpha = 0.5f),
color.copy(alpha = 1.0f),
color.copy(alpha = 0.5f),
color.copy(alpha = 0.3f),
)
} else {
val color = Color.Black

listOf(
color.copy(alpha = 0.0f),
color.copy(alpha = 0.3f),
color.copy(alpha = 0.5f),
color.copy(alpha = 0.3f),
color.copy(alpha = 0.0f),
)
}
}
}

As you can see, the new version of our function includes two new parameters:

  1. isLoadingCompleted: To manage start/stop for animation. If it is false, it returns just itself of Modifier. So, we have if(isLoadingCompleted) { return this } else { … } — Step 1.
  2. isLightModeActive: To manage display mode for animation. Basically, I would like to change the colour list according to it’s value; ShimmerAnimationData(isLightMode = isLightModeActive).getColours() — Step 2.

Now, we can focus on HomeScreen.kt file. Firstly, we need to handle the state of animation status and display mode. So, let’s create two variables in HomeScreen function:

@Composable
fun HomeScreen() {

var isLoadingCompleted by remember { mutableStateOf(true) } // <-- Here.
var isLightModeActive by remember { mutableStateOf(true) } // <-- Here.

Box(
modifier = Modifier
.fillMaxSize()
// Do not forget to update backgroud according to display mode!
.background(color = if (isLightModeActive) Color.White else Color.Black)
.border(border = BorderStroke(width = 4.dp, color = Color.Black))
.padding(48.dp)
) {
...
}

As you know, we need to use remember delegation. Otherwise, recomposition will reset our variables with the initial value. That means isLoadingCompleted and isLightModeActive will always be true.

In addition, do not forget to update the background according to the display mode. Otherwise, it always has a white background.

Now, we can pass them into our components:

@Composable
fun HomeScreen() {

var isLoadingCompleted by remember { mutableStateOf(true) }
var isLightModeActive by remember { mutableStateOf(true) }

Box(
modifier = Modifier
.fillMaxSize()
.background(color = if (isLightModeActive) Color.White else Color.Black)
.border(border = BorderStroke(width = 4.dp, color = Color.Black))
.padding(48.dp)
) {
Column(
modifier = Modifier.align(alignment = Alignment.TopCenter)
) {

Column {
ComponentRectangle(isLoadingCompleted, isLightModeActive)
Spacer(modifier = Modifier.padding(8.dp))
ComponentRectangleLineLong(isLoadingCompleted, isLightModeActive)
Spacer(modifier = Modifier.padding(4.dp))
ComponentRectangleLineShort(isLoadingCompleted, isLightModeActive)
}

Spacer(modifier = Modifier.padding(24.dp))

Row {
ComponentCircle(isLoadingCompleted, isLightModeActive)
Spacer(modifier = Modifier.padding(4.dp))
Column {
Spacer(modifier = Modifier.padding(8.dp))
ComponentRectangleLineLong(isLoadingCompleted, isLightModeActive)
Spacer(modifier = Modifier.padding(4.dp))
ComponentRectangleLineShort(isLoadingCompleted, isLightModeActive)
}
}
Spacer(modifier = Modifier.padding(24.dp))

Row {
ComponentSquare(isLoadingCompleted, isLightModeActive)
Spacer(modifier = Modifier.padding(4.dp))
Column {
Spacer(modifier = Modifier.padding(8.dp))
ComponentRectangleLineLong(isLoadingCompleted, isLightModeActive)
Spacer(modifier = Modifier.padding(4.dp))
ComponentRectangleLineShort(isLoadingCompleted, isLightModeActive,)
}
}
}
}
}

@Composable
fun ComponentCircle(
isLoadingCompleted: Boolean,
isLightModeActive: Boolean,
) {
Box(
modifier = Modifier
.background(color = Color.LightGray, shape = CircleShape)
.size(100.dp)
.shimmerLoadingAnimation(isLoadingCompleted, isLightModeActive)
)
}

@Composable
fun ComponentSquare(
isLoadingCompleted: Boolean,
isLightModeActive: Boolean,
) {
Box(
modifier = Modifier
.clip(shape = RoundedCornerShape(24.dp))
.background(color = Color.LightGray)
.size(100.dp)
.shimmerLoadingAnimation(isLoadingCompleted, isLightModeActive)
)
}

@Composable
fun ComponentRectangle(
isLoadingCompleted: Boolean,
isLightModeActive: Boolean,
) {
Box(
modifier = Modifier
.clip(shape = RoundedCornerShape(24.dp))
.background(color = Color.LightGray)
.height(200.dp)
.fillMaxWidth()
.shimmerLoadingAnimation(isLoadingCompleted, isLightModeActive)
)
}

@Composable
fun ComponentRectangleLineLong(
isLoadingCompleted: Boolean,
isLightModeActive: Boolean,
) {
Box(
modifier = Modifier
.clip(shape = RoundedCornerShape(8.dp))
.background(color = Color.LightGray)
.size(height = 30.dp, width = 200.dp)
.shimmerLoadingAnimation(isLoadingCompleted, isLightModeActive)
)
}

@Composable
fun ComponentRectangleLineShort(
isLoadingCompleted: Boolean,
isLightModeActive: Boolean,
) {
Box(
modifier = Modifier
.clip(shape = RoundedCornerShape(8.dp))
.background(color = Color.LightGray)
.size(height = 30.dp, width = 100.dp)
.shimmerLoadingAnimation(isLoadingCompleted, isLightModeActive)
)
}

At this moment, only one part is missing which are buttons to start/stop animation and change display mode. Let’s create these buttons. First is start/stop animation button:

@Composable
fun ContentLoadingButton(
modifier: Modifier,
onClick: () -> Unit, // <-- Here.
isLoadingCompleted: Boolean,
isLightMode: Boolean,
) {
var isStartMode by remember { mutableStateOf(true) }

Button(
modifier = modifier,
onClick = {
isStartMode = !isStartMode
onClick() // <-- Here.
},
colors = ButtonDefaults.buttonColors(
containerColor =
if (isLightMode) {
if (isStartMode) Color.Blue else Color.Red
} else {
if (isStartMode) Color.Green else Color.Red
}
)
) {
Text(
text = if (isLoadingCompleted) {
"Start Shimmer Loading Animation ▶\uFE0F"
} else {
"Stop Shimmer Loading Animation ⏹\uFE0F"
},
color = if (isLightMode) {
Color.White
} else {
if (isStartMode) Color.Black else Color.White
}
)
}
}

When you click the button, onClick() function parameter will run. We did not define it yet. Otherwise, it has just isStartMode as a local state to manage its own UI parts such as text and colour.

Start/Stop shimmer loading effect animation button.

Secondly, we can create a display mode button:

@Composable
fun ContentModeButton(
modifier: Modifier,
onClick: () -> Unit, // <-- Here.
isLightMode: Boolean,
) {
Button(
modifier = modifier,
onClick = {
onClick() // <-- Here.
},
colors = ButtonDefaults.buttonColors(
containerColor = if (isLightMode) Color.Blue else Color.Green
)
) {
Text(
text = if (isLightMode) {
"Display Dark Mode \uD83C\uDF19"
} else {
"Display Light Mode ☀\uFE0F"
},
color = if (isLightMode) Color.White else Color.Black
)
}
}

It has a similar logic as the start/stop button, for instance:

Display mode button and its effects.

Now, we can move to use them in our HomeScreen function:

@Composable
fun HomeScreen() {

var isLoadingCompleted by remember { mutableStateOf(true) }
var isLightModeActive by remember { mutableStateOf(true) }

Box(
modifier = Modifier
.fillMaxSize()
.background(color = if (isLightModeActive) Color.White else Color.Black)
.border(border = BorderStroke(width = 4.dp, color = Color.Black))
.padding(48.dp)
) {
Column(
modifier = Modifier.align(alignment = Alignment.TopCenter)
) {

Column {
ComponentRectangle(isLoadingCompleted, isLightModeActive)
Spacer(modifier = Modifier.padding(8.dp))
ComponentRectangleLineLong(isLoadingCompleted, isLightModeActive)
Spacer(modifier = Modifier.padding(4.dp))
ComponentRectangleLineShort(isLoadingCompleted, isLightModeActive)
}

Spacer(modifier = Modifier.padding(24.dp))

Row {
ComponentCircle(isLoadingCompleted, isLightModeActive)
Spacer(modifier = Modifier.padding(4.dp))
Column {
Spacer(modifier = Modifier.padding(8.dp))
ComponentRectangleLineLong(isLoadingCompleted, isLightModeActive)
Spacer(modifier = Modifier.padding(4.dp))
ComponentRectangleLineShort(isLoadingCompleted, isLightModeActive)
}
}
Spacer(modifier = Modifier.padding(24.dp))

Row {
ComponentSquare(isLoadingCompleted, isLightModeActive)
Spacer(modifier = Modifier.padding(4.dp))
Column {
Spacer(modifier = Modifier.padding(8.dp))
ComponentRectangleLineLong(isLoadingCompleted, isLightModeActive)
Spacer(modifier = Modifier.padding(4.dp))
ComponentRectangleLineShort(isLoadingCompleted, isLightModeActive,)
}
}
}

// New part starts for two buttons.
Column(
modifier = Modifier.align(alignment = Alignment.BottomCenter),
) {

ContentLoadingButton(
modifier = Modifier.align(alignment = Alignment.CenterHorizontally),
isLightMode = isLightModeActive,
isLoadingCompleted = isLoadingCompleted,
onClick = {
isLoadingCompleted = !isLoadingCompleted
}
)

Spacer(modifier = Modifier.padding(8.dp))

ContentModeButton(
modifier = Modifier.align(alignment = Alignment.CenterHorizontally),
onClick = {
isLightModeActive = !isLightModeActive
},
isLightMode = isLightModeActive
)
}
// New part ends for two buttons.
}
}

By adding a code block between “New part starts for two buttons” and “New part ends for two buttons”, you will be able to see the two buttons and manage all logic, like below:

Full functionality of the project.

Congratulations! You completed: Shimmer Loading Effect Animation with Jetpack Compose 👏

You can reach me on social media and other platforms, stay tuned: 🤝

Thanks! ☕️

Source link