ViewGroup2——自定义实现流式布局

简介: Android中的线性布局LinearLayout,只能横向或纵向排列子控件,而且横向排列时不能自动换行。实际上,通过扩展ViewGroup就能够实现控件自动的往右添加,如果当前行剩余空间不足,则自动添加到下一行,也就是所谓的流式布局,上代码自定义CustomViewGroup.
Android中的线性布局LinearLayout,只能横向或纵向排列子控件,而且横向排列时不能自动换行。实际上,通过扩展ViewGroup就能够实现控件自动的往右添加,如果当前行剩余空间不足,则自动添加到下一行,也就是所谓的流式布局,上代码

自定义CustomViewGroup.java如下
public class CustomViewGroup extends ViewGroup {

    int mCellWidth;
    int mCellHeight;

    public CustomViewGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
        mCellWidth = 120;
        mCellHeight = 120;
    }

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

    public void setCellWidth(int width) {
        mCellWidth = width;
        requestLayout();
    }

    public void setCellHeight(int height) {
        mCellHeight = height;
        requestLayout();
    }

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

        int cellWidthSpec = MeasureSpec.makeMeasureSpec(mCellWidth,
                MeasureSpec.AT_MOST);
        int cellHeightSpec = MeasureSpec.makeMeasureSpec(mCellHeight,
                MeasureSpec.AT_MOST);

        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            View childView = getChildAt(i);
            measureChild(childView, cellWidthSpec, cellHeightSpec);
            // childView.measure(cellWidthSpec, cellHeightSpec);
        }
        // 使用父容器给我们的尺寸和计算出的尺寸进行比较,选择正确的尺寸设置容器控件所占区域大小
        setMeasuredDimension(resolveSize(mCellWidth * count, widthMeasureSpec),
                resolveSize(mCellHeight * count, heightMeasureSpec));
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int cellWidth = mCellWidth;
        int cellHeight = mCellHeight;
        int columns = (r - l) / cellWidth;
        if (columns < 0) {
            columns = 1;
        }
        int x = 0;// 横坐标
        int y = 0;// 纵坐标
        int i = 0;
        int count = getChildCount();
        for (int index = 0; index < count; index++) {
            final View childView = getChildAt(index);

            int cWidth = childView.getMeasuredWidth(); // childView的宽度
            int cHeight = childView.getMeasuredHeight();

            int left = x + ((cellWidth - cWidth) / 2);// 相对父容器,左边的位置
            int top = y + ((cellHeight - cHeight) / 2);

            childView.layout(left, top, left + cWidth, top + cHeight);
            if (i >= (columns - 1)) {
                // 转到下一行
                i = 0;
                x = 0;
                y += cellHeight;
            } else {
                i++;
                x += cellWidth;
            }
        }
    }
}

在xml布局中使用CustomViewGroup
<?xml version="1.0" encoding="utf-8"?>
<com.example.view.CustomViewGroup xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/viewgroup_main_flow"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_launcher" />

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_launcher" />

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_launcher" />

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_launcher" />

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_launcher" />

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_launcher" />

</com.example.view.CustomViewGroup>

效果如下:

因为在创建CustomViewGroup时,系统会调用构造函数初始化
public CustomViewGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
        mCellWidth = 120;
        mCellHeight = 120;
    }
因此所有子控件的单元格都设置为120,当然也可以通过代码改变控件大小,如下:
CustomViewGroup customViewGroup=(CustomViewGroup) findViewById(R.id.viewgroup_main_flow);
        customViewGroup.setCellWidth(200);
        customViewGroup.setCellHeight(200);


这个例子的效果很像GridView,但又有些不同,这只是简单的实现,我们还可以更加完善一下它。虽然这样,这个例子也有很大的局限性——那就是每个控件的大小都是一样的,如果我们的子控件不相同怎么办?
直接盗用鸿洋大神的劳动成果了,看代码

CustomViewGroup.java如下

public class CustomViewGroup extends ViewGroup {

    public CustomViewGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected ViewGroup.LayoutParams generateLayoutParams(
            ViewGroup.LayoutParams p) {
        return new MarginLayoutParams(p);
    }

    @Override
    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }

    @Override
    protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
        return new MarginLayoutParams(LayoutParams.MATCH_PARENT,
                LayoutParams.MATCH_PARENT);
    }

    /**
     * 负责设置子控件的测量模式和大小,根据所有子控件设置自己的宽和高
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // 获得ViewGroup的父容器为它设置的测量模式和大小
        int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
        int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
        int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
        int modeHeight = MeasureSpec.getMode(heightMeasureSpec);

        // 如果是warp_content,记录宽和高
        int width = 0;
        int height = 0;
        /**
         * 记录每一行的宽度,width取lineWidth最大宽度
         */
        int lineWidth = 0;
        /**
         * 每一行的高度,累加至height
         */
        int lineHeight = 0;

        int count = getChildCount();

        // 遍历每个子元素
        for (int i = 0; i < count; i++) {
            View childView = getChildAt(i);
            // 测量每一个child的宽和高
            measureChild(childView, widthMeasureSpec, heightMeasureSpec);
            // 得到childView的lp
            MarginLayoutParams lp = (MarginLayoutParams) childView
                    .getLayoutParams();
            // 当前子空间实际占据的宽度
            int childWidth = childView.getMeasuredWidth() + lp.leftMargin
                    + lp.rightMargin;
            // 当前子空间实际占据的高度
            int childHeight = childView.getMeasuredHeight() + lp.topMargin
                    + lp.bottomMargin;
            /**
             * 如果加入当前childView,则超出最大宽度,则得到目前最大宽度给width,类加height 然后开启新的一行
             */
            if (lineWidth + childWidth > sizeWidth) {
                width = Math.max(lineWidth, childWidth);// 取最大的
                lineWidth = childWidth; // 重新开启新行,开始记录
                // 叠加当前高度,
                height += lineHeight;
                // 开启记录下一行的高度
                lineHeight = childHeight;
            } else
            // 否则累加值lineWidth,lineHeight取最大高度
            {
                lineWidth += childWidth;
                lineHeight = Math.max(lineHeight, childHeight);
            }
            // 如果是最后一个,则将当前记录的最大宽度和当前lineWidth做比较
            if (i == count - 1) {
                width = Math.max(width, lineWidth);
                height += lineHeight;
            }

        }
        setMeasuredDimension((modeWidth == MeasureSpec.EXACTLY) ? sizeWidth
                : width, (modeHeight == MeasureSpec.EXACTLY) ? sizeHeight
                : height);

    }

    /**
     * 存储所有的View,按行记录
     */
    private List<List<View>> mAllViews = new ArrayList<List<View>>();
    /**
     * 记录每一行的最大高度
     */
    private List<Integer> mLineHeight = new ArrayList<Integer>();

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        mAllViews.clear();
        mLineHeight.clear();

        int width = getWidth();

        int lineWidth = 0;
        int lineHeight = 0;
        // 存储每一行所有的childView
        List<View> lineViews = new ArrayList<View>();
        int count = getChildCount();
        // 遍历所有的孩子
        for (int i = 0; i < count; i++) {
            View childView = getChildAt(i);
            MarginLayoutParams lp = (MarginLayoutParams) childView
                    .getLayoutParams();
            int childWidth = childView.getMeasuredWidth();
            int childHeight = childView.getMeasuredHeight();

            // 如果已经需要换行
            if (childWidth + lp.leftMargin + lp.rightMargin + lineWidth > width) {
                // 记录这一行所有的View以及最大高度
                mLineHeight.add(lineHeight);
                // 将当前行的childView保存,然后开启新的ArrayList保存下一行的childView
                mAllViews.add(lineViews);
                lineWidth = 0;// 重置行宽
                lineViews = new ArrayList<View>();
            }
            /**
             * 如果不需要换行,则累加
             */
            lineWidth += childWidth + lp.leftMargin + lp.rightMargin;
            lineHeight = Math.max(lineHeight, childHeight + lp.topMargin
                    + lp.bottomMargin);
            lineViews.add(childView);
        }
        // 记录最后一行
        mLineHeight.add(lineHeight);
        mAllViews.add(lineViews);

        int left = 0;
        int top = 0;
        // 得到总行数
        int lineNums = mAllViews.size();
        for (int i = 0; i < lineNums; i++) {
            // 每一行的所有的views
            lineViews = mAllViews.get(i);
            // 当前行的最大高度
            lineHeight = mLineHeight.get(i);

            // 遍历当前行所有的View
            for (int j = 0; j < lineViews.size(); j++) {
                View child = lineViews.get(j);
                if (child.getVisibility() == View.GONE) {
                    continue;
                }
                MarginLayoutParams lp = (MarginLayoutParams) child
                        .getLayoutParams();

                // 计算childView的left,top,right,bottom
                int lc = left + lp.leftMargin;
                int tc = top + lp.topMargin;
                int rc = lc + child.getMeasuredWidth();
                int bc = tc + child.getMeasuredHeight();

                child.layout(lc, tc, rc, bc);

                left += child.getMeasuredWidth() + lp.rightMargin
                        + lp.leftMargin;
            }
            left = 0;
            top += lineHeight;
        }

    }
}
在onMeasure方法中首先得到父容器传入的测量模式和宽高值,然后遍历所有的childView,使用measureChild方法对所有的childView进行测 量。然后根据所有childView测量得出的宽和高得到该ViewGroup如果设置为wrap_content时的宽和高。最后根据模式,如果是 MeasureSpec.EXACTLY则直接使用父ViewGroup传入的宽和高,否则设置为自己计算的宽和高。然后在onLayout中完成对所有childView的布局。

看下activity_main.xml
<com.example.view.CustomViewGroup xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="200dp"
    android:layout_height="wrap_content" >

    <TextView
        style="@style/text_style"
        android:background="@drawable/flag_01"
        android:text="Welcome" />

    <TextView
        style="@style/text_style"
        android:background="@drawable/flag_01"
        android:text="You" />

    <TextView
        style="@style/text_style"
        android:background="@drawable/flag_02"
        android:text="自定义" />

    <TextView
        style="@style/text_style"
        android:background="@drawable/flag_02"
        android:text="ViewGroup" />

    <TextView
        style="@style/text_style"
        android:background="@drawable/flag_03"
        android:text="努力" />

    <TextView
        style="@style/text_style"
        android:background="@drawable/flag_03"
        android:text="学习" />

    <TextView
        style="@style/text_style"
        android:background="@drawable/flag_04"
        android:text="Try you best" />

</com.example.view.CustomViewGroup>

实现效果如下:

篇幅有限,先说这么多吧


源代码

参考:

http://blog.csdn.net/lmj623565791/article/details/38352503

http://blog.csdn.net/u013045971/article/details/45257677

相关文章
|
5月前
|
Android开发
NestedScrollView,ScrollView中嵌套listView 或者RecyclerView会自动跳到顶部,中部,底部的问题。
NestedScrollView,ScrollView中嵌套listView 或者RecyclerView会自动跳到顶部,中部,底部的问题。
159 0
|
XML Java Android开发
Android 中ScrollView垂直滚动视图之隐藏滚动条的三种方法
Android 中ScrollView垂直滚动视图之隐藏滚动条的三种方法
165 0
|
Android开发
【RecyclerView】 九、为 RecyclerView 设置不同的布局样式
【RecyclerView】 九、为 RecyclerView 设置不同的布局样式
277 0
【RecyclerView】 九、为 RecyclerView 设置不同的布局样式
ScrollView和HorizontalScrollView无法设置点击事件的源码解析
最近的开发过程中,发现存在ScrollView和HorizontalScrollView无法设置点击事件的现象。 我们知道,通常在设置点击事件时,位于View树上方的子View的OnClickListener,会优先于父View的OnClickListener执行。 开发过程中我们会经常使用类似的方式来给布局设置点击事件,比如给ListView的Item背景设置OnClickListener,用于点击item空白区域的跳转操作;然后再给item内部的子元素分别设置OnClickListener用于各自不同的点击操作。
|
Android开发
Android使用绝对布局AbsoluteLayout动态添加控件
Android使用绝对布局AbsoluteLayout动态添加控件
181 0
|
Android开发
同一页面实现recycleView三种布局【recycleView + adapter】
同一页面实现recycleView三种布局【recycleView + adapter】
152 0
同一页面实现recycleView三种布局【recycleView + adapter】
|
前端开发 Android开发
自定义View实例(三)----滑动刻度尺与流式布局
最近在系统学习自定义View这一块的知识,前面几篇基本都是理论知识,这篇博客着重从实战来加强对自定义View的理解与运用。实现的两种效果,分别代表自定义View与自定义ViewGroup。
1309 0
自定义控件基础 之 3.4 ViewGroup的测量 & 3.5 ViewGroup的绘制
ViewGroup的测量 之前分析中说了,ViewGroup会去管理其子View,其中一个管理项目就是负责子View的显示大小。当ViewGroup的大小为wrap_content时,ViewGroup就需要对子View进行遍历,以便获得所有子View的大小,从而来决定自己的大小。
708 0
|
前端开发 Android开发 数据格式
无上下边距自定义TextView
由于UI的奇葩作图稿,要求文字要贴边,否则会导致上下的View的margin会变大(因为TextView的文字绘制时有上下间距)。 Paint.FontMetrics /** * Class that describes the various metrics for a font at a given text size.
2791 0
|
Android开发
android:elevation属性,控制View底部渐变阴影
android:elevation属性,控制View底部渐变阴影 android:elevation这一属性,可以控制View底部渐变阴影,给一个View在其底部增加一定的灰度渐变阴影效果,如图: 上图是一个简单的TextView,TextView底部阴影渐变。
2716 0