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

转载请注明出处: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
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值