My toolbar contains pretty large numbers of elements so its performance is especially important for me. Here is my toolbar:
In action it looks like this:
On the GIF you can see performance drop when scroll occur. It's important to node that the GIF is not compressed.
Here is the code of my toolbar layout:
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.annotation.FloatRange
import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.AppBarDefaults
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.material.contentColorFor
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Share
import androidx.compose.material.primarySurface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import me.onebone.toolbar.ui.theme.CollapsingToolbarTheme
class PerformanceTestActivity: ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
CollapsingToolbarTheme {
Surface(color = MaterialTheme.colors.background) {
MyScaffold(
modifier = Modifier.fillMaxSize(),
)
}
}
}
}
}
@Composable
private fun MyScaffold(
modifier: Modifier = Modifier,
) {
MyAppBarScaffold(
modifier = modifier.fillMaxSize(),
) {
val timestamp = System.currentTimeMillis()
LazyColumn {
items(100) {
val timestamp2 = System.currentTimeMillis()
Text(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
text = "I'm item $it"
)
Log.d("perf list item", "list item draw speed = ${System.currentTimeMillis() - timestamp2}")
}
}
Log.d("perf list", "list draw speed = ${System.currentTimeMillis() - timestamp}")
}
}
@Composable
private fun MyAppBarScaffold(
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
) {
val collapsingToolbarScaffoldState = rememberCollapsingToolbarScaffoldState()
CollapsingToolbarScaffold(
modifier = modifier,
state = collapsingToolbarScaffoldState,
scrollStrategy = ScrollStrategy.ExitUntilCollapsed,
toolbarModifier = Modifier.shadow(AppBarDefaults.TopAppBarElevation),
toolbar = {
val timestamp = System.currentTimeMillis()
val progress = collapsingToolbarScaffoldState.toolbarState.progress
// val progress = 1f
Surface(
modifier = Modifier
.height(IntrinsicSize.Min)
// .height(300.dp)
.parallax(0.5f), // TODO: Affects performance
color = MaterialTheme.colors.primarySurface,
elevation = AppBarDefaults.TopAppBarElevation,
) {
MyAppBarContent(
progress = progress,
)
}
MyExpandedAppBar(
modifier = Modifier
.road(Alignment.BottomStart, Alignment.BottomStart), // TODO: Affects performance
progress = progress, // TODO: Affects performance
// progress = 1f,
)
// Collapsing toolbar collapses its size as small as the that of a smallest child (this)
MyCollapsedAppBar(
modifier = Modifier.clickable(onClick = { }), // TODO: Affects performance
progress = progress, // TODO: Affects performance
// progress = 1f,
)
Log.d("perf", "toolbar draw speed = ${System.currentTimeMillis() - timestamp}, progress = $progress")
},
body = content
)
}
@Composable
private fun MyExpandedAppBar(
modifier: Modifier = Modifier,
@FloatRange(from = 0.0, to = 1.0) progress: Float,
) {
Log.d("redraw", "expanded bar redrawing")
MyAppBar(
modifier = modifier,
title = {
Log.d("redraw", "expanded bar title redrawing")
val progressReversed = 1f - progress
Text(
modifier = Modifier.alpha(progressReversed.configureProgress(0.5f)),
text = stringResource(R.string.app_name),
color = MaterialTheme.colors.onPrimary
)
},
actions = {
Log.d("redraw", "expanded bar actions redrawing")
IconButton(
modifier = Modifier.alpha(progress.configureProgress(0.5f)),
onClick = { }
) {
Icon(imageVector = Icons.Filled.Share, contentDescription = null)
}
}
)
}
@Composable
private fun MyCollapsedAppBar(
modifier: Modifier = Modifier,
@FloatRange(from = 0.0, to = 1.0) progress: Float,
) {
Log.d("redraw", "collapsed bar redrawing")
val popupExpanded = remember { mutableStateOf(false) }
val popupOptions = arrayOf("option #1", "option #2")
MyAppBar(
modifier = modifier,
title = {
Log.d("redraw", "collapsed bar title redrawing")
Text(
modifier = Modifier.alpha(progress),
text = "Collapsed app bar",
color = MaterialTheme.colors.onPrimary
)
},
actions = {
Log.d("redraw", "collapsed actions redrawing")
IconButton(onClick = { popupExpanded.value = true }) {
Icon(
imageVector = Icons.Filled.MoreVert,
contentDescription = null
)
}
}
)
}
@Composable
private fun MyAppBarContent(
modifier: Modifier = Modifier,
@FloatRange(from = 0.0, to = 1.0) progress: Float,
) {
Log.d("redraw", "content redrawing")
Box(
modifier = modifier
.fillMaxWidth()
.alpha(progress.configureProgress(0.5f)),
contentAlignment = Alignment.Center
) {
Log.d("redraw", "image redrawing")
Image(
modifier = Modifier.fillMaxSize(),
painter = painterResource(R.drawable.ic_launcher_foreground),
contentDescription = null,
contentScale = ContentScale.Crop
)
Row(
modifier = Modifier
.padding(horizontal = 16.dp, vertical = MaterialAppBarHeight)
.fillMaxSize(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Log.d("redraw", "tile row redrawing")
MyTile(
title = "title #1",
value = "123"
)
MyTile(
title = "title #2",
value = "456"
)
}
}
}
@Composable
private fun MyTile(
modifier: Modifier = Modifier,
title: String,
value: String,
) {
Log.d("redraw", "tile redrawing")
val fontScale = LocalContext.current.resources.configuration.fontScale
Column(
modifier = modifier.height(MyStatisticsTileHeight.times(fontScale)),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
modifier = Modifier.padding(vertical = 8.dp),
text = title
)
Box(
modifier = Modifier
.padding(bottom = 8.dp)
.aspectRatio(1f)
.border(
width = MyStatisticsTileBorderWidth,
color = MaterialTheme.colors.onPrimary,
shape = RoundedCornerShape(8.dp)
)
.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
Text(
modifier = Modifier
.padding(horizontal = MyStatisticsTileBorderWidth.times(2)),
text = value,
textAlign = TextAlign.Center
)
}
}
}
@Composable
private fun MyAppBar(
modifier: Modifier = Modifier,
title: @Composable () -> Unit,
actions: @Composable RowScope.() -> Unit = {},
) {
TopAppBar(
modifier = modifier.height(MaterialAppBarHeight),
title = title,
actions = actions,
backgroundColor = Color.Transparent,
contentColor = contentColorFor(MaterialTheme.colors.primarySurface),
elevation = 0.dp
)
}
private val MyStatisticsTileHeight = 118.dp
private val MyStatisticsTileBorderWidth = 5.dp
private val MaterialAppBarHeight = 56.dp
/**
* Applies configurations on value that represent a progress (e.g. animation)
*
* @param startAt Sets the starting point for the value. The resulting progress will begin
* to increase only when the original progress value reaches passed value.
*/
fun Float.configureProgress(@FloatRange(from = 0.0, to = 1.0) startAt: Float): Float {
val start = (1f - startAt).coerceAtLeast(0f)
val multiplier = 1f / start
return (this - start) * multiplier
}
I added some logs to figure out what parts of UI affected by recomposition. It turned out that they all were affected after any change in the toolbar state. Even if I remove alpha modifier the recomposition process still affect all toolbar composables. I think this behaviour directly contradicts the basic principle of Compose - doing recomposition only if state of composable has changed
In my case there is no need to recompose toolbars (only they titles). So there must a way not to trigger recomposition of all tolbar content
I'm still in research of this problem. But it's important to discuss this problem.
performance