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"
|
||||
minSdk = 24
|
||||
targetSdk = 36
|
||||
versionCode = 952
|
||||
versionCode = 955
|
||||
versionName = "2.3.3"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
package cn.super12138.todo.ui
|
||||
|
||||
import androidx.compose.animation.core.FiniteAnimationSpec
|
||||
import androidx.compose.foundation.shape.CornerBasedShape
|
||||
import androidx.compose.material3.ButtonShapes
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
|
@ -33,7 +36,7 @@ object TodoDefaults {
|
|||
*/
|
||||
val settingsItemVerticalPadding = 16.dp
|
||||
|
||||
val settingsItemPadding = 2.dp
|
||||
val settingsItemPadding = 4.dp
|
||||
|
||||
/**
|
||||
* 待办进度条粗度
|
||||
|
|
@ -70,13 +73,27 @@ object TodoDefaults {
|
|||
@Composable
|
||||
get() = MaterialTheme.colorScheme.surfaceContainer
|
||||
|
||||
val SettingsItemDefaultShape: CornerBasedShape
|
||||
@Composable
|
||||
get() = MaterialTheme.shapes.small
|
||||
|
||||
val SettingsItemRoundedShape: CornerBasedShape
|
||||
val fadedEdgeWidth = 8.dp
|
||||
|
||||
val defaultShape: CornerBasedShape
|
||||
@Composable
|
||||
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
|
||||
}
|
||||
},
|
||||
topRounded = true
|
||||
}
|
||||
)
|
||||
}
|
||||
item {
|
||||
|
|
@ -104,8 +103,7 @@ fun SettingsAbout(
|
|||
leadingIconRes = R.drawable.ic_code_blocks,
|
||||
title = stringResource(R.string.pref_developer_options),
|
||||
description = stringResource(R.string.pref_developer_options_desc),
|
||||
onClick = toDevPage,
|
||||
bottomRounded = true
|
||||
onClick = toDevPage
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import androidx.compose.foundation.layout.Column
|
|||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
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.text.selection.SelectionContainer
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
|
|
@ -16,10 +16,8 @@ import androidx.compose.material3.FilledTonalButton
|
|||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -55,17 +53,11 @@ fun SettingsAboutLicence(
|
|||
modifier = modifier
|
||||
) {
|
||||
SettingsContainer(Modifier.fillMaxSize()) {
|
||||
itemsIndexed(
|
||||
items(
|
||||
items = libraries?.libraries ?: listOf(),
|
||||
key = { _, library -> library.artifactId }
|
||||
) { index, library ->
|
||||
key = { it.artifactId }
|
||||
) { library ->
|
||||
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(
|
||||
headlineContent = {
|
||||
Row(
|
||||
|
|
@ -126,9 +118,7 @@ fun SettingsAboutLicence(
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
topRounded = topRounded,
|
||||
bottomRounded = bottomRounded
|
||||
}
|
||||
)
|
||||
|
||||
BasicDialog(
|
||||
|
|
|
|||
|
|
@ -46,8 +46,7 @@ fun SettingsAppearance(
|
|||
leadingIconRes = R.drawable.ic_wand_stars,
|
||||
title = stringResource(R.string.pref_appearance_dynamic_color),
|
||||
description = stringResource(R.string.pref_appearance_dynamic_color_desc),
|
||||
onCheckedChange = { scope.launch { DataStoreManager.setDynamicColor(it) } },
|
||||
topRounded = true
|
||||
onCheckedChange = { scope.launch { DataStoreManager.setDynamicColor(it) } }
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -71,8 +70,7 @@ fun SettingsAppearance(
|
|||
item(key = 4) {
|
||||
ContrastPicker(
|
||||
currentContrast = ContrastLevel.fromFloat(contrastLevel),
|
||||
onContrastChange = { scope.launch { DataStoreManager.setContrastLevel(it.value) } },
|
||||
bottomRounded = true
|
||||
onContrastChange = { scope.launch { DataStoreManager.setContrastLevel(it.value) } }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,8 +113,7 @@ fun SettingsData(
|
|||
description = stringResource(R.string.pref_backup_desc),
|
||||
onClick = {
|
||||
backupLauncher.launch("Todo-backup-${SystemUtils.getTime()}.zip")
|
||||
},
|
||||
topRounded = true
|
||||
}
|
||||
)
|
||||
}
|
||||
item {
|
||||
|
|
@ -124,8 +123,7 @@ fun SettingsData(
|
|||
description = stringResource(R.string.pref_restore_desc),
|
||||
onClick = {
|
||||
restoreLauncher.launch(arrayOf("application/zip"))
|
||||
},
|
||||
bottomRounded = true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -135,9 +133,7 @@ fun SettingsData(
|
|||
leadingIconRes = R.drawable.ic_category,
|
||||
title = stringResource(R.string.pref_category_category_management),
|
||||
description = stringResource(R.string.pref_category_management_desc),
|
||||
onClick = toCategoryManager,
|
||||
topRounded = true,
|
||||
bottomRounded = true
|
||||
onClick = toCategoryManager
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,11 +92,6 @@ fun SettingsDataCategory(
|
|||
items = categories,
|
||||
key = { it }
|
||||
) { 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(
|
||||
headlineContent = {
|
||||
Text(
|
||||
|
|
@ -125,8 +120,6 @@ fun SettingsDataCategory(
|
|||
initialCategory = category
|
||||
showDialog = true
|
||||
},
|
||||
topRounded = topRounded,
|
||||
bottomRounded = bottomRounded,
|
||||
modifier = Modifier.animateItem(
|
||||
fadeInSpec = MaterialTheme.motionScheme.defaultEffectsSpec(),
|
||||
placementSpec = MaterialTheme.motionScheme.defaultSpatialSpec(),
|
||||
|
|
|
|||
|
|
@ -27,9 +27,7 @@ fun SettingsDeveloperOptions(
|
|||
SettingsItem(
|
||||
leadingIconRes = R.drawable.ic_padding,
|
||||
title = stringResource(R.string.pref_padding),
|
||||
onClick = toPaddingPage,
|
||||
topRounded = true,
|
||||
bottomRounded = true
|
||||
onClick = toPaddingPage
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import cn.super12138.todo.R
|
||||
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 hapticFeedback by DataStoreManager.hapticFeedbackFlow.collectAsState(initial = Constants.PREF_HAPTIC_FEEDBACK_DEFAULT)
|
||||
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
var showSortingMethodDialog by rememberSaveable { mutableStateOf(false) }
|
||||
TopAppBarScaffold(
|
||||
|
|
@ -57,8 +55,7 @@ fun SettingsInterface(
|
|||
title = stringResource(R.string.pref_show_completed),
|
||||
description = stringResource(R.string.pref_show_completed_desc),
|
||||
checked = showCompleted,
|
||||
onCheckedChange = { scope.launch { DataStoreManager.setShowCompleted(it) } },
|
||||
topRounded = true
|
||||
onCheckedChange = { scope.launch { DataStoreManager.setShowCompleted(it) } }
|
||||
)
|
||||
}
|
||||
item {
|
||||
|
|
@ -66,8 +63,7 @@ fun SettingsInterface(
|
|||
leadingIconRes = R.drawable.ic_sort,
|
||||
title = stringResource(R.string.pref_sorting_method),
|
||||
description = stringResource(SortingMethod.fromId(sortingMethod).nameRes),
|
||||
onClick = { showSortingMethodDialog = true },
|
||||
bottomRounded = true
|
||||
onClick = { showSortingMethodDialog = true }
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -78,8 +74,7 @@ fun SettingsInterface(
|
|||
leadingIconRes = R.drawable.ic_shield,
|
||||
title = stringResource(R.string.pref_secure_mode),
|
||||
description = stringResource(R.string.pref_secure_mode_desc),
|
||||
onCheckedChange = { scope.launch { DataStoreManager.setSecureMode(it) } },
|
||||
topRounded = true
|
||||
onCheckedChange = { scope.launch { DataStoreManager.setSecureMode(it) } }
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -89,8 +84,7 @@ fun SettingsInterface(
|
|||
leadingIconRes = R.drawable.ic_touch_long,
|
||||
title = stringResource(R.string.pref_haptic_feedback),
|
||||
description = stringResource(R.string.pref_haptic_feedback_desc),
|
||||
onCheckedChange = { scope.launch { DataStoreManager.setHapticFeedback(it) } },
|
||||
bottomRounded = true
|
||||
onCheckedChange = { scope.launch { DataStoreManager.setHapticFeedback(it) } }
|
||||
)
|
||||
SettingsPlainBox(stringResource(R.string.pref_haptic_feedback_more_info))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,8 +30,7 @@ fun SettingsMain(
|
|||
leadingIconRes = R.drawable.ic_palette,
|
||||
title = stringResource(R.string.pref_appearance),
|
||||
description = stringResource(R.string.pref_appearance_desc),
|
||||
onClick = toAppearancePage,
|
||||
topRounded = true
|
||||
onClick = toAppearancePage
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -58,8 +57,7 @@ fun SettingsMain(
|
|||
leadingIconRes = R.drawable.ic_info,
|
||||
title = stringResource(R.string.pref_about),
|
||||
description = stringResource(R.string.pref_about_desc),
|
||||
onClick = toAboutPage,
|
||||
bottomRounded = true
|
||||
onClick = toAboutPage
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ import androidx.compose.ui.unit.Dp
|
|||
import androidx.compose.ui.unit.sp
|
||||
import cn.super12138.todo.ui.TodoDefaults
|
||||
import cn.super12138.todo.utils.drawFadedEdge
|
||||
import cn.super12138.todo.utils.getPartialRoundedShape
|
||||
|
||||
@Composable
|
||||
fun RowSettingsItem(
|
||||
|
|
@ -44,8 +43,6 @@ fun RowSettingsItem(
|
|||
scrollState: ScrollState = rememberScrollState(),
|
||||
fadedEdgeWidth: Dp,
|
||||
maskColor: Color = TodoDefaults.ContainerColor,
|
||||
topRounded: Boolean = false,
|
||||
bottomRounded: Boolean = false,
|
||||
content: @Composable RowScope.() -> Unit
|
||||
) {
|
||||
MoreContentSettingsItem(
|
||||
|
|
@ -54,8 +51,6 @@ fun RowSettingsItem(
|
|||
description = description,
|
||||
trailingContent = trailingContent,
|
||||
background = background,
|
||||
topRounded = topRounded,
|
||||
bottomRounded = bottomRounded,
|
||||
modifier = modifier
|
||||
) {
|
||||
Row(
|
||||
|
|
@ -98,8 +93,6 @@ fun LazyRowSettingsItem(
|
|||
verticalAlignment: Alignment.Vertical = Alignment.Top,
|
||||
fadedEdgeWidth: Dp,
|
||||
maskColor: Color = TodoDefaults.ContainerColor,
|
||||
topRounded: Boolean = false,
|
||||
bottomRounded: Boolean = false,
|
||||
content: LazyListScope.() -> Unit
|
||||
) {
|
||||
MoreContentSettingsItem(
|
||||
|
|
@ -107,8 +100,6 @@ fun LazyRowSettingsItem(
|
|||
title = title,
|
||||
description = description,
|
||||
trailingContent = trailingContent,
|
||||
topRounded = topRounded,
|
||||
bottomRounded = bottomRounded,
|
||||
background = background,
|
||||
modifier = modifier
|
||||
) {
|
||||
|
|
@ -147,17 +138,14 @@ fun MoreContentSettingsItem(
|
|||
description: String? = null,
|
||||
trailingContent: (@Composable () -> Unit)? = null,
|
||||
background: Color = TodoDefaults.ContainerColor,
|
||||
shape: CornerBasedShape = TodoDefaults.SettingsItemDefaultShape,
|
||||
roundedShape: CornerBasedShape = TodoDefaults.SettingsItemRoundedShape,
|
||||
topRounded: Boolean = false,
|
||||
bottomRounded: Boolean = false,
|
||||
shape: CornerBasedShape = TodoDefaults.defaultShape,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.clip(shape.getPartialRoundedShape(topRounded, bottomRounded, roundedShape))
|
||||
.clip(shape)
|
||||
.background(background)
|
||||
.padding(
|
||||
horizontal = TodoDefaults.settingsItemHorizontalPadding,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
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.Column
|
||||
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.selectableGroup
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
|
|
@ -22,6 +27,7 @@ import androidx.compose.ui.semantics.Role
|
|||
import androidx.compose.ui.unit.dp
|
||||
import cn.super12138.todo.ui.TodoDefaults
|
||||
import cn.super12138.todo.ui.components.BasicDialog
|
||||
import cn.super12138.todo.ui.theme.shapeByInteraction
|
||||
import cn.super12138.todo.utils.VibrationUtils
|
||||
|
||||
@Composable
|
||||
|
|
@ -42,7 +48,6 @@ fun SettingsRadioDialog(
|
|||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||
modifier = Modifier
|
||||
.clip(TodoDefaults.SettingsItemRoundedShape)
|
||||
.selectableGroup()
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
|
|
@ -63,6 +68,7 @@ fun SettingsRadioDialog(
|
|||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun RadioItem(
|
||||
selected: Boolean,
|
||||
|
|
@ -71,13 +77,23 @@ fun RadioItem(
|
|||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val view = LocalView.current
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val pressed by interactionSource.collectIsPressedAsState()
|
||||
|
||||
Row(
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp)
|
||||
.clip(TodoDefaults.SettingsItemDefaultShape)
|
||||
.clip(
|
||||
shapeByInteraction(
|
||||
shapes = TodoDefaults.shapes(),
|
||||
pressed = pressed,
|
||||
animationSpec = TodoDefaults.shapesDefaultAnimationSpec
|
||||
)
|
||||
)
|
||||
// .background(MaterialTheme.colorScheme.surfaceContainerHighest)
|
||||
.selectable(
|
||||
interactionSource = interactionSource,
|
||||
selected = selected,
|
||||
onClick = {
|
||||
VibrationUtils.performHapticFeedback(view)
|
||||
|
|
|
|||
|
|
@ -3,17 +3,21 @@ package cn.super12138.todo.ui.pages.settings.components
|
|||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.background
|
||||
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.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
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.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
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.unit.sp
|
||||
import cn.super12138.todo.ui.TodoDefaults
|
||||
import cn.super12138.todo.ui.theme.shapeByInteraction
|
||||
import cn.super12138.todo.utils.VibrationUtils
|
||||
import cn.super12138.todo.utils.getPartialRoundedShape
|
||||
|
||||
// Leading icon as drawable resource
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun SettingsItem(
|
||||
modifier: Modifier = Modifier,
|
||||
@DrawableRes leadingIconRes: Int,
|
||||
title: String,
|
||||
description: String? = null,
|
||||
topRounded: Boolean = false,
|
||||
bottomRounded: Boolean = false,
|
||||
enableClick: Boolean = true,
|
||||
onClick: () -> Unit = {}
|
||||
) = SettingsItem(
|
||||
|
|
@ -44,8 +47,6 @@ fun SettingsItem(
|
|||
title = title,
|
||||
description = description,
|
||||
trailingContent = null,
|
||||
topRounded = topRounded,
|
||||
bottomRounded = bottomRounded,
|
||||
enableClick = enableClick,
|
||||
onClick = onClick,
|
||||
modifier = modifier
|
||||
|
|
@ -61,8 +62,7 @@ fun SettingsItem(
|
|||
description: String? = null,
|
||||
trailingContent: (@Composable () -> Unit)? = null,
|
||||
background: Color = TodoDefaults.ContainerColor,
|
||||
topRounded: Boolean = false,
|
||||
bottomRounded: Boolean = false,
|
||||
shapes: ButtonShapes = TodoDefaults.shapes(),
|
||||
enableClick: Boolean = true,
|
||||
onClick: () -> Unit = {}
|
||||
) = SettingsItem(
|
||||
|
|
@ -91,22 +91,20 @@ fun SettingsItem(
|
|||
description = description,
|
||||
trailingContent = trailingContent,
|
||||
background = background,
|
||||
topRounded = topRounded,
|
||||
bottomRounded = bottomRounded,
|
||||
shapes = shapes,
|
||||
enableClick = enableClick,
|
||||
onClick = onClick,
|
||||
modifier = modifier
|
||||
)
|
||||
|
||||
// Leading icon as ImageVector
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun SettingsItem(
|
||||
modifier: Modifier = Modifier,
|
||||
leadingIcon: ImageVector? = null,
|
||||
title: String,
|
||||
description: String? = null,
|
||||
topRounded: Boolean = false,
|
||||
bottomRounded: Boolean = false,
|
||||
enableClick: Boolean = true,
|
||||
onClick: () -> Unit = {}
|
||||
) = SettingsItem(
|
||||
|
|
@ -114,13 +112,12 @@ fun SettingsItem(
|
|||
title = title,
|
||||
description = description,
|
||||
trailingContent = null,
|
||||
topRounded = topRounded,
|
||||
bottomRounded = bottomRounded,
|
||||
enableClick = enableClick,
|
||||
onClick = onClick,
|
||||
modifier = modifier
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun SettingsItem(
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
@ -129,8 +126,7 @@ fun SettingsItem(
|
|||
description: String? = null,
|
||||
trailingContent: (@Composable () -> Unit)? = null,
|
||||
background: Color = TodoDefaults.ContainerColor,
|
||||
topRounded: Boolean = false,
|
||||
bottomRounded: Boolean = false,
|
||||
shapes: ButtonShapes = TodoDefaults.shapes(),
|
||||
enableClick: Boolean = true,
|
||||
onClick: () -> Unit = {}
|
||||
) = SettingsItem(
|
||||
|
|
@ -148,9 +144,8 @@ fun SettingsItem(
|
|||
description = description,
|
||||
trailingContent = trailingContent,
|
||||
background = background,
|
||||
topRounded = topRounded,
|
||||
bottomRounded = bottomRounded,
|
||||
enableClick = enableClick,
|
||||
shapes = shapes,
|
||||
onClick = onClick,
|
||||
modifier = modifier
|
||||
)
|
||||
|
|
@ -165,10 +160,7 @@ fun SettingsItem(
|
|||
description: String? = null,
|
||||
trailingContent: (@Composable () -> Unit)? = null,
|
||||
background: Color = TodoDefaults.ContainerColor,
|
||||
shape: CornerBasedShape = TodoDefaults.SettingsItemDefaultShape,
|
||||
roundedShape: CornerBasedShape = TodoDefaults.SettingsItemRoundedShape,
|
||||
topRounded: Boolean = false,
|
||||
bottomRounded: Boolean = false,
|
||||
shapes: ButtonShapes = TodoDefaults.shapes(),
|
||||
enableClick: Boolean = true,
|
||||
onClick: () -> Unit = {},
|
||||
) = SettingsItem(
|
||||
|
|
@ -199,10 +191,7 @@ fun SettingsItem(
|
|||
},
|
||||
trailingContent = trailingContent,
|
||||
background = background,
|
||||
shape = shape,
|
||||
roundedShape = roundedShape,
|
||||
topRounded = topRounded,
|
||||
bottomRounded = bottomRounded,
|
||||
shapes = shapes,
|
||||
enableClick = enableClick,
|
||||
onClick = onClick
|
||||
)
|
||||
|
|
@ -216,20 +205,23 @@ fun SettingsItem(
|
|||
supportingContent: (@Composable () -> Unit)? = null,
|
||||
trailingContent: (@Composable () -> Unit)? = null,
|
||||
background: Color = TodoDefaults.ContainerColor,
|
||||
shape: CornerBasedShape = TodoDefaults.SettingsItemDefaultShape,
|
||||
roundedShape: CornerBasedShape = TodoDefaults.SettingsItemRoundedShape,
|
||||
topRounded: Boolean = false,
|
||||
bottomRounded: Boolean = false,
|
||||
shapes: ButtonShapes = TodoDefaults.shapes(),
|
||||
interactionSource: MutableInteractionSource? = null,
|
||||
enableClick: Boolean = true,
|
||||
onClick: () -> Unit = {},
|
||||
) {
|
||||
val view = LocalView.current
|
||||
val userInteractionSource = interactionSource ?: remember { MutableInteractionSource() }
|
||||
val pressed by userInteractionSource.collectIsPressedAsState()
|
||||
val animatedShape = shapeByInteraction(shapes, pressed, TodoDefaults.shapesDefaultAnimationSpec)
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.clip(shape.getPartialRoundedShape(topRounded, bottomRounded, roundedShape))
|
||||
.clip(animatedShape)
|
||||
.clickable(
|
||||
interactionSource = userInteractionSource,
|
||||
enabled = enableClick,
|
||||
onClick = {
|
||||
VibrationUtils.performHapticFeedback(view)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package cn.super12138.todo.ui.pages.settings.components
|
|||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.runtime.Composable
|
||||
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.utils.VibrationUtils
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun SwitchSettingsItem(
|
||||
modifier: Modifier = Modifier,
|
||||
@DrawableRes leadingIconRes: Int,
|
||||
title: String,
|
||||
description: String? = null,
|
||||
topRounded: Boolean = false,
|
||||
bottomRounded: Boolean = false,
|
||||
checked: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit
|
||||
) {
|
||||
|
|
@ -34,12 +34,11 @@ fun SwitchSettingsItem(
|
|||
)
|
||||
},
|
||||
onClick = { onCheckedChange(!checked) },
|
||||
topRounded = topRounded,
|
||||
bottomRounded = bottomRounded,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun SwitchSettingsItem(
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
|
|||
|
|
@ -38,16 +38,12 @@ fun ContrastPicker(
|
|||
modifier: Modifier = Modifier,
|
||||
currentContrast: ContrastLevel,
|
||||
onContrastChange: (ContrastLevel) -> Unit,
|
||||
topRounded: Boolean = false,
|
||||
bottomRounded: Boolean = false
|
||||
) {
|
||||
val view = LocalView.current
|
||||
val context = LocalContext.current
|
||||
MoreContentSettingsItem(
|
||||
title = stringResource(R.string.pref_contrast_level),
|
||||
description = stringResource(R.string.pref_contrast_level_desc),
|
||||
topRounded = topRounded,
|
||||
bottomRounded = bottomRounded,
|
||||
modifier = modifier
|
||||
) {
|
||||
val contrastLevelName = ContrastLevel.entries.map { stringResource(it.nameRes) }
|
||||
|
|
|
|||
|
|
@ -21,8 +21,6 @@ fun DarkModePicker(
|
|||
modifier: Modifier = Modifier,
|
||||
currentDarkMode: () -> DarkMode,
|
||||
onDarkModeChange: (darkMode: DarkMode) -> Unit,
|
||||
topRounded: Boolean = false,
|
||||
bottomRounded: Boolean = false
|
||||
) {
|
||||
val isInDarkTheme = isSystemInDarkTheme()
|
||||
|
||||
|
|
@ -33,8 +31,6 @@ fun DarkModePicker(
|
|||
description = stringResource(R.string.pref_dark_mode_desc),
|
||||
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
||||
fadedEdgeWidth = TodoDefaults.fadedEdgeWidth,
|
||||
topRounded = topRounded,
|
||||
bottomRounded = bottomRounded,
|
||||
modifier = modifier
|
||||
) {
|
||||
items(items = darkModeList, key = { it.id }) {
|
||||
|
|
|
|||
|
|
@ -25,8 +25,6 @@ fun PalettePicker(
|
|||
isDynamicColor: Boolean,
|
||||
isDarkMode: DarkMode,
|
||||
contrastLevel: ContrastLevel,
|
||||
topRounded: Boolean = false,
|
||||
bottomRounded: Boolean = false
|
||||
) {
|
||||
val paletteOptions = remember { PaletteStyle.entries.toList() }
|
||||
|
||||
|
|
@ -35,8 +33,6 @@ fun PalettePicker(
|
|||
description = stringResource(R.string.pref_palette_style_desc),
|
||||
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
||||
fadedEdgeWidth = TodoDefaults.fadedEdgeWidth,
|
||||
topRounded = topRounded,
|
||||
bottomRounded = bottomRounded,
|
||||
modifier = modifier
|
||||
) {
|
||||
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.size
|
||||
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.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
|
|
@ -119,12 +119,10 @@ fun SharedTransitionScope.TasksPage(
|
|||
)
|
||||
}
|
||||
} else {
|
||||
itemsIndexed(
|
||||
items(
|
||||
items = filteredTodoList,
|
||||
key = { _, task -> task.id }
|
||||
) { index, task ->
|
||||
val topRounded by remember { derivedStateOf { index == 0 } }
|
||||
val bottomRounded by remember { derivedStateOf { index == filteredTodoList.size - 1 } }
|
||||
key = { it.id }
|
||||
) { task ->
|
||||
TodoCard(
|
||||
// id = item.id,
|
||||
content = task.content,
|
||||
|
|
@ -144,8 +142,6 @@ fun SharedTransitionScope.TasksPage(
|
|||
viewModel.updateTodo(task.copy(isCompleted = true))
|
||||
viewModel.playConfetti()
|
||||
},
|
||||
topRounded = topRounded,
|
||||
bottomRounded = bottomRounded,
|
||||
modifier = Modifier
|
||||
.sharedBounds(
|
||||
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.basicMarquee
|
||||
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.Box
|
||||
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.padding
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.CornerBasedShape
|
||||
import androidx.compose.material3.Badge
|
||||
import androidx.compose.material3.ButtonShapes
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
|
|
@ -28,6 +30,7 @@ import androidx.compose.material3.Text
|
|||
import androidx.compose.material3.contentColorFor
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
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.logic.model.Priority
|
||||
import cn.super12138.todo.ui.TodoDefaults
|
||||
import cn.super12138.todo.ui.theme.shapeByInteraction
|
||||
import cn.super12138.todo.utils.VibrationUtils
|
||||
import cn.super12138.todo.utils.containerColor
|
||||
import cn.super12138.todo.utils.getPartialRoundedShape
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
|
|
@ -57,111 +60,115 @@ fun TodoCard(
|
|||
onCardClick: () -> Unit = {},
|
||||
onCardLongClick: () -> Unit = {},
|
||||
onChecked: () -> Unit = {},
|
||||
shape: CornerBasedShape = TodoDefaults.SettingsItemDefaultShape,
|
||||
roundedShape: CornerBasedShape = TodoDefaults.SettingsItemRoundedShape,
|
||||
topRounded: Boolean = false,
|
||||
bottomRounded: Boolean = false
|
||||
shapes: ButtonShapes = TodoDefaults.shapes(),
|
||||
) {
|
||||
val view = LocalView.current
|
||||
// TODO: 滑动删除
|
||||
val animatedContainerColor by animateColorAsState(if (selected) MaterialTheme.colorScheme.secondaryContainer else TodoDefaults.ContainerColor)
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(TodoDefaults.toDoCardHeight)
|
||||
.clip(shape.getPartialRoundedShape(topRounded, bottomRounded, roundedShape))
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
VibrationUtils.performHapticFeedback(view)
|
||||
onCardClick()
|
||||
},
|
||||
// 不再需要使用:VibrationUtils.performHapticFeedback(view, HapticFeedbackConstants.LONG_PRESS)
|
||||
// 因为 combinedClickable 在更新的 Compose 里已经处理好了触感反馈
|
||||
onLongClick = onCardLongClick
|
||||
)
|
||||
.background(animatedContainerColor)
|
||||
.padding(horizontal = TodoDefaults.screenHorizontalPadding)
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val pressed by interactionSource.collectIsPressedAsState()
|
||||
val animatedShape = shapeByInteraction(
|
||||
shapes = shapes,
|
||||
pressed = if (selected) true else pressed,
|
||||
animationSpec = TodoDefaults.shapesDefaultAnimationSpec
|
||||
)
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(TodoDefaults.toDoCardHeight)
|
||||
.clip(animatedShape)
|
||||
.combinedClickable(
|
||||
interactionSource = interactionSource,
|
||||
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(
|
||||
visible = selected,
|
||||
enter = fadeIn(MaterialTheme.motionScheme.fastSpatialSpec()) + expandHorizontally(
|
||||
MaterialTheme.motionScheme.fastSpatialSpec()
|
||||
),
|
||||
exit = fadeOut(MaterialTheme.motionScheme.fastSpatialSpec()) + shrinkHorizontally(
|
||||
MaterialTheme.motionScheme.fastSpatialSpec()
|
||||
)
|
||||
Box(
|
||||
Modifier
|
||||
.padding(end = 15.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.secondary)
|
||||
.padding(5.dp)
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.padding(end = 15.dp)
|
||||
.clip(CircleShape)
|
||||
.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)
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_check),
|
||||
tint = contentColorFor(MaterialTheme.colorScheme.secondary),
|
||||
contentDescription = stringResource(R.string.tip_selected)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(
|
||||
space = 5.dp,
|
||||
alignment = Alignment.CenterVertically
|
||||
),
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxSize()
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(
|
||||
space = 5.dp,
|
||||
alignment = Alignment.CenterVertically
|
||||
),
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.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(
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
Badge(containerColor = MaterialTheme.colorScheme.primary) {
|
||||
Text(
|
||||
text = stringResource(priority.nameRes),
|
||||
style = MaterialTheme.typography.labelMedium.copy(priority.containerColor()),
|
||||
text = category.ifEmpty { stringResource(R.string.tip_default_category) },
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
textDecoration = if (completed) TextDecoration.LineThrough else TextDecoration.None,
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(!selected && !completed) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
VibrationUtils.performHapticFeedback(view)
|
||||
onChecked()
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_check),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
contentDescription = stringResource(R.string.tip_mark_completed)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(priority.nameRes),
|
||||
style = MaterialTheme.typography.labelMedium.copy(priority.containerColor()),
|
||||
textDecoration = if (completed) TextDecoration.LineThrough else TextDecoration.None,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(!selected && !completed) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
VibrationUtils.performHapticFeedback(view)
|
||||
onChecked()
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_check),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
contentDescription = stringResource(R.string.tip_mark_completed)
|
||||
)
|
||||
}
|
||||
|
||||
/*Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
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