-文章来源:itsCoder 的 WeeklyBolg 项目
一、概要
笔记内容源于Android 开发艺术探索。
本次介绍的主要内容是 View 的工作原理下 View 的 layout 和 draw 过程,同时介绍自定义 View 的注意事项并结合一个小的 Demo 进行说明,其中涉及到的 onMeasre 测量部分知识可以看上一篇文章View 的工作原理上 View 绘制流程梳理及 Measure 过程详解 ,以下开始正文:
二、 layout 过程详解
layout 的作用是 ViewGroup 来确定子元素的位置,当 ViewGroup 的位置被确定后,在 layout 中会调用 onLayout ,在 onLayout 中会遍历所有的子元素并调用子元素的 layout 方法,在子元素的 layout 方法中 onLayout 方法又会被调用,layout 方法是确定 View 本身在屏幕上显示的具体位置,即在代码中设置其成员变量 mLeft,mTop,mRight,mBottom 的值,这几个值是在屏幕上构成矩形区域的四个坐标点,就是该 View 显示的位置,不过这里的具体位置都是相对与父视图的位置而言,而 onLayout 方法则会确定所有子元素位置,ViewGroup 在 onLayout 函数中通过调用其 children 的 layout 函数来设置子视图相对与父视图中的位置,具体位置由函数 layout 的参数决定。下面我们先看 View 的layout 方法如下:
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
|
*@param l view 左边缘相对于父布局左边缘距离
*@param t view 上边缘相对于父布局上边缘位置
*@param r view 右边缘相对于父布局左边缘距离
*@param b view 下边缘相对于父布局上边缘距离
*/
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) !=
0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
ListenerInfo li = mListenerInfo;
if (li !=
null && li.mOnLayoutChangeListeners !=
null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (
int i =
0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(
this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
|
layout 方法大致流程:先通过上面代码第一步调用 setFrame 设置 view 本身四个顶点位置,其中 setOpticalFrame 内部也是调用 setFrame 方法来完成设置的,即为 View 的4个成员变量(mLeft,mTop,mRight,mBottom)赋值,View 的四个顶点一旦确定,那么 View 在父容器中的位置就确定了,接着进行第二步,调用 onLayout 方法,这个方法用途是父容器确定子 View 位置,和 onMeasure 方法类似, onLayout 方法的具体实现同样和具体布局有关,所以 View 和 ViewGroup 中都没有真正实现 onLayout 方法,都是一个空方法。
先看一下 View 的 onLayout 方法:
void onLayout(boolean changed, int left, int top, int right, int bottom) {}```
1
2
3
4
5
6
7
8
|
那么对于我们自定义的 View 是继承自 View 的情况下,我们一般不需要重写 onLayout 方法,因为 这个方法用途是父容器确定子 View 位置,对于 View 来说是没有子 View 的,所以一般不需要重写。
再看一下 ViewGroup 的 onLayout 方法:
```java
protected abstract void onLayout(boolean changed,
int l, int t, int r, int b);
|
相对于 view 来说,ViewGroup 中 onLayout 多了关键字abstract的修饰,也就说对于继承自 ViewGroup 的自定义 View 必须要重写 onLayout 方法,而重载 onLayout 的目的就是安排其子元素在父视图中的具体位置,为了更好的理解,接下来我们看一下 LinearLayout 的 onLayout 方法:
1
2
3
4
5
6
7
8
|
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
}
else {
layoutHorizontal(l, t, r, b);
}
}
|
和 onMeasure 类似,这里也是分为竖直方向和水平方向的布局安排,二者原理一样,我们选择竖直方向的 layoutVertical 来进行分析,这里给出主要代码如下:
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
|
void layoutVertical(int left, int top, int right, int bottom) {
final
int paddingLeft = mPaddingLeft;
int childTop;
int childLeft;
switch (majorGravity) {
case Gravity.BOTTOM:
childTop = mPaddingTop + bottom - top - mTotalLength;
break;
case Gravity.CENTER_VERTICAL:
childTop = mPaddingTop + (bottom - top - mTotalLength) /
2;
break;
case Gravity.TOP:
default:
childTop = mPaddingTop;
break;
}
...............................
for (
int i =
0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child ==
null) {
childTop += measureNullChild(i);
}
else
if (child.getVisibility() != GONE) {
final
int childWidth = child.getMeasuredWidth();
final
int childHeight = child.getMeasuredHeight();
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
.............
if (hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
}
childTop += lp.topMargin;
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
|
简单梳理下整个流程,此方法会遍历所有子 view ,并调用 setChildFrame 方法来设定子元素位置,然后重新计算 childTop ,childTop 随着子元素的遍历而逐渐增大,这就意味着后面的子元素会被放置在当前子元素的下方,这正是我们平时使用竖直方向 LinearLayout 的特性。这里我们看一下第三步执行的 setChildFrame 方法类设置子元素位置方法代码:
1
2
3
|
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
|
可以发现,这个方法只是调用子元素的 layout 方法而已,这样父元素在自己的 layout 方法中完成自己的定位之后,通过 onLayout 方法去调用了子元素的 layout 方法,子元素又会通过自己的 layout 方法完成自己的位置设定,这样一层一层的传递下去就完成了整个 view 数的 layout 过程。
这里我们注意到在第三步调用 setChildFrame 方法中的 传入的参数 childWidth 和 childHeight 是上面第2.1步获取的子元素的测量宽/高,而在 layout 过程中会通过 setFrame 方法设置子元素四个顶点位置,这样子元素的位置就确定了,在 setFrame 中有如下赋值语句:
1
2
3
4
|
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
|
也就是说在 LinearLayout 中其子视图显示的宽和高由 measure 过程来决定的,因此 measure 过程的意义就是为 layout 过程提供视图显示范围的参考值。为什么说是提供参考值呢?因为 layout 过程中的4个参数 left, top, iwidth, height 完全可以由视图设计者任意指定,而最终视图的布局位置和大小完全由这4个参数决定,measure 过程得到的mMeasuredWidth 和 mMeasuredHeight 提供了视图大小的测量值,只是提供一个参考一般情况下我们使用这个参考值,但我们完全可以不使用这两个值,而自己在 layout 过程中去设定一个值,可见 measure 过程并不是必须的。
说到这里就不得说一下 getWidth() 、getHeight() 和 getMeasuredWidth()、getMeasuredHeight() 这两对函数之间的区别,即 View 的测量宽/高和最终显示宽/高之间的区别。首先我们看一下 getWith() 和 getHeight() 方法的具体实现:
1
2
3
4
5
6
7
|
public final int getWidth() {
return mRight - mLeft;
}
public final int getHeight() {
return mBottom - mTop;
}
|
通过 getWith() 和 getHeight() 源码和上面 setChildFrame(View child, int left, int top, int width, int height) 方法设置子元素四个顶点位置的四个变量 mLeft、mTop、mRight、mBottom 的赋值过程来看,默认情况下 getWidth() 、getHeight() 方法返回的值正好就是 view 的测量宽/高,只不过 view 的测量宽/高形成于 view 的measure 过程,而最终宽/高形成于 view 的 layout 方法中,但是对于特殊情况,两者的值是不相等的,就是我们在 layout 过程中不按默认常规套路出牌,即不使用 measure 过程得到的 mMeasuredWidth 和 mMeasuredHeight ,而是人为的去自己根据需要设定的一个值的情况,例如以下代码,重写 view 的 layout 方法:
1
2
3
4
|
public void layout(int l, int t, int r, int b) {
super.layout(
int l,
int t,
int r+
100,
int b+
100);
}
|
上面代码会导致在任何情况下 view 的最终宽/高总会比测量宽高大100px。
三、 draw 过程详解
draw 的作用是将 view 绘制到屏幕上,view 的绘制过程准守以下几个步骤:
- 绘制背景:
background.draw(canvas)
; - 绘制自己:
onDraw()
; - 绘制 children:
dispatchDraw
; -
绘制装饰:onDrawScrollBars
。
通过源码可以看出来,部分源码如下:
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
|
public 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)
*/
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
final
int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) !=
0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) !=
0;
if (!verticalEdges && !horizontalEdges) {
if (!dirtyOpaque) onDraw(canvas);
dispatchDraw(canvas);
if (mOverlay !=
null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
onDrawForeground(canvas);
return;
}
|
通过上面代码可以发现,View 绘制过程的传递是通过 dispatchDraw() 方法完成,这个方法会遍历调用所有子视图的 draw ()方法,这样事件就一层一层的传递下去了。其中 View 中有一个特殊方法 setWillNotDraw ,源码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
* If this view doesn't do any drawing on its own, set this flag to
* allow further optimizations. By default, this flag is not set on
* View, but could be set on some View subclasses such as ViewGroup.
*
* Typically, if you override {
@link #onDraw(android.graphics.Canvas)}
* you should clear this flag.
*
*
@param willNotDraw whether or not this View draw on its own
*/
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW :
0, DRAW_MASK);
}
|
看注释部分大概意思是,如果一个 View 不需要绘制任何内容,那么设置这个标记位为 true 后,系统会进行相应的优化,默认情况下 View 没有启动这个默认标记位,但 viewGroup 默认启用这个标记位,这个标记位对实际开发的意义是:当我们的自定义的控件继承自 viewGroup 并且本身不具备绘制功能的时候,就可以开启这个标记位,从而便于系统进行后续的优化工作,当我们明确知道 viewGrop 需要通过 onDraw 来绘制本身内容时,需要我们去关闭 WILL_NOT_DRAW 这个标记位。
四、自定义 View
4.1 自定义 View 分类
-
继承自 View 重写 ondraw 方法
这种方法主要用于实现一些不规则的效果,即想要达到的 View 效果无法使用已有的 View 通过布局组合的方式来实现,所以需要我们自己去绘制去画一个出来,即重写 onDraw 方法,采用这种方式需要注意处理自定义的 View 支持 wrap_content ,并且 padding 也需要自己处理。
-
继承自 ViewGroup 实现特殊的 Layout 容器
主要实现除了 LinearLayout 、 RelativeLayout 等系统已有的 View 容器之外的特殊 View 容器,需要处理 ViewGroup 的测量 onMeasure 和布局 onLayout 这两个方法,并同时处理子元素的测量和布局。
-
继承自 Android 系统本身已有的特定 View (如 TextView)
这种方法是要是为拓展某个已有 View 的功能,在已有的 View 的基础上添加一些功能,方便我们重复使用,这种方法不需要我们进行特殊的处理。
-
继承自 Android 系统本身已有的特定的 ViewGroup (如 LinearLayout)
这种方法主要是为了实现将几个 View 组合在一起形成一个特定的组合模块,来方便我们后续进行使用,例如我们想要一个特定的 TitleBar ,我们可以可以将几个 TextView 和 Button 放在一个 LinearLayout 布局中组合成一个自定义的控件,采用这种方式不需要进行特殊的处理。
4.2 自定义 View 须知
自定义 View 过程中需要注意一些事项,如果这些问题处理不好,可能会影响 View 的正常使用和性能。
-
让 View 支持 wrap_content
在自定义 View 时,如果是直接继承自 View 或者 View Group ,并且不在 onMeasure 中对 wrap_content 做特殊处理,那么在我们使用这个自定义的 View 的 wrap_content 属性时,就无法达到预期效果,而是和使用 match_parent 属性效果一样。
-
如果有必要,让自定义的 View 支持 padding 属性
在自定义 View 时,如果是直接继承自 View ,不在 onDraw 方法中处理 padding ,那么该自定义的 View padding属性将失效;如果是直接继承自 ViewGrop 需要在 onMeasure 和 onLayout 中考虑 padding 和 margin 对其造成的影响,否则将导致自定义的控件 padding 和子元素的 margin 属性失效。
-
尽量不要在 View 中使用 Handler ,没必要
View 本身内部提供了一些列的 post 方法,完全可以替代 Handler 作用。
-
View 中如果有线程或者动画需要在特定生命周期进行停止
当包含此 View 的 Activity 退出或者当前 View 被 remove 掉时,View 的 onDetachedFromWindow() 方法会被调用,所以如果有需要停止的线程或者动画可以在这个方法中执行,和此方法相对应的是 onAttachedToWindow() 方法,当包含该 View 的 Activity 启动的时候,该方法就会被调用。同时当 View 变得不可见时,我们需要及时停止线程和动画,否则可能造成内存泄露。
-
View 带有滑动嵌套情形时,需要处理好滑动冲突
如果有滑动冲突需要合适的进行处理。如果要处理好滑动处理可以看一下View 事件的分发机制
4.3 自定义 View 示例
4.3.1 继承自 View 重写 onDraw 方法
这种方法一般为了实现一些不规则的效果,需要我们自己去绘制去画一个出来 View 出来,即重写 onDraw 方法,采用这种方式需要考虑 View 四周的空白即处理 padding 值,而 margin 值是受父容器控制所以不需要进行处理,并且需要注意处理自定义的 View 支持 wrap_content ,即重写 onMeasure 方法,如果不进行处理那么当在 xml 文件中使用 wrap_content 属性的时候,就相当于 match_parent 属性,这里为了更详细的说明问题,我们一起来实现一个简单的自定义 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
24
25
26
27
28
29
30
|
* 这里进行重写 onMeasure 方法,让自定义的 View 支持 Wrap_content 模式
* 当使用该自定义的 View 时候,如果使用了 Wrap_content 属性后
* 该 View 的宽和高都为 200dp ,这个尺寸在实际应用中需要根据具体需要和情况进行计算
* 这里只是为了解释这个原理,任意给定了一个值恰巧是 200dp 而已
*
@param widthMeasureSpec 父容器给定的宽度约束条件
*
@param heightMeasureSpec 父容器给定的高度约束条件
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width=
200;
int height=
200;
int widthMode=MeasureSpec.getMode(widthMeasureSpec);
int heightMode=MeasureSpec.getMode(heightMeasureSpec);
int withSize=MeasureSpec.getSize(widthMeasureSpec);
int heightSize=MeasureSpec.getSize(heightMeasureSpec);
if(widthMode==MeasureSpec.AT_MOST&&heightMode==MeasureSpec.AT_MOST){
setMeasuredDimension(width,height);
}
else
if(widthMode==MeasureSpec.AT_MOST){
setMeasuredDimension(width,heightSize);
}
else
if(heightMode==MeasureSpec.AT_MOST){
setMeasuredDimension(withSize,height);
}
}
|
以上代码就解决了让 自定义的 View 支持 wrap_content 的问题。下面在看看在 onDraw 方法中进行绘制的时候处理 padding 值的问题,代码如下:
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
|
* 这里进行绘制 View 的内容,这里要注意需要处理 padding 值,
* 让自定义的 View 支持 padding 属性,如果不处理,
*那么在 xml 文件中使用该自定义的 View 的 padding
* 属性时候,将会失效
*
@param canvas 画布
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width=getWidth();
int height=getHeight();
int paddingLeft=getPaddingLeft();
int paddingRight=getPaddingRight();
int paddingTop=getPaddingTop();
int paddingBottom=getPaddingBottom();
int withFinal=width-paddingLeft-paddingRight;
int heightFinal=height-paddingTop-paddingBottom;
int radius=Math.min(withFinal/
2,heightFinal/
2);
canvas.drawCircle(paddingLeft+withFinal/
2,paddingTop+heightFinal/
2,radius,paint);
}
|
以上代码解决了让自定义的 View 支持 padding 属性。
4.3.2 继承自 ViewGroup 实现特殊的 Layout 容器
这种自定义的 ViewGroup 需要处理 onMeasure 测量和 onLayout 布局两个过程,同时需要处理子元素的测量和布局过程。采用这种方法实现一个规范的自定义 View 是相当复杂的,通过前面分析的 LinearLayout 代码就可以发现,因为要考虑如何摆放子视图以及各种细节的处理,Android 开发艺术探索书中给出了一个相对规范(不完全规范)的自定义的 HorizontalScrollViewEx 视图容器,实现了一个类似 ViewPaper 的控件,内部子视图可以水平方向滑动,并且子视图的内部子元素可以实现竖直方向滑动,很显然这个控件解决了水平方向和竖直方向滑动冲突的问题,该部分知识可以看一下View 的事件分发机制(Android 开发艺术探索读书笔记) 这篇文章从源码角度分析了事件分发机制,理解了就可以解决滑动冲突的问题。下面一起看一下关键部分代码,先看 onMeasure 部分代码如下:
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
|
* 重写 onMeasure 方法,处理自定义 View 支持 wrap_content 模式
*
@param widthMeasureSpec 父容器给定宽度约束条件
*
@param heightMeasureSpec 父容器给定高度约束条件
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measuredWidth =
0;
int measuredHeight =
0;
final
int childCount = getChildCount();
measureChildren(widthMeasureSpec, heightMeasureSpec);
int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
if (childCount ==
0) {
setMeasuredDimension(
0,
0);
}
else
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
final View childView = getChildAt(
0);
measuredWidth = childView.getMeasuredWidth() * childCount;
measuredHeight = childView.getMeasuredHeight();
setMeasuredDimension(measuredWidth, measuredHeight);
}
else
if (heightSpecMode == MeasureSpec.AT_MOST) {
final View childView = getChildAt(
0);
measuredHeight = childView.getMeasuredHeight();
setMeasuredDimension(widthSpaceSize, childView.getMeasuredHeight());
}
else
if (widthSpecMode == MeasureSpec.AT_MOST) {
final View childView = getChildAt(
0);
measuredWidth = childView.getMeasuredWidth() * childCount;
setMeasuredDimension(measuredWidth, heightSpaceSize);
}
}
|
以上代码实现了让自定义的 View 支持 wrap_content 属性。
说明,这里为了方便处理有几处不规范的地方如下:
- 假设了所有子视图的高度和宽度都相等,而实际应用中这是不可能的,所以计算起来会更复杂。
- 没有子元素的时候不应该直接设置宽/高为 0,而是应该根据 LayoutParams 的宽/高来做相应的处理,因为当使用 padding 属性的时候,虽然没有子视图,但 padding 值也会占据一定空间,你可以设置 LinearLayout 子视图个数为 0,然后给定一个 padding 值去试试。
- 在测量 HorizontalScrollViewEx 的高/宽的时候没有考虑它的 padding 值和子视图的 margin 值,因为自己的 padding 值和子视图的 margin 值都是占据空间的。
下面再看一下 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
|
* 重写 onLayout 方法,实现摆放子视图功能
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childLeft =
0;
final
int childCount = getChildCount();
mChildrenSize = childCount;
for (
int i =
0; i < childCount; i++) {
final View childView = getChildAt(i);
if (childView.getVisibility() != View.GONE) {
final
int childWidth = childView.getMeasuredWidth();
mChildWidth = childWidth;
childView.layout(childLeft,
0, childLeft + childWidth,
childView.getMeasuredHeight());
childLeft += childWidth;
}
}
}
|
以上代码实现了摆放子视图的功能,从代码可以看出放置子视图是从左至右依次摆放。
说明,以上代码不规范之处:
在摆放子视图的过程中,没有考虑自身的 padding 和子视图的 margin 值。
4.3.3 自定义 View 的总结
到这里,关于 View 的基础知识基本学习完毕,笔者写到这里也完全不能写出了一个牛逼的自定义控件(一个基友曾经这样问我:你学完了这些知识,还不徒手撸出一个牛逼的自定义 View 啊——–阿风 ,我的回答当然不能。因为自定义 View 是一个综合的知识体系,需要灵活的运用各种知识和经验,这里我们只是学习了一下基础理论知识,知其原理,懂其思路,如果我们想自定义 View,首先要掌握基本功,比如 View 的弹性滑动,滑动冲突,绘制原理等,这些都是自定义 View 所必须知识点,再复杂的自定义 View 也是离不开这些知识点,尤其是那些看起来很炫酷的自定义 View,往往对这些技术点要求更高,只有熟悉掌握这些基础知识点以后,在面对新的自定义 View 时,才能够根据需求情况选择合适的实现思路,实现大体方法就是 4.1 节中介绍的四种分类,另外还需要学习一下 Canvas 这个类的用法才能画出想要的 View 。
最后,文章中的 Demo 会上传在 github,链接地址 ,这里只是一个简单的 Demo ,分析一下原理,(这些 Demo 源码出自 Android 开发艺术探索)如果想撸出更炫酷的 View 还需要掌握 Canvas 这个类的用法,后面需要续学习。
以上就是本次的笔记内容,如有错误,希望指出,谢谢!!!