The effect

Analysis of the

Click on the event to trigger the view to expand and collapse, and the first child view remains in the collapse state. This expansion and collapse is actually the height change of the view, so as long as the height is controlled, it can be very simple to achieve this effect.

steps

  • 1. Initialize the direction of parameters
  • 2. Calculate the height according to the animation execution progress

Initialize the

class ExpandLinearLayout : LinearLayout {

    // Whether to expand, the default expansion
    private var isOpen = true

    // The height of the first child view
    private var firstChildHeight = 0

    // The total height of all subviews
    private var allChildHeight = 0

    /** * Request relayout when animation values change */
    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(a) {
        // Change the width of the field
        orientation = VERTICAL

        animPercent = 1f
        isOpen = true}}Copy the code

Define a class, ExpandLinearLayout, that inherits from LinearLayout, or any other view.

Then override the constructor and call the initView method inside the constructor.

In the initView method, we initialize parameters such as orientation and default expansion.

Calculate height

Ok, that’s the point.

Since only the height of the view itself changes, we just need to rewrite onMeasure to calculate the height.

Look at onMeasure:

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

        // Reset the height
        allChildHeight = 0
        firstChildHeight = 0

        if (childCount > 0) {

            // Traverse to calculate the height
            for (index in 0 until childCount) {
                // The measuredHeight, margin, and so on should also be counted
                if (index == 0) {
                    firstChildHeight = getChildAt(index).measuredHeight
                    +getChildAt(index).marginTop + getChildAt(index).marginBottom
                    +this.paddingTop + this.paddingBottom
                }
                // The actual use may include padding, etc
                allChildHeight += getChildAt(index).measuredHeight + getChildAt(index).marginTop + getChildAt(index).marginBottom

                // The last one adds the padding of the current view itself
                if (index == childCount - 1) {
                    allChildHeight += this.paddingTop + this.paddingBottom
                }
            }

            // Set height based on expansion
            if (isOpen) {
                setMeasuredDimension(
                    widthMeasureSpec,
                    firstChildHeight + ((allChildHeight - firstChildHeight) * animPercent).toInt()
                )
            } else {
                setMeasuredDimension(
                    widthMeasureSpec,
                    allChildHeight - ((allChildHeight - firstChildHeight) * animPercent).toInt()
                )
            }
        }
    }
Copy the code

There are two steps in onMeasure:

  • Traverse to calculate the height
// Traverse to calculate the height
for (index in 0 until childCount) {
    // The measuredHeight, margin, and so on should also be counted
    if (index == 0) {
        firstChildHeight = getChildAt(index).measuredHeight
        +getChildAt(index).marginTop + getChildAt(index).marginBottom
        +this.paddingTop + this.paddingBottom
    }
    // The actual use may include padding, etc
    allChildHeight += getChildAt(index).measuredHeight + getChildAt(index).marginTop + getChildAt(index).marginBottom
    // The last one adds the padding of the current view itself
    if (index == childCount - 1) {
        allChildHeight += this.paddingTop + this.paddingBottom
    }
}
Copy the code

Let’s look at the first if judgment, which is measuring the height of the first child view, and notice that in addition to measuredHeight, margin has to be added, and the padding of the parent view has to be added, because if the parent view has a larger padding, The view might not show up when you fold it up.

And then the total height, same thing.

Let’s look at the last if judgment. Again, after calculating the total height, we need to add the upper and lower padding of the parent view to make the full height.

The first judgment can be understood as the height of the folded state, and the second judgment as the height of the expanded state.

  • Unfurl logic
// Set height based on expansion
if (isOpen) {
    setMeasuredDimension(
        widthMeasureSpec,
        firstChildHeight + ((allChildHeight - firstChildHeight) * animPercent).toInt()
    )
} else {
    setMeasuredDimension(
        widthMeasureSpec,
        allChildHeight - ((allChildHeight - firstChildHeight) * animPercent).toInt()
    )
}
Copy the code

Since the first child view is reserved for display, the height of the first child view is subtracted from the remaining height.

The remaining height can be easily calculated, but how to display it without obfuscating.

Add an animation here, and calculate according to the progress of the animation.

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

Pack up: Total height – remaining height × 0 to 1 Float animation value

Author: yechaoa

animation

Write a method to control unfurl and animate it when it is unfurl.

    fun toggle(a): Boolean{ isOpen = ! isOpen startAnim()return isOpen
    }
    
    /** * Change the value of the animPercent property from 0 to 1 */
    @SuppressLint("AnimatorKeep")
    private fun startAnim(a) {
        //ofFloat, of xxxX is determined by parameter type
        //1, animation object, that is, the current view. 2. Name of the animation property. 3, the starting value. 4, target value.
        val animator = ObjectAnimator.ofFloat(this."animPercent".0f.1f)
        animator.duration = 500
        animator.start()
    }
Copy the code

And modify our animation parameters:

    /** * Request relayout when animation values change */
    private var animPercent: Float = 0f
        set(value) {
            field = value
            requestLayout()
        }
Copy the code

When set value, call requestLayout() and re-execute onMeasure.

call

  • 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>
Copy the code
  • code
        ll_btn.setOnClickListener {
            val toggle = ell.toggle()
            tv_tip.text = if (toggle) "Fold" else "A"
        }
Copy the code

extension

  • The transverse: Change the calculated height to the calculated width
  • highly: You can control the retention height based on XML custom attributes

conclusion

In general, the effect is more practical, the difficulty coefficient is not high, can expand their own to further improve.

If it helps you a little bit, give it a like ^ _ ^

Github

Github.com/yechaoa/Cus…