日期:
来源:Android 开发者收集编辑:朱涛
在上一篇文章《 2 小时入门 Jetpack Compose (上) 》里,我们已经完成了 Splash 页面的 UI 和动画了。
这篇文章,让我们来完成:「首页」+「详情页」吧。
首页
跟 Splash 页面不一样,ToDoApp 的首页,要复杂不少。从结构上来讲,它主要分为三个部分:
第一,页面顶部的 TopBar; 第二,页面的主要内容 Content,也就是 "待完成的任务列表"; 第三,页面右下角的 FloatingActionButton。
// 代码段 1
@Composable
fun HomeScreen() {
Scaffold(
scaffoldState = scaffoldState,
// 1
topBar = {
HomeAppBar()
},
// 2
content = {
HomeContent()
},
// 3
floatingActionButton = {
HomeFab()
}
)
}
// 1
@Composable
fun HomeAppBar() {}
// 2
@Composable
fun HomeContent() {}
// 3
@Composable
fun HomeFab() {}
// 代码段 2
@Composable
fun Scaffold(
modifier: Modifier = Modifier,
scaffoldState: ScaffoldState = rememberScaffoldState(),
topBar: @Composable () -> Unit = {},
bottomBar: @Composable () -> Unit = {},
snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) },
floatingActionButton: @Composable () -> Unit = {},
floatingActionButtonPosition: FabPosition = FabPosition.End,
isFloatingActionButtonDocked: Boolean = false,
drawerContent: @Composable (ColumnScope.() -> Unit)? = null,
drawerGesturesEnabled: Boolean = true,
drawerShape: Shape = MaterialTheme.shapes.large,
drawerElevation: Dp = DrawerDefaults.Elevation,
drawerBackgroundColor: Color = MaterialTheme.colors.surface,
drawerContentColor: Color = contentColorFor(drawerBackgroundColor),
drawerScrimColor: Color = DrawerDefaults.scrimColor,
backgroundColor: Color = MaterialTheme.colors.background,
contentColor: Color = contentColorFor(backgroundColor),
content: @Composable (PaddingValues) -> Unit
) {}
TopBar
// 代码段 3
@Composable
fun HomeAppBar(
onDeleteAllConfirmed : () -> Unit
) {
HomeTopAppBar(
onDeleteAllConfirmed = {
onDeleteAllConfirmed()
}
)
}
// 代码段 4
@Composable
fun HomeTopAppBar(
onDeleteAllConfirmed: () -> Unit
) {
TopAppBar(
// 1,Title:任务列表
title = {
Text(
text = stringResource(id = R.string.list_screen_title),
color = MaterialTheme.colors.topAppBarContent
)
},
// 2,选项:清空任务
actions = {
HomeAppBarActions(
onDeleteAllConfirmed = onDeleteAllConfirmed
)
},
backgroundColor = MaterialTheme.colors.topAppBarBackground
)
}
@Composable
fun HomeAppBarActions(
onDeleteAllConfirmed: () -> Unit
) {
DeleteAllAction(onDeleteAllConfirmed = { isShowDialog = true })
}
// 代码段 5
@Composable
fun DeleteAllAction(
onDeleteAllConfirmed: () -> Unit
) {
var expanded by remember { mutableStateOf(false) }
IconButton(
onClick = { expanded = true }
) {
// 1,更多按钮
Icon(
painter = painterResource(id = R.drawable.ic_more),
contentDescription = stringResource(id = R.string.delete_all_action),
tint = MaterialTheme.colors.topAppBarContent
)
// 2,下拉列表
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
// 3,下拉列表,「清空任务」
DropdownMenuItem(
onClick = {
expanded = false
onDeleteAllConfirmed()
}
) {
Text(
modifier = Modifier
.padding(start = MEDIUM_PADDING),
text = stringResource(id = R.string.delete_all_action),
style = Typography.subtitle2
)
}
}
}
}
@Composable
fun TodoAlertDialog(
title: String,
msg: String,
isShowDialog: Boolean, // 1
onNoClicked: () -> Unit, // 2
onYesClicked: () -> Unit, // 3
) {
if (isShowDialog) { // 4
AlertDialog(
title = {
Text(
text = title,
fontSize = MaterialTheme.typography.h5.fontSize,
fontWeight = FontWeight.Bold
)
},
text = {
Text(
text = msg,
fontSize = MaterialTheme.typography.subtitle1.fontSize,
fontWeight = FontWeight.Normal
)
},
confirmButton = {
Button(
onClick = {
onYesClicked()
onNoClicked()
})
{
Text(text = stringResource(id = R.string.yes))
}
},
dismissButton = {
OutlinedButton(onClick = { onNoClicked() })
{
Text(text = stringResource(id = R.string.no))
}
},
onDismissRequest = { onNoClicked() }
)
}
}
任务列表
@Composable
fun HomeContent(
allTasks: RequestState<List<Task>>,
onSwipeToDelete: (Task) -> Unit,
gotoTaskDetail: (taskId: Int) -> Unit,
onUpdateTask: (Task) -> Unit
) {
if (allTasks is RequestState.Success &&
allTasks.data.isNotEmpty()
) {
// 不为空
HomeTasksColumn()
} else {
// 空页面
HomeEmptyContent()
}
}
@Composable
fun HomeTasksColumn1(
tasks: List<Task>,
gotoTaskDetail: (taskId: Int) -> Unit,
onUpdateTask: (Task) -> Unit,
) {
LazyColumn {
itemsIndexed(
items = tasks,
key = { _, task ->
task.id
}
) { index, task ->
TaskItem(
task = task,
gotoTaskDetail = gotoTaskDetail,
onUpdateTask = onUpdateTask
)
}
}
}
@Composable
fun HomeTasksColumn1(
tasks: List<Task>,
gotoTaskDetail: (taskId: Int) -> Unit,
onUpdateTask: (Task) -> Unit,
) {
LazyColumn {
itemsIndexed(
items = tasks,
key = { _, task ->
task.id
}
) { index, task ->
val size = remember(tasks) {
tasks.size
}
// 省略部分代码
AnimatedVisibility(
visible = true,
enter = slideInHorizontally(
animationSpec = tween(
durationMillis = 300
),
// index 越大,初始偏移越大
initialOffsetX = { -(it * (index + 1) / (size + 2)) }
),
exit = shrinkVertically(
animationSpec = tween(
durationMillis = 300
)
)
) {
TaskItem(
task = task,
gotoTaskDetail = gotoTaskDetail,
onUpdateTask = onUpdateTask
)
}
}
}
}
@Composable
fun HomeTasksColumn(
tasks: List<Task>,
onSwipeToDelete: (Task) -> Unit,
gotoTaskDetail: (taskId: Int) -> Unit,
onUpdateTask: (Task) -> Unit,
) {
LazyColumn {
itemsIndexed(
items = tasks,
key = { _, task ->
task.id
}
) { index, task ->
val size = remember(tasks) {
tasks.size
}
// 省略部分
AnimatedVisibility(
visible = itemAppeared && !isDismissed,
enter = slideInHorizontally(
animationSpec = tween(
durationMillis = 300
),
initialOffsetX = { -(it * (index + 1) / (size + 2)) }
),
exit = shrinkVertically(
animationSpec = tween(
durationMillis = 300
)
)
) {
// 1,变化
SwipeToDismiss(
state = dismissState,
directions = setOf(DismissDirection.EndToStart),
dismissThresholds = { FractionalThreshold(fraction = 0.2f) },
background = { SwipeBackground(degrees = degrees) },
dismissContent = {
TaskItem(
task = task,
gotoTaskDetail = gotoTaskDetail,
onUpdateTask = onUpdateTask
)
}
)
}
}
}
}
@Composable
fun HomeTasksColumn(
tasks: List<Task>,
onSwipeToDelete: (Task) -> Unit,
gotoTaskDetail: (taskId: Int) -> Unit,
onUpdateTask: (Task) -> Unit,
) {
LazyColumn {
itemsIndexed(
items = tasks,
key = { _, task ->
task.id
}
) { index, task ->
// 省略部分
// 1,变化
val degrees by animateFloatAsState(
if (dismissState.targetValue == DismissValue.Default)
0f
else
-180f
)
// 省略部分
AnimatedVisibility() {
}
}
}
}
@Composable
fun HomeTasksColumn(
tasks: List<Task>,
onSwipeToDelete: (Task) -> Unit,
gotoTaskDetail: (taskId: Int) -> Unit,
onUpdateTask: (Task) -> Unit,
) {
LazyColumn {
itemsIndexed(
items = tasks,
key = { _, task ->
task.id
}
) { index, task ->
// 省略部分
// 1
val isDeleteEnable by remember(degrees) {
derivedStateOf { degrees == -180f }
}
val context = LocalContext.current
// 2
DisposableEffect(key1 = isDeleteEnable) {
if (isDeleteEnable) {
showToast(context, "松手后删除!")
}
onDispose {}
}
// 省略部分
AnimatedVisibility() {
}
}
}
}
App 运行期间,Composable 方法是会被反复调用的,我们需要 isDeleteEnable 来标记用户是否触发了「侧滑删除」的阈值。这里我们使用了 derivedStateOf 来优化 Compose 的性能。
详情页
@Composable
fun TaskDetailScreen() {
Scaffold(
topBar = {
TaskDetailAppBar()
},
content = {
TaskDetailContent()
}
)
}
第一排,包含一个 TextField、CheckBox; 第二排,还是一个 TextField。
@Composable
fun TaskDetailContent() {
Column() {
Row(modifier = Modifier.fillMaxWidth()) {
TextField()
Checkbox()
}
Divider()
TextField()
}
}
@Composable
fun TaskDetailContent(
modifier: Modifier = Modifier,
title: String,
onTitleChange: (String) -> Unit,
description: String,
onDescriptionChange: (String) -> Unit,
isDone: Boolean,
isDoneChange: (Boolean) -> Unit,
) {
Column(
modifier = modifier
.fillMaxSize()
.background(MaterialTheme.colors.background)
.padding(all = MEDIUM_PADDING)
) {
Row(modifier = Modifier.fillMaxWidth()) {
TextField(
modifier = Modifier
.fillMaxWidth()
.weight(8F),
value = title,
onValueChange = { onTitleChange(it) },
label = { Text(stringResource(id = R.string.enter_title)) },
placeholder = { Text(text = stringResource(id = R.string.title)) },
textStyle = MaterialTheme.typography.body1,
singleLine = true,
colors = TextFieldDefaults.textFieldColors(
backgroundColor = Transparent
)
)
Checkbox(
modifier = Modifier.weight(1F),
checked = isDone,
onCheckedChange = isDoneChange
)
}
Divider(
modifier = Modifier.height(SMALL_PADDING),
color = MaterialTheme.colors.background
)
TextField(
modifier = Modifier
.fillMaxWidth()
.height(200.dp),
value = description,
onValueChange = { onDescriptionChange(it) },
label = { Text(stringResource(id = R.string.enter_description)) },
placeholder = { Text(text = stringResource(id = R.string.description)) },
textStyle = MaterialTheme.typography.body1,
)
}
}
结束语
恭喜!您已经完成课程 80% 的内容了。
接下来需要做的,就是跟随文章里的步骤,一行行的敲代码了。读文章只需要 10 分钟,只有真正花 2 小时写代码,才可能:「2 小时入门 Jetpack Compose」!
长按右侧二维码
查看更多开发者精彩分享
"开发者说·DTalk" 面向中国开发者们征集 Google 移动应用 (apps & games) 相关的产品/技术内容。欢迎大家前来分享您对移动应用的行业洞察或见解、移动开发过程中的心得或新发现、以及应用出海的实战经验总结和相关产品的使用反馈等。我们由衷地希望可以给这些出众的中国开发者们提供更好展现自己、充分发挥自己特长的平台。我们将通过大家的技术内容着重选出优秀案例进行谷歌开发技术专家 (GDE) 的推荐。
点击屏末 | 阅读原文 | 即刻报名参与 "开发者说·DTalk"