Expand and collapse Layout of Android custom View

effect

analysis

From the effect diagram, viewthe expansion and collapse triggered by the click event , and 保留the first subview is displayed in the collapsed state , this expansion and collapse is actually the 高度change of the view , so as long as the height is controlled, 很简单this effect can be achieved.

step

  • 1. Initialization parameter setting direction, etc.
  • 2. Calculate the height according to the animation execution progress

initialization

class ExpandLinearLayout : LinearLayout {

    //是否展开,默认展开
    private var isOpen = true

    //第一个子view的高度,即收起保留高度
    private var firstChildHeight = 0

    //所有子view高度,即总高度
    private var allChildHeight = 0

    /**
     * 动画值改变的时候 请求重新布局
     */
    private var animPercent: Float = 0f

    constructor(context: Context) : super(context) {
        initView()
    }

    constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet) {
        initView()
    }

    constructor(context: Context, attributeSet: AttributeSet, defStyleAttr: Int) : super(
        context,
        attributeSet,
        defStyleAttr
    ) {
        initView()
    }

    private fun initView() {
        //横向的话 稍加修改计算宽度即可
        orientation = VERTICAL

        animPercent = 1f
        isOpen = true
    }

}
复制代码

Define a class ExpandLinearLayout, inherit from LinearLayout, of course, it can also be other views.

Then rewrite the construction method and call the initViewmethod inside the construction method.

In the initViewmethod, we initialize some parameters, such as direction and default expansion.

Calculated height

Ok, this is the point.

Because only the height of the view itself changes, we only need to rewrite onMeasureto calculate the height.

Look at onMeasure:

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)

        //重置高度
        allChildHeight = 0
        firstChildHeight = 0

        if (childCount > 0) {

            //遍历计算高度
            for (index in 0 until childCount) {
                //这个地方实际使用中除了measuredHeight,以及margin等,也要计算在内
                if (index == 0) {
                    firstChildHeight = getChildAt(index).measuredHeight
                    +getChildAt(index).marginTop + getChildAt(index).marginBottom
                    +this.paddingTop + this.paddingBottom
                }
                //实际使用时或包括padding等
                allChildHeight += getChildAt(index).measuredHeight + getChildAt(index).marginTop + getChildAt(index).marginBottom

                //最后一条的时候 加上当前view自身的padding
                if (index == childCount - 1) {
                    allChildHeight += this.paddingTop + this.paddingBottom
                }
            }

            // 根据是否展开设置高度
            if (isOpen) {
                setMeasuredDimension(
                    widthMeasureSpec,
                    firstChildHeight + ((allChildHeight - firstChildHeight) * animPercent).toInt()
                )
            } else {
                setMeasuredDimension(
                    widthMeasureSpec,
                    allChildHeight - ((allChildHeight - firstChildHeight) * animPercent).toInt()
                )
            }
        }
    }
复制代码

onMeasureThere are also two steps:

  • Traverse calculation height
//遍历计算高度
for (index in 0 until childCount) {
    //这个地方实际使用中除了measuredHeight,以及margin等,也要计算在内
    if (index == 0) {
        firstChildHeight = getChildAt(index).measuredHeight
        +getChildAt(index).marginTop + getChildAt(index).marginBottom
        +this.paddingTop + this.paddingBottom
    }
    //实际使用时或包括padding等
    allChildHeight += getChildAt(index).measuredHeight + getChildAt(index).marginTop + getChildAt(index).marginBottom
    //最后一条的时候 加上当前view自身的padding
    if (index == childCount - 1) {
        allChildHeight += this.paddingTop + this.paddingBottom
    }
}
复制代码

Look at the first ifjudgment, the first sub-record height of view, there should be noted that, in addition measuredHeight, marginhave to count, and padding of the parent view paddingshould add, because if a lot of padding, then the parent view, The view may not be displayed when it is stowed.

Then there is the calculation of the total height, the same as above.

Looking at the last ifjudgment, after the same total height is calculated, the upper and lower padding of the parent view must be added to be 完整the height.

The first judgment can be understood as 收起the height of the state, and the second judgment can be understood as 展开the height of the state.

  • Expand Collapse Logic
// 根据是否展开设置高度
if (isOpen) {
    setMeasuredDimension(
        widthMeasureSpec,
        firstChildHeight + ((allChildHeight - firstChildHeight) * animPercent).toInt()
    )
} else {
    setMeasuredDimension(
        widthMeasureSpec,
        allChildHeight - ((allChildHeight - firstChildHeight) * animPercent).toInt()
    )
}
复制代码

Because the first subview is reserved for display, it is necessary to subtract the height of the first subview when calculating, that is 剩余高度.

The remaining height can be easily calculated, but how can it be displayed without being obtrusive.

Add an animation here, 执行进度and calculate it based on the animation .

Expand: the height of the first child view + the remaining height × the Float animation value from 0 to 1

Collapse: Total height-remaining height × Float animation value from 1 to 0

author:yechaoa

Animation

Write a method to control the expansion and collapse, and execute the animation when the expansion and collapse.

    fun toggle(): Boolean {
        isOpen = !isOpen
        startAnim()
        return isOpen
    }

    /**
     * 执行动画的时候 更改 animPercent 属性的值 即从0-1
     */
    @SuppressLint("AnimatorKeep")
    private fun startAnim() {
        //ofFloat,of xxxX 根据参数类型来确定
        //1,动画对象,即当前view。2.动画属性名。3,起始值。4,目标值。
        val animator = ObjectAnimator.ofFloat(this, "animPercent", 0f, 1f)
        animator.duration = 500
        animator.start()
    }
复制代码

And modify our animation parameters:

    /**
     * 动画值改变的时候 请求重新布局
     */
    private var animPercent: Float = 0f
        set(value) {
            field = value
            requestLayout()
        }
复制代码

Called when set value requestLayout(), re-execute onMeasure.

transfer

  • xml
    <com.yechaoa.customviews.expand.ExpandLinearLayout
        android:id="@+id/ell"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#f5f5f5"
        android:orientation="vertical"
        android:padding="10dp">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:padding="10dp"
            android:text="@string/app_name"
            android:textColor="@android:color/holo_red_dark"
            android:textSize="20sp" />

        ...

    </com.yechaoa.customviews.expand.ExpandLinearLayout>
复制代码
  • Code
        ll_btn.setOnClickListener {
            val toggle = ell.toggle()
            tv_tip.text = if (toggle) "收起" else "展开"
        }
复制代码

Expand

  • 横向: The calculated height can be changed to the calculated width
  • 高度: The retention height can be controlled according to xml custom attributes

to sum up

In general, the effect is relatively practical, and the difficulty coefficient is not high. You can further improve it according to the expansion.

If it helps you a little bit, please give me a thumbs up ^ _ ^

Guess you like

Origin blog.csdn.net/weixin_55596273/article/details/114160599