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