自己定义控件三部曲视图篇——測量与布局

Posted mthoutai

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自己定义控件三部曲视图篇——測量与布局相关的知识,希望对你有一定的参考价值。

前言:生命总是要有信仰,有梦想才干一直前行。哪怕走的再慢,也是在前行。



相关文章:

Android自己定义控件三部曲文章索引》http://blog.csdn.net/harvic880925/article/details/50995268


今天给大家讲讲有关自己定义布局控件的问题,大家来看这样一个需求,你须要设计一个container,实现内部控件自己主动换行。即里面的控件能够依据长度来推断当前行是否容得下它,进而决定是否转到下一行显示。效果图例如以下

技术分享

在上图中,全部的紫色部分是FlowLayout控件,明显能够看出,内部的每一个TextView控件。能够依据大小自己主动排列。
效果图就是这样子了,第一篇先讲下预备知识。

一、ViewGroup绘制流程

注意,View及ViewGroup基本同样。仅仅是在ViewGroup中不仅要绘制自己还是绘制当中的子控件,而View则仅仅须要绘制自己就能够了。所以我们这里就以ViewGroup为例来讲述整个绘制流程。


绘制流程分为三步:測量、布局、绘制
分别相应:onMeasure()、onLayout()、onDraw()

当中,他们三个的作用分别例如以下:
onMeasure():測量自己的大小。为正式布局提供建议。(注意。仅仅是建议,至于用不用,要看onLayout);
onLayout():使用layout()函数对全部子控件布局;
onDraw():依据布局的位置画图;
有关画图的部分,大家能够參考我的系列博客《android Graphics(一):概述及基本几何图形绘制》共同拥有四篇。讲述了有关android 画图的90%内容,大家能够參考。


这篇文章着重将内容放在分析onMeasure()和onLayout()上。

二、onMeasure与MeasureSpec

布局绘画涉及两个过程:測量过程和布局过程。測量过程通过measure方法实现。是View树自顶向下的遍历,每一个View在循环过程中将尺寸细节往下传递,当測量过程完毕之后。全部的View都存储了自己的尺寸。

第二个过程则是通过方法layout来实现的。也是自顶向下的。

在这个过程中,每一个父View负责通过计算好的尺寸放置它的子View。


前面讲过,onMeasure()是用来測量当前控件大小的。给onLayout()提供数值參考,须要特别注意的是:測量完毕以后通过setMeasuredDimension(int,int)设置给系统。


1、onMeasure

首先,看一下onMeasure()的声明:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
这里我们主要关注传进来的两个參数:int widthMeasureSpec, int heightMeasureSpec
与这两个參数有关的是两个问题:意义和组成。

即他们是怎么来的,表示什么意思;还有,他们是组成方式是如何的。
我们先说他们的意义:
他们是父类传递过来给当前view的一个建议值,即想把当前view的尺寸设置为宽widthMeasureSpec,高heightMeasureSpec
有关他们的组成。我们就直接转到MeasureSpec部分。


2、MeasureSpec

尽管表面上看起来他们是int类型的数字,事实上他们是由mode+size两部分组成的。
widthMeasureSpec和heightMeasureSpec转化成二进制数字表示,他们都是32位的。

前两位代表mode(測量模式)。后面30位才是他们的实际数值(size)。
(1)模式分类
它有三种模式:
①、UNSPECIFIED(未指定),父元素部队自元素施加不论什么束缚,子元素能够得到随意想要的大小。
②、EXACTLY(全然)。父元素决定自元素的确切大小,子元素将被限定在给定的边界里而忽略它本身大小。
③、AT_MOST(至多)。子元素至多达到指定大小的值。

他们相应的二进制值各自是:
UNSPECIFIED=00000000000000000000000000000000
EXACTLY =01000000000000000000000000000000
AT_MOST =10000000000000000000000000000000
由于最前面两位代表模式,所以他们分别相应十进制的0。1,2;
(2)模式提取
如今我们知道了widthMeasureSpec和heightMeasureSpec是由模式和数值组成的。而且二进制的前两位代表模式,后28位代表数字。
我们先想想,假设我们自己来提取widthMeasureSpec和heightMeasureSpec中的模式和数值是怎么提取呢?
首先想到的肯定是通过MASK和与运算去掉不须要的部分而得到相应的模式或数值。


讲到这大家可能会迷茫。我们写段代码来提取模式部分吧:

//相应11000000000000000000000000000000;总共32位,前两位是1
int MODE_MASK  = 0xc0000000;

//提取模式
public static int getMode(int measureSpec) {
    return (measureSpec & MODE_MASK);
}
//提取数值
public static int getSize(int measureSpec) {
    return (measureSpec & ~MODE_MASK);
}
相信大家看了代码就应该清楚模式和数值提取的方法了吧,主要用到了MASK的与、非运算,难度不大,假设有问题。自行谷歌一下与、非运算方法吧。
(3)、MeasureSpec
上面我们自已实现了模式和数值的提取。但在强大的andorid面前。肯定有提供提取模式和数值的类。这个类就是MeasureSpec
以下两个函数就能够实现这个功能:
MeasureSpec.getMode(int spec) //获取MODE
MeasureSpec.getSize(int spec) //获取数值
另外MODE的取值为:
MeasureSpec.AT_MOST
MeasureSpec.EXACTLY
MeasureSpec.UNSPECIFIED
通过以下的代码就能够分别获取widthMeasureSpec和heightMeasureSpec的MODE和数值
int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);
事实上大家通过查看代码能够知道,我们的实现就是MeasureSpec.getSize()和MeasureSpec.getMode()的实现代码。
(4)、模式有什么用呢
我们知道这里有三个模式:EXACTLY、AT_MOST、UNSPECIFIED
须要注意的是widthMeasureSpec和heightMeasureSpec各自都有它相应的模式,模式的由来分别来自于XML定义:
简单来说。XML布局和模式有例如以下相应关系:
  • wrap_content-> MeasureSpec.AT_MOST
  • match_parent -> MeasureSpec.EXACTLY
  • 详细值 -> MeasureSpec.EXACTLY

比如。以下这个XML

<com.example.harvic.myapplication.FlowLayout
     android:layout_width="match_parent"
     android:layout_height="wrap_content">
     
</com.example.harvic.myapplication.FlowLayout>
那FlowLayout在onMeasure()中传值时widthMeasureSpec的模式就是 MeasureSpec.EXACTLY。即父窗体宽度值。

heightMeasureSpec的模式就是 MeasureSpec.AT_MOST,即不确定的。

一定要注意是,当模式是MeasureSpec.EXACTLY时,我们就不必要设定我们计算的大小了,由于这个大小是用户指定的,我们不应更改。

但当模式是MeasureSpec.AT_MOST时。也就是说用户将布局设置成了wrap_content,我们就须要将大小设定为我们计算的数值,由于用户根本没有设置详细值是多少,须要我们自己计算。


即。假如width和height是我们经过计算的控件所占的宽度和高度。那在onMeasure()中使用setMeasuredDimension()最后设置时,代码应该是这种:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
	int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
    int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
    int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
    int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);
    
    //经过计算,控件所占的宽和高分别相应width和height
    //计算过程。我们会在下篇细讲
    …………
    
	setMeasuredDimension((measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth: width, (measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight: height);
}

三、onLayout()

1、概述

上面说了,onLayout()是实现全部子控件布局的函数。注意。是全部子控件!!

那它自己的布局怎么办?后面我们再讲,先讲讲在onLayout()中我们应该做什么。
我们先看看ViewGroup的onLayout()函数的默认行为是什么
在ViewGroup.java中

@Override
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);
是一个抽象方法。说明凡是派生自ViewGroup的类都必须自己去实现这种方法。

像LinearLayout、RelativeLayout等布局,都是重写了这种方法,然后在内部依照各自的规则对子视图进行布局的。

2、实例

以下我们就举个样例来看一下有关onMeasure()和onLayout()的详细使用:
以下是效果图: 

技术分享

这个效果图主要有两点:
1、三个TextView竖直排列
2、背景的Layout宽度是match_parent,高度是wrap_content.
以下我们就看一下。代码上如何实现:
(1)、XML布局
首先我们看一下XML布局:(activity_main.xml)

<com.harvic.simplelayout.MyLinLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="#ff00ff"
    tools:context=".MainActivity">

    <TextView android:text="第一个VIEW"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <TextView android:text="第二个VIEW"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <TextView android:text="第三个VIEW"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</com.harvic.simplelayout.MyLinLayout>
可见里面有三个TextView,然后自己定义的MyLinLayout布局,宽度设为了match_parent,高度设为了wrap_content.
(2)、MyLinLayout实现:重写onMeasure()函数
我们前面讲过,onMeasure()的作用就是依据container内部的子控件计算自己的宽和高。最后通过setMeasuredDimension(int width,int height设置进去);
以下看看onMeasure()的完整代码,然后再逐步解说:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
    int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
    int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
    int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);

    int height = 0;
    int width = 0;
    int count = getChildCount();
    for (int i=0;i<count;i++) {
		//測量子控件
        View child = getChildAt(i);
        measureChild(child, widthMeasureSpec, heightMeasureSpec);
		//获得子控件的高度和宽度
        int childHeight = child.getMeasuredHeight();
        int childWidth = child.getMeasuredWidth();
		//得到最大宽度,而且累加高度
        height += childHeight;
        width = Math.max(childWidth, width);
    }

    setMeasuredDimension((measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth: width, (measureHeightMode == MeasureSpec.EXACTLY) ?

measureHeight: height); }

首先,是从父类传过来的建议宽度和高度值:widthMeasureSpec、heightMeasureSpec
从他们里面利用MeasureSpec提取宽高值和相应的模式:
    int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
    int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
    int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
    int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);
接下来就是通过測量它全部的子控件来决定它所占位置的大小:
int height = 0;
int width = 0;
int count = getChildCount();
for (int i=0;i<count;i++) {
	//測量子控件
    View child = getChildAt(i);
    measureChild(child, widthMeasureSpec, heightMeasureSpec);
	//获得子控件的高度和宽度
    int childHeight = child.getMeasuredHeight();
    int childWidth = child.getMeasuredWidth();
	//得到最大宽度,而且累加高度
    height += childHeight;
    width = Math.max(childWidth, width);
}
我们这里要计算的是整个VIEW当被设置成layout_width="wrap_content",layout_height="wrap_content"所占用的大小。由于我们是垂直排列其内部全部的VIEW。所以container所占宽度应该是各个TextVIew中的最大宽度。所占高度应该是全部控件的高度和。
最后,依据当前用户的设置来推断是否将计算出来的值设置进onMeasure()中。用它来计算当前container所在位置。


setMeasuredDimension((measureWidthMode == MeasureSpec.EXACTLY) ?

measureWidth: width, (measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight: height);

前面我们讲过,模式与XML布局的相应关系:
* wrap_content-> MeasureSpec.AT_MOST
* match_parent -> MeasureSpec.EXACTLY
* 详细值 -> MeasureSpec.EXACTLY

再看我们前面XML中针对MyLinLayout的设置:
<com.harvic.simplelayout.MyLinLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="#ff00ff"
    tools:context=".MainActivity">
所以这里的measureWidthMode应该是MeasureSpec.EXACTLY,measureHeightMode应该是MeasureSpec.AT_MOST。所以在最后利用setMeasuredDimension(width,height)来终于设置时,width使用的是从父类传过来的measureWidth,而高度则是我们自己计算的height.即实际的运算结果是这种:
setMeasuredDimension(measureWidth,height);

总体来讲,onMeasure()中计算出的width和height,就是当XML布局设置为layout_width="wrap_content"、layout_height="wrap_content"时所占的宽和高。即整个container所占的最小矩形

(3)、MyLinLayout实现:重写onLayout()函数

在这部分,就是依据自己的意愿把内部的各个控件排列起来。我们要完毕的是将全部的控件垂直排列。
先看完整的代码。然后再细讲:
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int top = 0;
    int count = getChildCount();
    for (int i=0;i<count;i++) {

        View child = getChildAt(i);

        int childHeight = child.getMeasuredHeight();
        int childWidth = child.getMeasuredWidth();

        child.layout(0, top, childWidth, top + childHeight);
        top += childHeight;
    }
}
最核心的代码,就是调用layout()函数设置子控件所在的位置:
int childHeight = child.getMeasuredHeight();
int childWidth = child.getMeasuredWidth();

child.layout(0, top, childWidth, top + childHeight);
top += childHeight;

在这里top指的是控件的顶点,那bottom的坐标就是top+childHeight,我们从最左边開始布局,那么right的坐标就肯定是子控件的宽度值了childWidth.

到这里,这个样例就讲完了。源代码会在文章底部给出,以下来讲一个非常easy混淆的问题。

(4)、getMeasuredWidth()与getWidth()
趁热打铁,就这个样例。我们讲一个非常easy出错的问题:getMeasuredWidth()与getWidth()的差别。

他们的值大部分时间都是同样的,但意义确是根本不一样的。我们就来简单分析一下。
差别主要体如今以下几点:
- 首先getMeasureWidth()方法在measure()过程结束后就能够获取到了,而getWidth()方法要在layout()过程结束后才干获取到。


- getMeasureWidth()方法中的值是通过setMeasuredDimension()方法来进行设置的,而getWidth()方法中的值则是通过layout(left,top,right,bottom)方法设置的。
还记得吗。我们前面讲过,setMeasuredDimension()提供的測量结果仅仅是为布局提供建议。终于的取用与否要看layout()函数。大家再看看我们上面重写的MyLinLayout,是不是我们自己使用child.layout(left,top,right,bottom)来定义了各个子控件所应在的位置:

int childHeight = child.getMeasuredHeight();
int childWidth = child.getMeasuredWidth();

child.layout(0, top, childWidth, top + childHeight);
从代码中能够看到。我们使用child.layout(0, top, childWidth, top + childHeight);来布局控件的位置,当中getWidth()的取值就是这里的右坐标减去左坐标的宽度;由于我们这里的宽度是直接使用的child.getMeasuredWidth()的值。当然会导致getMeasuredWidth()与getWidth()的值是一样的。假设我们在调用layout()的时候传进去的宽度值不与getMeasuredWidth()同样,那必定getMeasuredWidth()与getWidth()的值就不再一样了。


一定要注意的一点是:getMeasureWidth()方法在measure()过程结束后就能够获取到了,而getWidth()方法要在layout()过程结束后才干获取到。再重申一遍!!

!!


源代码在文章底部给出

3、疑问:container自己什么时候被布局

前面我们说了。在派生自ViewGroup的container中。比方我们上面的MyLinLayout。在onLayout()中布局它全部的子控件。

那它自己什么时候被布局呢?

它当然也有父控件,它的布局也是在父控件中由它的父控件完毕的。就这样一层一层地向上由各自的父控件完毕对自己的布局。

真到全部控件的最顶层结点,在全部的控件的最顶部有一个ViewRoot,它才是全部控件的终于祖先结点。

那让我们来看看它是怎么来做的吧。

在它布局里,会调用它自己的一个layout()函数(不能被重载。代码位于View.java):

/* final 标识符 , 不能被重载 , 參数为每一个视图位于父视图的坐标轴 
 * @param l Left position, relative to parent 
 * @param t Top position, relative to parent 
 * @param r Right position, relative to parent 
 * @param b Bottom position, relative to parent 
 */  
public final void layout(int l, int t, int r, int b) {  
    boolean changed = setFrame(l, t, r, b); //设置每一个视图位于父视图的坐标轴  
    if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {  
        if (ViewDebug.TRACE_HIERARCHY) {  
            ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);  
        }  
  
        onLayout(changed, l, t, r, b);//回调onLayout函数 。设置每一个子视图的布局  
        mPrivateFlags &= ~LAYOUT_REQUIRED;  
    }  
    mPrivateFlags &= ~FORCE_LAYOUT;  
在SetFrame(l,t,r,b)就是设置自己的位置,设置结束以后才会调用onLayout(changed, l, t, r, b)来设置内部全部子控件的位置。
OK啦。到这里有关onMeasure()和onLayout()的内容就讲完啦,想必大家应该也对整个布局流程有了一个清楚的认识了,以下我们再看一个紧要的问题:如何得到自己定义控件的左右间距margin值。


四、获取子控件Margin的方法

1、获取方法及演示样例

在这部分。大家先不必纠结这个样例为什么要这么写,我会先简单粗暴的教大家怎么先获取到margin值。然后再细讲为什么这样写,他们的原理是如何的。

假设要自己定义ViewGroup支持子控件的layout_margin參数,则自己定义的ViewGroup类必须重载generateLayoutParams()函数,而且在该函数中返回一个ViewGroup.MarginLayoutParams派生类对象,这样才干使用margin參数。
我们在上面MyLinLayout样例的基础上,加入上layout_margin參数;

(1)、首先,在XML中加入上layout_margin參数

<com.harvic.simplelayout.MyLinLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="#ff00ff"
    tools:context=".MainActivity">

    <TextView android:text="第一个VIEW"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:background="#ff0000"/>

    <TextView android:text="第二个VIEW"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:background="#00ff00"/>

    <TextView android:text="第三个VIEW"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="30dp"
        android:background="#0000ff"/>

</com.harvic.simplelayout.MyLinLayout>
我们在每一个TextView中都加入了一layout_marginTop參数,而且值各自是10dp,20dp,30dp。背景也都分别改为了红,绿,蓝;
如今我们执行一上,看看效果:
技术分享

从图中能够看到。根本没作用!!!这是为什么呢?由于測量和布局都是我们自己实现的,我们在onLayout()中没有依据Margin来布局,当然不会出现有关Margin的效果啦。须要特别注意的是。假设我们在onLayout()中依据margin来布局的话,那么我们在onMeasure()中计算container的大小时,也要加上margin,不然会导致container太小,而控件显示不全的问题。

费话不多说。我们直接看代码实现。

(2)、重写generateLayoutParams()函数

重写代码例如以下:

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

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

@Override
protected LayoutParams generateDefaultLayoutParams() {
    return new MarginLayoutParams(LayoutParams.MATCH_PARENT,
            LayoutParams.MATCH_PARENT);
}
在这里,我们重写了两个函数。一个是generateLayoutParams()函数,一个是generateDefaultLayoutParams()函数。直接返回相应的MarginLayoutParams()的实例。至于为什么要这么写,我们后面再讲,这里先把Margin信息获取到再说。

(3)、重写onMeasure()

让我们先看一下重写好的onMeasure()函数代码:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
    int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
    int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
    int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);

    int height = 0;
    int width = 0;
    int count = getChildCount();
    for (int i=0;i<count;i++) {

        View child = getChildAt(i);
        measureChild(child, widthMeasureSpec, heightMeasureSpec);

        MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
        int childHeight = child.getMeasuredHeight()+lp.topMargin+lp.bottomMargin;
        int childWidth = child.getMeasuredWidth()+lp.leftMargin+lp.rightMargin;

        height += childHeight;
        width = Math.max(childWidth, width);
    }

    setMeasuredDimension((measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth: width, (measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight: height);
}
最关键的地方是改了这句:
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int childHeight = child.getMeasuredHeight()+lp.topMargin+lp.bottomMargin;
int childWidth = child.getMeasuredWidth()+lp.leftMargin+lp.rightMargin;
通过 child.getLayoutParams()获取child相应的LayoutParams实例。将其强转成MarginLayoutParams;
然后在计算childHeight时加入上顶部间距和底部间距。计算childWidth时加入上左边间距和右边间距。


也就是说,我们在计算宽度和高度时不仅考虑到子控件的本身的大小还要考虑到子控件间的间距问题。


(4)、重写onLayout()函数

同样,我们在布局时仍然将间距加到控件里就好了,完整代码例如以下:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int top = 0;
    int count = getChildCount();
    for (int i=0;i<count;i++) {

        View child = getChildAt(i);

        MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
        int childHeight = child.getMeasuredHeight()+lp.topMargin+lp.bottomMargin;
        int childWidth = child.getMeasuredWidth()+lp.leftMargin+lp.rightMargin;

        child.layout(0, top, childWidth, top + childHeight);
        top += childHeight;
    }
}
在这里同样在布局子控件时,加入上子控件间的间距,详细就不讲了,非常easy理解。


终于的效果图例如以下:
技术分享

从效果图中能够明显的看到每一个ITEM都加入上了间距了。


源代码在文章底部给出

2、原理

上面我们看了要重写generateDefaultLayoutParams()函数才干获取控件的margin间距。那为什么要重写呢?以下这句就为什么非要强转呢?

MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
以下我们来看看这么做的原因。
首先,在container在初始化子控件时。会调用LayoutParams generateLayoutParams(LayoutParams p)来为子控件生成相应的布局属性。但默认仅仅是生成layout_width和layout_height所以相应的布局參数。即在正常情况下的generateLayoutParams()函数生成的LayoutParams实例是不能够取到margin值的。即:
/**
*从指定的XML中获取相应的layout_width和layout_height值
*/
public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new LayoutParams(getContext(), attrs);
}
/*
*假设要使用默认的构造方法,就生成layout_width="wrap_content"、layout_height="wrap_content"相应的參数
*/
protected LayoutParams generateDefaultLayoutParams() {
     return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
所以。假设我们还须要margin相关的參数就仅仅能重写generateLayoutParams()函数了:
public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new MarginLayoutParams(getContext(), attrs);
}
由于generateLayoutParams()的返回值是LayoutParams实例。而MarginLayoutParams是派生自LayoutParam的。所以依据类的多态的特性,能够直接将此时的LayoutParams实例直接强转成MarginLayoutParams实例;
所以以下这句在这里是不会报错的:
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
大家也能够为了安全起见利用instanceOf来做下推断,例如以下:
MarginLayoutParams lp = null
if (child.getLayoutParams() instanceof  MarginLayoutParams) {
    lp = (MarginLayoutParams) child.getLayoutParams();
    …………
}
所以总体来讲,就是利用了类的多态特性!以下来看看MarginLayoutParams和generateLayoutParams()都做了什么。


3、MarginLayoutParams与generateLayoutParams()的实现

写在前面:本部分涉及自己定义控件属性的内容。假设对于TypedArray和自己定义控件属性不明确的同学请先移动<PullScrollView详细解释(一)——自己定义控件属性>

(1)generateLayoutParams()实现

首先,我们看看generateLayoutPararms()都做了什么吧,它是怎么得到布局值的:

//位于ViewGrop.java中
public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new LayoutParams(getContext(), attrs);
}
public LayoutParams(Context c, AttributeSet attrs) {
    TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
    setBaseAttributes(a,
            R.styleable.ViewGroup_Layout_layout_width,
            R.styleable.ViewGroup_Layout_layout_height);
    a.recycle();
}
protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
    width = a.getLayoutDimension(widthAttr, "layout_width");
    height = a.getLayoutDimension(heightAttr, "layout_height");
}
从上面的代码中明显能够看出,generateLayoutParams()调用LayoutParams()产生布局信息。而LayoutParams()终于调用setBaseAttributes()来获得相应的宽,高属性。


这里是通过TypedArray对自己定义的XML进行值提取的过程,难度不大。不再细讲。

从这里也能够看到,generateLayoutParams生成的LayoutParams属性仅仅有layout_width和layout_height的属性值。


2、MarginLayoutParams实现

以下再来看看MarginLayoutParams的详细实现。事实上通过上面的过程。大家也应该想到。它也是通过TypeArray来解析自己定义属性来获得用户的定义值的(大家看到长代码不要害怕,先列出完整代码,以下会分段讲):

public MarginLayoutParams(Context c, AttributeSet attrs) {
    super();

    TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_MarginLayout);
    int margin = a.getDimensionPixelSize(
            com.android.internal.R.styleable.ViewGroup_MarginLayout_layout_margin, -1);
    if (margin >= 0) {
        leftMargin = margin;
        topMargin = margin;
        rightMargin= margin;
        bottomMargin = margin;
    } else {
       leftMargin = a.getDimensionPixelSize(
               R.styleable.ViewGroup_MarginLayout_layout_marginLeft,
               UNDEFINED_MARGIN);
       rightMargin = a.getDimensionPixelSize(
               R.styleable.ViewGroup_MarginLayout_layout_marginRight,
               UNDEFINED_MARGIN);

       topMargin = a.getDimensionPixelSize(
               R.styleable.ViewGroup_MarginLayout_layout_marginTop,
               DEFAULT_MARGIN_RESOLVED);

       startMargin = a.getDimensionPixelSize(
               R.styleable.ViewGroup_MarginLayout_layout_marginStart,
               DEFAULT_MARGIN_RELATIVE);
       endMargin = a.getDimensionPixelSize(
               R.styleable.ViewGroup_MarginLayout_layout_marginEnd,
               DEFAULT_MARGIN_RELATIVE);
    }
    a.recycle();
}
这段代码分为两部分:
第一部分:提取layout_margin的值并设置
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_MarginLayout);
int margin = a.getDimensionPixelSize(
        com.android.internal.R.styleable.ViewGroup_MarginLayout_layout_margin, -1);
if (margin >= 0) {
    leftMargin = margin;
    topMargin = margin;
    rightMargin= margin;
    bottomMargin = margin;
} else {
  …………
}
在这段代码中就是通过提取layout_margin的值来设置上,下,左。右边距的。
第二部分:假设用户没有设置layout_margin,而是单个设置的,那么就一个个提取,代码例如以下:
leftMargin = a.getDimensionPixelSize(
        R.styleable.ViewGroup_MarginLayout_layout_marginLeft,
        UNDEFINED_MARGIN);
rightMargin = a.getDimensionPixelSize(
        R.styleable.ViewGroup_MarginLayout_layout_marginRight,
        UNDEFINED_MARGIN);

topMargin = a.getDimensionPixelSize(
        R.styleable.ViewGroup_MarginLayout_layout_marginTop,
        DEFAULT_MARGIN_RESOLVED);

startMargin = a.getDimensionPixelSize(
        R.styleable.ViewGroup_MarginLayout_layout_marginStart,
        DEFAULT_MARGIN_RELATIVE);
endMargin = a.getDimensionPixelSize(
        R.styleable.ViewGroup_MarginLayout_layout_marginEnd,
        DEFAULT_MARGIN_RELATIVE);
这里就是对layout_marginLeft、layout_marginRight、layout_marginTop、layout_marginBottom的值一个个提取的过程。

难度不大。也没什么好讲的了。


从这里大家也能够看到为什么非要重写generateLayoutParams()函数了,就是由于默认的generateLayoutParams()函数仅仅会提取layout_width、layout_height的值,仅仅有MarginLayoutParams()才具有提取margin间距的功能!!。。

好啦,这篇就到这啦。下篇咱们就開始实现FlowLayout了。


源代码内容:

1、《SimpleLayout》:第三部分相应源代码:MyLinLayout的初步实现

2、《SimpleLayoutAdvance》:第四部分相应源代码:加入上margin值的获取方法


假设本文有帮到你。记得加关注哦

源代码下载地址:http://download.csdn.net/detail/harvic880925/8928283

请大家尊重原创者版权。转载请标明出处:http://blog.csdn.net/harvic880925/article/details/47029169 谢谢


以上是关于自己定义控件三部曲视图篇——測量与布局的主要内容,如果未能解决你的问题,请参考以下文章

自定义控件三部曲视图篇——测量与布局

自定义控件三部曲视图篇——瀑布流容器WaterFallLayout实现

自定义控件三部曲之动画篇——联合动画的代码实现

自定义控件三部曲之动画篇——联合动画的代码实现

自定义控件三部曲之绘图篇(十八)——BitmapShader与望远镜效果

自定义控件三部曲之绘图篇(二十)——RadialGradient与水波纹按钮效果