refactor: 对组件进行 Material 3 Expressive 适配

This commit is contained in:
Super12138 2025-10-25 16:47:11 +08:00
parent bc189c9b9c
commit 96fdb86e91
11 changed files with 177 additions and 85 deletions

View file

@ -6,14 +6,19 @@ import cn.super12138.todo.R
enum class SortingMethod(val id: Int) {
// 按添加先后顺序
Sequential(1),
// 按学科
Category(2),
// 按优先级
Priority(3),
// 按完成情况
Completion(4),
// 按字母升序
AlphabeticalAscending(5),
// 按字母降序
AlphabeticalDescending(6);

View file

@ -22,4 +22,24 @@ object TodoDefaults {
* 设置项垂直边距
*/
val settingsItemVerticalPadding = 20.dp
/**
* 待办进度条粗度
*/
val trackThickness = 7.0.dp
/**
* 待办进度条波长
*/
val waveLength = 35.dp
/**
* 待办进度条波速
*/
val waveSpeed = 3.dp
/**
* 待办进度条波幅
*/
const val waveAmplitude = 0.6f
}

View file

@ -1,11 +1,5 @@
package cn.super12138.todo.ui.components
import android.util.Log
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandIn
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkOut
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.padding
@ -18,19 +12,16 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import cn.super12138.todo.R
import cn.super12138.todo.utils.VibrationUtils
import kotlin.math.log
/**
* 部分参考https://github.com/Rhythamtech/FilterChipGroup-Compose-Android/blob/main/FilterChipGroup.kt
@ -75,20 +66,19 @@ private fun FilterChipItem(
FilterChip(
selected = selected,
onClick = onClick,
leadingIcon = {
AnimatedVisibility(
visible = selected,
enter = expandIn(expandFrom = Alignment.CenterStart) + fadeIn(),
exit = shrinkOut(shrinkTowards = Alignment.CenterStart) + fadeOut()
) {
Icon(
imageVector = Icons.Outlined.Check,
contentDescription = stringResource(R.string.tip_select_this),
modifier = Modifier.size(FilterChipDefaults.IconSize)
)
}
},
label = { Text(text) },
leadingIcon =
if (selected) {
{
Icon(
imageVector = Icons.Outlined.Check,
contentDescription = stringResource(R.string.tip_select_this),
modifier = Modifier.size(FilterChipDefaults.IconSize)
)
}
} else {
null
},
modifier = modifier.padding(end = 10.dp)
)
}

View file

@ -6,6 +6,8 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ErrorOutline
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
@ -50,6 +52,7 @@ fun ConfirmDialog(
)
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun BasicDialog(
modifier: Modifier = Modifier,
@ -83,7 +86,8 @@ fun BasicDialog(
onClick = {
VibrationUtils.performHapticFeedback(view)
onConfirm()
}
},
shapes = ButtonDefaults.shapes()
) {
Text(confirmButton)
}
@ -94,7 +98,8 @@ fun BasicDialog(
onClick = {
VibrationUtils.performHapticFeedback(view)
onDismiss()
}
},
shapes = ButtonDefaults.shapes()
) {
Text(it)
}

View file

@ -1,39 +1,47 @@
package cn.super12138.todo.ui.components
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.CircularWavyProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.ProgressIndicatorDefaults
import androidx.compose.material3.WavyProgressIndicatorDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.unit.Dp
@OptIn(ExperimentalMaterial3Api::class)
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun AnimatedCircularProgressIndicator(
fun AnimatedCircularWavyProgressIndicator(
progress: Float,
modifier: Modifier = Modifier,
color: Color = ProgressIndicatorDefaults.circularColor,
strokeWidth: Dp = ProgressIndicatorDefaults.CircularStrokeWidth,
trackColor: Color = ProgressIndicatorDefaults.circularDeterminateTrackColor,
strokeCap: StrokeCap = ProgressIndicatorDefaults.CircularDeterminateStrokeCap,
gapSize: Dp = ProgressIndicatorDefaults.CircularIndicatorTrackGapSize
color: Color = WavyProgressIndicatorDefaults.indicatorColor,
trackColor: Color = WavyProgressIndicatorDefaults.trackColor,
stroke: Stroke = WavyProgressIndicatorDefaults.circularIndicatorStroke,
trackStroke: Stroke = WavyProgressIndicatorDefaults.circularTrackStroke,
gapSize: Dp = WavyProgressIndicatorDefaults.CircularIndicatorTrackGapSize,
amplitude: (progress: Float) -> Float = WavyProgressIndicatorDefaults.indicatorAmplitude,
wavelength: Dp = WavyProgressIndicatorDefaults.CircularWavelength,
waveSpeed: Dp = wavelength,
) {
val animatedProgress by animateFloatAsState(
targetValue = progress,
animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec
)
CircularProgressIndicator(
CircularWavyProgressIndicator(
progress = { animatedProgress },
modifier = modifier,
color = color,
strokeWidth = strokeWidth,
trackColor = trackColor,
strokeCap = strokeCap,
gapSize = gapSize
stroke = stroke,
trackStroke = trackStroke,
gapSize = gapSize,
amplitude = amplitude,
wavelength = wavelength,
waveSpeed = waveSpeed,
)
}

View file

@ -13,9 +13,12 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ExitToApp
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.LargeTopAppBar
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SmallExtendedFloatingActionButton
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
@ -40,12 +43,11 @@ import cn.super12138.todo.ui.activities.CrashActivity.Companion.BRAND_PREFIX
import cn.super12138.todo.ui.activities.CrashActivity.Companion.CRASH_TIME_PREFIX
import cn.super12138.todo.ui.activities.CrashActivity.Companion.DEVICE_SDK_PREFIX
import cn.super12138.todo.ui.activities.CrashActivity.Companion.MODEL_PREFIX
import cn.super12138.todo.ui.components.AnimatedExtendedFloatingActionButton
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
@OptIn(ExperimentalMaterial3Api::class)
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun CrashPage(
crashLog: String,
@ -77,11 +79,16 @@ fun CrashPage(
)
},
floatingActionButton = {
AnimatedExtendedFloatingActionButton(
onClick = exitApp,
icon = Icons.AutoMirrored.Outlined.ExitToApp,
text = stringResource(R.string.action_exit_app),
expanded = isExpanded
SmallExtendedFloatingActionButton(
text = { Text(stringResource(R.string.action_exit_app)) },
icon = {
Icon(
imageVector = Icons.AutoMirrored.Outlined.ExitToApp,
contentDescription = null
)
},
expanded = isExpanded,
onClick = exitApp
)
},
contentWindowInsets = WindowInsets(0, 0, 0, 0)

View file

@ -23,7 +23,10 @@ import androidx.compose.material.icons.automirrored.outlined.Undo
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.Save
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SmallExtendedFloatingActionButton
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
@ -45,7 +48,6 @@ import cn.super12138.todo.constants.Constants
import cn.super12138.todo.logic.database.TodoEntity
import cn.super12138.todo.logic.datastore.DataStoreManager
import cn.super12138.todo.ui.TodoDefaults
import cn.super12138.todo.ui.components.AnimatedExtendedFloatingActionButton
import cn.super12138.todo.ui.components.ChipItem
import cn.super12138.todo.ui.components.ConfirmDialog
import cn.super12138.todo.ui.components.LargeTopAppBarScaffold
@ -56,7 +58,10 @@ import cn.super12138.todo.ui.pages.editor.components.TodoPrioritySlider
import cn.super12138.todo.ui.pages.editor.state.rememberEditorState
import cn.super12138.todo.utils.VibrationUtils
@OptIn(ExperimentalMaterial3Api::class, ExperimentalSharedTransitionApi::class)
@OptIn(
ExperimentalMaterial3Api::class, ExperimentalSharedTransitionApi::class,
ExperimentalMaterial3ExpressiveApi::class
)
@Composable
fun TodoEditorPage(
modifier: Modifier = Modifier,
@ -119,22 +124,32 @@ fun TodoEditorPage(
with(sharedTransitionScope) {
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
if (toDo !== null) {
AnimatedExtendedFloatingActionButton(
icon = Icons.Outlined.Delete,
text = stringResource(R.string.action_delete),
SmallExtendedFloatingActionButton(
text = { Text(stringResource(R.string.action_delete)) },
icon = {
Icon(
imageVector = Icons.Outlined.Delete,
contentDescription = null
)
},
expanded = true,
containerColor = MaterialTheme.colorScheme.errorContainer,
onClick = { uiState.showDeleteConfirmDialog = true },
modifier = Modifier.imePadding()
)
}
AnimatedExtendedFloatingActionButton(
icon = Icons.Outlined.Save,
text = stringResource(R.string.action_save),
SmallExtendedFloatingActionButton(
text = { Text(stringResource(R.string.action_save)) },
icon = {
Icon(
imageVector = Icons.Outlined.Save,
contentDescription = null
)
},
expanded = true,
onClick = {
if (uiState.setErrorIfNotValid()) {
return@AnimatedExtendedFloatingActionButton
return@SmallExtendedFloatingActionButton
} else {
uiState.clearError()
val newTodo = TodoEntity(

View file

@ -18,7 +18,11 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Add
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SmallExtendedFloatingActionButton
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
@ -35,12 +39,11 @@ import cn.super12138.todo.R
import cn.super12138.todo.constants.Constants
import cn.super12138.todo.logic.database.TodoEntity
import cn.super12138.todo.logic.datastore.DataStoreManager
import cn.super12138.todo.ui.components.AnimatedExtendedFloatingActionButton
import cn.super12138.todo.ui.components.ConfirmDialog
import cn.super12138.todo.ui.pages.main.components.TodoTopAppBar
import cn.super12138.todo.ui.viewmodels.MainViewModel
@OptIn(ExperimentalSharedTransitionApi::class)
@OptIn(ExperimentalSharedTransitionApi::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun MainPage(
viewModel: MainViewModel,
@ -65,6 +68,7 @@ fun MainPage(
val completedTasks by remember { derivedStateOf { toDoList.count { it.isCompleted } } }
val filteredTodoList =
if (showCompleted) toDoList else toDoList.filter { item -> !item.isCompleted }
val expandedFab by remember { derivedStateOf { listState.firstVisibleItemIndex == 0 } }
// 当按下返回键(或进行返回操作)时清空选择,仅在非选择模式下生效
BackHandler(inSelectedMode) { viewModel.clearAllTodoSelection() }
@ -88,7 +92,7 @@ fun MainPage(
exit = shrinkOut() + fadeOut()
) {
// TODO: 修复在滑动列表时FAB位移导致的动画不连贯临时方案为底部加padding
AnimatedExtendedFloatingActionButton(
/*AnimatedExtendedFloatingActionButton(
icon = Icons.Outlined.Add,
text = stringResource(R.string.action_add_task),
expanded = true,
@ -100,6 +104,24 @@ fun MainPage(
sharedContentState = rememberSharedContentState(key = Constants.KEY_TODO_FAB_TRANSITION),
animatedVisibilityScope = animatedVisibilityScope
)
)*/
SmallExtendedFloatingActionButton(
text = { Text(stringResource(R.string.action_add_task)) },
icon = {
Icon(
imageVector = Icons.Outlined.Add,
contentDescription = null
)
},
expanded = expandedFab,
onClick = {
viewModel.setEditTodoItem(null) // 每次添加待办前清除上一次已选待办
toTodoEditPage()
},
modifier = Modifier.sharedElement(
sharedContentState = rememberSharedContentState(key = Constants.KEY_TODO_FAB_TRANSITION),
animatedVisibilityScope = animatedVisibilityScope
)
)
}
}

View file

@ -10,14 +10,18 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import cn.super12138.todo.R
import cn.super12138.todo.ui.components.AnimatedCircularProgressIndicator
import cn.super12138.todo.ui.TodoDefaults
import cn.super12138.todo.ui.components.AnimatedCircularWavyProgressIndicator
@Composable
fun ProgressFragment(
@ -25,6 +29,11 @@ fun ProgressFragment(
completedTasks: Int,
modifier: Modifier = Modifier
) {
val stroke = Stroke(
width = with(LocalDensity.current) { TodoDefaults.trackThickness.toPx() },
cap = StrokeCap.Round,
)
val context = LocalContext.current
val remainTasks = totalTasks - completedTasks
@ -39,10 +48,19 @@ fun ProgressFragment(
modifier = modifier,
contentAlignment = Alignment.Center
) {
AnimatedCircularProgressIndicator(
AnimatedCircularWavyProgressIndicator(
progress = progress,
strokeWidth = 10.dp,
gapSize = 10.dp,
stroke = stroke,
trackStroke = stroke,
amplitude = { progress ->
if (progress <= 0.1f || progress >= 0.95f) {
0f
} else {
TodoDefaults.waveAmplitude
}
},
wavelength = TodoDefaults.waveLength,
waveSpeed = TodoDefaults.waveSpeed,
modifier = Modifier
.size(175.dp)
.clearAndSetSemantics {}

View file

@ -120,33 +120,33 @@ fun TodoCard(
}
) {
// with(sharedTransitionScope) {
Text(
text = content,
style = MaterialTheme.typography.titleLarge,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
textDecoration = if (completed) TextDecoration.LineThrough else TextDecoration.None,
modifier = Modifier
/*.sharedBounds(
sharedContentState = rememberSharedContentState("${Constants.KEY_TODO_CONTENT_TRANSITION}_$id"),
animatedVisibilityScope = animatedVisibilityScope
)*/
.basicMarquee() // TODO: 后续评估性能影响
)
Text(
text = content,
style = MaterialTheme.typography.titleLarge,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
textDecoration = if (completed) TextDecoration.LineThrough else TextDecoration.None,
modifier = Modifier
/*.sharedBounds(
sharedContentState = rememberSharedContentState("${Constants.KEY_TODO_CONTENT_TRANSITION}_$id"),
animatedVisibilityScope = animatedVisibilityScope
)*/
.basicMarquee() // TODO: 后续评估性能影响
)
// }
}
// with(sharedTransitionScope) {
Text(
text = category.ifEmpty { stringResource(R.string.tip_default_category) },
style = MaterialTheme.typography.labelMedium,
textDecoration = if (completed) TextDecoration.LineThrough else TextDecoration.None,
maxLines = 1,
/*modifier = Modifier.sharedBounds(
sharedContentState = rememberSharedContentState("${Constants.KEY_TODO_CATEGORY_TRANSITION}_$id"),
animatedVisibilityScope = animatedVisibilityScope
)*/
)
Text(
text = category.ifEmpty { stringResource(R.string.tip_default_category) },
style = MaterialTheme.typography.labelMedium,
textDecoration = if (completed) TextDecoration.LineThrough else TextDecoration.None,
maxLines = 1,
/*modifier = Modifier.sharedBounds(
sharedContentState = rememberSharedContentState("${Constants.KEY_TODO_CATEGORY_TRANSITION}_$id"),
animatedVisibilityScope = animatedVisibilityScope
)*/
)
// }
}

View file

@ -2,11 +2,13 @@ package cn.super12138.todo.ui.theme
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialExpressiveTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.colorResource
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun ToDoTheme(
color: Color? = null,
@ -33,7 +35,7 @@ fun ToDoTheme(
contrastLevel = contrastLevel
)
MaterialTheme(
MaterialExpressiveTheme(
colorScheme = colorScheme,
typography = Typography,
content = content