refactor: 重构点击动画和页面样式
This commit is contained in:
parent
a487e4b890
commit
bddc9af2f7
20 changed files with 366 additions and 223 deletions
|
|
@ -38,7 +38,7 @@ android {
|
||||||
applicationId = "cn.super12138.todo"
|
applicationId = "cn.super12138.todo"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 952
|
versionCode = 955
|
||||||
versionName = "2.3.3"
|
versionName = "2.3.3"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
package cn.super12138.todo.ui
|
package cn.super12138.todo.ui
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.FiniteAnimationSpec
|
||||||
import androidx.compose.foundation.shape.CornerBasedShape
|
import androidx.compose.foundation.shape.CornerBasedShape
|
||||||
|
import androidx.compose.material3.ButtonShapes
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
@ -33,7 +36,7 @@ object TodoDefaults {
|
||||||
*/
|
*/
|
||||||
val settingsItemVerticalPadding = 16.dp
|
val settingsItemVerticalPadding = 16.dp
|
||||||
|
|
||||||
val settingsItemPadding = 2.dp
|
val settingsItemPadding = 4.dp
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 待办进度条粗度
|
* 待办进度条粗度
|
||||||
|
|
@ -70,13 +73,27 @@ object TodoDefaults {
|
||||||
@Composable
|
@Composable
|
||||||
get() = MaterialTheme.colorScheme.surfaceContainer
|
get() = MaterialTheme.colorScheme.surfaceContainer
|
||||||
|
|
||||||
val SettingsItemDefaultShape: CornerBasedShape
|
|
||||||
@Composable
|
|
||||||
get() = MaterialTheme.shapes.small
|
|
||||||
|
|
||||||
val SettingsItemRoundedShape: CornerBasedShape
|
val fadedEdgeWidth = 8.dp
|
||||||
|
|
||||||
|
val defaultShape: CornerBasedShape
|
||||||
@Composable
|
@Composable
|
||||||
get() = MaterialTheme.shapes.large
|
get() = MaterialTheme.shapes.large
|
||||||
|
|
||||||
val fadedEdgeWidth = 8.dp
|
val pressedShape: CornerBasedShape
|
||||||
|
@Composable
|
||||||
|
get() = MaterialTheme.shapes.small
|
||||||
|
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
fun shapes() = ButtonShapes(
|
||||||
|
shape = defaultShape,
|
||||||
|
pressedShape = pressedShape
|
||||||
|
)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
val shapesDefaultAnimationSpec: FiniteAnimationSpec<Float>
|
||||||
|
@Composable
|
||||||
|
get() = MaterialTheme.motionScheme.defaultEffectsSpec<Float>()
|
||||||
}
|
}
|
||||||
|
|
@ -71,8 +71,7 @@ fun SettingsAbout(
|
||||||
}
|
}
|
||||||
clickCount = 0
|
clickCount = 0
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
topRounded = true
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
|
|
@ -104,8 +103,7 @@ fun SettingsAbout(
|
||||||
leadingIconRes = R.drawable.ic_code_blocks,
|
leadingIconRes = R.drawable.ic_code_blocks,
|
||||||
title = stringResource(R.string.pref_developer_options),
|
title = stringResource(R.string.pref_developer_options),
|
||||||
description = stringResource(R.string.pref_developer_options_desc),
|
description = stringResource(R.string.pref_developer_options_desc),
|
||||||
onClick = toDevPage,
|
onClick = toDevPage
|
||||||
bottomRounded = true
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
|
@ -16,10 +16,8 @@ import androidx.compose.material3.FilledTonalButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
|
|
@ -55,17 +53,11 @@ fun SettingsAboutLicence(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
) {
|
) {
|
||||||
SettingsContainer(Modifier.fillMaxSize()) {
|
SettingsContainer(Modifier.fillMaxSize()) {
|
||||||
itemsIndexed(
|
items(
|
||||||
items = libraries?.libraries ?: listOf(),
|
items = libraries?.libraries ?: listOf(),
|
||||||
key = { _, library -> library.artifactId }
|
key = { it.artifactId }
|
||||||
) { index, library ->
|
) { library ->
|
||||||
var openDialog by rememberSaveable { mutableStateOf(false) }
|
var openDialog by rememberSaveable { mutableStateOf(false) }
|
||||||
val topRounded by remember { derivedStateOf { index == 0 } }
|
|
||||||
val bottomRounded by remember {
|
|
||||||
derivedStateOf {
|
|
||||||
index == (libraries?.libraries?.size ?: 1) - 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SettingsItem(
|
SettingsItem(
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
Row(
|
Row(
|
||||||
|
|
@ -126,9 +118,7 @@ fun SettingsAboutLicence(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
topRounded = topRounded,
|
|
||||||
bottomRounded = bottomRounded
|
|
||||||
)
|
)
|
||||||
|
|
||||||
BasicDialog(
|
BasicDialog(
|
||||||
|
|
|
||||||
|
|
@ -46,8 +46,7 @@ fun SettingsAppearance(
|
||||||
leadingIconRes = R.drawable.ic_wand_stars,
|
leadingIconRes = R.drawable.ic_wand_stars,
|
||||||
title = stringResource(R.string.pref_appearance_dynamic_color),
|
title = stringResource(R.string.pref_appearance_dynamic_color),
|
||||||
description = stringResource(R.string.pref_appearance_dynamic_color_desc),
|
description = stringResource(R.string.pref_appearance_dynamic_color_desc),
|
||||||
onCheckedChange = { scope.launch { DataStoreManager.setDynamicColor(it) } },
|
onCheckedChange = { scope.launch { DataStoreManager.setDynamicColor(it) } }
|
||||||
topRounded = true
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -71,8 +70,7 @@ fun SettingsAppearance(
|
||||||
item(key = 4) {
|
item(key = 4) {
|
||||||
ContrastPicker(
|
ContrastPicker(
|
||||||
currentContrast = ContrastLevel.fromFloat(contrastLevel),
|
currentContrast = ContrastLevel.fromFloat(contrastLevel),
|
||||||
onContrastChange = { scope.launch { DataStoreManager.setContrastLevel(it.value) } },
|
onContrastChange = { scope.launch { DataStoreManager.setContrastLevel(it.value) } }
|
||||||
bottomRounded = true
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -113,8 +113,7 @@ fun SettingsData(
|
||||||
description = stringResource(R.string.pref_backup_desc),
|
description = stringResource(R.string.pref_backup_desc),
|
||||||
onClick = {
|
onClick = {
|
||||||
backupLauncher.launch("Todo-backup-${SystemUtils.getTime()}.zip")
|
backupLauncher.launch("Todo-backup-${SystemUtils.getTime()}.zip")
|
||||||
},
|
}
|
||||||
topRounded = true
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
|
|
@ -124,8 +123,7 @@ fun SettingsData(
|
||||||
description = stringResource(R.string.pref_restore_desc),
|
description = stringResource(R.string.pref_restore_desc),
|
||||||
onClick = {
|
onClick = {
|
||||||
restoreLauncher.launch(arrayOf("application/zip"))
|
restoreLauncher.launch(arrayOf("application/zip"))
|
||||||
},
|
}
|
||||||
bottomRounded = true
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -135,9 +133,7 @@ fun SettingsData(
|
||||||
leadingIconRes = R.drawable.ic_category,
|
leadingIconRes = R.drawable.ic_category,
|
||||||
title = stringResource(R.string.pref_category_category_management),
|
title = stringResource(R.string.pref_category_category_management),
|
||||||
description = stringResource(R.string.pref_category_management_desc),
|
description = stringResource(R.string.pref_category_management_desc),
|
||||||
onClick = toCategoryManager,
|
onClick = toCategoryManager
|
||||||
topRounded = true,
|
|
||||||
bottomRounded = true
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -92,11 +92,6 @@ fun SettingsDataCategory(
|
||||||
items = categories,
|
items = categories,
|
||||||
key = { it }
|
key = { it }
|
||||||
) { category ->
|
) { category ->
|
||||||
// compute rounding by content rather than by index so insertion/moves
|
|
||||||
// correctly update rounded corners while keeping content key for animations
|
|
||||||
val topRounded = category == categories.first()
|
|
||||||
val bottomRounded = category == categories.last()
|
|
||||||
|
|
||||||
SettingsItem(
|
SettingsItem(
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -125,8 +120,6 @@ fun SettingsDataCategory(
|
||||||
initialCategory = category
|
initialCategory = category
|
||||||
showDialog = true
|
showDialog = true
|
||||||
},
|
},
|
||||||
topRounded = topRounded,
|
|
||||||
bottomRounded = bottomRounded,
|
|
||||||
modifier = Modifier.animateItem(
|
modifier = Modifier.animateItem(
|
||||||
fadeInSpec = MaterialTheme.motionScheme.defaultEffectsSpec(),
|
fadeInSpec = MaterialTheme.motionScheme.defaultEffectsSpec(),
|
||||||
placementSpec = MaterialTheme.motionScheme.defaultSpatialSpec(),
|
placementSpec = MaterialTheme.motionScheme.defaultSpatialSpec(),
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,7 @@ fun SettingsDeveloperOptions(
|
||||||
SettingsItem(
|
SettingsItem(
|
||||||
leadingIconRes = R.drawable.ic_padding,
|
leadingIconRes = R.drawable.ic_padding,
|
||||||
title = stringResource(R.string.pref_padding),
|
title = stringResource(R.string.pref_padding),
|
||||||
onClick = toPaddingPage,
|
onClick = toPaddingPage
|
||||||
topRounded = true,
|
|
||||||
bottomRounded = true
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import cn.super12138.todo.R
|
import cn.super12138.todo.R
|
||||||
import cn.super12138.todo.constants.Constants
|
import cn.super12138.todo.constants.Constants
|
||||||
|
|
@ -38,7 +37,6 @@ fun SettingsInterface(
|
||||||
val sortingMethod by DataStoreManager.sortingMethodFlow.collectAsState(initial = Constants.PREF_SORTING_METHOD_DEFAULT)
|
val sortingMethod by DataStoreManager.sortingMethodFlow.collectAsState(initial = Constants.PREF_SORTING_METHOD_DEFAULT)
|
||||||
val hapticFeedback by DataStoreManager.hapticFeedbackFlow.collectAsState(initial = Constants.PREF_HAPTIC_FEEDBACK_DEFAULT)
|
val hapticFeedback by DataStoreManager.hapticFeedbackFlow.collectAsState(initial = Constants.PREF_HAPTIC_FEEDBACK_DEFAULT)
|
||||||
|
|
||||||
val context = LocalContext.current
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
var showSortingMethodDialog by rememberSaveable { mutableStateOf(false) }
|
var showSortingMethodDialog by rememberSaveable { mutableStateOf(false) }
|
||||||
TopAppBarScaffold(
|
TopAppBarScaffold(
|
||||||
|
|
@ -57,8 +55,7 @@ fun SettingsInterface(
|
||||||
title = stringResource(R.string.pref_show_completed),
|
title = stringResource(R.string.pref_show_completed),
|
||||||
description = stringResource(R.string.pref_show_completed_desc),
|
description = stringResource(R.string.pref_show_completed_desc),
|
||||||
checked = showCompleted,
|
checked = showCompleted,
|
||||||
onCheckedChange = { scope.launch { DataStoreManager.setShowCompleted(it) } },
|
onCheckedChange = { scope.launch { DataStoreManager.setShowCompleted(it) } }
|
||||||
topRounded = true
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
|
|
@ -66,8 +63,7 @@ fun SettingsInterface(
|
||||||
leadingIconRes = R.drawable.ic_sort,
|
leadingIconRes = R.drawable.ic_sort,
|
||||||
title = stringResource(R.string.pref_sorting_method),
|
title = stringResource(R.string.pref_sorting_method),
|
||||||
description = stringResource(SortingMethod.fromId(sortingMethod).nameRes),
|
description = stringResource(SortingMethod.fromId(sortingMethod).nameRes),
|
||||||
onClick = { showSortingMethodDialog = true },
|
onClick = { showSortingMethodDialog = true }
|
||||||
bottomRounded = true
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -78,8 +74,7 @@ fun SettingsInterface(
|
||||||
leadingIconRes = R.drawable.ic_shield,
|
leadingIconRes = R.drawable.ic_shield,
|
||||||
title = stringResource(R.string.pref_secure_mode),
|
title = stringResource(R.string.pref_secure_mode),
|
||||||
description = stringResource(R.string.pref_secure_mode_desc),
|
description = stringResource(R.string.pref_secure_mode_desc),
|
||||||
onCheckedChange = { scope.launch { DataStoreManager.setSecureMode(it) } },
|
onCheckedChange = { scope.launch { DataStoreManager.setSecureMode(it) } }
|
||||||
topRounded = true
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -89,8 +84,7 @@ fun SettingsInterface(
|
||||||
leadingIconRes = R.drawable.ic_touch_long,
|
leadingIconRes = R.drawable.ic_touch_long,
|
||||||
title = stringResource(R.string.pref_haptic_feedback),
|
title = stringResource(R.string.pref_haptic_feedback),
|
||||||
description = stringResource(R.string.pref_haptic_feedback_desc),
|
description = stringResource(R.string.pref_haptic_feedback_desc),
|
||||||
onCheckedChange = { scope.launch { DataStoreManager.setHapticFeedback(it) } },
|
onCheckedChange = { scope.launch { DataStoreManager.setHapticFeedback(it) } }
|
||||||
bottomRounded = true
|
|
||||||
)
|
)
|
||||||
SettingsPlainBox(stringResource(R.string.pref_haptic_feedback_more_info))
|
SettingsPlainBox(stringResource(R.string.pref_haptic_feedback_more_info))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,7 @@ fun SettingsMain(
|
||||||
leadingIconRes = R.drawable.ic_palette,
|
leadingIconRes = R.drawable.ic_palette,
|
||||||
title = stringResource(R.string.pref_appearance),
|
title = stringResource(R.string.pref_appearance),
|
||||||
description = stringResource(R.string.pref_appearance_desc),
|
description = stringResource(R.string.pref_appearance_desc),
|
||||||
onClick = toAppearancePage,
|
onClick = toAppearancePage
|
||||||
topRounded = true
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -58,8 +57,7 @@ fun SettingsMain(
|
||||||
leadingIconRes = R.drawable.ic_info,
|
leadingIconRes = R.drawable.ic_info,
|
||||||
title = stringResource(R.string.pref_about),
|
title = stringResource(R.string.pref_about),
|
||||||
description = stringResource(R.string.pref_about_desc),
|
description = stringResource(R.string.pref_about_desc),
|
||||||
onClick = toAboutPage,
|
onClick = toAboutPage
|
||||||
bottomRounded = true
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,6 @@ import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import cn.super12138.todo.ui.TodoDefaults
|
import cn.super12138.todo.ui.TodoDefaults
|
||||||
import cn.super12138.todo.utils.drawFadedEdge
|
import cn.super12138.todo.utils.drawFadedEdge
|
||||||
import cn.super12138.todo.utils.getPartialRoundedShape
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RowSettingsItem(
|
fun RowSettingsItem(
|
||||||
|
|
@ -44,8 +43,6 @@ fun RowSettingsItem(
|
||||||
scrollState: ScrollState = rememberScrollState(),
|
scrollState: ScrollState = rememberScrollState(),
|
||||||
fadedEdgeWidth: Dp,
|
fadedEdgeWidth: Dp,
|
||||||
maskColor: Color = TodoDefaults.ContainerColor,
|
maskColor: Color = TodoDefaults.ContainerColor,
|
||||||
topRounded: Boolean = false,
|
|
||||||
bottomRounded: Boolean = false,
|
|
||||||
content: @Composable RowScope.() -> Unit
|
content: @Composable RowScope.() -> Unit
|
||||||
) {
|
) {
|
||||||
MoreContentSettingsItem(
|
MoreContentSettingsItem(
|
||||||
|
|
@ -54,8 +51,6 @@ fun RowSettingsItem(
|
||||||
description = description,
|
description = description,
|
||||||
trailingContent = trailingContent,
|
trailingContent = trailingContent,
|
||||||
background = background,
|
background = background,
|
||||||
topRounded = topRounded,
|
|
||||||
bottomRounded = bottomRounded,
|
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
|
|
@ -98,8 +93,6 @@ fun LazyRowSettingsItem(
|
||||||
verticalAlignment: Alignment.Vertical = Alignment.Top,
|
verticalAlignment: Alignment.Vertical = Alignment.Top,
|
||||||
fadedEdgeWidth: Dp,
|
fadedEdgeWidth: Dp,
|
||||||
maskColor: Color = TodoDefaults.ContainerColor,
|
maskColor: Color = TodoDefaults.ContainerColor,
|
||||||
topRounded: Boolean = false,
|
|
||||||
bottomRounded: Boolean = false,
|
|
||||||
content: LazyListScope.() -> Unit
|
content: LazyListScope.() -> Unit
|
||||||
) {
|
) {
|
||||||
MoreContentSettingsItem(
|
MoreContentSettingsItem(
|
||||||
|
|
@ -107,8 +100,6 @@ fun LazyRowSettingsItem(
|
||||||
title = title,
|
title = title,
|
||||||
description = description,
|
description = description,
|
||||||
trailingContent = trailingContent,
|
trailingContent = trailingContent,
|
||||||
topRounded = topRounded,
|
|
||||||
bottomRounded = bottomRounded,
|
|
||||||
background = background,
|
background = background,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
) {
|
) {
|
||||||
|
|
@ -147,17 +138,14 @@ fun MoreContentSettingsItem(
|
||||||
description: String? = null,
|
description: String? = null,
|
||||||
trailingContent: (@Composable () -> Unit)? = null,
|
trailingContent: (@Composable () -> Unit)? = null,
|
||||||
background: Color = TodoDefaults.ContainerColor,
|
background: Color = TodoDefaults.ContainerColor,
|
||||||
shape: CornerBasedShape = TodoDefaults.SettingsItemDefaultShape,
|
shape: CornerBasedShape = TodoDefaults.defaultShape,
|
||||||
roundedShape: CornerBasedShape = TodoDefaults.SettingsItemRoundedShape,
|
|
||||||
topRounded: Boolean = false,
|
|
||||||
bottomRounded: Boolean = false,
|
|
||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.wrapContentHeight()
|
.wrapContentHeight()
|
||||||
.clip(shape.getPartialRoundedShape(topRounded, bottomRounded, roundedShape))
|
.clip(shape)
|
||||||
.background(background)
|
.background(background)
|
||||||
.padding(
|
.padding(
|
||||||
horizontal = TodoDefaults.settingsItemHorizontalPadding,
|
horizontal = TodoDefaults.settingsItemHorizontalPadding,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package cn.super12138.todo.ui.pages.settings.components
|
package cn.super12138.todo.ui.pages.settings.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
|
@ -10,10 +12,13 @@ import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.selection.selectable
|
import androidx.compose.foundation.selection.selectable
|
||||||
import androidx.compose.foundation.selection.selectableGroup
|
import androidx.compose.foundation.selection.selectableGroup
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.RadioButton
|
import androidx.compose.material3.RadioButton
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
|
@ -22,6 +27,7 @@ import androidx.compose.ui.semantics.Role
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import cn.super12138.todo.ui.TodoDefaults
|
import cn.super12138.todo.ui.TodoDefaults
|
||||||
import cn.super12138.todo.ui.components.BasicDialog
|
import cn.super12138.todo.ui.components.BasicDialog
|
||||||
|
import cn.super12138.todo.ui.theme.shapeByInteraction
|
||||||
import cn.super12138.todo.utils.VibrationUtils
|
import cn.super12138.todo.utils.VibrationUtils
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -42,7 +48,6 @@ fun SettingsRadioDialog(
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(TodoDefaults.SettingsItemRoundedShape)
|
|
||||||
.selectableGroup()
|
.selectableGroup()
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
) {
|
) {
|
||||||
|
|
@ -63,6 +68,7 @@ fun SettingsRadioDialog(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun RadioItem(
|
fun RadioItem(
|
||||||
selected: Boolean,
|
selected: Boolean,
|
||||||
|
|
@ -71,13 +77,23 @@ fun RadioItem(
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
val pressed by interactionSource.collectIsPressedAsState()
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier
|
modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(56.dp)
|
.height(56.dp)
|
||||||
.clip(TodoDefaults.SettingsItemDefaultShape)
|
.clip(
|
||||||
|
shapeByInteraction(
|
||||||
|
shapes = TodoDefaults.shapes(),
|
||||||
|
pressed = pressed,
|
||||||
|
animationSpec = TodoDefaults.shapesDefaultAnimationSpec
|
||||||
|
)
|
||||||
|
)
|
||||||
// .background(MaterialTheme.colorScheme.surfaceContainerHighest)
|
// .background(MaterialTheme.colorScheme.surfaceContainerHighest)
|
||||||
.selectable(
|
.selectable(
|
||||||
|
interactionSource = interactionSource,
|
||||||
selected = selected,
|
selected = selected,
|
||||||
onClick = {
|
onClick = {
|
||||||
VibrationUtils.performHapticFeedback(view)
|
VibrationUtils.performHapticFeedback(view)
|
||||||
|
|
|
||||||
|
|
@ -3,17 +3,21 @@ package cn.super12138.todo.ui.pages.settings.components
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.wrapContentHeight
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
import androidx.compose.foundation.shape.CornerBasedShape
|
import androidx.compose.material3.ButtonShapes
|
||||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
|
@ -25,18 +29,17 @@ import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import cn.super12138.todo.ui.TodoDefaults
|
import cn.super12138.todo.ui.TodoDefaults
|
||||||
|
import cn.super12138.todo.ui.theme.shapeByInteraction
|
||||||
import cn.super12138.todo.utils.VibrationUtils
|
import cn.super12138.todo.utils.VibrationUtils
|
||||||
import cn.super12138.todo.utils.getPartialRoundedShape
|
|
||||||
|
|
||||||
// Leading icon as drawable resource
|
// Leading icon as drawable resource
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsItem(
|
fun SettingsItem(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
@DrawableRes leadingIconRes: Int,
|
@DrawableRes leadingIconRes: Int,
|
||||||
title: String,
|
title: String,
|
||||||
description: String? = null,
|
description: String? = null,
|
||||||
topRounded: Boolean = false,
|
|
||||||
bottomRounded: Boolean = false,
|
|
||||||
enableClick: Boolean = true,
|
enableClick: Boolean = true,
|
||||||
onClick: () -> Unit = {}
|
onClick: () -> Unit = {}
|
||||||
) = SettingsItem(
|
) = SettingsItem(
|
||||||
|
|
@ -44,8 +47,6 @@ fun SettingsItem(
|
||||||
title = title,
|
title = title,
|
||||||
description = description,
|
description = description,
|
||||||
trailingContent = null,
|
trailingContent = null,
|
||||||
topRounded = topRounded,
|
|
||||||
bottomRounded = bottomRounded,
|
|
||||||
enableClick = enableClick,
|
enableClick = enableClick,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
|
|
@ -61,8 +62,7 @@ fun SettingsItem(
|
||||||
description: String? = null,
|
description: String? = null,
|
||||||
trailingContent: (@Composable () -> Unit)? = null,
|
trailingContent: (@Composable () -> Unit)? = null,
|
||||||
background: Color = TodoDefaults.ContainerColor,
|
background: Color = TodoDefaults.ContainerColor,
|
||||||
topRounded: Boolean = false,
|
shapes: ButtonShapes = TodoDefaults.shapes(),
|
||||||
bottomRounded: Boolean = false,
|
|
||||||
enableClick: Boolean = true,
|
enableClick: Boolean = true,
|
||||||
onClick: () -> Unit = {}
|
onClick: () -> Unit = {}
|
||||||
) = SettingsItem(
|
) = SettingsItem(
|
||||||
|
|
@ -91,22 +91,20 @@ fun SettingsItem(
|
||||||
description = description,
|
description = description,
|
||||||
trailingContent = trailingContent,
|
trailingContent = trailingContent,
|
||||||
background = background,
|
background = background,
|
||||||
topRounded = topRounded,
|
shapes = shapes,
|
||||||
bottomRounded = bottomRounded,
|
|
||||||
enableClick = enableClick,
|
enableClick = enableClick,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
|
|
||||||
// Leading icon as ImageVector
|
// Leading icon as ImageVector
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsItem(
|
fun SettingsItem(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
leadingIcon: ImageVector? = null,
|
leadingIcon: ImageVector? = null,
|
||||||
title: String,
|
title: String,
|
||||||
description: String? = null,
|
description: String? = null,
|
||||||
topRounded: Boolean = false,
|
|
||||||
bottomRounded: Boolean = false,
|
|
||||||
enableClick: Boolean = true,
|
enableClick: Boolean = true,
|
||||||
onClick: () -> Unit = {}
|
onClick: () -> Unit = {}
|
||||||
) = SettingsItem(
|
) = SettingsItem(
|
||||||
|
|
@ -114,13 +112,12 @@ fun SettingsItem(
|
||||||
title = title,
|
title = title,
|
||||||
description = description,
|
description = description,
|
||||||
trailingContent = null,
|
trailingContent = null,
|
||||||
topRounded = topRounded,
|
|
||||||
bottomRounded = bottomRounded,
|
|
||||||
enableClick = enableClick,
|
enableClick = enableClick,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsItem(
|
fun SettingsItem(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
|
@ -129,8 +126,7 @@ fun SettingsItem(
|
||||||
description: String? = null,
|
description: String? = null,
|
||||||
trailingContent: (@Composable () -> Unit)? = null,
|
trailingContent: (@Composable () -> Unit)? = null,
|
||||||
background: Color = TodoDefaults.ContainerColor,
|
background: Color = TodoDefaults.ContainerColor,
|
||||||
topRounded: Boolean = false,
|
shapes: ButtonShapes = TodoDefaults.shapes(),
|
||||||
bottomRounded: Boolean = false,
|
|
||||||
enableClick: Boolean = true,
|
enableClick: Boolean = true,
|
||||||
onClick: () -> Unit = {}
|
onClick: () -> Unit = {}
|
||||||
) = SettingsItem(
|
) = SettingsItem(
|
||||||
|
|
@ -148,9 +144,8 @@ fun SettingsItem(
|
||||||
description = description,
|
description = description,
|
||||||
trailingContent = trailingContent,
|
trailingContent = trailingContent,
|
||||||
background = background,
|
background = background,
|
||||||
topRounded = topRounded,
|
|
||||||
bottomRounded = bottomRounded,
|
|
||||||
enableClick = enableClick,
|
enableClick = enableClick,
|
||||||
|
shapes = shapes,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
|
|
@ -165,10 +160,7 @@ fun SettingsItem(
|
||||||
description: String? = null,
|
description: String? = null,
|
||||||
trailingContent: (@Composable () -> Unit)? = null,
|
trailingContent: (@Composable () -> Unit)? = null,
|
||||||
background: Color = TodoDefaults.ContainerColor,
|
background: Color = TodoDefaults.ContainerColor,
|
||||||
shape: CornerBasedShape = TodoDefaults.SettingsItemDefaultShape,
|
shapes: ButtonShapes = TodoDefaults.shapes(),
|
||||||
roundedShape: CornerBasedShape = TodoDefaults.SettingsItemRoundedShape,
|
|
||||||
topRounded: Boolean = false,
|
|
||||||
bottomRounded: Boolean = false,
|
|
||||||
enableClick: Boolean = true,
|
enableClick: Boolean = true,
|
||||||
onClick: () -> Unit = {},
|
onClick: () -> Unit = {},
|
||||||
) = SettingsItem(
|
) = SettingsItem(
|
||||||
|
|
@ -199,10 +191,7 @@ fun SettingsItem(
|
||||||
},
|
},
|
||||||
trailingContent = trailingContent,
|
trailingContent = trailingContent,
|
||||||
background = background,
|
background = background,
|
||||||
shape = shape,
|
shapes = shapes,
|
||||||
roundedShape = roundedShape,
|
|
||||||
topRounded = topRounded,
|
|
||||||
bottomRounded = bottomRounded,
|
|
||||||
enableClick = enableClick,
|
enableClick = enableClick,
|
||||||
onClick = onClick
|
onClick = onClick
|
||||||
)
|
)
|
||||||
|
|
@ -216,20 +205,23 @@ fun SettingsItem(
|
||||||
supportingContent: (@Composable () -> Unit)? = null,
|
supportingContent: (@Composable () -> Unit)? = null,
|
||||||
trailingContent: (@Composable () -> Unit)? = null,
|
trailingContent: (@Composable () -> Unit)? = null,
|
||||||
background: Color = TodoDefaults.ContainerColor,
|
background: Color = TodoDefaults.ContainerColor,
|
||||||
shape: CornerBasedShape = TodoDefaults.SettingsItemDefaultShape,
|
shapes: ButtonShapes = TodoDefaults.shapes(),
|
||||||
roundedShape: CornerBasedShape = TodoDefaults.SettingsItemRoundedShape,
|
interactionSource: MutableInteractionSource? = null,
|
||||||
topRounded: Boolean = false,
|
|
||||||
bottomRounded: Boolean = false,
|
|
||||||
enableClick: Boolean = true,
|
enableClick: Boolean = true,
|
||||||
onClick: () -> Unit = {},
|
onClick: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
|
val userInteractionSource = interactionSource ?: remember { MutableInteractionSource() }
|
||||||
|
val pressed by userInteractionSource.collectIsPressedAsState()
|
||||||
|
val animatedShape = shapeByInteraction(shapes, pressed, TodoDefaults.shapesDefaultAnimationSpec)
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.wrapContentHeight()
|
.wrapContentHeight()
|
||||||
.clip(shape.getPartialRoundedShape(topRounded, bottomRounded, roundedShape))
|
.clip(animatedShape)
|
||||||
.clickable(
|
.clickable(
|
||||||
|
interactionSource = userInteractionSource,
|
||||||
enabled = enableClick,
|
enabled = enableClick,
|
||||||
onClick = {
|
onClick = {
|
||||||
VibrationUtils.performHapticFeedback(view)
|
VibrationUtils.performHapticFeedback(view)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package cn.super12138.todo.ui.pages.settings.components
|
||||||
|
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
|
@ -11,14 +12,13 @@ import androidx.compose.ui.res.painterResource
|
||||||
import cn.super12138.todo.ui.TodoDefaults
|
import cn.super12138.todo.ui.TodoDefaults
|
||||||
import cn.super12138.todo.utils.VibrationUtils
|
import cn.super12138.todo.utils.VibrationUtils
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SwitchSettingsItem(
|
fun SwitchSettingsItem(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
@DrawableRes leadingIconRes: Int,
|
@DrawableRes leadingIconRes: Int,
|
||||||
title: String,
|
title: String,
|
||||||
description: String? = null,
|
description: String? = null,
|
||||||
topRounded: Boolean = false,
|
|
||||||
bottomRounded: Boolean = false,
|
|
||||||
checked: Boolean,
|
checked: Boolean,
|
||||||
onCheckedChange: (Boolean) -> Unit
|
onCheckedChange: (Boolean) -> Unit
|
||||||
) {
|
) {
|
||||||
|
|
@ -34,12 +34,11 @@ fun SwitchSettingsItem(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onClick = { onCheckedChange(!checked) },
|
onClick = { onCheckedChange(!checked) },
|
||||||
topRounded = topRounded,
|
|
||||||
bottomRounded = bottomRounded,
|
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SwitchSettingsItem(
|
fun SwitchSettingsItem(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
|
|
||||||
|
|
@ -38,16 +38,12 @@ fun ContrastPicker(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
currentContrast: ContrastLevel,
|
currentContrast: ContrastLevel,
|
||||||
onContrastChange: (ContrastLevel) -> Unit,
|
onContrastChange: (ContrastLevel) -> Unit,
|
||||||
topRounded: Boolean = false,
|
|
||||||
bottomRounded: Boolean = false
|
|
||||||
) {
|
) {
|
||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
MoreContentSettingsItem(
|
MoreContentSettingsItem(
|
||||||
title = stringResource(R.string.pref_contrast_level),
|
title = stringResource(R.string.pref_contrast_level),
|
||||||
description = stringResource(R.string.pref_contrast_level_desc),
|
description = stringResource(R.string.pref_contrast_level_desc),
|
||||||
topRounded = topRounded,
|
|
||||||
bottomRounded = bottomRounded,
|
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
) {
|
) {
|
||||||
val contrastLevelName = ContrastLevel.entries.map { stringResource(it.nameRes) }
|
val contrastLevelName = ContrastLevel.entries.map { stringResource(it.nameRes) }
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,6 @@ fun DarkModePicker(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
currentDarkMode: () -> DarkMode,
|
currentDarkMode: () -> DarkMode,
|
||||||
onDarkModeChange: (darkMode: DarkMode) -> Unit,
|
onDarkModeChange: (darkMode: DarkMode) -> Unit,
|
||||||
topRounded: Boolean = false,
|
|
||||||
bottomRounded: Boolean = false
|
|
||||||
) {
|
) {
|
||||||
val isInDarkTheme = isSystemInDarkTheme()
|
val isInDarkTheme = isSystemInDarkTheme()
|
||||||
|
|
||||||
|
|
@ -33,8 +31,6 @@ fun DarkModePicker(
|
||||||
description = stringResource(R.string.pref_dark_mode_desc),
|
description = stringResource(R.string.pref_dark_mode_desc),
|
||||||
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
||||||
fadedEdgeWidth = TodoDefaults.fadedEdgeWidth,
|
fadedEdgeWidth = TodoDefaults.fadedEdgeWidth,
|
||||||
topRounded = topRounded,
|
|
||||||
bottomRounded = bottomRounded,
|
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
) {
|
) {
|
||||||
items(items = darkModeList, key = { it.id }) {
|
items(items = darkModeList, key = { it.id }) {
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,6 @@ fun PalettePicker(
|
||||||
isDynamicColor: Boolean,
|
isDynamicColor: Boolean,
|
||||||
isDarkMode: DarkMode,
|
isDarkMode: DarkMode,
|
||||||
contrastLevel: ContrastLevel,
|
contrastLevel: ContrastLevel,
|
||||||
topRounded: Boolean = false,
|
|
||||||
bottomRounded: Boolean = false
|
|
||||||
) {
|
) {
|
||||||
val paletteOptions = remember { PaletteStyle.entries.toList() }
|
val paletteOptions = remember { PaletteStyle.entries.toList() }
|
||||||
|
|
||||||
|
|
@ -35,8 +33,6 @@ fun PalettePicker(
|
||||||
description = stringResource(R.string.pref_palette_style_desc),
|
description = stringResource(R.string.pref_palette_style_desc),
|
||||||
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
||||||
fadedEdgeWidth = TodoDefaults.fadedEdgeWidth,
|
fadedEdgeWidth = TodoDefaults.fadedEdgeWidth,
|
||||||
topRounded = topRounded,
|
|
||||||
bottomRounded = bottomRounded,
|
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
) {
|
) {
|
||||||
items(items = paletteOptions, key = { it.id }) {
|
items(items = paletteOptions, key = { it.id }) {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
|
@ -119,12 +119,10 @@ fun SharedTransitionScope.TasksPage(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
itemsIndexed(
|
items(
|
||||||
items = filteredTodoList,
|
items = filteredTodoList,
|
||||||
key = { _, task -> task.id }
|
key = { it.id }
|
||||||
) { index, task ->
|
) { task ->
|
||||||
val topRounded by remember { derivedStateOf { index == 0 } }
|
|
||||||
val bottomRounded by remember { derivedStateOf { index == filteredTodoList.size - 1 } }
|
|
||||||
TodoCard(
|
TodoCard(
|
||||||
// id = item.id,
|
// id = item.id,
|
||||||
content = task.content,
|
content = task.content,
|
||||||
|
|
@ -144,8 +142,6 @@ fun SharedTransitionScope.TasksPage(
|
||||||
viewModel.updateTodo(task.copy(isCompleted = true))
|
viewModel.updateTodo(task.copy(isCompleted = true))
|
||||||
viewModel.playConfetti()
|
viewModel.playConfetti()
|
||||||
},
|
},
|
||||||
topRounded = topRounded,
|
|
||||||
bottomRounded = bottomRounded,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.sharedBounds(
|
.sharedBounds(
|
||||||
sharedContentState = rememberSharedContentState(key = "${Constants.KEY_TODO_ITEM_TRANSITION}_${task.id}"),
|
sharedContentState = rememberSharedContentState(key = "${Constants.KEY_TODO_ITEM_TRANSITION}_${task.id}"),
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ import androidx.compose.animation.shrinkHorizontally
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.basicMarquee
|
import androidx.compose.foundation.basicMarquee
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
|
@ -18,8 +20,8 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.CornerBasedShape
|
|
||||||
import androidx.compose.material3.Badge
|
import androidx.compose.material3.Badge
|
||||||
|
import androidx.compose.material3.ButtonShapes
|
||||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
|
|
@ -28,6 +30,7 @@ import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.contentColorFor
|
import androidx.compose.material3.contentColorFor
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
|
@ -40,9 +43,9 @@ import androidx.compose.ui.unit.dp
|
||||||
import cn.super12138.todo.R
|
import cn.super12138.todo.R
|
||||||
import cn.super12138.todo.logic.model.Priority
|
import cn.super12138.todo.logic.model.Priority
|
||||||
import cn.super12138.todo.ui.TodoDefaults
|
import cn.super12138.todo.ui.TodoDefaults
|
||||||
|
import cn.super12138.todo.ui.theme.shapeByInteraction
|
||||||
import cn.super12138.todo.utils.VibrationUtils
|
import cn.super12138.todo.utils.VibrationUtils
|
||||||
import cn.super12138.todo.utils.containerColor
|
import cn.super12138.todo.utils.containerColor
|
||||||
import cn.super12138.todo.utils.getPartialRoundedShape
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -57,111 +60,115 @@ fun TodoCard(
|
||||||
onCardClick: () -> Unit = {},
|
onCardClick: () -> Unit = {},
|
||||||
onCardLongClick: () -> Unit = {},
|
onCardLongClick: () -> Unit = {},
|
||||||
onChecked: () -> Unit = {},
|
onChecked: () -> Unit = {},
|
||||||
shape: CornerBasedShape = TodoDefaults.SettingsItemDefaultShape,
|
shapes: ButtonShapes = TodoDefaults.shapes(),
|
||||||
roundedShape: CornerBasedShape = TodoDefaults.SettingsItemRoundedShape,
|
|
||||||
topRounded: Boolean = false,
|
|
||||||
bottomRounded: Boolean = false
|
|
||||||
) {
|
) {
|
||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
// TODO: 滑动删除
|
// TODO: 滑动删除
|
||||||
val animatedContainerColor by animateColorAsState(if (selected) MaterialTheme.colorScheme.secondaryContainer else TodoDefaults.ContainerColor)
|
val animatedContainerColor by animateColorAsState(if (selected) MaterialTheme.colorScheme.secondaryContainer else TodoDefaults.ContainerColor)
|
||||||
|
|
||||||
Row(
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
val pressed by interactionSource.collectIsPressedAsState()
|
||||||
modifier = modifier
|
val animatedShape = shapeByInteraction(
|
||||||
.fillMaxWidth()
|
shapes = shapes,
|
||||||
.height(TodoDefaults.toDoCardHeight)
|
pressed = if (selected) true else pressed,
|
||||||
.clip(shape.getPartialRoundedShape(topRounded, bottomRounded, roundedShape))
|
animationSpec = TodoDefaults.shapesDefaultAnimationSpec
|
||||||
.combinedClickable(
|
)
|
||||||
onClick = {
|
|
||||||
VibrationUtils.performHapticFeedback(view)
|
Row(
|
||||||
onCardClick()
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
},
|
modifier = modifier
|
||||||
// 不再需要使用:VibrationUtils.performHapticFeedback(view, HapticFeedbackConstants.LONG_PRESS)
|
.fillMaxWidth()
|
||||||
// 因为 combinedClickable 在更新的 Compose 里已经处理好了触感反馈
|
.height(TodoDefaults.toDoCardHeight)
|
||||||
onLongClick = onCardLongClick
|
.clip(animatedShape)
|
||||||
)
|
.combinedClickable(
|
||||||
.background(animatedContainerColor)
|
interactionSource = interactionSource,
|
||||||
.padding(horizontal = TodoDefaults.screenHorizontalPadding)
|
onClick = {
|
||||||
|
VibrationUtils.performHapticFeedback(view)
|
||||||
|
onCardClick()
|
||||||
|
},
|
||||||
|
// 不再需要使用:VibrationUtils.performHapticFeedback(view, HapticFeedbackConstants.LONG_PRESS)
|
||||||
|
// 因为 combinedClickable 在更新的 Compose 里已经处理好了触感反馈
|
||||||
|
onLongClick = onCardLongClick
|
||||||
|
)
|
||||||
|
.background(animatedContainerColor)
|
||||||
|
.padding(horizontal = TodoDefaults.screenHorizontalPadding)
|
||||||
|
) {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = selected,
|
||||||
|
enter = fadeIn(MaterialTheme.motionScheme.fastSpatialSpec()) + expandHorizontally(
|
||||||
|
MaterialTheme.motionScheme.fastSpatialSpec()
|
||||||
|
),
|
||||||
|
exit = fadeOut(MaterialTheme.motionScheme.fastSpatialSpec()) + shrinkHorizontally(
|
||||||
|
MaterialTheme.motionScheme.fastSpatialSpec()
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
AnimatedVisibility(
|
Box(
|
||||||
visible = selected,
|
Modifier
|
||||||
enter = fadeIn(MaterialTheme.motionScheme.fastSpatialSpec()) + expandHorizontally(
|
.padding(end = 15.dp)
|
||||||
MaterialTheme.motionScheme.fastSpatialSpec()
|
.clip(CircleShape)
|
||||||
),
|
.background(MaterialTheme.colorScheme.secondary)
|
||||||
exit = fadeOut(MaterialTheme.motionScheme.fastSpatialSpec()) + shrinkHorizontally(
|
.padding(5.dp)
|
||||||
MaterialTheme.motionScheme.fastSpatialSpec()
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
Box(
|
Icon(
|
||||||
Modifier
|
painter = painterResource(R.drawable.ic_check),
|
||||||
.padding(end = 15.dp)
|
tint = contentColorFor(MaterialTheme.colorScheme.secondary),
|
||||||
.clip(CircleShape)
|
contentDescription = stringResource(R.string.tip_selected)
|
||||||
.background(MaterialTheme.colorScheme.secondary)
|
)
|
||||||
.padding(5.dp)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(R.drawable.ic_check),
|
|
||||||
tint = contentColorFor(MaterialTheme.colorScheme.secondary),
|
|
||||||
contentDescription = stringResource(R.string.tip_selected)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(
|
verticalArrangement = Arrangement.spacedBy(
|
||||||
space = 5.dp,
|
space = 5.dp,
|
||||||
alignment = Alignment.CenterVertically
|
alignment = Alignment.CenterVertically
|
||||||
),
|
),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = content,
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
textDecoration = if (completed) TextDecoration.LineThrough else TextDecoration.None,
|
||||||
|
modifier = Modifier.basicMarquee() // TODO: 后续评估性能影响
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(5.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Badge(containerColor = MaterialTheme.colorScheme.primary) {
|
||||||
text = content,
|
|
||||||
style = MaterialTheme.typography.titleLarge,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
textDecoration = if (completed) TextDecoration.LineThrough else TextDecoration.None,
|
|
||||||
modifier = Modifier.basicMarquee() // TODO: 后续评估性能影响
|
|
||||||
)
|
|
||||||
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(5.dp)
|
|
||||||
) {
|
|
||||||
Badge(containerColor = MaterialTheme.colorScheme.primary) {
|
|
||||||
Text(
|
|
||||||
text = category.ifEmpty { stringResource(R.string.tip_default_category) },
|
|
||||||
style = MaterialTheme.typography.labelMedium,
|
|
||||||
textDecoration = if (completed) TextDecoration.LineThrough else TextDecoration.None,
|
|
||||||
maxLines = 1
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(priority.nameRes),
|
text = category.ifEmpty { stringResource(R.string.tip_default_category) },
|
||||||
style = MaterialTheme.typography.labelMedium.copy(priority.containerColor()),
|
style = MaterialTheme.typography.labelMedium,
|
||||||
textDecoration = if (completed) TextDecoration.LineThrough else TextDecoration.None,
|
textDecoration = if (completed) TextDecoration.LineThrough else TextDecoration.None,
|
||||||
|
maxLines = 1
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
AnimatedVisibility(!selected && !completed) {
|
Text(
|
||||||
IconButton(
|
text = stringResource(priority.nameRes),
|
||||||
onClick = {
|
style = MaterialTheme.typography.labelMedium.copy(priority.containerColor()),
|
||||||
VibrationUtils.performHapticFeedback(view)
|
textDecoration = if (completed) TextDecoration.LineThrough else TextDecoration.None,
|
||||||
onChecked()
|
)
|
||||||
}
|
}
|
||||||
) {
|
}
|
||||||
Icon(
|
|
||||||
painter = painterResource(R.drawable.ic_check),
|
AnimatedVisibility(!selected && !completed) {
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
IconButton(
|
||||||
contentDescription = stringResource(R.string.tip_mark_completed)
|
onClick = {
|
||||||
)
|
VibrationUtils.performHapticFeedback(view)
|
||||||
|
onChecked()
|
||||||
}
|
}
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_check),
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
contentDescription = stringResource(R.string.tip_mark_completed)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*Box(
|
/*Box(
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -179,7 +186,7 @@ fun TodoCard(
|
||||||
)
|
)
|
||||||
}*/
|
}*/
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
||||||
175
app/src/main/kotlin/cn/super12138/todo/ui/theme/Shape.kt
Normal file
175
app/src/main/kotlin/cn/super12138/todo/ui/theme/Shape.kt
Normal file
|
|
@ -0,0 +1,175 @@
|
||||||
|
package cn.super12138.todo.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.Animatable
|
||||||
|
import androidx.compose.animation.core.AnimationVector1D
|
||||||
|
import androidx.compose.animation.core.FiniteAnimationSpec
|
||||||
|
import androidx.compose.foundation.shape.CornerBasedShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.ButtonShapes
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.SideEffect
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.key
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.graphics.Outline
|
||||||
|
import androidx.compose.ui.graphics.Shape
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.unit.Density
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
internal val ButtonShapes.hasRoundedCornerShapes: Boolean
|
||||||
|
get() = shape is RoundedCornerShape && pressedShape is RoundedCornerShape
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 来自 Compose 内部类
|
||||||
|
*
|
||||||
|
* 根据交互状态返回对应的形状
|
||||||
|
*
|
||||||
|
* * 如果 *原形状* 和 *按下形状* **均为圆角形状**则在形状变换时使用动画过渡
|
||||||
|
*
|
||||||
|
* @param shapes 按钮形状(需包含原形状和按下形状)
|
||||||
|
* @param pressed 是否处于按下状态
|
||||||
|
* @param animationSpec 动画曲线
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
fun shapeByInteraction(
|
||||||
|
shapes: ButtonShapes,
|
||||||
|
pressed: Boolean,
|
||||||
|
animationSpec: FiniteAnimationSpec<Float>,
|
||||||
|
): Shape {
|
||||||
|
val shape =
|
||||||
|
if (pressed) {
|
||||||
|
shapes.pressedShape
|
||||||
|
} else {
|
||||||
|
shapes.shape
|
||||||
|
}
|
||||||
|
|
||||||
|
ButtonDefaults.shapes()
|
||||||
|
if (shapes.hasRoundedCornerShapes)
|
||||||
|
return key(shapes) { rememberAnimatedShape(shape as RoundedCornerShape, animationSpec) }
|
||||||
|
|
||||||
|
return shape
|
||||||
|
}
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
internal class AnimatedShapeState(
|
||||||
|
val shape: RoundedCornerShape,
|
||||||
|
val spec: FiniteAnimationSpec<Float>,
|
||||||
|
) {
|
||||||
|
var size: Size = Size.Zero
|
||||||
|
var density: Density = Density(0f, 0f)
|
||||||
|
|
||||||
|
private var topStart: Animatable<Float, AnimationVector1D>? = null
|
||||||
|
|
||||||
|
private var topEnd: Animatable<Float, AnimationVector1D>? = null
|
||||||
|
|
||||||
|
private var bottomStart: Animatable<Float, AnimationVector1D>? = null
|
||||||
|
|
||||||
|
private var bottomEnd: Animatable<Float, AnimationVector1D>? = null
|
||||||
|
|
||||||
|
fun topStart(size: Size = this.size, density: Density = this.density): Float {
|
||||||
|
return (topStart ?: Animatable(shape.topStart.toPx(size, density)).also { topStart = it })
|
||||||
|
.value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun topEnd(size: Size = this.size, density: Density = this.density): Float {
|
||||||
|
return (topEnd ?: Animatable(shape.topEnd.toPx(size, density)).also { topEnd = it }).value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bottomStart(size: Size = this.size, density: Density = this.density): Float {
|
||||||
|
return (bottomStart
|
||||||
|
?: Animatable(shape.bottomStart.toPx(size, density)).also { bottomStart = it })
|
||||||
|
.value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bottomEnd(size: Size = this.size, density: Density = this.density): Float {
|
||||||
|
return (bottomEnd
|
||||||
|
?: Animatable(shape.bottomEnd.toPx(size, density)).also { bottomEnd = it })
|
||||||
|
.value
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun animateToShape(shape: CornerBasedShape) = coroutineScope {
|
||||||
|
launch { topStart?.animateTo(shape.topStart.toPx(size, density), spec) }
|
||||||
|
launch { topEnd?.animateTo(shape.topEnd.toPx(size, density), spec) }
|
||||||
|
launch { bottomStart?.animateTo(shape.bottomStart.toPx(size, density), spec) }
|
||||||
|
launch { bottomEnd?.animateTo(shape.bottomEnd.toPx(size, density), spec) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun rememberAnimatedShape(state: AnimatedShapeState): Shape {
|
||||||
|
val density = LocalDensity.current
|
||||||
|
state.density = density
|
||||||
|
|
||||||
|
return remember(density, state) {
|
||||||
|
object : ShapeWithHorizontalCenterOptically {
|
||||||
|
var clampedRange by mutableStateOf(0f..1f)
|
||||||
|
|
||||||
|
override fun offset(): Float {
|
||||||
|
val topStart = state.topStart().coerceIn(clampedRange)
|
||||||
|
val topEnd = state.topEnd().coerceIn(clampedRange)
|
||||||
|
val bottomStart = state.bottomStart().coerceIn(clampedRange)
|
||||||
|
val bottomEnd = state.bottomEnd().coerceIn(clampedRange)
|
||||||
|
val avgStart = (topStart + bottomStart) / 2
|
||||||
|
val avgEnd = (topEnd + bottomEnd) / 2
|
||||||
|
return CenterOpticallyCoefficient * (avgStart - avgEnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createOutline(
|
||||||
|
size: Size,
|
||||||
|
layoutDirection: LayoutDirection,
|
||||||
|
density: Density,
|
||||||
|
): Outline {
|
||||||
|
state.size = size
|
||||||
|
|
||||||
|
clampedRange = 0f..size.height / 2
|
||||||
|
return RoundedCornerShape(
|
||||||
|
topStart = state.topStart().coerceIn(clampedRange),
|
||||||
|
topEnd = state.topEnd().coerceIn(clampedRange),
|
||||||
|
bottomStart = state.bottomStart().coerceIn(clampedRange),
|
||||||
|
bottomEnd = state.bottomEnd().coerceIn(clampedRange),
|
||||||
|
)
|
||||||
|
.createOutline(size, layoutDirection, density)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun rememberAnimatedShape(
|
||||||
|
currentShape: RoundedCornerShape,
|
||||||
|
animationSpec: FiniteAnimationSpec<Float>,
|
||||||
|
): Shape {
|
||||||
|
val state =
|
||||||
|
remember(animationSpec) { AnimatedShapeState(shape = currentShape, spec = animationSpec) }
|
||||||
|
|
||||||
|
val channel = remember { Channel<RoundedCornerShape>(Channel.CONFLATED) }
|
||||||
|
|
||||||
|
SideEffect { channel.trySend(currentShape) }
|
||||||
|
LaunchedEffect(state, channel) {
|
||||||
|
for (target in channel) {
|
||||||
|
val newTarget = channel.tryReceive().getOrNull() ?: target
|
||||||
|
launch { state.animateToShape(newTarget) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rememberAnimatedShape(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal interface ShapeWithHorizontalCenterOptically : Shape {
|
||||||
|
fun offset(): Float
|
||||||
|
}
|
||||||
|
|
||||||
|
internal const val CenterOpticallyCoefficient = 0.11f
|
||||||
Loading…
Reference in a new issue