Android自定义ViewGroup—ExpansionPanel
程序员文章站
2022-07-05 09:40:17
前言最近在开发中需要实现一个伸缩效果,就像Flutter中的ExpansionPanel控件一样,效果如下图,发现Android中竟然没有类似的控件,网上也有较多的实现,效果不太理想,因此决定自己实现一个。思路为了方便可伸缩内容的拓展以及达到图中的效果,我选择直接继承于CardView来实现,这样比较方便,不需要自己实现测量等方法。在CardView内部填充一个ConstraintLayout(减少布局嵌套层数)作为根布局,使其他控件在根布局的特点位置进行摆放,最后利用动画效果实现伸缩。从效果图中可...
前言
最近在开发中需要实现一个伸缩效果,就像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){
// 得到控件高度
expansionHeight = if (views.size == 0){
toggle.measuredHeight
}else{
// 得到顶部高度
topHeight = if (views[0].measuredHeight > toggle.measuredHeight) views[0].measuredHeight else toggle.measuredHeight
topHeight
}
// 得到内容高度
contentHeight = if (views.size > 1) views[1].measuredHeight else 0
isAdd = true // 更改标记
}
}
这样各个控件的高度就得到了,然后根据这些高度与动画结合起来就可以实现伸缩效果了。
- 增加动画
这里使用View的属性动画进行伸缩操作。
private fun startExpand(){
if (isExpand){
// 将按钮旋转180°
toggle.animate().rotation(180f).setUpdateListener {
// 动画进度值
val v = it.animatedValue as Float
// 更改控件的高度
expansionHeight = topHeight + (v * contentHeight).toInt()
// 重新测量
requestLayout()
}
}else{
toggle.animate().rotation(0f).setUpdateListener {
val v = it.animatedValue as Float
expansionHeight = topHeight + (( 1f - v) * contentHeight).toInt()
requestLayout()
}
}
}
最后设置伸缩按钮的点击事件,整个效果就完成了,我们来看看最终效果。
- 最终效果
布局文件
<?xml version="1.0" encoding="utf-8"?>
<com.example.myapplication.widget.NewExpansionPanel android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="NewExpansionPanel"
android:textSize="30sp"/>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:layout_width="96dp"
android:layout_height="94dp"
android:src="@mipmap/ic_launcher"
android:layout_centerInParent="true"
android:layout_margin="16dp"/>
</RelativeLayout>
</com.example.myapplication.widget.NewExpansionPanel>
贴上最终效果图:
完整代码如下:
/**
* Created by Holo.
* On 2020/11/20.
*/
class NewExpansionPanel(
context: Context,
attributeSet: AttributeSet
) : CardView(context,attributeSet){
companion object{
private const val TAG = "NewExpansionPanel"
}
private val size = dp2Px(64)
private val views = mutableListOf<View>()
private val constraintSet = ConstraintSet()
private var root = ConstraintLayout(context)
private var toggle = ImageButton(context)
private var initial = false
private var add = false
private var isExpand = false
private var topHeight = 0
private var expansionHeight = 0
private var contentHeight = 0
init {
with(root){
id = View.generateViewId()
}
with(toggle){
id = View.generateViewId()
root.addView(this)
setImageResource(R.drawable.ic_baseline_keyboard_arrow_up_24)
background = getSelectedItemBackground()
setOnClickListener {
isExpand = !isExpand
startExpand()
}
}
}
private fun getSelectedItemBackground() : Drawable{
val typedValue = TypedValue()
context.theme.resolveAttribute(android.R.attr.selectableItemBackground,typedValue,false)
val typedArray = context.theme.obtainStyledAttributes(typedValue.resourceId, IntArray(1) { android.R.attr.selectableItemBackground })
return typedArray.getDrawable(0)!!
}
private fun startExpand(){
if (isExpand){
toggle.animate().rotation(180f).setUpdateListener {
val v = it.animatedValue as Float
expansionHeight = topHeight + (v * contentHeight).toInt()
requestLayout()
}
}else{
toggle.animate().rotation(0f).setUpdateListener {
val v = it.animatedValue as Float
expansionHeight = topHeight + (( 1f - v) * contentHeight).toInt()
requestLayout()
}
}
}
fun expand(){
isExpand = true
startExpand()
}
fun fold(){
isExpand = false
startExpand()
}
private fun initConstraint(){
root.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.WRAP_CONTENT)
// 设置toggle的约束
constraintSet.apply {
clone(root)
// toggle
constrainHeight(toggle.id,size)
constrainWidth(toggle.id,size)
connect(toggle.id,ConstraintSet.END,ConstraintSet.PARENT_ID,ConstraintSet.END)
connect(toggle.id,ConstraintSet.TOP,ConstraintSet.PARENT_ID,ConstraintSet.TOP)
if (views.size > 0){
// titleView
setHorizontalWeight(views[0].id,1f)
constrainWidth(views[0].id,0)
connect(views[0].id,ConstraintSet.START,ConstraintSet.PARENT_ID,ConstraintSet.START)
connect(views[0].id,ConstraintSet.END,toggle.id,ConstraintSet.START)
connect(views[0].id,ConstraintSet.TOP,ConstraintSet.PARENT_ID,ConstraintSet.TOP)
//connect(toggle.id,ConstraintSet.START,views[0].id,ConstraintSet.END)
}
if (views.size > 1){
// 取高度最大的View作为顶部约束的对象
val v = if (toggle.measuredHeight > views[0].measuredHeight) toggle else views[0]
// contentView
connect(views[1].id,ConstraintSet.TOP,v.id,ConstraintSet.BOTTOM)
connect(views[1].id,ConstraintSet.START,ConstraintSet.PARENT_ID,ConstraintSet.START)
connect(views[1].id,ConstraintSet.END,ConstraintSet.PARENT_ID,ConstraintSet.END)
connect(views[1].id,ConstraintSet.BOTTOM,ConstraintSet.PARENT_ID,ConstraintSet.BOTTOM)
}
applyTo(root)
}
}
private fun addAllViews(){
if (!add){
views.forEach {
if (it.id == View.NO_ID){
it.id = View.generateViewId()
}
root.addView(it)
}
}
initConstraint()
}
private fun getExpansionHeight(){
if (!add){
expansionHeight = if (views.size == 0){
toggle.measuredHeight
}else{
topHeight = if (views[0].measuredHeight > toggle.measuredHeight) views[0].measuredHeight else toggle.measuredHeight
topHeight
}
contentHeight = if (views.size > 1) views[1].measuredHeight else 0
add = true
}
}
// 重写addView获取子控件
override fun addView(child: View?, index: Int, params: ViewGroup.LayoutParams?) {
Log.d(TAG, "addView: ")
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()
super.addView(root, index, params)
initial = true
Log.d(TAG, "addView: success !")
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
addAllViews()
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
getExpansionHeight()
Log.d(TAG, "onMeasure: toggle:${toggle.measuredHeight} top:${views[0].measuredHeight}")
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),expansionHeight)
}
private fun dp2Px(v: Int) = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,v.toFloat(),resources.displayMetrics).toInt()
}
本文地址:https://blog.csdn.net/qq_31054411/article/details/110070512