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:
parent
d7c06fff50
commit
131f9c4bc9
1 changed files with 226 additions and 228 deletions
|
@ -87,235 +87,233 @@ class AppIconSelectionFragment : ComposeFragment() {
|
|||
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 {
|
||||
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 = {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue