前言 —— 为什么不必 ExpandableListView
ExpandableListView
大家必定很熟悉(没关系,你就当你很熟悉)。
它是这样的(随意从别人博客里扒了一张相对美观的图,为表敬重,附:原文地址)——
可是它有什么问题呢?
- 仅有二级
- 它是ListView(在recyclerView当道乃至要被compose要挟方位的今日,ListView早该退出舞台了)
- 它的动画或许……它或许没有动画这个概念
- 根据第二点,它在和其他内容协作时或许有很多bug,比如嵌套翻滚时
完成目标
- 无限层级(每层什么样式取决于你对itemView的定制,和动画无关)
- “掉下来”的动效
完成思路 – 界面结构和数据结构
数据结构
明显,需求界说对应的数据结构才好做到打开和缩短。 界说了树形结构的数据后,有两条路线二选一:
- 添加和删去数据后,将树形结构按需铺平成另一个list,一切界面数据都来自这个list。
- 为该树形数据结构的读写定制专门的办法,让树形结构能够像条形的list相同遍历。
明显写出2比写出1并没有功能更好,主要是看起来更cooooooool,那我选择了2,价值是写的时分找数据一顿好找 (对现在的程序员有难度,对高中大学生刚刚好)
各位能够试试写成1,读写处理的时分就会简便很多,价值是每次改动数据结构时需求new List。——这点功能洒浇水啦!
不说了,想说的都在注释里——
/**
* @param title 标题,你要杂乱的数据就自己替换掉,demo就只需标题
* @param isExpand 是否是打开状况
* @param child 我孩子竟是我自己
*/
data class MenuData(
val title: String,
var isExpand: Boolean = false,
val child: List<MenuData> = emptyList()
) {
/**
* 仅在加入adapter的数据中时被主动赋值
*/
internal var level: Int = 0
/**
* 是否可打开——取决于它有没有child,以及child有没有内容
*/
val canExpand: Boolean
get() = child.isNotEmpty()
/**
* 到底有多少子item,这儿得进行一个
*/
val childSize: Int
get() {
if (!isExpand) return 0
var count = child.size // 先加上每个child
// 每个child再调用自己的相同办法递归计算
child.forEach {
count += it.childSize
}
return count
}
/**
* 获取指定方位的child,这个position将假定数据现已从树形变成条形,
* 然后获取条形数据中对应的[MenuData]
*
* @return 回来空是对调用者水平的不尊重,没错,我就不尊重了
*/
fun getChildAt(position: Int): MenuData? {
if (position < 0 || !isExpand) {
return null
}
var count = 0 // 计数器
// 问问子女们
child.forEach {// for child
if (count == position) { // 先看看自己是不是目标
return it
}
count++ // 哦,我不是
// count还剩position-count项,测验找一下
val result = it.getChildAt(position - count) 自己孩子
if (result != null) { // 如果我有孩子、且我在孩子中找到了
return result // good,找到了,交差!
}
// 没找到,把我孩子都加上,找下一个兄弟姐妹问问
count += it.childSize
}
// 问完了子女们
return null //的确没找到
}
}
Adapter完成
完成这么一个列表,榜首重要的就是支持二级乃至多级状况,
且打开状况能够正确且自由切换的 adapter
。
数据
不另外构建列表了,直接用现已界说好的数据结构即可。
完成后边的item相关办法时,能够让root节点通明。
private val rootMenu = MenuData("root", true)
界面
/**
* 给外界在expand时干事的接口
*/
var onExpandStateChange: (position: Int, isExpand: Boolean) -> Unit
= { position, isExpande -> }
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): MultiLevelMenuVH {
// itemView就用compose快速完成吧
// 用view也很简略,但简略美化样式会需求支付很多额外精力
// 所以算了算了用compose
val itemView = ComposeView(parent.context)
return MultiLevelMenuVH(itemView)
}
override fun onBindViewHolder(
holder: MultiLevelMenuVH,
position: Int
) {
val data = getItemAtPosition(position) ?: return
var rotate by mutableStateOf(if (data.isExpand) 90f else 0f)
// compose 大法好,就不笼统和管那么多了
holder.root.setContent {
val rotateAmin by animateFloatAsState(rotate, label = "rotate")
Row(Modifier.fillMaxWidth()
.defaultMinSize(minHeight = 32.dp)
.padding(vertical = 1.dp)
// 每级item的缩进不同就源于此
// 界面上我期望每次一级目录都比上一级多缩进个20dp
.padding(horizontal = (data.level * 20).dp)
.clip(RoundedCornerShape(8.dp))
// 自己定的色彩,你没有这个色彩正常,随意整一个你的色彩
.background(MaterialDesignColor.LightBlue300)
.clickable {
// 点击处理
if (!data.canExpand) { // 不行expand就别添乱
return@clickable
}
// 调用对应切换办法
switchItemExpandState(holder.bindingAdapterPosition, !data.isExpand)
// 更新布局状况
rotate = if (data.isExpand) 90f else 0f
// 调用布局状况改动的监听
onExpandStateChange(position, data.isExpand)
},
verticalAlignment = Alignment.CenterVertically
) {
// 文字居中这么套一层会更舒服,信任我,让Text()保持朴实
Box(Modifier.weight(1f), Alignment.Center) {
Text(data.title)
}
if (data.canExpand) { // 不行打开的item就别显现图标了
Image(
Icons.Filled.KeyboardArrowRight,
"expand",
Modifier.size(28.dp)
.rotate(rotateAmin)
)
}
}
}
}
一些处理数据的办法
主要是add和clear,仅有要注意的就是更新其地点层级。
——当然,由于数据单向链接,才需求更新层级。
你能够自行完成双向链接,这样子不只能够主动生成层级,还能添加满足感。
fun add(menuList: List<MenuData>) {
val oldSize = itemCount
updateLevel(menuList)
rootMenu.child.addAll(menuList)
notifyItemRangeInserted((oldSize - 1).coerceAtLeast(0), itemCount - oldSize)
}
fun insert(position: Int, menuList: List<MenuData>) {
// todo:懒得写了
// todo:remove也懒得写了
// 关于多级菜单而言,insert和remove这两种场景能够疏忽
}
/**
* 递归更新level
*/
private fun updateLevel(menuList: List<MenuData>?, initLevel: Int = 0) {
menuList?.forEach {
it.level = initLevel
updateLevel(it.child, initLevel + 1) // 下一级的level+1
}
}
fun clear() {
val dataSize = itemCount
rootMenu.child.clear()
notifyItemRangeRemoved(0, dataSize)
}
要害办法:获取总item数、获取指定item
由于前面打下的根底,此处两行即可
fun getItemAtPosition(position: Int): MenuData? =
rootMenu.getChildAt(position)
override fun getItemCount(): Int =
rootMenu.childSize
要害办法:切换可打开item的打开状况
fun switchItemExpandState(position: Int, expand: Boolean) {
if (recyclerView.isAnimating) {
// 这个动画执行过程中不能再次启动动画
// 否则方位就会计算出错了
// 你能够解除这儿的return,然后修改对应的ItemAnimator
// 完成更杂乱的方位计算,以使得此处的动画可打断。
return
}
val curItem = getItemAtPosition(position) ?: return
// 不行打开或打开状况不改变的情况下直接回来
if (!curItem.canExpand || curItem.isExpand == expand) {
return
}
if (curItem.isExpand) { // 打开 -> 缩短
// 先计算下有多少孩子,等下isExpand改动会造成计算错误!
val changeSize = curItem.childSize
curItem.isExpand = false
notifyItemRangeRemoved(position + 1, changeSize)
} else { //缩短 -> 打开
curItem.isExpand = true
// 等isExpand改动了再找childSize
notifyItemRangeInserted(position + 1, curItem.childSize)
// 你notifyChange了还怎样执行箭头旋转动画?
// 修改数据后,手动把界面的状况和数据同步的情况下
// 是不必notifyChange的
// notifyItemRangeChanged(position + 1 + changeSize, itemCount - position - changeSize + 1)
}
}
完成思路:动效
完成该动效,需求完成哪些点?
榜首点,是承继SimpleAnimator
,理清DefaultItemAnimator
是怎样重写的。
——但怎样说呢,谷歌这一段写得不那么简单了解,跟着我来。
第二点,是完成这个【掉落效果】,怎样完成呢?
拆解一下
- 假定
parentView
仅一个childView
——
嘿嘿随意画了下,如下图
明显不管是添加还是删去 Item, childView
都存在一个需求被躲藏一部分的时刻。
- 假定
parentView
不止一个childView
,这就更杂乱了——- 有的处于彻底躲藏状况
- 有的处于彻底可见状况
- 还有一个处于部分可见状况的
childView
如图:
.
.
现在咱们现已构建好了正确的界面,动效细节点比较多,下期再讲。
下期预览
- 界面状况 data class ViewState
- 使用data class是由于它主动完成了
equals
、toString
和copy
办法,咱们稍后会用到copy
- 使用data class是由于它主动完成了
- 界面操作 data class Op
- 界面操作队列 open class AnimSet : TreeSet<Op>
- 动画完成:start / end
- 动画完成:add / remove / move
- 界面裁剪计算逻辑