自定义View:3、编写自己的ViewGroup

前面我们已经分析过两种自定义View的方法:

今天我们来继续学习第三种自定义View的方法,继承ViewGroup,实现自己的Layout。什么意思呢?其实前两种自定义View的方法,我们都是编写的具体的每一个View,然后整合到我们现有的Layout(比如LinearLayout,RelativeLayout等等)当中,但很多情况下,自定义View并不能完全满足我们的需求,或者说,我们想要使用现成的控件,但我们希望我们的界面上,我们可以完全自己来控制如何摆放这些控件。那这个时候怎么做呢? 我们先来看一下我们想要实现的效果是什么样子的? device-2014-12-13-190320 简单分解一下上面我们要实现的需求:

  • 每一小块为自定义View(称之为磁贴),大小共有3中规格,分别是:横向占据屏幕1/4(size=one),横向占据屏幕1/2(size=two),横向占据屏幕全部(size=four)。其高度始终为屏幕1/4。颜色与文字均可以通过XML直接指定。
  • 要求在XML定义各个View之后,要求能够从左上角开始能够自动占据铺满屏幕。

好了,要求说完了,那么该怎么去实现呢?首先我们来分解任务,要实现上面的功能,其实我们总共需要两个步骤,第一步,参考自定义View:1、定制自己的饼形图,定制我们自己的磁贴。第二步,指定我们自定的ViewGroup,自动对添加的磁贴贴到合适的位置上。

一、定制磁贴

1、定制属性

首先,我们需要在attrs.xml中为磁贴定义一些属性,在这边定义之后,我们就可以直接在Layout的XML中为磁贴指定颜色,文字,大小等内容。代码如下:

1
2
3
4
5
6
7
8
9
<declare-styleable name="Tile">
<attr name="background" format="color"/>
<attr name="title" format="string"/>
<attr name="size" format="enum">
<enum name="one" value="1"/>
<enum name="two" value="2"/>
<enum name="four" value="4"/>
</attr>
</declare-styleable>

如上所示,我们总共为磁贴定义了3个属性,其中包括背景颜色,文字内容,以及尺寸,one代表占据宽度的1/4,two代表占据宽度的1/2,而four则代表了占据宽度的全部。

2、定义View:Tile

接下来就是很重要的一步,定义View,即Tile,还记得我们之前的文章具体是怎么做的吗?没做,就是分3步走:

  1. 构造函数从XML中获取参数,初始化。
  2. 覆写onMeasure方法,指定View具体的大小。
  3. 覆写onDraw方法,按照参数指定的颜色,文字及大小进行绘制。

初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public Tile(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);

TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.Tile, 0, 0);
mContent = new TileContent();
try {
mContent.mBackgroundColor = typedArray.getColor(R.styleable.Tile_background, Color.BLUE);
mContent.mTitle = typedArray.getString(R.styleable.Tile_title);
mContent.mSize = typedArray.getInt(R.styleable.Tile_size, 1);
} finally {
typedArray.recycle();;
}

init();
}

private void init() {
mPaint = new Paint(Paint.ANTI\_ALIAS\_FLAG);
mRect = new Rect();
mTextPaint = new TextPaint(Paint.ANTI\_ALIAS\_FLAG);
mTextPaint.setTextSize(50.0f);
mTextSize = new Rect();
mTextPaint.getTextBounds(mContent.mTitle, 0, mContent.mTitle.length(), mTextSize);
}

初始化代码比较简单了:

  1. 从context.getTheme()/obtainStyeAttributes中获取包含属性的TypeArray。
  2. 从中读取设置的参数
  3. 初始化画笔,获取文字所占的边界大小。

onMeasure(init widthMeasureSpec, int heightMeasureSpec)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);

int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);

int width = 0;
int height = 0;

//如果指定了尺寸,那么就使用指定的尺寸,否则使用我们容器尺寸的一半
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
height = width;
} else {
switch (mContent.mSize) {
case SIZE_ONE :
width = widthSize >> 2;
height = width;
break;
case SIZE_TWO:
width = widthSize >> 1;
height = width >> 1;
break;
case SIZE_FOUR:
width = widthSize;
height = width >> 2;
break;
}
}
mRect.set(0, 0, width, height);
setMeasuredDimension(width, height);
}

在这个界面设置当中,onMeasure方法是比较关键的一步,因为我们要在这个地方为每一个磁贴,根据他们配置的属性为其指定合适的尺寸,那么我们具体是怎么做的呢?

  1. 获取widthMode,如果是MeasureSpec.EACTLY,即在XML中指定了具体的大小,那么我们就应该使用指定的大小。
  2. 如果没有指定具体的大小,而是让View根据需求来自己指定的话,我们就按照原先的设计,如果尺寸为one,那么就将宽度设置为全部的宽度1/4,高度则与宽度想的呢个,如果尺寸为two,那么则宽度为全部宽度的一半,高度为对应宽度的1/2,如果为four,则宽度为全部的宽度,高度是宽度的1/4。

onDraw

onDraw方法是我们经常用到的方法了,简单的讲就是根据我们的需求画图呗,看看代码就行。

1
2
3
4
5
6
7
@Override
protected void onDraw(Canvas canvas) {
mPaint.setColor(mContent.mBackgroundColor);
canvas.drawRect(mRect, mPaint);
mTextPaint.setColor(Color.WHITE);
canvas.drawText(mContent.mTitle, getMeasuredWidth()/2 - mTextSize.width()/2, getMeasuredHeight()/2 + mTextSize.height()/2, mTextPaint);
}

上面的代码中,我们首先画好北京颜色,然后设置文字的画笔颜色,为其在中间写好文字。 好了,至此,具体的Tile就准备完毕了,再来看怎么编写我们自定的ViewGroup

二、定制ViewGroup:TileLayout

1、覆写onMeasure方法

自定义View的onMeasure方法我们有写过,但是自定义Layout的onMeasure方法怎么写呢?因为ViewGroup继承自View,因此思路和View基本上差别不大,我们这里先简单考虑,我们先将整个屏幕的全部空间都占据使用。那该怎么写呢?

1
2
3
4
5
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
measureChildren(widthMeasureSpec, heightMeasureSpec);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

其中做了两个工作:

  • 依次通知子View们进行测量
  • 调用父类的onMeasure方法。

2、onLayout布局

该方法是ViewGroup的核心方法之一,简单的理解,在该方法中,我们需要设计将子元素放在合适的位置上。根据需求,我们对TileLayout的onLayout方法可以如下实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
Log.i(TAG, "onLayout:" + " position:" + left + "," + top + "," + right + "," + bottom);

int positionX = left;
int positionY = top;

int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
if(! (childView instanceof Tile) ) {
throw new IllegalArgumentException("Catch Exception not Tile!");
}
Tile item = (Tile) childView;

int width = childView.getMeasuredWidth();
int height = childView.getMeasuredHeight();

int itemPostitionX = left;
int itemPositionY = top;
switch (item.getSize()) {
case Tile.SIZE_ONE:

if( !isSpaceEmpty(Tile.SIZE_ONE) ) {
LayoutPositions params = mSizeOneSpaces.remove();
itemPostitionX = params.x;
itemPositionY = params.y;
} else {
if (positionX + width <= right) {
itemPostitionX = positionX;
itemPositionY = positionY;
positionX += width;
if (positionX >= right) {
positionX = left;
positionY = positionY + height;
}
} else {
itemPostitionX = left;
itemPostitionX = positionY + height;
positionX = itemPostitionX + width;
positionY = itemPositionY;
}
}
break;
case Tile.SIZE_TWO:
if (positionX + width <= right) {
itemPostitionX = positionX;
itemPositionY = positionY;
positionX += width;
if (positionX >= right) {
positionX = left;
positionY = positionY + height;
}
} else {
for (int start=positionX; start + (width >> 1) <= right; start=start + (width >> 1)) {
mSizeOneSpaces.add(new LayoutPositions(start, positionY));
}
positionX = left;
positionY = positionY + height;
itemPostitionX = positionX;
itemPositionY = positionY;
}
break;
case Tile.SIZE_FOUR:
if (positionX == left) {
itemPostitionX = positionX;
itemPositionY = positionY;
positionX += width;
positionY += height;
} else {
for (int start=positionX; start + (width >> 2) <= right; start=start + (width >> 2)) {
mSizeOneSpaces.add(new LayoutPositions(start, positionY));
}
positionX = left;
positionY = positionY + height;
itemPostitionX = positionX;
itemPositionY = positionY;
positionX = left;
positionY = positionY + height;
}
break;
}
Log.i(TAG, "Item:" + item.getTitle() + " position:" + itemPostitionX + "," + itemPositionY + "," + (itemPostitionX + width) + "," + (itemPositionY + height));
childView.layout(itemPostitionX, itemPositionY, itemPostitionX + width, itemPositionY + height);
}
}

其实思路也很简单,首先尝试摆放,如果能将子View摆在某处,那么则摆放这里,继续摆放下面的地方,如果不能,则说明此行空间不足,那么将剩下的空间根据大小分配给合适的数量的size=“one”的元素。依次摆放就可以。

三、使用

按照我们设计的,将所有的属性设置都放在XML当中,Activity代码如下:

1
2
3
4
5
6
7
8
public class TestTileLayout extends Activity{

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.tile);
}
}

布局代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
<?xml version="1.0" encoding="utf-8"?>
<me.happyhls.androiddemo.view.TileLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tile="http://schemas.android.com/apk/res-auto"
android:layout\_width="match\_parent"
android:layout\_height="match\_parent"
>

<me.happyhls.androiddemo.view.Tile
xmlns:tile="http://schemas.android.com/apk/res-auto"
android:layout\_width="wrap\_content"
android:layout\_height="wrap\_content"
tile:background="#0000FF"
tile:title="Title1"
tile:size="one"
/>
<me.happyhls.androiddemo.view.Tile
xmlns:tile="http://schemas.android.com/apk/res-auto"
android:layout\_width="wrap\_content"
android:layout\_height="wrap\_content"
tile:background="#00FF00"
tile:title="Title2"
tile:size="two"
/>
<me.happyhls.androiddemo.view.Tile
xmlns:tile="http://schemas.android.com/apk/res-auto"
android:layout\_width="wrap\_content"
android:layout\_height="wrap\_content"
tile:background="#FF0000"
tile:title="Title3"
tile:size="four"
/>
<me.happyhls.androiddemo.view.Tile
xmlns:tile="http://schemas.android.com/apk/res-auto"
android:layout\_width="wrap\_content"
android:layout\_height="wrap\_content"
tile:background="#0000FF"
tile:title="Title4"
tile:size="one"
/>

<me.happyhls.androiddemo.view.Tile
xmlns:tile="http://schemas.android.com/apk/res-auto"
android:layout\_width="wrap\_content"
android:layout\_height="wrap\_content"
tile:background="#00FF00"
tile:title="Title5"
tile:size="one"
/>
<me.happyhls.androiddemo.view.Tile
xmlns:tile="http://schemas.android.com/apk/res-auto"
android:layout\_width="wrap\_content"
android:layout\_height="wrap\_content"
tile:background="#FF00FF"
tile:title="Title6"
tile:size="two"
/>
<me.happyhls.androiddemo.view.Tile
xmlns:tile="http://schemas.android.com/apk/res-auto"
android:layout\_width="wrap\_content"
android:layout\_height="wrap\_content"
tile:background="#FF000F"
tile:title="Title7"
tile:size="four"
/>
<me.happyhls.androiddemo.view.Tile
xmlns:tile="http://schemas.android.com/apk/res-auto"
android:layout\_width="wrap\_content"
android:layout\_height="wrap\_content"
tile:background="#0000FF"
tile:title="Title8"
tile:size="one"
/>
</me.happyhls.androiddemo.view.TileLayout>

好了,上面就是简单的对自定义ViewGroup的使用。总结来说,我们只是大体了解了一下如何自定义View,如何自定义ViewGroup,但深入的我们依然没有设计到,因此后面会有至少两篇文章,我们来分析Android原生控件TextView和Android原生Layout:LinearLayout的源代码。