携手创作,共同生长!这是我参加「日新计划 8 月更文挑战」的第11天,点击查看活动概况
前言
作用预览
在开端之前,先看看终究的完成作用:
需求确定
不久之前,我运用 compose 做了一个 TODO 应用,其中有一个设置页面。
不过在 compose 中没有相似 PreferenceFragment
的东西,所以咱们需求自己完成一个。
后来一想,既然都要自己完成了,为什么还要照着 PreferenceFragment
写呢?
所以我决定做一个能够打开的菜单列表作用。
终究完成如上图所示。
开端完成
完成思路
依据需求,咱们想要的是一个列表,点击列表后打开躲藏的子列表,再次点击继续躲藏。
清楚明了的,咱们首要想到的当然是运用 Column
嵌套标题和子列表:
@Preview(showSystemUi = true)
@Composable
fun PreviewExpandableItem2() {
Column {
Text(text = "我是标题")
Text(
text = "我是子内容",
// 为了漂亮,给子内容加个 padding
modifier = Modifier.padding(start = 8.dp)
)
}
}
大致样式是这么没错,那么下一步便是增加躲藏和显现子内容的状况,并给标题控件增加点击回调:
@Preview(showSystemUi = true)
@Composable
fun PreviewExpandableItem2() {
// 是否显现子内容的状况
var isShowSubItem by remember { mutableStateOf(false) }
Column {
Text(text = "我是标题", modifier = Modifier.clickable {
// 点击标题则将状况取反
isShowSubItem = !isShowSubItem
})
if (isShowSubItem) { // 只有 isShowSubItem 为 true 才显现子内容
Text(
text = "我是子内容",
// 为了漂亮,给子内容加个 padding
modifier = Modifier.padding(start = 8.dp)
)
}
}
}
好像更加有感觉了。
可是总觉得标题的右边空空的,或许能够加上一个箭头来指示列表打开状况。
这儿咱们把本来的标题 Text
包裹到一个 Row
中,并设置水平对齐方式为 Arrangement.SpaceBetween
即让子项在水平上均匀分布,而且前后不留空格,形如: 1##2##3
。
然后再把点击回调从标题 Text
改到 Row
上。
最终在 Row
中,Text
后增加一个 Icon
, 而且给 Icon
依照当前是否打开增加一个旋转 90 的润饰符
Modifier.rotate(if (isShowSubItem) 90f else 0f)
这儿的 rotate
表明把润饰的组件依照中心旋转指定的度数,正数表明顺时针旋转,负数表明逆时针旋转。
修改后代码如下:
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.fillMaxWidth()
.clickable {
isShowSubItem = !isShowSubItem
}
) {
Text(text = "我是标题")
Icon(
Icons.Outlined.ArrowRight,
contentDescription = "我是箭头",
modifier = Modifier.rotate(if (isShowSubItem) 90f else 0f)
)
}
好,我想要便是这种作用,可是感觉有点僵硬啊,对了,能够加上动画作用和亿点细节。
加一点动画
简略调查这个组件,能够加动画的当地有两个:
- 箭头的旋转动画
- 子列表的显现和躲藏动画
至于怎样挑选动画类型,能够参考谷歌官方的这篇挑选攻略:
箭头旋转动画
依据攻略,咱们想要完成的箭头旋转动画归于基于状况的(isShowSubItem
),而且不是无限的动画(只需求基于状况进行有限的动画)、不需求一起为多个值设置动画(咱们只需求设置旋转角度)。
所以咱们应该挑选 animate*AsState
动画,其中的 *
能够是多种数据类型,因为 Modifier.rotate()
的参数是 float 类型,所以咱们运用 animateFloatAsState
动画:
val arrowRotateDegrees: Float by animateFloatAsState(if (isShowSubItem) 90f else 0f)
animateFloatAsState
的参数 targetValue
顾名思义,便是目标值,当这个目标值改变时,compose 会主动开端动画,把 arrowRotateDegrees
的当前值依照动画作用逐渐变为 targetValue
。
将这个 arrowRotateDegrees
作为参数传递给 rotate()
即可, 当动画开端运行,conpose 会主动重组运用到 arrowRotateDegrees
的组件。
终究代码如下:
@Preview(showSystemUi = true)
@Composable
fun PreviewExpandableItem2() {
// 是否显现子内容的状况
var isShowSubItem by remember { mutableStateOf(false) }
// 界说旋转动画
val arrowRotateDegrees: Float by animateFloatAsState(if (isShowSubItem) 90f else 0f)
Column {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.fillMaxWidth()
.clickable {
isShowSubItem = !isShowSubItem
}
) {
Text(text = "我是标题")
Icon(
Icons.Outlined.ArrowRight,
contentDescription = "我是箭头",
modifier = Modifier.rotate(arrowRotateDegrees)
)
}
if (isShowSubItem) { // 只有 isShowSubItem 为 true 才显现子内容
Text(
text = "我是子内容",
// 为了漂亮,给子内容加个 padding
modifier = Modifier.padding(start = 8.dp)
)
}
}
}
作用如下:
能够明显的看到,箭头的旋转不再是干巴巴的硬切,而是带有旋转过程的动画作用了。
子列表显隐动画
依然是依照官方攻略,明显,子列表显隐动画归于显现和躲藏动画,所以应该运用 AnimatedVisibility
。
AnimatedVisibility
的参数很简略:
@Composable
fun ColumnScope.AnimatedVisibility(
visible: Boolean,
modifier: Modifier = Modifier,
enter: EnterTransition = fadeIn() + expandVertically(),
exit: ExitTransition = fadeOut() + shrinkVertically(),
label: String = "AnimatedVisibility",
content: @Composable AnimatedVisibilityScope.() -> Unit
)
尽管有多个参数,可是咱们这儿只需求关心
visible
: 用于控制是否显现
enter
: 由躲藏转为显现时的动画作用,默以为 淡入(fadeIn)+垂直打开(expandVertically)
exit
: 由显现转为躲藏时的动画作用,默以为 淡出(fadeOut)+垂直收缩(shrinkVertically)
由于默许的动画作用恰好便是咱们想要的作用,所以咱们只需求设置 visible
为 isShowSubItem
即可。
然后把一切子项包裹进 AnimatedVisibility
,为了看起来更明显,我多复制了几个子项:
AnimatedVisibility(visible = isShowSubItem) {
Column {
Text(
text = "我是子内容1",
modifier = Modifier.padding(start = 8.dp)
)
Text(
text = "我是子内容2",
modifier = Modifier.padding(start = 8.dp)
)
Text(
text = "我是子内容3",
modifier = Modifier.padding(start = 8.dp)
)
}
}
作用如下:
这下是不是看起来顺眼多了?
抽出参数
尽管根本作用已经到达咱们的需求了,可是肯定不能直接这样写啊,咱们应该把它抽出成一个函数,便利复用。
而且再给它加亿点点小细节,便得到了这个函数:
/**
* 可打开的列表
*
* @param title 列表标题
* @param modifier Modifier
* @param endText 列表标题的尾部文字,默以为空
* @param subItemStartPadding 子项间隔 start 的 padding 值
* @param subItem 子项
* */
@Composable
fun ExpandableItem(
title: String,
modifier: Modifier = Modifier,
endText: String = "",
subItemStartPadding: Int = 8,
subItem: @Composable () -> Unit
) {
var isShowSubItem by remember { mutableStateOf(false) }
val arrowRotateDegrees: Float by animateFloatAsState(if (isShowSubItem) 90f else 0f)
Column(modifier = modifier) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.fillMaxWidth()
.clickable {
isShowSubItem = !isShowSubItem
}
) {
Text(text = title)
Row {
if (endText.isNotBlank()) {
Text(text = endText,
modifier = modifier.padding(end = 4.dp).widthIn(0.dp, 100.dp),
maxLines = 1,
overflow = TextOverflow.Ellipsis)
}
Icon(
Icons.Outlined.ArrowRight,
contentDescription = title,
modifier = Modifier.rotate(arrowRotateDegrees)
)
}
}
AnimatedVisibility(visible = isShowSubItem) {
Column(modifier = Modifier.padding(start = subItemStartPadding.dp)) {
subItem()
}
}
}
}
最终,再看一下我在项目中实际运用的作用:
总结
compose 的根底组件根本涵盖了一切的根本需求,即使是没有的组件咱们也能够很快速的运用已有根底组件组合出咱们需求的组件作用。
别的,compose 的动画创立相比于传统 view 便利了许多,例如参数值改变的动画,现在只需求运用 animate*AsState
创立一个带动画的参数,再放到需求动画的当地即可,完全不需求其他多余的操作。