前言
最近在开发中需要实现一个伸缩效果,就像Flutter中的ExpansionPanel控件一样,效果如下图,发现Android中竟然没有类似的控件,网上也有较多的实现,效果不太理想,因此决定自己实现一个。
思路
为了方便可伸缩内容的拓展以及达到图中的效果,我选择直接继承于CardView
来实现,这样比较方便,不需要自己实现测量等方法。在CardView
内部填充一个ConstraintLayout
(减少布局嵌套层数)作为根布局,使其他控件在根布局的特点位置进行摆放,最后利用动画效果实现伸缩。从效果图中可以得知,该控件分为标题(也就是处于收缩状态下显示的控件)、伸缩按钮以及展开后需要显示的控件。知道思路之后那就直接开干。
开始
- 新建类
ExpansionPanel
并继承于CardView
,定义根布局root
。
class NewExpansionPanel(
context: Context,
attributeSet: AttributeSet
) : CardView(context,attributeSet){
private var root = ConstraintLayout(context) // 根布局
private var toggle = ImageButton(context) // 伸缩按钮
}
这里直接创建ConstraintLayout
是为了减少解析XML的时间,如果闲麻烦的话可以直接在构造方法中加载一个XML布局,然后得到根布局。
- 添加
View
到NewExpansionPanel
在解析XML时,系统会自动调用ViewGroup
中的addView
方法将解析得到的子控件添加到其中,因此,我们想要得到XML中的子控件,我才用重写addView
方法实现。
private val views = mutableListOf<View>() // 存放子控件
private var initial = false // 是否初始化
// 重写addView获取子控件
override fun addView(child: View?, index: Int, params: ViewGroup.LayoutParams?) {
views.add(child!!) // 保存子控件
child.layoutParams = params
if (views.size > 2){
throw IllegalArgumentException("childCount must less or equals two. Your childCount is ${
views.size} !")
}
if (!initial){
// 由于可能有多个子控件,保证以下代码只执行一次,减少不必要的测量
removeAllViews()
// 添加根布局root
super.addView(root, index, params)
initial = true
}
}
这样既得到了子控件也让根布局成功的添加到了NewExpansionPanel
中。
- 默认显示收缩状态
定义变量isExpand
作为是否伸缩的标志,定义变量expansionHeight
存放整个控件的高度,重写onMeasure
方法,控制控件高度。
private var isExpand = false
private var expansionHeight = 0
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
addAllViews() // 将XML中的子控件添加到root
super.onMeasure(widthMeasureSpec, heightMeasureSpec) // 测量各控件的高宽
getExpansionHeight() // 获取整个控件的高度
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),expansionHeight) // 设置控件的高宽
}
在第一次测量之前,把XML中的子控件添加到根布局,并为这些控件添加约束,完成后再进行测量,测量后获取各个控件的高度以及设置根布局的高度。
private var isAdd = false // 是否添加布局
private fun addAllViews(){
if (!isAdd){
// 遍历子控件并添加到root
views.forEach {
if (it.id == View.NO_ID){
it.id = View.generateViewId()
}
root.addView(it)
}
}
initConstraint() // 为各个控件添加约束
}
定义变量topHeight
和contentHeight
分别存放顶部最大高度(顶部View与伸缩按钮的高度最大值)以及底部内容的高度。
private fun getExpansionHeight(){
if (!isAdd){
// 得到控件高度
expansi