第12章 CustomView封装控件

一、自定义属性与自定义Style

概述:

在一个自 定义控件的XML中经常会发现类似下面的代码 :

<com.trydeclarestyle.MyTextView
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    attrstest:headerHeight="300dp"
    attrstest:headerVisibleHeight="100dp"
    attrstest:age="young" />

注意到最后三个属性,明显不是系统自带的,而是人为添加上去的。怎么添加自定义的属性呢?利用XML中的declare-styleable标签来实现。

declare-styleable标签的使用方法:

1.自定义一个类MyTextView

public class MyTextView extends TextView {
    public MyTextView(Context context) {
        super(context);
    }
}

2.新建res/values/attrs.xml文件

<?xml version="1.0" encoding="utf-8"?>
<resources>
	<declare-styleable name="MyTextView">
		<attr name="header" format="reference"/>
		<attr name="headerHeight" format="dimension"/>
		<attr name="headerVisibleHeight" format="dimension"/>
		<attr name="age">
			<flag name="child" value="10"/>
			<flag name="young" value="18"/>
			<flag name="old" value="60"/>
		</attr>
	</declare-styleable>
</resources>

(1)最重要的一点是,declare-styleable旁边有一个name属性,这个属性的取值对应所定义的类名。也就是说,要为哪个类添加自定义的属性,那么这个name属性的值就是哪个类的类名。这里为自定义的MyTextView类添加XML属性,所以name="MyTextView"。
(2)自定义属性值可以组合使用。比如<attr name="border_color" format="color|reference"/>表示既可以自定义color值(比如#ff00ff),也可以利用@color/XXX来引用color.xml中已有的值。

这里先看一下 declare-styleable 标签中所涉及的标签的用法。

• reference 指的是从string.xml、drawable.xml、color.xml等文件中引用过来的值。
• flag 是自己定义的,类似于android:gravity="top"。
• dimension 指的是从dimension.xml文件中引用过来的值。注意,这里如果是dp,就会进行像素转换。

使用方法如下 :

<com.harvic.com.trydeclarestyle.MyTextView
	android:layout_width="fill_parent"
	android:layout_height="match_parent"
	attrstest:header="@drawable/pic1"
	attrstest:headerHeight="300dp"
	attrstest:headerVisibleHeight="1OOdp"
	attrstest:age="young"/>

可以看到,header 的取值是从其他XML文件中引用过来的;dimension表示尺寸,直接输入数字;flag相当于代码里的常量,比如这里的young就表示数字18。

在XML中使用自定义的属性:

1.添加自定义控件

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:attrstest="http://schemas.android.com/apk/res/com.harvic.com.trydeclarestyle"
	android:layout_width="match parent"
	android:layout_height="match parent">
	<com.harvic.com.trydeclarestyle.MyTextView
		android:layout_width="fill_parent"
		android:layout_height="match_parent"
		attrstest:header="@drawable/picl"
		attrstest:headerHeight="300dp"
		attrstest:headerVisibleHeight="100dp"
		attrstest:age="young"/>
</RelativeLayout>

2.导入自定义的属性集(方法一)

要让XML识别我们自定义的属性也非常简单,在根布局上添加如下代码即可。

xmlns:attrstest="http://schemas.android.com/apk/res/com.harvic.com.trydeclarestyle"

这里有两点需要注意。
(1)xmlns:attrstest:这里的attrstest是自定义的,你想定义成什么就可以定义成什么。但要注意的是,在访问你定义的XML控件属性时,就是通过这个标识符访问的。比如,这里定义成attrstest,那么对应的访问自定义控件的方式就是attrstest:headerHeight="300dp"。
(2)com.harvic.com.trydeclarestyle:它是AndroidManifest.xml中的包名,即AndroidManifest.xml中package字段对应的值。

3.导入自定义的属性集(方法二)

另一种自动导入自定义属性集的方式要相对简单,只需在根布局上添加如下代码即可 。

xmlns:attrstest="http://schemas.android.com/apk/res-auto"

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

使用代码获取用户所定义的某个属性的值,主要使用TypedArray类,这个类提供了获取某个属性值的所有方法,如下所示。需要注意的是,在使用完以后必须调用 TypedArray类的recycle()函数来释放资源。

typedArray.getInt(int index, float defValue);
typedArray.getDimension(int index, float defValue);
typedArray.getBoolean(int index, float defValue);
typedArray.getColor(int index, float defValue);
typedArray.getString(int index);
typedArray.getDrawable(int index);
typedArray.getResources();
public class MyTextView extends TextView {
    public MyTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyTextView);
        float headerHeight = typedArray.getDimenSion(R.styleable.MyTextView_headerHeight, -1);
        int age = typedArray.getInt(R.styleable.MyTextView_age, -1);
        typedArray.recycle();// a must!!!
        this.setText("headerHeight:" + headerHeight + " age:" + age);
    }
}

declare-styleable标签其他属性的用法:

1.reference:参考某一资源ID

属性定义:
<declare-styleable name ="名称">
	<attr name="background" format ="reference"/>
</declare-styleable>

属性使用 :
<ImageView
	android:layout_width="42dip"
	android:layout_height="42dip"
	android:background="@drawable/图片ID"/>

2.color:颜色值

属性定义:
<declare-styleable name="名称">
	<attr name="textColor" format="color"/>
</declare-styleable>

属性使用:
<TextView
	android:layout_width="42dip"
	android:layout_height="42dip"
	android:textColor="#00FF00"/>

3.boolean:布尔值

属性定义:
<declare-styleable name="名称">
	<attr name="focusable" format="boolean"/>
</declare-styleable>

属性使用:
<Button
	android:layout_width="42dip"
	android:layout_height="42dip"
	android:focusable="true"/>

4.dimension:尺寸值

属性定义:
<declare-styleable name="名称">
	<attr name="layout_width" format="dimension"/>
</declare-styleable>

属性使用:
<Button
	android:layout_width="42dip"
	android:layout_height="42dip"/>

5.float:浮点值

属性定义:
<declare-styleable name="AlphaAnimation">
	<attr name="fromAlpha" format="float"/>
	<attr name="toAlpha" format="float"/>
</declare-styleable>

属性使用:
<alpha
	android:fromAlpha="l.0"
	android:toAlpha="0.7"/>

6.integer:整型值

属性定义 :
<declare-styleable name="AnimatedRotateDrawable">
	<attr name="visible"/>
	<attr name="frameDuration" format="integer"/>
	<attr name="framesCount" format="integer"/>
	<attr name="pivotX"/>
	<attr name="pivotY"/>
	<attr name="drawable"/>
</declare-styleable>

属性使用 :
<animated-rotate
	xmlns:android="http://schemas.android.com/apk/res/android"
	android:drawable="@drawable/图片ID"
	android:pivotX="50%"
	android:pivotY="50%"
	android:framesCount ="12"
	android:frameDuration ="100"/>

7.string:字符串

属性定义 :
<declare-styleable name="MapView">
	<attr name="apiKey" format="string"/>
</declare-styleable>

属性使用 :
<com.google.android.maps.MapView
	android:layout_width="fill_parent"
	android:layout_height="fill_parent"
	android:apiKey="OjOkQ80oDlJL9C6HAja99uGXCRiS2CGjKO_bc_g"/>

8.fraction:百分数

属性定义 :
<declare-styleable name="RotateDrawable">
	<attr name="visible"/>
	<attr name="fromDegrees" format="float"/>
	<attr name="toDegrees" format="float"/>
	<attr name="pivotX" format="fraction"/>
	<attr name="pivotY" format="fraction" />
	<attr name="drawable"/>
</declare-styleable>

属性使用 :
<rotate
	xmlns:android="http://schemas.android.com/apk/res/android"
	android:interpolator="@anim/动画ID"
	android:fromDegrees="0"
	android:toDegrees="360"
	android:pivotX="200%"
	android:pivotY="300%"
	android:duration="5000"
	android:repeatMode="restart"
	android:repeatCount="infinite"/>

9.enum:枚举值

属性定义 :
<declare-styleable name="名称">
	<attr name="orientation">
	    <enum name="horizontal" value="0"/>
	    <enum name="vertical" value="l"/>
    </attr>
</declare-styleable>

属性使用:
<LinearLayout
	xmlns:android="http://schemas.android.com/apk/res/android"
	android:orientation="vertical"
	android:layout_width="fill_parent"
	android:layout_height="fill_parent" />

10.flag:位或运算

属性定义:
<declare-styleable name="名称">
	<attr name="windowSoftlnputMode">
		<flag nane="stateUnspecified" value="0"/>
		<flag name="stateUnchanged" value="l"/>
		<flag name="stateHidden" value="2"/>
		<flag name="stateAlwaysHidden" value="3"/>
		<flag name="stateVisible" value="4"/>
		<flag name="stateAlwaysVisible" value="5"/>
		<flag name="adjustUnspecified" value="0x00"/>
		<flag name="adjustResize" value="0xl0"/>
		<flag name="adjustPan" value="0x20"/>
		<flag name="adjustNothing" value="0x30"/>
	</attr>
</declare-styleable>

属性使用 :
<activity
	android:name=".StyleAndThemeActivity"
	android:label="@string/app name "
	android:windowSoftlnputMode="stateUnspecified | stateUnchanged | stateHidden">
	<intent-filter>
		<action android:name="android.intent.action.MAIN"/>
		<category android:name="android.intent.category.LAUNCHER"/>
	</intent-filter>
</activity>

二、测量与布局

ViewGroup绘制流程:

注意:View及ViewGroup基本相同,只是在ViewGroup中不仅要绘制自己,还要绘制其中的子控件,而View只需妥绘制自己就可以了,所以这里就以ViewGroup为例来讲述整个绘制流程。

绘制流程分为三步:测量、布局、绘制,分别对应onMeasure()、onLayout()、onDraw()函数。

• onMeasure():测量当前控件的大小,为正式布局提供建议(注意:只是建议,至于用不用,要看onLayout()函数)。
• onLayout():使用layout()函数对所有子控件进行布局。
• onDraw():根据布局的位置绘图 。

onMeasure()函数与MeasureSpec:

布局绘画涉及两个过程:测量过程和布局过程。测量过程通过measure()函数来实现,是View树自顶向下的遍历,每个View在循环过程中将尺寸细节往下传递,当测量过程完成之后,所有的View都存储了自己的尺寸。布局过程则通过layout()函数来实现,也是自顶向下的,在这个过程中,每个父View负责通过计算好的尺寸放置它的子View。

前面提到,onMeasure()函数是用来测量当前控件大小的,给onLayout()函数提供数值参考。需要特别注意的是,测量完成以后,要通过setMeasuredDimension(int,int)函数设置给系统。

1.onMeasure()函数

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
● 它们是指父类传递过来给当前View的一个建议值,即想把当前View的尺寸设置为宽widthMeasureSpec、高heightMeasureSpec。

2.MeasureSpec的组成

虽然从表面上看起来它们是int类型的数字,但它们是由mode+size两部分组成的。
widthMeasureSpec和heightMeasureSpec转换为二进制数字表示,它们都是32位的,前2位代表模式(mode),后面30位代表数值(size)。

1)模式分类

它有三种模式。

(1) UNSPECIFIED(未指定):父元素不对子元素施加任何束缚,子元素可以得到任意想要的大小。
(2) EXACTLY(完全):父元素决定子元素的确切大小,子元素将被限定在给定的边界里而忽略它本身的大小。
(3) AT MOST(至多):子元素至多达到指定大小的值。

它们对应的二进制值分别是:
UNSPECIFIED = 00000000000000000000000000000000
        EXACTLY = 01000000000000000000000000000000
       AT_MOST = 10000000000000000000000000000000
由于前2位代表模式,所以它们分别对应十进制的0、l、2。

2)模式提取

如果我们需要自己来提取widthMeasureSpc和heightMeasureSpec中的模式和数值该怎么办呢?

首先想到的肯定是通过 MASK 和与运算去掉不需要的部分,从而得到对应的模式或数值。
 

// 对应 11000000000000000000000000000000;共32位,前2位是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

Android 己经为我们提供了 MeasureSpec 类来辅助实现这个功能。

MeasureSpec.getMode(int spec) // 获取模式
MeasureSpec.getSize(int spec) // 获取数值

模式的取值为:MeasureSpec.AT_MOST、MeasureSpec.EXACTLY、MeasureSpec.UNSPECIFIED

通过下面的代码就可以分别获取widthMeasureSpec和heightMeasureSpec的模式和数值了。

int measureWidth = MeasureSpec.getSize(widthMeasureSpec) ;
int measureHeight = MeasureSpec.getSize(heightMeasureSpec) ;
int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec) ;
int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec) ;

4)模式的用处

需要注意的是,widthMasureSpec和heightMeasureSpec各自都有对应的模式,而模式分别来自XML定义。
简单来说,XML布局和模式有如下的对应关系:
• wrap_content → MeasureSpec.AT_MOST
• match_parent → MeasureSpec.EXACTLY
• 具体值 → MeasureSpec.EXACTLY

<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.ATMOST,即值不确定。

一定要注意的是,当模式是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()是实现所有子控件布局 的函数。注意,是所有子控件!

ViewGroup的onLayout()函数的默认行为是什么?在 ViewGroup.java中的源码如下:

@Override
protected abstract void onLayout(boolean changed, int l, int t , int r , int b);

这是一个抽象函数,说明凡是派生自ViewGroup的类都必须自己去实现这个函数。像LinearLayout、RelativeLayout等布局都重写了这个函数,然后在内部按照各自的规则对子视图进行布局。

2.示例

(1)三个TextView竖直排列;(2)背景的Layout宽度是match_parent,高度是wrap_content。

1)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"
		android:background="#ff0000"/>
	<TextView 
		android:text="第二个VIEW"
		android:layout_width="wrap_content"
		android:layout_height="wrap_content"
		android:background="#00ff00"/>
	<TextView 
		android:text="第三个VIEW"
		android:layout_width="wrap_content"
		android:layout_height="wrap_content"
		android:background="#0000ff"/>
</com.harvic.simplelayout.MyLinLayout>

2)MyLinLayout实现:重写onMeasure()函数

我们提到过,onMeasure()函数的作用就是根据container内部的子控件计算自己的宽和高,然后通过setMeasuredDimension(int width,int height)函数设置进去。

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);
}

这里的measureWidthMode应该是MeasureSpec.EXACTLY,measureHeightMode应该是MeasureSpec.AT_MOST。在最后利用setMeasuredDimension(width,height)函数来进行设置时,width使用的是从父类传过来的measureWidth,而高度则是我们自己计算的height。即实际的运算结果是这样的:setMeasureDimension(measureWidth, height);

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

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

在这一部分就是根据自己的意愿把 container 内部的各个控件排列起来,在这里要实现的是将所有的控件垂直排列。

protected void onLayout(boolean changed, int 1 , 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;
    }
}

4)getMeasuredWidth()与getWidth()函数

通过这个例子,讲解一个很容易出错的问题: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()函数的返回值就不再一样了。

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

前面我们说了,在派生自ViewGroup的container中,比如MyLinLayout,在onLayout()函数中布局它所有的子控件。那它自己什么时候被布局呢?

它当然也有父控件,它的布局也是在父控件中由它的父控件完成的,就这样一层一层地向上由各自的父控件完成对自己的布局,直到所有控件的顶层节点。在所有控件的顶部有一个ViewRoot,它才是所有控件的祖先节点。让我们来看看它是怎么做的吧。

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

/* final标识符,不能被重载,参数为每个视图位于父视图的坐标轴
* @param 1 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 1, 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,1,t,r,b)函数来设置内部所有子控件的位置。
到这里,有关onMeasure()和onLayout()函数的内容就结束了。

获取子控件margin值的方法:

1.获取方法及示例

如果要自定义ViewGroup支持子控件的layout_margin参数,则自定义的ViewGroup类必须重写generateLayoutParams()函数,并且在该函数中返回一个ViewGroup.MarginLayoutParams派生类对象。我们在上面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>

运行后效果同上例的是一样的,设置的margin根本没起作用!这是为什么呢?因为测量和布局都是我们自己实现的,我们在onLayout()函数中没有根据margin来布局,当然不会出现有关margin的效果。需要特别注意的是,如果我们在onLayout()函数中根据margin来布局,那么在onMeasure()函数中计算container的大小时,也要加上layout_margin参数,否则会导致container太小而控件显示不全的问题。

2)重写generateLayoutParams()和generateDefaultLayoutParams()函数

@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);
}

3)重写onMeasure()函数

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);
        
        // 获得子控件的高度和宽度 + margin值
        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);
}

4)重写onLayout()函数

protected void onLayout(boolean changed, int 1 , 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;
    }
}

2.原理

只有重写generateLayoutParams()函数才能获取到控件的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 LayoutParams(getContext(), attrs);
}

由于generateLayoutParams()函数的返回值是LayoutParams实例,而MarginLayoutParams是派生自LayoutParams的,所以,根据类的多态特性,可以直接将此时的LayoutParams实例强转成MarginLayoutParams实例。
所以下面这句在这里是不会报错的:

MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

为了安全起见,也可以利用 instanceof 来进行判断。
 

MarginLayoutParams lp = null;
if (child.getLayoutParams() instanceof MarginLayoutParams) {
    lp = (MarginLayoutParams) child.getLayoutParams();
}

3.generateLayoutParams()与MarginLayoutParams()函数的实现

1)generateLayoutParams()函数的实现

先来看generateLayoutPararms()函数是如何得到布局值的。(位于ViewGroup.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()函数的实现

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();
}

这段代码分为两部分:第一部分是if语句部分,主要作用是提取layout_margin的值并进行设置;第二部分是else语句部分,如果用户没有设置layout_margin,而是单个设置的,就一个个提取。这段代码就是对layout_marginLeft、layout_marginRight、layout_marginTop、layout_marginBottom的值逐个提取的过程。

从这里大家也可以看到为什么非要重写generateLayoutParams()函数了,就是因为默认的generateLayoutParams()函数只会提取layout_width和layout_height的值,只有MarginLayoutParams()函数才具有提取margin值的功能。

三、实现FlowLayout容器

XML布局:

先定义一个style标签,这是为FlowLayout中的TextView定义的。

<style name="text_flag_01">
	<item name="android:layout_width">wrap_content</item>
	<item name="android:layout_height">wrap_content</item>
	<item name="android:layout_margin">4dp</item>
	<item name="android:background">@drawable/flag_01</item>
	<item name="android:textColor">#ffffff</item>
</style>

activity_main.xml布局文件:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <com.example.harvic.myapplication.FlowLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <TextView
            style="@style/text_flag_0l"
            android:background="@drawable/flag_03"
            android:text="Welcome"
            android:textColor="@android:color/white"/>
        <TextView
            style="@style/text_flag_01"
            android:background="@drawable/flag_03"
            android:text="IT 工程师"
            android:textColor="@android:color/white"/>
        <TextView
            style="@style/text_flag_0l"
            android:background="@drawable/flag_03"
            android:text="我真是可以的"
            android:textColor="@android:color/white"/>
        <TextView
            style="@style/text_flag_0l"
            android:background="@drawable /flag_03"
            android:text="你觉得呢"
            android:textColor="@android:color/white"/>
        <TextView
            style="@style/text_flag_0l"
            android:background="@drawable/flag_03"
            android:text="不要只知道挣钱"
            android:textColor="@android:color/white"/>
        <TextView
            style="@style/text_flag_0l"
            android:background="@drawable/flag_03"
            android:text="努力 ing"
            android:textColor="@android:color/white" />
        <TextView
            style="@style/text_flag_0l"
            android:background="@drawable/flag_03"
            android:text="I thick i can"
            android:textColor="@android:color/white"/>
    </com.example.harvic.myapplication.FlowLayout>
</LinearLayout>

提取margin值与重写onMeasure()函数:

1.提取margin值

我们讲过,要提取margin值,就一定要重写 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);
}

2.重写onMeasure()函数——计算当前FlowLayout所占的区域大小

1)何时换行
FlowLayout的布局是一行行的,如果当前行己经放不下下一个控件了,就把这个控件移到下一行显示。所以需要一个变量来计算当前行己经占据的宽度,以判断剩下的空间是否还能容得下下一个控件。
2)如何得到FlowLayout的宽度
FlowLayout的宽度是所有行宽度的最大值,所以我们要记录每一行所占据的宽度值,进而找到所有值中的最大值。
3)如何得到FlowLayout的高度
很显然,FlowLayout的高度是每一行高度的总和,而每一行的高度则取该行中所有控件高度的最大值。

(1)利用MeasureSpec获取系统建议的数值和模式。

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);
    ...    
}

(2)计算FlowLayout所占用的区域大小。

// 先申请几个变量
int lineWidth = 0;// 记录每一行的宽度
int lineHeight = 0;// 记录每一行的高度
int height = 0;// 记录整个FlowLayout所占高度
int width = 0;// 记录整个FlowLayout所占宽度

int count= getChildCount();
for (int i=0; i<count; i++) {
    View child = getChildAt(i);
    measureChild(child, widthMeasureSpec, heightMeasureSpec);
    MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    int childWidth = child.getMeasuredWidth () + lp.leftMargin + lp.rightMargin;
    int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
    if (lineWidth + childWidth > measureWidth) {// 需要换行
        width= Math.max(lineWidth, childWidth);
        height += lineHeight;
        // 因为当前行放不下当前控件,而将此控件调到下一行,所以将此控件的高度和宽度初始化给lineHeight、lineWidth
        lineHeight = childHeight;
        lineWidth = childWidth;
    } else {// 否则累加值lineWidth;lineHeight取最大高度
        lineHeight = Math.max(lineHeight, childHeight);
        lineWidth += childWidth;
    }
    if (i == count -1) {// 因为最后一行是不会超出width范围的,所以需要单独处理
        height += lineHeight;
        width = Math.max(width,lineWidth);
    }
}

这里一定要注意的是,在i用用child.getMeasuredWidth()、child.getMeasuredHeight()函数之前,一定要先调用measureChiId(chiId,widthMeasureSpec,heightMeasureSpec); 我们讲过,在调用onMeasure()函数之后才能调用getMeasuredWidth()函数获取值;同样,只有在调用onLayout()函数后,getWidth()函数才能获取值。

下面就要判断当前控件是否换行及计算出最大高度和宽度了。

(3)通过setMeasuredDimension()函数设置到系统中。

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

3.重写onLayout函数——布局所有子控件

在onLayout()函数中需要一个个布局子控件。由于控件要后移和换行,所以我们要标记当前控件的top坐标和left坐标。

protected void onLayout(boolean changed, int 1, int t, int r, int b) {
    // 先申请下面几个变量
    int count = getChildCount();
    int lineWidth = 0;// 累加当前行的行宽
    int lineHeight = 0;// 当前行的行高
    int top = 0, left = 0;// 当前控件的top坐标和left坐标

    for (int i=0; i<count; i++) {
        View child = getChildAt(i);
        MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
        int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
        int childHeight = child.getMeasuredHeight() +lp.topMargin + lp .bottomMargin;
        if (childWidth + lineWidth > getMeasuredWidth()) {// 如果换行
            top += lineHeight;
            left = 0;
            lineHeight = childHeight;
            lineWidth = childWidth;
        } else {
            lineHeight = Math .max(lineHeight , childHeight};
            lineWidth += childWidth;
        }
        // 计算 childView 的 left , top , right , bottom
        int le = left + lp.leftMargin;
        int tc = top + lp.topMargin;
        int re = le + child.getMeasuredWidth();
        int be = tc + child.getMeasuredHeight();
        child.layout(le, te, re, be);
        // 将left置为下一个子控件的起始点
        left += childWidth;
    }
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

itzyjr

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值