Slideshow (segmented progress bar)
In this article I will explain how to implement a slideshow with continuously updated progress bar.
Topics covered
- Implementing your own View with dynamic colors.
- Flow.
- Jetpack Compose.
The project
The project I use for the demonstation is Slideshow. It is a copy of the prototype project MyApplication, so it may contain some redundant code and dependencies, which you may want to remove for the sake of YAGNI.
The images
The images come from a Wikipedia article about Michael Jackson. Their respective URLs are hardcoded.
Segmented progress bar
The segmented progress bar View draws three rounded rectangles that corelate with the three images of Michael Jackson.
The problem is that while the subsequent segments are rounded, the edge indicating the continuous progress is straight. To achieve this effect I use clipRect
:
@SuppressLint("DrawAllocation")
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val segmentSize = (width / numberOfSegments)
fun drawSegment(id: Int, paint: Paint) {
canvas.drawRoundRect(
3F + id * segmentSize,
0F,
(id + 1) * segmentSize - 3F,
height.toFloat(),
40F,
40F,
paint,
)
}
for (i in 0 until numberOfSegments) {
drawSegment(i, if (i < progress) progressedPaint else backgroundPaint)
}
if (fraction != null) {
canvas.clipRect(
Rect(
(3F + progress * segmentSize + fraction!! * segmentSize).roundToInt(),
0,
(id + 1) * segmentSize - 3,
height,
)
)
}
drawSegment(progress, progressedPaint)
}
The colors of the progress bar are taken from your wallpaper, if you are using Android 12 or above. Using of dynamics colors is explained in my other article:
private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val progressedPaint = Paint(Paint.ANTI_ALIAS_FLAG)
...
init {
backgroundPaint.color = resources.getColor(com.google.android.material.R.color.material_dynamic_primary20, null)
backgroundPaint.style = Paint.Style.FILL
progressedPaint.color = resources.getColor(com.google.android.material.R.color.material_dynamic_primary80, null)
progressedPaint.style = Paint.Style.FILL
}
Jetpack Compose
This is how I place the view inside Jetpack Compose:
val timer = ...
Column {
repeat(slides.size) {
val slide = slides[it]
AnimatedVisibility(visible = timer == it) {
val fraction = ...
Box(modifier = Modifier.padding(16.dp)) {
Image(
painter = rememberCoilPainter(
request = slide,
),
contentDescription = null,
contentScale = ContentScale.FillWidth,
modifier = Modifier.fillMaxWidth(),
)
key(fraction) {
AndroidView(
factory = { context ->
SegmentedProgressbar(
context,
null,
).apply {
this.fraction = fraction
numberOfSegments = slides.size
progress = it
}
},
modifier = Modifier
.padding(bottom = 24.dp)
.align(Alignment.BottomCenter)
.width(120.dp)
.height(6.dp)
)
}
}
}
}
}
For creation of the SegmentedProgressBar
instance I could use the builder pattern, but using the correct design patterns is not within the scope of this article. Furthermore, I save some allocations by using apply
instead, and this code is being executed several times per second.
The key
function above forces redrawing of the SegmentedProgressBar whenever fraction
changes. Without this, the progress bar would be updated only once every five seconds when timer
updates, and this is not what I want to demonstrate.
I use Column
at the top to limit the height of the contained Box
. Without Column
, the Box
would occupy all available space (the whole screen), but I want the progress bar to be displayed on the top of the pictures. Using of Column
here limits the height of the included content.
Animation
This is the animation:
private val animation = TargetBasedAnimation(tween(5000, easing = LinearEasing), Int.VectorConverter, 0, 1000)
The main parts worth noticing are tween
and LinearEasing
. This assures that the animation will be performed in a linear way.
Because the desired animation is linear, I could just increment the progress by a constant value every so many milliseconds, but I want this to be more universal. You can experiment with replacing tween
and LinearEasing
with other options.
Coroutines
These are the coroutines that measure time:
val timer = remember {
flow {
while (true) {
repeat(slides.size) {
emit(it)
delay(5_000L)
}
}
}
}.collectAsState(initial = 0).value
...
val fraction = remember(it) {
flow {
val beginning = System.nanoTime()
while (true) {
emit(
animation.getValueFromNanos(System.nanoTime() - beginning)
.toFloat() / 1000f
)
delay(10L)
}
}
}.collectAsState(initial = 0f).value
Conclusion
The article shows how to achieve the effect of continuously updated segmented progress bar displayed at the top of a slideshow.
Assumpion is made that the reader is able to download the code from GitHub, and that they understand the basics of View, coroutines and Jetpack Compose.
The code could be further improved by using the builder pattern, and by avoiding the use of magic numbers, but it is not the point of this article.
Perhaps I could use a custom @Composable
instead of a View
, but I haven’t been able to find documentation on that.