前言
Android
自定义View
的详细步骤是我们每一个Android
开发人员都必须掌握的技能,因为在开发中总会遇到自定义View
的需求。为了提高自己的技术水平,自己就系统的去研究了一下,在这里写下一点心得,有不足之处希望大家及时指出。
流程
在Android
中对于布局的请求绘制是在Android framework
层开始处理的。绘制是从根节点开始,对布局树进行measure
与draw
。在RootViewImpl
中的performTraversals
展开。它所做的就是对需要的视图进行measure
(测量视图大小)、layout
(确定视图的位置)与draw
(绘制视图)。下面的图能很好的展现视图的绘制流程:
当用户调用requestLayout
时,只会触发measure
与layout
,但系统开始调用时还会触发draw
下面来详细介绍这几个流程。
measure
measure
是View
中的final
型方法不可以进行重写。它是对视图的大小进行测量计算,但它会回调onMeasure
方法,所以我们在自定义View
的时候可以重写onMeasure
方法来对View
进行我们所需要的测量。它有两个参数widthMeasureSpec
与heightMeasureSpec
。其实这两个参数都包含两部分,分别为size
与mode
。size
为测量的大小而mode
为视图布局的模式
我们可以通过以下代码分别获取:
1
2
3
4int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec);
获取到的mode
种类分为以下三种:
MODE | EXPLAIN |
---|---|
UNSPECIFiED | 父视图不对子视图进行约束,子视图大小可以是任意大小,一般是对ListView 、ScrollView 等进行自定义,一般用不到 |
EXACTLY | 父视图对子视图设定了一个精确的尺寸,子视图不超过该尺寸,一般为精确的值例如200dp 或者使用了match_parent |
AT_MOST | 父视图对子视图指定了一最大的尺寸,确保子视图的所以内容都刚好能在该尺寸中显示出来,一般为wrap_content ,这种父视图不能获取子视图的大小,只能由子视图自己去计算尺寸,这也是我们测量要实现的逻辑情况 |
setMeasuredDimension
通过以上逻辑获取视图的宽高,最后要调用setMeasuredDimension
方法将测量好的宽高进行传递出去。其实最终是调用setMeasuredDimensionRaw
方法对传过来的值进行属性赋值。调用super.onMeasure()
的调用逻辑也是一样的。
下面以自定义一个验证码的View
为例,它的onMeasure
方法如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); if (widthMode == MeasureSpec.EXACTLY) { //直接获取精确的宽度 width = widthSize; } else if (widthMode == MeasureSpec.AT_MOST) { //计算出宽度(文本的宽度+padding的大小) width = bounds.width() + getPaddingLeft() + getPaddingRight(); } if (heightMode == MeasureSpec.EXACTLY) { //直接获取精确的高度 height = heightSize; } else if (heightMode == MeasureSpec.AT_MOST) { //计算出高度(文本的高度+padding的大小) height = bounds.height() + getPaddingBottom() + getPaddingTop(); } //设置获取的宽高 setMeasuredDimension(width, height); }
可以对自定义View
的layout_width
与layout_height
进行设置不同的属性,达到不同的mode
类型,就可以看到不同的效果
measureChildren
如果你是对继承ViewGroup
的自定义View
那么在进行测量自身的大小时还要测量子视图的大小。一般通过measureChildren(int widthMeasureSpec, int heightMeasureSpec)
方法来测量子视图的大小。
1
2
3
4
5
6
7
8
9
10protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) { final int size = mChildrenCount; final View[] children = mChildren; for (int i = 0; i < size; ++i) { final View child = children[i]; if ((child.mViewFlags & VISIBILITY_MASK) != GONE) { measureChild(child, widthMeasureSpec, heightMeasureSpec); } } }
通过上面的源码会发现,它其实是遍历每一个子视图,如果该子视图不是隐藏的就调用measureChild
方法,那么来看下measureChild
源码:
1
2
3
4
5
6
7
8
9protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) { final LayoutParams lp = child.getLayoutParams(); final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom, lp.height); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }
会发现它首先调用了getChildMeasureSpec
方法来分别获取宽高,最后再调用的就是View
的measure
方法,而通过前面的分析我们已经知道它做的就是对视图大小的计算。而对于measure
中的参数是通过getChildMeasureSpec
获取,再来看下其源码:
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
63public static int getChildMeasureSpec(int spec, int padding, int childDimension) { int specMode = MeasureSpec.getMode(spec); int specSize = MeasureSpec.getSize(spec); int size = Math.max(0, specSize - padding); int resultSize = 0; int resultMode = 0; switch (specMode) { // Parent has imposed an exact size on us case MeasureSpec.EXACTLY: if (childDimension >= 0) { resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size. So be it. resultSize = size; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size. It can't be // bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // Parent has imposed a maximum size on us case MeasureSpec.AT_MOST: if (childDimension >= 0) { // Child wants a specific size... so be it resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size, but our size is not fixed. // Constrain child to not be bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size. It can't be // bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // Parent asked to see how big we want to be case MeasureSpec.UNSPECIFIED: if (childDimension >= 0) { // Child wants a specific size... let him have it resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size... find out how big it should // be resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size.... find out how // big it should be resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } break; } //noinspection ResourceType return MeasureSpec.makeMeasureSpec(resultSize, resultMode); }
是不是容易理解了点呢。它做的就是前面所说的根据mode
的类型,获取相应的size
。根据父视图的mode
类型与子视图的LayoutParams
类型来决定子视图所属的mode
,最后再将获取的size
与mode
通过MeasureSpec.makeMeasureSpec
方法整合返回。最后传递到measure
中,这就是前面所说的widthMeasureSpec
与heightMeasureSpec
中包含的两部分的值。整个过程为measureChildren
->measureChild
->getChildMeasureSpec
->measure
->onMeasure
->setMeasuredDimension
,所以通过measureChildren
就可以对子视图进行测量计算。
layout
layout
也是一样的内部会回调onLayout
方法,该方法是用来确定子视图的绘制位置,但这个方法在ViewGroup
中是个抽象方法,所以如果要自定义的View
是继承ViewGroup
的话就必须实现该方法。但如果是继承View
的话就不需要了,View
中有一个空实现。而对子视图位置的设置是通过View
的layout
方法通过传递计算出来的left
、top
、right
与bottom
值,而这些值一般都要借助View
的宽高来计算,视图的宽高则可以通过getMeasureWidth
与getMeasureHeight
方法获取,这两个方法获取的值就是上面onMeasure
中setMeasuredDimension
传递的值,即子视图测量的宽高。
getWidth
、getHeight
与getMeasureWidth
、getMeasureHeight
是不同的,前者是在onLayout
之后才能获取到的值,分别为left
-right
与top
-bottom
;而后者是在onMeasure
之后才能获取到的值。只不过这两种获取的值一般
都是相同的,所以要注意调用的时机
。
下面以定义一个把子视图放置于父视图的四个角的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
35
36@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int count = getChildCount(); MarginLayoutParams params; int cl; int ct; int cr; int cb; for (int i = 0; i < count; i++) { View child = getChildAt(i); params = (MarginLayoutParams) child.getLayoutParams(); if (i == 0) { //左上角 cl = params.leftMargin; ct = params.topMargin; } else if (i == 1) { //右上角 cl = getMeasuredWidth() - params.rightMargin - child.getMeasuredWidth(); ct = params.topMargin; } else if (i == 2) { //左下角 cl = params.leftMargin; ct = getMeasuredHeight() - params.bottomMargin - child.getMeasuredHeight() - params.topMargin; } else { //右下角 cl = getMeasuredWidth() - params.rightMargin - child.getMeasuredWidth(); ct = getMeasuredHeight() - params.bottomMargin - child.getMeasuredHeight() - params.topMargin; } cr = cl + child.getMeasuredWidth(); cb = ct + child.getMeasuredHeight(); //确定子视图在父视图中放置的位置 child.layout(cl, ct, cr, cb); } }
至于
onMeasure
的实现源码我后面会给链接,如果要看效果图的话,我后面也会贴出来,前面的那个验证码的也是一样
draw
draw
是由dispatchDraw
发动的,dispatchDraw
是ViewGroup
中的方法,在View
是空实现。自定义View
时不需要去管理该方法。而draw
方法只在View
中存在,ViewGoup
做的只是在dispatchDraw
中调用drawChild
方法,而drawChild
中调用的就是View
的draw
方法。那么我们来看下draw
的源码:
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
49public void draw(Canvas canvas) { final int privateFlags = mPrivateFlags; final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE && (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState); mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN; /* * Draw traversal performs several drawing steps which must be executed * in the appropriate order: * * 1. Draw the background * 2. If necessary, save the canvas' layers to prepare for fading * 3. Draw view's content * 4. Draw children * 5. If necessary, draw the fading edges and restore layers * 6. Draw decorations (scrollbars for instance) */ // Step 1, draw the background, if needed int saveCount; if (!dirtyOpaque) { drawBackground(canvas); } // skip step 2 & 5 if possible (common case) final int viewFlags = mViewFlags; boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0; boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0; if (!verticalEdges && !horizontalEdges) { // Step 3, draw the content if (!dirtyOpaque) onDraw(canvas); // Step 4, draw the children dispatchDraw(canvas); // Overlay is part of the content and draws beneath Foreground if (mOverlay != null && !mOverlay.isEmpty()) { mOverlay.getOverlayView().dispatchDraw(canvas); } // Step 6, draw decorations (foreground, scrollbars) onDrawForeground(canvas); // we're done... return; } //省略2&5的情况 .... }
源码已经非常清晰了draw
总共分为6步;
- 绘制背景
- 如果需要的话,保存
layers
- 绘制自身文本
- 绘制子视图
- 如果需要的话,绘制
fading edges
- 绘制
scrollbars
其中 第2
步与第5
步不是必须的。在第3
步调用了onDraw
方法来绘制自身的内容,在View
中是空实现,这就是我们为什么在自定义View
时必须要重写该方法。而第4
步调用了dispatchDraw
对子视图进行绘制。还是以验证码
为例:
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@Override protected void onDraw(Canvas canvas) { //绘制背景 mPaint.setColor(getResources().getColor(R.color.autoCodeBg)); canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint); mPaint.getTextBounds(autoText, 0, autoText.length(), bounds); //绘制文本 for (int i = 0; i < autoText.length(); i++) { mPaint.setColor(getResources().getColor(colorRes[random.nextInt(6)])); canvas.drawText(autoText, i, i + 1, getWidth() / 2 - bounds.width() / 2 + i * bounds.width() / autoNum , bounds.height() + random.nextInt(getHeight() - bounds.height()) , mPaint); } //绘制干扰点 for (int j = 0; j < 250; j++) { canvas.drawPoint(random.nextInt(getWidth()), random.nextInt(getHeight()), pointPaint); } //绘制干扰线 for (int k = 0; k < 20; k++) { int startX = random.nextInt(getWidth()); int startY = random.nextInt(getHeight()); int stopX = startX + random.nextInt(getWidth() - startX); int stopY = startY + random.nextInt(getHeight() - startY); linePaint.setColor(getResources().getColor(colorRes[random.nextInt(6)])); canvas.drawLine(startX, startY, stopX, stopY, linePaint); } }
其实很简单,就是一些绘制的业务逻辑。好了基本就到这里了,下面上传一张示例的效果图,与源码链接
示例图
对了还有自定义属性,这里简单说一下。自定义View
时一般都要自定义属性,所以都会在res/values/attr.xml
中定义attr
与declare-styleable
,最后在自定义View
中通过TypedArray
获取。
示例源码地址:https://github.com/idisfkj/Cu...
个人博客:https://idisfkj.github.io/arc...
关注
最后
以上就是时尚火龙果最近收集整理的关于 Android 自定义View前言流程示例图关注的全部内容,更多相关内容请搜索靠谱客的其他文章。
发表评论 取消回复