自定义自动换行布局

注:这篇文章在onmeasure方法的实现上有不合理的地方(见评论),故只能作为一种参考,要当成一个合格控件使用还需要完善。

暴风影音的搜索页面有一个效果,每一行的组件如果最后一个显示即将超出屏幕宽度,则自动换行。效果图:

本demo的效果图:

实现该效果不能使用已有的组件,必须自定义一个布局。

重点介绍:自定义布局可以继承ViewGroup,要想实现该效果,我们的自定义布局里面必须要继承父类中的onMeasurt()和onLayout()两个方法。

        onMeasure()方法的作用是设置布局的显示范围,超出该范围的部分将被遮挡.核心代码是:setMeasuredDimension(int width,int height).

        onLayout()方法的作用是在父容器内布局子组件,核心功能是根据计算出来的子组件的四个”坐标”将其放在父容器的指定位置,核心代码为:childview.layout(int left,int top,int right,int bottom)

              下面贴出Demo源码,算法解释已经在注释中了。

自定义布局:

HorizantalFallWaterLayout.java

package com.example.baofendeflectdemo.view;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
/**
 * @Description:仿暴风影音搜索页面效果
 * @company XX(北京)有限公司
 * @author Joe
 * @date 2013-12-18 下午4:38:01
 */
public class HorizantalFallWaterLayout extends ViewGroup {
    private int maxWidth;// 可使用的最大宽度
    public HorizantalFallWaterLayout(Context context) {
        super(context);
    }
    public HorizantalFallWaterLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public HorizantalFallWaterLayout(Context context, AttributeSet attrs,
            int defStyle) {
        super(context, attrs, defStyle);
    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        maxWidth = MeasureSpec.getSize(widthMeasureSpec);
        int containorHeight = 0;// 容器的高度,也就是本布局的高度。初始化赋值为0.
        int count = getChildCount();// 获取该布局内子组件的个数
        for (int i = 0; i < count; i++) {
            View view = getChildAt(i);
            /**
             * measure(int widthMeasureSpec,int
             * heightMeasureSpec)用于设置子组件显示模式.有三个值:<br/>
             * MeasureSpec.AT_MOST 该组件可以设置自己的大小,但是最大不能超过其父组件的限定<br/>
             * MeasureSpec.EXACTLY 无论该组件设置大小是多少,都只能按照父组件限制的大小来显示<br/>
             * MeasureSpec.UNSPECIFIED 该组件不受父组件的限制,可以设置任意大小
             */
            view.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
            // 把每个子组件的高度相加就是该组件要显示的高度。
            containorHeight += view.getMeasuredHeight();
        }
        setMeasuredDimension(maxWidth, containorHeight);// onMeasure方法的关键代码,该句设置父容器的大小。
    }
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childCount = getChildCount();// 获取子组件数
        int row = 1;// 子组件行数,初始化赋值为1
        int left = 0;// 子组件的左边“坐标”
        int right = 0;// 子组件的右边“坐标”
        int top = 0;// 子组件的顶部“坐标”
        int bottom = 0;// 子组件的底部“坐标”
        int p = getPaddingLeft();// 在父组件中设置的padding属性的值,该值显然也会影响到子组件在屏幕的显示位置
        for (int i = 0; i < childCount; i++) {
            View view = getChildAt(i);
            int width = view.getMeasuredWidth();// 测量子组件的宽
            int height = view.getMeasuredHeight();// 测量子组件的高
            left = p + right;// ---------------------------------------------------备注1
            right = left + width;// -----------------------------------------------备注2
            top = p * row + height * (row - 1);// ---------------------------------备注3
            bottom = top + height;// ----------------------------------------------备注4
            if (right > maxWidth) {
                row++;
                left = 0;//每次换行后要将子组件左边“坐标”与右边“坐标”重新初始化
                right = 0;
                left = p + right;
                right = left + width;
                top = p * row + height * (row - 1);
                bottom = top + height;
            }
            view.layout(left, top, right, bottom);// 最后按照计算出来的“坐标”将子组件放在父容器内
        }
    }
}

MainActivity.java什么都没有做

package com.example.baofendeflectdemo;
import android.os.Bundle;
import android.app.Activity;
public class MainActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

下面使用自定义布局作为MainActivity的主布局,显示几个TextView。

activity_main.xml

<com.example.baofendeflectdemo.view.HorizantalFallWaterLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="5sp" >
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#DBE8F0"
        android:text="生化危机:重返基地" />
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#F8E199"
        android:text="失恋33天" />
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#FAD0D1"
        android:text="宫" />
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#D2E39C"
        android:text="南极大冒险" />
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#DBE8F0"
        android:text="战争之王" />
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#F8E199"
        android:text="最贫穷的哈佛女孩" />
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#FAD0D1"
        android:text="蒙古王" />
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#D2E39C"
        android:text="黑客帝国:重装出击" />
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#DBE8F0"
        android:text="拯救大兵瑞恩" />
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#F8E199"
        android:text="希特勒:帝国的毁灭" />
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#FAD0D1"
        android:text="十二金刚" />
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#D2E39C"
        android:text="我们俩" />
</com.example.baofendeflectdemo.view.HorizantalFallWaterLayout>

备注1:这里假设容器内的每个组件前面都有一个兄弟组件,则该组件的左边“坐标”其实就是padding值与其上一个兄弟组件的右边“坐标”之和。

备注2:前面已经计算出该组件的左边坐标,则其右边坐标肯定是其左边坐标与其宽度之和。

备注3:子组件的顶部“坐标”也有一定规律:第N行的组件,其顶部坐标值是N倍的padding值与(N-1)倍的自身高的和。这个效果中所有子组件的高度是相同的,所以可以这样计算

备注4:每个组件底部“坐标”是其顶部“坐标”的值与其高度之和。