欢迎访问宙启技术站
智能推送

Android LinearLayout实现自动换行效果

发布时间:2023-05-15 14:43:13

Android LinearLayout是一种常用的布局方式,能够将子View垂直或水平排列。但是,我们在实际开发中,很多时候需要实现自动换行的效果,比如说,当我们的屏幕上显示一些标签子View时,如果一行放不下了,下一个标签应该自动跳到下一行。

本文将介绍一种基于LinearLayout实现自动换行效果的方法。这里我们主要用到LinearLayout的一个属性:weightSum。首先我们先看一下如何基于LinearLayout实现标签子View的水平布局。

1. 水平布局

我们将一个标签包装成一个TextView,然后把这些TextView依次添加到LinearLayout中,设置水平排列。为了让TextView之间有一定的间隔,这里我们添加了一个0.5dp的padding。

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:padding="0.5dp">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@drawable/tag_bg"
        android:paddingLeft="8dp"
        android:paddingRight="8dp"
        android:text="标签1"/>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@drawable/tag_bg"
        android:paddingLeft="8dp"
        android:paddingRight="8dp"
        android:text="标签2"/>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@drawable/tag_bg"
        android:paddingLeft="8dp"
        android:paddingRight="8dp"
        android:text="标签3"/>

    ...

</LinearLayout>

布局效果如下:

![image-20210820172524435](https://i.loli.net/2021/08/20/RhOUZbtxMPdQ4aw.png)

我们可以看到,这里将三个标签View放在了同一行中,但我们还没有实现自动换行的效果。那么,怎样才能实现呢?接下来,我们将介绍自动换行的实现思路。

2. 自动换行

在布局中,我们可以设置LinearLayout的weightSum属性,用来平分整个布局的权值。在这个例子中,由于子View的个数是不确定的,所以我们需要计算子View总长度,并根据子View的长度平分权值。

我们定义一个类AutoLinearLayout,继承自LinearLayout。在AutoLinearLayout中,我们覆写了onMeasure()方法,以实现自动换行的功能。

public class AutoLinearLayout extends LinearLayout {

    private int mTotalLength = 0;

    public AutoLinearLayout(Context context) {
        super(context);
    }

    public AutoLinearLayout(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public AutoLinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        // 获取父布局的宽度
        int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
        int parentHeight = MeasureSpec.getSize(heightMeasureSpec);

        // 清空子View的权值
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            LayoutParams params = (LayoutParams) child.getLayoutParams();
            params.weight = 0;
        }

        // 计算所有子View的总长
        mTotalLength = 0;
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            LayoutParams params = (LayoutParams) child.getLayoutParams();
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            mTotalLength += child.getMeasuredWidth() + params.leftMargin + params.rightMargin;
        }

        // 计算每个子View的权值
        float weightSum = 0;
        float weightUnit = parentWidth/mTotalLength;
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            LayoutParams params = (LayoutParams) child.getLayoutParams();
            params.weight = weightUnit*(child.getMeasuredWidth() + params.leftMargin + params.rightMargin);
            weightSum += params.weight;
        }

        // 若子View的长度超出父布局的宽度,则自动换行
        if (mTotalLength > parentWidth) {
            // 若只有一个子View,则此子View单独一行
            if (getChildCount() == 1) {
                View child = getChildAt(0);
                child.layout((parentWidth - child.getMeasuredWidth())/2, 0, (parentWidth + child.getMeasuredWidth())/2, child.getMeasuredHeight());
            } else {
                float lineLength = 0;
                int lineStart = 0;
                int lineEnd = 0;
                for (int i = 0; i < getChildCount(); i++) {
                    View child = getChildAt(i);
                    LayoutParams params = (LayoutParams) child.getLayoutParams();

                    if (lineLength + params.weight/weightSum*(parentWidth - getPaddingLeft() - getPaddingRight()) > parentWidth - getPaddingLeft() - getPaddingRight()) {
                        lineEnd = i - 1;
                        layoutLine(lineStart, lineEnd, parentWidth, (int) (parentHeight - getPaddingTop() - getPaddingBottom() - mTotalLength/parentWidth*(child.getMeasuredHeight() + params.topMargin + params.bottomMargin)));
                        lineLength = 0;
                        lineStart = i;
                    }
                    lineLength += params.weight/weightSum*(parentWidth - getPaddingLeft() - getPaddingRight());
                }

                if (lineStart < getChildCount()) {
                    layoutLine(lineStart, getChildCount() - 1, parentWidth, (int) (parentHeight - getPaddingTop() - getPaddingBottom() - mTotalLength/parentWidth*(child.getMeasuredHeight() + params.topMargin + params.bottomMargin)));
                }
            }
        }
    }

    /**
     * 布局当前行
     */
    private void layoutLine(int lineStart, int lineEnd, int parentWidth, int top) {
        float sumWeight = 0;
        for (int i = lineStart; i <= lineEnd; i++) {
            View child = getChildAt(i);
            LayoutParams params = (LayoutParams) child.getLayoutParams();
            sumWeight += params.weight;
        }

        float marginLeft = 0;
        for (int i = lineStart; i <= lineEnd; i++) {
            View child = getChildAt(i);
            LayoutParams params = (LayoutParams) child.getLayoutParams();

            float ratio = params.weight / sumWeight;
            int width = (int) (ratio * (parentWidth - getPaddingLeft() - getPaddingRight()));
            int height = child.getMeasuredHeight();

            int left = (int) (getPaddingLeft() + marginLeft);
            int topMargin = params.topMargin;
            int bottomMargin = params.bottomMargin;
            int bottom = top + height;

            // 布局子View
            child.layout(left, topMargin + top, left + width, bottom - bottomMargin);

            marginLeft += width;
        }
    }
}

我们在AutoLinearLayout的onMeasure()方法中,通过循环获取所有子View长度,并根据此计算每个子View的权值。若子View的总长度超出父布局的宽度,则自动换行。当只有一个子View时,此子View单独一行;当有多个子View时,循环计算每一行的总长度,当此长度超出父布局宽度时,则布局当前行,并开始下一行的计算。

通过这种方式,在布局时动态计算每个子View的权值,然后根据子View的权值来控制子View的宽度,从而实现自动换行的效果。

完成自动换行的AutoLinearLayout了之后,我们只需要在布局中将LinearLayout的声明替换成AutoLinearLayout就可以实现标签的自动换行效果了。

`

<com.example.logic.AutoLinearLayout

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:orientation="horizontal"

android:padding="0.5dp">

<TextView

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:background="@drawable/tag_bg"

android:paddingLeft="8dp"