Move app icon composables outside of mutable Fragment class.

This way, the composables do not receive an implicit mutable parameter, which allows the compiler to mark them as skippable.
This commit is contained in:
Nicholas Tinsley 2023-05-19 10:57:07 -04:00 committed by Greyson Parrelli
parent d7c06fff50
commit 131f9c4bc9

View file

@ -87,235 +87,233 @@ class AppIconSelectionFragment : ComposeFragment() {
findNavController().safeNavigate(R.id.action_appIconSelectionFragment_to_appIconTutorialFragment) findNavController().safeNavigate(R.id.action_appIconSelectionFragment_to_appIconTutorialFragment)
} }
/**
* Screen allowing the user to view all the possible icon and select a new one to use.
*/
@Composable
fun IconSelectionScreen(activeIcon: AppIconPreset, onItemConfirmed: (AppIconPreset) -> Unit, onWarningClick: () -> Unit, modifier: Modifier = Modifier) {
var showDialog: Boolean by remember { mutableStateOf(false) }
var pendingIcon: AppIconPreset by remember {
mutableStateOf(activeIcon)
}
if (showDialog) {
ChangeIconDialog(
pendingIcon = pendingIcon,
onConfirm = {
onItemConfirmed(pendingIcon)
showDialog = false
},
onDismiss = {
pendingIcon = activeIcon
showDialog = false
}
)
}
Column(modifier = modifier.verticalScroll(rememberScrollState())) {
Spacer(modifier = Modifier.size(12.dp))
CaveatWarning(
onClick = onWarningClick,
modifier = Modifier.padding(horizontal = 24.dp)
)
Spacer(modifier = Modifier.size(12.dp))
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 18.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
enumValues<AppIconPreset>().toList().chunked(COLUMN_COUNT).map { it.toImmutableList() }.forEach { items ->
IconRow(
presets = items,
isSelected = { it == pendingIcon },
onItemClick = {
pendingIcon = it
showDialog = true
}
)
}
}
}
}
@Composable
fun ChangeIconDialog(pendingIcon: AppIconPreset, onConfirm: () -> Unit, onDismiss: () -> Unit, modifier: Modifier = Modifier) {
AlertDialog(
modifier = modifier,
onDismissRequest = onDismiss,
confirmButton = {
TextButton(
onClick = onConfirm
) {
Text(text = stringResource(id = R.string.preferences__app_icon_dialog_ok))
}
},
dismissButton = {
TextButton(
onClick = onDismiss
) {
Text(text = stringResource(id = R.string.preferences__app_icon_dialog_cancel))
}
},
icon = {
AppIcon(preset = pendingIcon, isSelected = false, onClick = {})
},
title = {
Text(
text = stringResource(id = R.string.preferences__app_icon_dialog_title, stringResource(id = pendingIcon.labelResId)),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.headlineSmall
)
},
text = {
Text(
text = stringResource(id = R.string.preferences__app_icon_dialog_description),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyMedium
)
}
)
}
/**
* Composable rendering the one row of icons that the user may choose from.
*/
@Composable
fun IconRow(presets: ImmutableList<AppIconPreset>, isSelected: (AppIconPreset) -> Boolean, onItemClick: (AppIconPreset) -> Unit, modifier: Modifier = Modifier) {
Row(modifier = modifier.fillMaxWidth()) {
presets.forEach { preset ->
val currentlySelected = isSelected(preset)
IconGridElement(
preset = preset,
isSelected = currentlySelected,
onClickHandler = {
if (!currentlySelected) {
onItemClick(preset)
}
},
modifier = Modifier
.padding(vertical = 18.dp)
.weight(1f)
)
}
}
}
/**
* Composable rendering an individual icon inside that grid, including the black border of the selected icon.
*/
@Composable
fun IconGridElement(preset: AppIconPreset, isSelected: Boolean, onClickHandler: () -> Unit, modifier: Modifier = Modifier) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
val boxModifier = Modifier.size(64.dp)
Box(
modifier = if (isSelected) boxModifier.border(3.dp, MaterialTheme.colorScheme.onBackground, CircleShape) else boxModifier
) {
AppIcon(preset = preset, isSelected = isSelected, onClickHandler, modifier = Modifier.align(Alignment.Center))
}
Spacer(modifier = Modifier.size(8.dp))
Text(
text = stringResource(id = preset.labelResId),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
/**
* Composable rendering the multiple layers of an adaptive icon onto one flattened rasterized Canvas.
*/
@Composable
fun AppIcon(preset: AppIconPreset, isSelected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
val bitmapSize by animateFloatAsState(if (isSelected) 48f else 64f, label = "Icon Size")
val imageModifier = modifier
.size(bitmapSize.dp)
.graphicsLayer(
shape = CircleShape,
shadowElevation = if (isSelected) 4f else 8f,
clip = true
)
.clickable(onClick = onClick)
Image(
painterResource(id = preset.iconPreviewResId),
contentDescription = stringResource(id = preset.labelResId),
modifier = imageModifier
)
}
/**
* A clickable "learn more" block of text.
*/
@Composable
fun CaveatWarning(onClick: () -> Unit, modifier: Modifier = Modifier) {
val learnMoreString = stringResource(R.string.preferences__app_icon_learn_more)
val completeString = stringResource(R.string.preferences__app_icon_warning_learn_more)
val learnMoreStartIndex = completeString.indexOf(learnMoreString).coerceAtLeast(0)
val learnMoreEndIndex = learnMoreStartIndex + learnMoreString.length
val doesStringEndWithLearnMore = learnMoreEndIndex >= completeString.lastIndex
val annotatedText = buildAnnotatedString {
withStyle(
style = SpanStyle(
color = MaterialTheme.colorScheme.onSurfaceVariant
)
) {
append(completeString.substring(0, learnMoreStartIndex))
}
pushStringAnnotation(
tag = URL_TAG,
annotation = LEARN_MORE_TAG
)
withStyle(
style = SpanStyle(
color = MaterialTheme.colorScheme.primary
)
) {
append(learnMoreString)
}
pop()
if (!doesStringEndWithLearnMore) {
append(completeString.substring(learnMoreEndIndex, completeString.lastIndex))
}
}
ClickableText(
text = annotatedText,
onClick = { _ ->
onClick()
},
style = MaterialTheme.typography.bodyMedium,
modifier = modifier
)
}
@Preview(name = "Light Theme")
@Composable
private fun MainScreenPreviewLight() {
SignalTheme(isDarkMode = false) {
Surface {
IconSelectionScreen(AppIconPreset.DEFAULT, onItemConfirmed = {}, onWarningClick = {})
}
}
}
@Preview(name = "Dark Theme")
@Composable
private fun MainScreenPreviewDark() {
SignalTheme(isDarkMode = true) {
Surface {
IconSelectionScreen(AppIconPreset.DEFAULT, onItemConfirmed = {}, onWarningClick = {})
}
}
}
companion object { companion object {
val TAG = Log.tag(AppIconSelectionFragment::class.java) val TAG = Log.tag(AppIconSelectionFragment::class.java)
}
private const val LEARN_MORE_TAG = "learn_more" }
private const val URL_TAG = "URL"
private const val COLUMN_COUNT = 4 private const val LEARN_MORE_TAG = "learn_more"
private const val URL_TAG = "URL"
private const val COLUMN_COUNT = 4
/**
* Screen allowing the user to view all the possible icon and select a new one to use.
*/
@Composable
fun IconSelectionScreen(activeIcon: AppIconPreset, onItemConfirmed: (AppIconPreset) -> Unit, onWarningClick: () -> Unit, modifier: Modifier = Modifier) {
var showDialog: Boolean by remember { mutableStateOf(false) }
var pendingIcon: AppIconPreset by remember {
mutableStateOf(activeIcon)
}
if (showDialog) {
ChangeIconDialog(
pendingIcon = pendingIcon,
onConfirm = {
onItemConfirmed(pendingIcon)
showDialog = false
},
onDismiss = {
pendingIcon = activeIcon
showDialog = false
}
)
}
Column(modifier = modifier.verticalScroll(rememberScrollState())) {
Spacer(modifier = Modifier.size(12.dp))
CaveatWarning(
onClick = onWarningClick,
modifier = Modifier.padding(horizontal = 24.dp)
)
Spacer(modifier = Modifier.size(12.dp))
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 18.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
enumValues<AppIconPreset>().toList().chunked(COLUMN_COUNT).map { it.toImmutableList() }.forEach { items ->
IconRow(
presets = items,
isSelected = { it == pendingIcon },
onItemClick = {
pendingIcon = it
showDialog = true
}
)
}
}
}
}
@Composable
fun ChangeIconDialog(pendingIcon: AppIconPreset, onConfirm: () -> Unit, onDismiss: () -> Unit, modifier: Modifier = Modifier) {
AlertDialog(
modifier = modifier,
onDismissRequest = onDismiss,
confirmButton = {
TextButton(
onClick = onConfirm
) {
Text(text = stringResource(id = R.string.preferences__app_icon_dialog_ok))
}
},
dismissButton = {
TextButton(
onClick = onDismiss
) {
Text(text = stringResource(id = R.string.preferences__app_icon_dialog_cancel))
}
},
icon = {
AppIcon(preset = pendingIcon, isSelected = false, onClick = {})
},
title = {
Text(
text = stringResource(id = R.string.preferences__app_icon_dialog_title, stringResource(id = pendingIcon.labelResId)),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.headlineSmall
)
},
text = {
Text(
text = stringResource(id = R.string.preferences__app_icon_dialog_description),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyMedium
)
}
)
}
/**
* Composable rendering the one row of icons that the user may choose from.
*/
@Composable
fun IconRow(presets: ImmutableList<AppIconPreset>, isSelected: (AppIconPreset) -> Boolean, onItemClick: (AppIconPreset) -> Unit, modifier: Modifier = Modifier) {
Row(modifier = modifier.fillMaxWidth()) {
presets.forEach { preset ->
val currentlySelected = isSelected(preset)
IconGridElement(
preset = preset,
isSelected = currentlySelected,
onClickHandler = {
if (!currentlySelected) {
onItemClick(preset)
}
},
modifier = Modifier
.padding(vertical = 18.dp)
.weight(1f)
)
}
}
}
/**
* Composable rendering an individual icon inside that grid, including the black border of the selected icon.
*/
@Composable
fun IconGridElement(preset: AppIconPreset, isSelected: Boolean, onClickHandler: () -> Unit, modifier: Modifier = Modifier) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
val boxModifier = Modifier.size(64.dp)
Box(
modifier = if (isSelected) boxModifier.border(3.dp, MaterialTheme.colorScheme.onBackground, CircleShape) else boxModifier
) {
AppIcon(preset = preset, isSelected = isSelected, onClickHandler, modifier = Modifier.align(Alignment.Center))
}
Spacer(modifier = Modifier.size(8.dp))
Text(
text = stringResource(id = preset.labelResId),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
/**
* Composable rendering the icon and optionally a border, to indicate selection.
*/
@Composable
fun AppIcon(preset: AppIconPreset, isSelected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
val bitmapSize by animateFloatAsState(if (isSelected) 48f else 64f, label = "Icon Size")
val imageModifier = modifier
.size(bitmapSize.dp)
.graphicsLayer(
shape = CircleShape,
shadowElevation = if (isSelected) 4f else 8f,
clip = true
)
.clickable(onClick = onClick)
Image(
painterResource(id = preset.iconPreviewResId),
contentDescription = stringResource(id = preset.labelResId),
modifier = imageModifier
)
}
/**
* A clickable "learn more" block of text.
*/
@Composable
fun CaveatWarning(onClick: () -> Unit, modifier: Modifier = Modifier) {
val learnMoreString = stringResource(R.string.preferences__app_icon_learn_more)
val completeString = stringResource(R.string.preferences__app_icon_warning_learn_more)
val learnMoreStartIndex = completeString.indexOf(learnMoreString).coerceAtLeast(0)
val learnMoreEndIndex = learnMoreStartIndex + learnMoreString.length
val doesStringEndWithLearnMore = learnMoreEndIndex >= completeString.lastIndex
val annotatedText = buildAnnotatedString {
withStyle(
style = SpanStyle(
color = MaterialTheme.colorScheme.onSurfaceVariant
)
) {
append(completeString.substring(0, learnMoreStartIndex))
}
pushStringAnnotation(
tag = URL_TAG,
annotation = LEARN_MORE_TAG
)
withStyle(
style = SpanStyle(
color = MaterialTheme.colorScheme.primary
)
) {
append(learnMoreString)
}
pop()
if (!doesStringEndWithLearnMore) {
append(completeString.substring(learnMoreEndIndex, completeString.lastIndex))
}
}
ClickableText(
text = annotatedText,
onClick = { onClick() },
style = MaterialTheme.typography.bodyMedium,
modifier = modifier
)
}
@Preview(name = "Light Theme")
@Composable
private fun MainScreenPreviewLight() {
SignalTheme(isDarkMode = false) {
Surface {
IconSelectionScreen(AppIconPreset.DEFAULT, onItemConfirmed = {}, onWarningClick = {})
}
}
}
@Preview(name = "Dark Theme")
@Composable
private fun MainScreenPreviewDark() {
SignalTheme(isDarkMode = true) {
Surface {
IconSelectionScreen(AppIconPreset.DEFAULT, onItemConfirmed = {}, onWarningClick = {})
}
} }
} }