android 自定义View——流布局FlowLayout

android 系统提供了五种布局方式:FrameLayout、RelativeLayout、LinearLayout、GridLayout、AbsoluteLayout;大部分Android界面都是通过这五种布局相互嵌套实现的,但是如果有这样的一个需求:控件的个数是通过从服务器获取数据后才知道的,并且希望控件水平排列,当超出屏幕时自动换行。这是就需要自己定义布局了。

流布局FlowLayout——实现了控件位置水平方向一次排列,在控件水平总宽度大于屏幕宽度时,自动切换到下一排显示。

自定义布局需要继承ViewGroup重写onMeasure和onLayout方法,如果布局要支持子控件的layout_margin属性,则自定义的ViewGroup类必须重载generateLayoutParams()函数,并且在该函数中返回一个ViewGroup.MarginLayoutParams派生类对象,这样才能使用margin参数。

public class FlowLayout extends ViewGroup {

/*自定义ViewGroup支持子控件的layout_margin参数,则自定义的ViewGroup类必须重载generateLayoutParams()函数,并且在该函数中返回一个ViewGroup.MarginLayoutParams派生类对象,这样才能使用margin参数。*/
   public static class LayoutParams extends ViewGroup.MarginLayoutParams {
       public LayoutParams(Context c, AttributeSet attrs)          
       {
           super(c, attrs);
       }

       public LayoutParams(int width, int height) {
          super(width, height);
       }
    }
    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) 
    {
        return new FlowLayout.LayoutParams(getContext(), attrs);
    }
}

onMeasure方法完成自己大小的测量和遍历调用所有子元素的measure方法;在这个方法里,View可以控制自己的大小。根据需求,我们要在这个方法里做两件事:

  1. 根据子控件的宽度和margin值,计算布局本身的宽度,当计算的宽度大于屏幕的宽度时,布局的宽度就等于屏幕的宽度。
  2. 根据每一排子控件的最大高度计算布局的高度

在onMeasure方法中使用了MeasureSpec类,MeasureSpec代表一个32位int值,高2位代表SpecMode,低30位代表SpecSize,SpecMode是指测量模式,SpecSize是指在某种模式下的规格大小,下面是MeasureSpec的源码:

public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

        /** @hide */
        @IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
        @Retention(RetentionPolicy.SOURCE)
        public @interface MeasureSpecMode {}


        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        public static final int EXACTLY     = 1 << MODE_SHIFT;


        public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                          @MeasureSpecMode int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }
        public static int makeSafeMeasureSpec(int size, int mode) {
            if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
                return 0;
            }
            return makeMeasureSpec(size, mode);
        }
        @MeasureSpecMode
        public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }
        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }

        static int adjust(int measureSpec, int delta) {
            final int mode = getMode(measureSpec);
            int size = getSize(measureSpec);
            if (mode == UNSPECIFIED) {
                // No need to adjust size for UNSPECIFIED mode.
                return makeMeasureSpec(size, UNSPECIFIED);
            }
            size += delta;
            if (size < 0) {
                Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +
                        ") spec: " + toString(measureSpec) + " delta: " + delta);
                size = 0;
            }
            return makeMeasureSpec(size, mode);
        }
        public static String toString(int measureSpec) {
            int mode = getMode(measureSpec);
            int size = getSize(measureSpec);

            StringBuilder sb = new StringBuilder("MeasureSpec: ");

            if (mode == UNSPECIFIED)
                sb.append("UNSPECIFIED ");
            else if (mode == EXACTLY)
                sb.append("EXACTLY ");
            else if (mode == AT_MOST)
                sb.append("AT_MOST ");
            else
                sb.append(mode).append(" ");

            sb.append(size);
            return sb.toString();
        }
    }

MeasureSpec通过将SpecMode和SpecSize导打包成一个int值来避免过多的对象内存分配,其还提供了打包和解包,一组SpecMode和SpecSize可以打包为一个MeasureSpec,而一个MeasureSpec可以通过解包的形式得到原始的SpecMode和SpecSize。

SpecMode有三种模式:
UNSPECIFIED:
父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量状态。
EXACTLY:
父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应于定义View大小的match_parent和具体的数值。
AT_MOST:
父容器指定一个可用大小即SpecSize,View的大小不能大于这个数值,具体是多少看不同View的具体表现,对应于定义View大小的wrap_content.

onMeasure方法代码

  @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int measureWidth=0;
        int sreenWidth=0;
        int measureHeight=0;
        int maxHeigth=0;
        final int childCount=getChildCount();
        measureChildren(widthMeasureSpec,heightMeasureSpec);
        int widthSpaceSize= MeasureSpec.getSize(widthMeasureSpec);
        int widthSpaceMoth= MeasureSpec.getMode(widthMeasureSpec);
        int heightSpaceSize= MeasureSpec.getSize(heightMeasureSpec);
        int heightSpaceMoth= MeasureSpec.getMode(heightMeasureSpec);
        if(childCount==0){
            setMeasuredDimension(0,0);
        }else{
            if(widthSpaceMoth== MeasureSpec.AT_MOST&&heightSpaceMoth== MeasureSpec.AT_MOST){
                for(int i=0;i<childCount;i++){
                    View childView=getChildAt(i);
                    if(childView.getVisibility()!= View.GONE){
                        final int childWidth=childView.getMeasuredWidth();
                        final int childHeight=childView.getMeasuredHeight();

                        final FlowLayout.LayoutParams lp=(FlowLayout.LayoutParams) childView.getLayoutParams();

                        if((sreenWidth+childWidth+lp.leftMargin+lp.rightMargin)>widthSpaceSize){
                            measureWidth=widthSpaceSize;
                            sreenWidth=childWidth+lp.leftMargin+lp.rightMargin;
                            maxHeigth=childHeight+lp.topMargin+lp.bottomMargin;
                            measureHeight+=maxHeigth;


                        }else{
                            measureWidth+=childWidth+lp.leftMargin+lp.rightMargin;
                            sreenWidth+=childWidth+lp.leftMargin+lp.rightMargin;
                            if((childHeight+lp.topMargin+lp.bottomMargin)>maxHeigth){
                                measureHeight=measureHeight-maxHeigth+childHeight+lp.topMargin+lp.bottomMargin;
                                maxHeigth=childHeight+lp.topMargin+lp.bottomMargin;

                            }
                        }
                    }
                }
                if(measureWidth>widthSpaceSize){
                    measureWidth=widthSpaceSize;
                }
                setMeasuredDimension(measureWidth,measureHeight);
            }else if(heightSpaceMoth== MeasureSpec.AT_MOST){
                for(int i=0;i<childCount;i++){
                    View childView=getChildAt(i);
                    if(childView.getVisibility()!= View.GONE){
                        final int childWidth=childView.getMeasuredWidth();
                        final int childHeight=childView.getMeasuredHeight();
                        final FlowLayout.LayoutParams lp=(FlowLayout.LayoutParams) childView.getLayoutParams();

                        if((sreenWidth+childWidth+lp.leftMargin+lp.rightMargin)>widthSpaceSize){
                            measureWidth=widthSpaceSize;
                            sreenWidth=childWidth+lp.leftMargin+lp.rightMargin;
                            maxHeigth=childHeight+lp.topMargin+lp.bottomMargin;
                            measureHeight+=maxHeigth;
                        }else{
                            measureWidth+=childWidth+lp.leftMargin+lp.rightMargin;
                            sreenWidth+=childWidth+lp.leftMargin+lp.rightMargin;
                            if((childHeight+lp.topMargin+lp.bottomMargin)>maxHeigth){
                                measureHeight=measureHeight-maxHeigth+childHeight+lp.topMargin+lp.bottomMargin;
                                maxHeigth=childHeight+lp.topMargin+lp.bottomMargin;
                            }
                        }
                    }
                }
                setMeasuredDimension(widthSpaceSize,measureHeight);
            }else if(widthSpaceMoth== MeasureSpec.AT_MOST){
                for(int i=0;i<childCount;i++){
                    View childView=getChildAt(i);
                    if(childView.getVisibility()!= View.GONE){
                        int childWidth=childView.getMeasuredWidth();
                        if((measureWidth+childWidth)>widthSpaceSize){
                            measureWidth=widthSpaceSize;
                        }else{
                            measureWidth+=childWidth;
                        }
                    }
                }
            }
        }

    }

onLayout方法作用是确定所有子元素的位置,我们要根据需求在水平宽度大于屏幕宽度的时候,通过调用layout方法控制子元素的位置。(注意margin属性)

 @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        int width=right-left;
        int childLeft=0;
        int childTop=0;
        int maxHeight=0;
        final int childCount=getChildCount();
        for(int i=0;i<childCount;i++){
            final View childView=getChildAt(i);
            if(childView.getVisibility()!= View.GONE){
                int childWidth=childView.getMeasuredWidth();
                int childHeight=childView.getMeasuredHeight();

                FlowLayout.LayoutParams lp=(FlowLayout.LayoutParams) childView.getLayoutParams();

                if((childLeft+childWidth+lp.leftMargin+lp.rightMargin)>width){
                    childLeft=0;
                    childTop+=maxHeight;
                    maxHeight=childHeight+lp.topMargin+lp.bottomMargin;
                }else{
                    if((childHeight+lp.topMargin+lp.bottomMargin)>maxHeight){
                        maxHeight=childHeight+lp.topMargin+lp.bottomMargin;
                    }
                }
                childView.layout(childLeft+lp.leftMargin,childTop+lp.topMargin,childLeft+lp.leftMargin+childWidth,childTop+lp.topMargin+childHeight);
                childLeft+=childWidth+lp.leftMargin+lp.rightMargin;
            }
        }

    }

写到这里,自定义布局就完成了,然后我们在XML中使用我们的自定义布局,看一下效果。
xml文件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.flowlayout.demo.FlowLayout
        android:id="@+id/flowlayout"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#ffffff">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="40dp"
            android:layout_margin="10dp"
            android:background="@drawable/shape_flow_layout"
            android:gravity="center"
            android:padding="5dp"
            android:text="textView1" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="40dp"
            android:layout_margin="10dp"
            android:background="@drawable/shape_flow_layout"
            android:gravity="center"
            android:padding="5dp"
            android:text="textView2" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="40dp"
            android:layout_margin="10dp"
            android:background="@drawable/shape_flow_layout"
            android:gravity="center"
            android:padding="5dp"
            android:text="textView3" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="40dp"
            android:layout_margin="10dp"
            android:background="@drawable/shape_flow_layout"
            android:gravity="center"
            android:padding="5dp"
            android:text="textView4" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="40dp"
            android:layout_margin="10dp"
            android:background="@drawable/shape_flow_layout"
            android:gravity="center"
            android:padding="5dp"
            android:text="textView5" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="40dp"
            android:layout_margin="10dp"
            android:background="@drawable/shape_flow_layout"
            android:gravity="center"
            android:padding="5dp"
            android:text="textView6" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="40dp"
            android:layout_margin="10dp"
            android:background="@drawable/shape_flow_layout"
            android:gravity="center"
            android:padding="5dp"
            android:text="textView7" />
    </com.flowlayout.demo.FlowLayout>
    <Button
        android:id="@+id/add"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAllCaps="false"
        android:layout_gravity="center_horizontal"
        android:text="增加TextView"/>

</LinearLayout>

源码下载

运行结果:
这里写图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值