android 自定义ViewGroup

概论:

  1. 三种自定义的比较
  2. 继承自ViewGroup
  3. onMeasure()
  4. onLayout()
  5. 自定义LayoutParams
  6. 稍微说下draw吧
  7. 参考链接

1.三种自定义的比较

Custom Views

  • Extending the View
  • Overriding onMeasure()
  • Overriding onDraw()
  • Saving state using the BaseSavedState pattern
  • Working with custom attributes
  • Understanding and applying requestLayout and invalidate

In contrast, you can ignore the following details when creating custom views:

  • Overriding onLayout()
  • Implementating and using LayoutParams

Compound Controls

  • Extend an existing layout
  • Saving state using the BaseSavedState pattern
  • Taking control of saving state for its child views
  • Working with custom attributes

While you can ignore:

  • Overriding onMeasure()
  • Overriding onDraw()
  • Overriding onLayout()
  • Worrying about requestLayout() and invalidate()
  • Implementing and using LayoutParams

Custom Layouts

  1. Inherit from ViewGroup
  2. Override onMeasure()
  3. Override onLayout()
  4. Implement custom LayoutParams with any additional layout attributes
  5. Override layout parameters construction methods in the custom layout class

2.继承自ViewGroup

流水线布局的样子:

这里的代码和自定义view差不多,如下:

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
public class FlowLayout extends ViewGroup
{
private static final String tag="FlowLayout";
private int hspace=10;
private int vspace=10;
public FlowLayout(Context context) {
super(context);
initialize(context);
}
public FlowLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
TypedArray t = context.obtainStyledAttributes(attrs,
R.styleable.FlowLayout, 0, 0);
hspace = t.getDimensionPixelSize(R.styleable.FlowLayout_hspace,
hspace);
vspace = t.getDimensionPixelSize(R.styleable.FlowLayout_vspace,
vspace);
Log.d(tag,"hspace:" + hspace);
Log.d(tag,"vspace:" + vspace);
t.recycle();
initialize(context);
}
public FlowLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
private void initialize(Context context) {
}
...
}

就是自定义属性多了个布局属性,如下:

1
2
3
4
5
6
7
8
9
<resources>
<declare-styleable name="FlowLayout">
<attr name="hspace" format="dimension"/>
<attr name="vspace" format="dimension" />
</declare-styleable>
<declare-styleable name="FlowLayout_Layout">
<attr name="layout_space" format="dimension"/>
</declare-styleable>
</resources>

3.onMeasure()

这里需要注意的地方比较多,也有点难理解。
由于是ViewGroup,你需要先measure children,但是比不能直接用child.measure(),因为这个方法完全没考虑ViewGroup的MeasureSpec,你应该用的是ViewGroup.measureChild()。到最后你setMeasuredDimension(),还需要用resolveSize(w, widthMeasureSpec)来计算ViewGroup的实际尺寸,因为你所有的children的尺寸有可能超出ViewGroup的尺寸,所以框架提供了这个方便的方法让我们使用。

看下这个图有助于了解接下来的代码:

示例代码如下:

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
//This is very basic
//doesn't take into account padding
//You can easily modify it to account for padding
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
//********************
//Initialize
//********************
int rw = MeasureSpec.getSize(widthMeasureSpec);
int rh = MeasureSpec.getSize(heightMeasureSpec);
int h = 0; //current height
int w = 0; //current width
int h1 = 0, w1=0; //Current point to hook the child to
//********************
//Loop through children
//********************
int numOfChildren = this.getChildCount();
for (int i=0; i < numOfChildren; i++ )
{
//********************
//Front of the loop
//********************
View child = this.getChildAt(i);
this.measureChild(child,widthMeasureSpec, heightMeasureSpec);
int vw = child.getMeasuredWidth();
int vh = child.getMeasuredHeight();
if (w1 + vw > rw)
{
//new line: max of current width and current width position
//when multiple lines are in play w could be maxed out
//or in uneven sizes is the max of the right side lines
//all lines don't have to have the same width
//some may be larger than others
w = Math.max(w,w1);
//reposition the point on the next line
w1 = 0; //start of the line
h1 = h1 + vh; //add view height to the current heigh
}
//********************
//Middle of the loop
//********************
int w2 = 0, h2 = 0; //new point for the next view
w2 = w1 + vw;
h2 = h1;
//latest height: current point + height of the view
//however if the previous height is larger use that one
h = Math.max(h,h1 + vh);
//********************
//Save the current coords for the view
//in its layout
//********************
LayoutParams lp = (LayoutParams)child.getLayoutParams();
lp.x = w1;
lp.y = h1;
//********************
//Restart the loop
//********************
w1=w2;
h1=h2;
}
//********************
//End of for
//********************
w = Math.max(w1,w);
//h = h;
setMeasuredDimension(
resolveSize(w, widthMeasureSpec),
resolveSize(h,heightMeasureSpec));
};

这里还需要说明的是当我们onLayout()的时候我们需要知道children的坐标和尺寸,尺寸不用担心,已经有了,但原点(左上角)坐标呢?这当然就是layout parameters的功用了,LayoutParams就是ViewGroup让children保存的数据,定义LayoutParams还是在ViewGroup中定义的。

1
2
3
LayoutParams lp = (LayoutParams)child.getLayoutParams();
int spacing = lp.spacing;
//Adjust your widths based on this spacing.

具体的LayoutParams的定义还有ViewGroup如何调用它稍后再讲。

4.onLayout()

这里没太多可说的,直接依次调用child.layout()方法就行了,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
protected void onLayout(boolean arg0, int arg1, int arg2, int arg3, int arg4)
{
//Call layout() on children
int numOfChildren = this.getChildCount();
for (int i=0; i < numOfChildren; i++ )
{
View child = this.getChildAt(i);
LayoutParams lp = (LayoutParams)child.getLayoutParams();
child.layout(lp.x,
lp.y,
lp.x + child.getMeasuredWidth(),
lp.y + child.getMeasuredHeight());
}
}

5.自定义LayoutParams

LayoutParams 是由child views生成和持有的,但是是在ViewGroup中定义的,原因很简单,ViewGroup需要child的这些信息,当然得由他来定义需要哪些信息。看代码吧:

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
//*********************************************************
//Custom Layout Definition
//*********************************************************
public static class LayoutParams extends ViewGroup.MarginLayoutParams {
public int spacing = -1;
public int x =0;
public int y =0;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
TypedArray a =
c.obtainStyledAttributes(attrs, R.styleable.FlowLayout_Layout);
spacing = a.getDimensionPixelSize(R.styleable.FlowLayout_Layout_layout_space, 0);
Log.d(tag,"child spacing:" + spacing);
a.recycle();
}
public LayoutParams(int width, int height) {
super(width, height);
spacing = 0;
}
public LayoutParams(ViewGroup.LayoutParams p) {
super(p);
}
public LayoutParams(MarginLayoutParams source) {
super(source);
}
}//eof-layout-params

还有个问题,child的ViewGroup可以是LinearLayout 或者FlowLayout,child view是怎么知道该构造哪个布局的LayoutParams呢?这当然要Android SDK来解决了。当一个view被放到布局中时,框架会调用如下四个方法来构造view的LayoutParams

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
//*********************************************************
//Layout Param Support
//*********************************************************
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new FlowLayout.LayoutParams(getContext(), attrs);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
@Override
protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new LayoutParams(p);
}
// Override to allow type-checking of LayoutParams.
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof FlowLayout.LayoutParams;
}

6.稍微说下draw吧

你当然可以在viewGroup上画点东西,当你想在所有child都画好后再话东西的话,就用dispatchDraw(),当然你得先call super.dispatchDraw()。现在画的东西就是在最上层。如果你想在child之前画点东西的话,当然就要用onDraw()了,注意,你先得配置setWillNotDraw(false),因为默认ViewGroup是不会画东西的,就不会调用onDraw()

7.参考链接

http://reacoder.github.io/2014/11/23/android-good-video/

《ExpertAndroid》