Android-浅析自定义ViewGroup(附一个子控件根据父控件行宽自动换行的LineWrapLayout 案例)

来源:互联网 发布:ubuntu cab软件 编辑:程序博客网 时间:2024/06/05 18:43

转载请注明出处:http://blog.csdn.net/teisun/article/details/45560095

前言

做了快4年的android开发没写过什么技术文章,最近工作空档较多所以想起来写一两篇博文分享出来并且提升自己,文采拙劣,欢迎拍砖。

View的绘制过程

ViewGroup继承View,ViewGourp可以包含很多个View,View的绘制过程分三个步骤:
- onMeasure 计算,可以把View想象成一张无限大的画布,那么你要画出来的内容的尺寸有多大? onMeasure就是用于计算内容尺寸边界的。
- onLayout 布局,内容是由元素组成的,决定好画布的尺寸后各个子View要放在画布的什么位置呢?onLayout就是让你决定各个子View要放在画布的什么位置。
- onDraw 画,决定好子View的位置后,那我们的子View长什么样子呢?是画鸡、画猫还是画狗呢,只有各个子View自己知道,dispatchDraw 通知各个子View在指定的位置draw出自己。

onMeasure

onMeasure函数的widthMeasureSpec与heightMeasureSpec并非只是个简单的数字而是一种特殊的值,是期望类型值与尺寸值相加得到的,而android中的View.MeasureSpec类提供了对这种特殊值得操作方法:getSize,getMode ,makeMeasureSpec。

@Override  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    //getSize 获取控件宽度   int with = MeasureSpec.getSize(widthMeasureSpec);  //getMode 获取父View对其宽度的期望值类型  int mode = MeasureSpec.getMode(widthMeasureSpec);  //makeMeasureSpec 根据给定的尺寸和期望类型生成一个MeasureSpec值,一般用于指定子View的尺寸  View child = getChildAt(0);  LayoutParams lp=(LayoutParams)child.getLayoutParams();  //MeasureSpec.AT_MOST:表示子View最多只能是lp.width指定的大小,开发人员和系统都应该按照这个规则去设置子View的大小,当然开发人员也可以任性地自己去设置想要的大小。  int wSpec=MeasureSpec.makeMeasureSpec(lp.width,MeasureSpec.AT_MOST);  int hSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.AT_MOST);   //MeasureSpec.EXACTLY:表示子View只能是lp.width指定的大小,开发人员和系统都应该按照这个尺寸去设置子View的大小,当然开发人员也可以任性地自己去设置想要的大小。  int wSpec=MeasureSpec.makeMeasureSpec(lp.width,MeasureSpec.EXACTLY);  int hSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);   //MeasureSpec.UNSPECIFIED:表示子View的尺寸大小没有任何限制.  int wSpec=MeasureSpec.makeMeasureSpec(lp.width,MeasureSpec.UNSPECIFIED);  int hSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.UNSPECIFIED);  child.measure(wSpec, hSpec); }

更多关于MeasureSpec与android中View的Flag设计会在其他文章中说明这里不再展开。

onLayout

官方解释:

Called from layout when this view should assign a size and position to each of its children.

也就是说当需要为每一个子View指定尺寸和位置时会被调用。

@Override    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {        int mTotalHeight = 0;        // 遍历子View        int childCount = getChildCount();        for (int i = 0; i < childCount; i++) {            View childView = getChildAt(i);            // 获取在onMeasure中计算的视图尺寸            int measureHeight = childView.getMeasuredHeight();            int measuredWidth = childView.getMeasuredWidth();          //告诉ViewGroup子View要的大小和位置        childView.layout(left, mTotalHeight, measuredWidth, mTotalHeight + measureHeight);                mTotalHeight += measureHeight;        }    }

onDraw

onDraw 方法一般自定义View的时候会用到,下一篇文章再详细分析。

案例:LineWrapLayout 实现自动换行的自定义ViewGroup

android提供的Linearlayout不可以自动换行显示,那么我们自己写一个,先看效果:

实现思路

  • 什么时候应该换行?
    答:一行内所有子View的宽度和View之间的间隔相加>=ViewGroup的宽度时就应该换行,而且最后一个View就应该被排列到下一行。
  • 如何计算ViewGroup的高度?
    答:ViewGroup的上下Padding+每一行高度+行之间纵向间隔
  • 可以设置子View与子View之间的横向纵向间隔
  • onMeasure函数中实现ViewGroup的宽高计算逻辑与每一个子View的宽高。
  • onLayout函数中实现各个子View的排列逻辑

好了,想太多没用有个大概的实现思路就可以动手写了。

主要代码

我想到的是使用xml文件配置子View之间的间隔这就需要用到自定义属性,在values文件夹下创建attrs.xml,代码:

//name写自定义控件的类名,每一个attr指定属性名和属性类型    <declare-styleable name="LineWrapLayout">        //横向间隔        <attr name="horizontal_spacing" format="dimension" />        //纵向间隔        <attr name="vertical_spacing" format="dimension" />    </declare-styleable>

使用:

<com.vclubs.ui.component.LineWrapLayout    //使用自定义属性必须加这一句    xmlns:app="http://schemas.android.com/apk/res-auto"    //使用自定义属性指定子View之间的横向纵向间隔    app:horizontal_spacing="@dimen/dp15"    app:vertical_spacing="@dimen/dp15"    android:id="@+id/pic_container"    android:layout_width="match_parent"    android:layout_height="wrap_content"    android:layout_marginLeft="@dimen/dp40"    android:layout_marginRight="@dimen/dp40"    android:orientation="horizontal"    android:paddingLeft="@dimen/dp20"    android:paddingRight="@dimen/dp20"    android:paddingTop="@dimen/dp20"    ></com.vclubs.ui.component.LineWrapLayout>

代码中获取自定义属性的值:

public LineWrapLayout(Context context, AttributeSet attrs) {    super(context, attrs);    //在构造函数中获得自定义属性的值,ViewGroup有三个构造函数最好都实现一下    TypedArray a = context.obtainStyledAttributes(attrs,    R.styleable.LineWrapLayout);    //得到横向间隔    hSpacing = a.getDimensionPixelSize(R.styleable.LineWrapLayout_horizontal_spacing, 15);    //得到纵向间隔    vSpacing = a.getDimensionPixelSize(R.styleable.LineWrapLayout_vertical_spacing, 15);    a.recycle();    }

在onMeasure函数中计算ViewGroup尺寸与各个子View的尺寸:

    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        //得到ViewGroup的初始宽高        final int width = MeasureSpec.getSize(widthMeasureSpec);        int height = MeasureSpec.getSize(heightMeasureSpec)                + getPaddingBottom()+getPaddingTop();        final int count = getChildCount();        int line_height = 0;        //获取第一个子View的起始点位置        int xpos = getPaddingLeft();        int ypos = getPaddingTop();        //计算每一个子View的尺寸,并算出ViewGroup的高度        for (int i = 0; i < count; i++) {            final View child = getChildAt(i);            final LayoutParams lp = child.getLayoutParams();            //算出子View宽的MeasureSpec值            int wSpec = MeasureSpec.makeMeasureSpec(            lp.width, MeasureSpec.AT_MOST);            //算出子View高的MeasureSpec值            int hSpec = MeasureSpec.makeMeasureSpec(            lp.height, MeasureSpec.AT_MOST);            //让子View记住自己宽高的MeasureSpec值,子View的            //onMeasure(int widthMeasureSpec,int heightMeasureSpec)            //函数传入的就是这里算出来的这两个值            child.measure(wSpec, hSpec);            //设置完MeasureSpec值后调用View.getMeasuredWidth()函数算出View的宽度            final int childw = child.getMeasuredWidth();            //记录最大行高(子View的高度有可能不一样,行高取最大高度)            line_height = Math.max(line_height,             child.getMeasuredHeight() + vSpacing);            if (xpos + childw > width) {                //初始坐标的x偏移值+子View宽度>ViewGroup宽度 就换行                xpos = getPaddingLeft();//坐标x偏移值归零                ypos += line_height;//坐标y偏移值再加上本行的行高也就是换行            }            //算出下一个子View的起始点x偏移值            xpos += childw + hSpacing;        }        this.line_height = line_height;        if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.UNSPECIFIED) {            //对高度期望值没有限制            height = ypos + line_height;        } else if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) {            //达不到指定高度则缩小高度            if (ypos + line_height < height) {                height = ypos + line_height;            }        } else {            height = ypos + line_height;        }        //设置ViewGroup宽高值        setMeasuredDimension(width, height);    }

在onLayout函数中设置各个子View的位置,有了上面的基础下面的代码应该很好理解了,由读者自行理解吧就不注释了。

    @Override    protected void onLayout(boolean changed, int l, int t, int r, int b) {        final int count = getChildCount();        final int width = r - l;        int xpos = getPaddingLeft();        int ypos = getPaddingTop();        //设置每一个子View的位置,左上角xy坐标与右下角xy坐标确定View的位置        for (int i = 0; i < count; i++) {            final View child = getChildAt(i);            if (child.getVisibility() != GONE) {                final int childw = child.getMeasuredWidth();                final int childh = child.getMeasuredHeight();                if (xpos + childw > width) {                    xpos = getPaddingLeft();                    ypos += line_height;                }                child.layout(xpos, ypos, xpos + childw, ypos + childh);                xpos += childw + hSpacing;            }        }    }

结束

大概内容已经写完了以后再完善,源码:
https://github.com/teisun/Android-LineWrapLayout
http://download.csdn.net/detail/teisun/8755303

0 0
原创粉丝点击