ChapterDownloadIndicator: Optimize further and reimplement error state (#7599)

In the context of a weaker device--remembering objects inside a list item
is expensive. So only do it when we really need to.

This also flattens the download button by drawing a single icon instead of using
separate icon and progress indicator.
This commit is contained in:
Ivan Iskandar 2022-07-24 21:27:00 +07:00 committed by GitHub
parent 6f94777530
commit aeffb5eeb8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 180 additions and 100 deletions

View File

@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDownward
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.ErrorOutline
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenuItem
@ -23,7 +24,9 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
@ -45,121 +48,186 @@ fun ChapterDownloadIndicator(
downloadProgressProvider: () -> Int,
onClick: (ChapterDownloadAction) -> Unit,
) {
val downloadState = downloadStateProvider()
val isDownloaded = downloadState == Download.State.DOWNLOADED
val isDownloading = downloadState != Download.State.NOT_DOWNLOADED
var isMenuExpanded by remember(downloadState) { mutableStateOf(false) }
when (val downloadState = downloadStateProvider()) {
Download.State.NOT_DOWNLOADED -> NotDownloadedIndicator(modifier = modifier, onClick = onClick)
Download.State.QUEUE, Download.State.DOWNLOADING -> DownloadingIndicator(
modifier = modifier,
downloadState = downloadState,
downloadProgressProvider = downloadProgressProvider,
onClick = onClick,
)
Download.State.DOWNLOADED -> DownloadedIndicator(modifier = modifier, onClick = onClick)
Download.State.ERROR -> ErrorIndicator(modifier = modifier, onClick = onClick)
}
}
@Composable
private fun NotDownloadedIndicator(
modifier: Modifier = Modifier,
onClick: (ChapterDownloadAction) -> Unit,
) {
Box(
modifier = modifier
.size(IconButtonTokens.StateLayerSize)
.combinedClickable(
onLongClick = {
val chapterDownloadAction = when {
isDownloaded -> ChapterDownloadAction.DELETE
isDownloading -> ChapterDownloadAction.CANCEL
else -> ChapterDownloadAction.START_NOW
}
onClick(chapterDownloadAction)
},
onClick = {
if (isDownloaded || isDownloading) {
isMenuExpanded = true
} else {
onClick(ChapterDownloadAction.START)
}
},
role = Role.Button,
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(
bounded = false,
radius = IconButtonTokens.StateLayerSize / 2,
),
.commonClickable(
onLongClick = { onClick(ChapterDownloadAction.START_NOW) },
onClick = { onClick(ChapterDownloadAction.START) },
)
.secondaryItemAlpha(),
contentAlignment = Alignment.Center,
) {
Icon(
painter = painterResource(id = R.drawable.ic_download_chapter_24dp),
contentDescription = null,
modifier = Modifier.size(IndicatorSize),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
@Composable
private fun DownloadingIndicator(
modifier: Modifier = Modifier,
downloadState: Download.State,
downloadProgressProvider: () -> Int,
onClick: (ChapterDownloadAction) -> Unit,
) {
var isMenuExpanded by remember { mutableStateOf(false) }
Box(
modifier = modifier
.size(IconButtonTokens.StateLayerSize)
.commonClickable(
onLongClick = { onClick(ChapterDownloadAction.CANCEL) },
onClick = { isMenuExpanded = true },
),
contentAlignment = Alignment.Center,
) {
if (isDownloaded) {
Icon(
imageVector = Icons.Default.CheckCircle,
contentDescription = null,
modifier = Modifier.size(IndicatorSize),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
val arrowColor: Color
val strokeColor = MaterialTheme.colorScheme.onSurfaceVariant
val downloadProgress = downloadProgressProvider()
val indeterminate = downloadState == Download.State.QUEUE ||
(downloadState == Download.State.DOWNLOADING && downloadProgress == 0)
if (indeterminate) {
arrowColor = strokeColor
CircularProgressIndicator(
modifier = IndicatorModifier,
color = strokeColor,
strokeWidth = IndicatorStrokeWidth,
)
DropdownMenu(expanded = isMenuExpanded, onDismissRequest = { isMenuExpanded = false }) {
DropdownMenuItem(
text = { Text(text = stringResource(R.string.action_delete)) },
onClick = {
onClick(ChapterDownloadAction.DELETE)
isMenuExpanded = false
},
)
}
} else {
val inactiveAlphaModifier = if (!isDownloading) Modifier.secondaryItemAlpha() else Modifier
val arrowColor: Color
val strokeColor = MaterialTheme.colorScheme.onSurfaceVariant
if (isDownloading) {
val downloadProgress = downloadProgressProvider()
val indeterminate = downloadState == Download.State.QUEUE ||
(downloadState == Download.State.DOWNLOADING && downloadProgress == 0)
if (indeterminate) {
arrowColor = strokeColor
CircularProgressIndicator(
modifier = IndicatorModifier,
color = strokeColor,
strokeWidth = IndicatorStrokeWidth,
)
} else {
val animatedProgress by animateFloatAsState(
targetValue = downloadProgress / 100f,
animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec,
)
arrowColor = if (animatedProgress < 0.5f) {
strokeColor
} else {
MaterialTheme.colorScheme.background
}
CircularProgressIndicator(
progress = animatedProgress,
modifier = IndicatorModifier,
color = strokeColor,
strokeWidth = IndicatorSize / 2,
)
}
DropdownMenu(expanded = isMenuExpanded, onDismissRequest = { isMenuExpanded = false }) {
DropdownMenuItem(
text = { Text(text = stringResource(R.string.action_start_downloading_now)) },
onClick = {
onClick(ChapterDownloadAction.START_NOW)
isMenuExpanded = false
},
)
DropdownMenuItem(
text = { Text(text = stringResource(R.string.action_cancel)) },
onClick = {
onClick(ChapterDownloadAction.CANCEL)
isMenuExpanded = false
},
)
}
val animatedProgress by animateFloatAsState(
targetValue = downloadProgress / 100f,
animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec,
)
arrowColor = if (animatedProgress < 0.5f) {
strokeColor
} else {
arrowColor = strokeColor
CircularProgressIndicator(
progress = 1f,
modifier = IndicatorModifier.then(inactiveAlphaModifier),
color = strokeColor,
strokeWidth = IndicatorStrokeWidth,
)
MaterialTheme.colorScheme.background
}
Icon(
imageVector = Icons.Default.ArrowDownward,
contentDescription = null,
modifier = ArrowModifier.then(inactiveAlphaModifier),
tint = arrowColor,
CircularProgressIndicator(
progress = animatedProgress,
modifier = IndicatorModifier,
color = strokeColor,
strokeWidth = IndicatorSize / 2,
)
}
DropdownMenu(expanded = isMenuExpanded, onDismissRequest = { isMenuExpanded = false }) {
DropdownMenuItem(
text = { Text(text = stringResource(R.string.action_start_downloading_now)) },
onClick = {
onClick(ChapterDownloadAction.START_NOW)
isMenuExpanded = false
},
)
DropdownMenuItem(
text = { Text(text = stringResource(R.string.action_cancel)) },
onClick = {
onClick(ChapterDownloadAction.CANCEL)
isMenuExpanded = false
},
)
}
Icon(
imageVector = Icons.Default.ArrowDownward,
contentDescription = null,
modifier = ArrowModifier,
tint = arrowColor,
)
}
}
@Composable
private fun DownloadedIndicator(
modifier: Modifier = Modifier,
onClick: (ChapterDownloadAction) -> Unit,
) {
var isMenuExpanded by remember { mutableStateOf(false) }
Box(
modifier = modifier
.size(IconButtonTokens.StateLayerSize)
.commonClickable(
onLongClick = { onClick(ChapterDownloadAction.DELETE) },
onClick = { isMenuExpanded = true },
),
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = Icons.Default.CheckCircle,
contentDescription = null,
modifier = Modifier.size(IndicatorSize),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
DropdownMenu(expanded = isMenuExpanded, onDismissRequest = { isMenuExpanded = false }) {
DropdownMenuItem(
text = { Text(text = stringResource(R.string.action_delete)) },
onClick = {
onClick(ChapterDownloadAction.DELETE)
isMenuExpanded = false
},
)
}
}
}
@Composable
private fun ErrorIndicator(
modifier: Modifier = Modifier,
onClick: (ChapterDownloadAction) -> Unit,
) {
Box(
modifier = modifier
.size(IconButtonTokens.StateLayerSize)
.commonClickable(
onLongClick = { onClick(ChapterDownloadAction.START) },
onClick = { onClick(ChapterDownloadAction.START) },
),
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = Icons.Default.ErrorOutline,
contentDescription = null,
modifier = Modifier.size(IndicatorSize),
tint = MaterialTheme.colorScheme.error,
)
}
}
private fun Modifier.commonClickable(
onLongClick: () -> Unit,
onClick: () -> Unit,
) = composed {
this.combinedClickable(
onLongClick = onLongClick,
onClick = onClick,
role = Role.Button,
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(
bounded = false,
radius = IconButtonTokens.StateLayerSize / 2,
),
)
}
private val IndicatorSize = 26.dp
private val IndicatorPadding = 2.dp

View File

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M11.99,2C6.47,2 2,6.48 2,12C2,17.52 6.47,22 11.99,22C17.52,22 22,17.52 22,12C22,6.48 17.52,2 11.99,2zM12,4C16.42,4 20,7.58 20,12C20,16.42 16.42,20 12,20C7.58,20 4,16.42 4,12C4,7.58 7.58,4 12,4z"
android:fillColor="#000000"/>
<path
android:pathData="M18.041,12 L16.976,10.935 12.755,15.149L12.755,5.959L11.245,5.959L11.245,15.149L7.031,10.928 5.959,12l6.041,6.041z"
android:fillColor="#000000"/>
</vector>