Android View 的工作原理

Android View 的工作原理

Android 开发艺术探索—-View的工作原理学习和思考总结。

初识 ViewRoot 和 DecorView

ViewRoot 对应的是ViewRootImpl类,它是连接WindowManagerDecorView的桥梁,并且 View 的三大流程都是通过 ViewRoot 来完成的。

WindowManager 是用于管理整个 Android 的窗口(View),它能够管理窗口(View)的一些状态、属性、view 的添加、删除、更新等。

DecorView 是窗口的顶级 View。它其实是一个FrameLayout ,内部一般还会包含一个 LinearLayout ,上面是标题栏,下面是内容栏。View层的事件都先经过 DecorView ,然后才传递给我们的 View。

View 的三大流程是指:measure,layout, draw。measure 过程是用来测量 View 的宽高;layout 过程是用来测量 View 在父容器中的放置位置;draw 过程是将 View 绘制到屏幕上。

理解 MeasureSpec

MeasureSpec

MeasureSpec 在很大程度上决定了一个 View 的规格尺寸,之所以说很大程度是因为在决定 View 的规格的过程中会受到父容器的影响。因为在测量过程中,系统会将 View 的 LayoutParams 转为对应的 MeasureSpec ,然后再根据这个measureSpec 来测量出 View 的宽高。

LayoutParams : 是子控件控制自己在父控件中布局的一个类。例如子控件 TextView 想在父控件 LinearLayout 中显示,并且想子控件想距离父控件左边 20dp 的距离,或者是子控件想要居中显示,可以使用 LayoutParams 去控制。

MeasureSpec 代表一个 32 位 int 值,高两位代表 SpecMode,低 30 位代表 SpecSize 。SpecMode是指测量模式;SpecSize 是指某种测量模式下的 View 的规格大小。

SpecMode 测量模式有三种:UNSPECIFIED、EXACTLY、AT_MOST。

UNSPECIFIED 测量模式:父容器不对 View 有任何限制,View 是多大就给 View 多大的空间。

EXACTLY 测量模式:父容器已经检测出 View 所需要的精确大小的空间,这时候 View 的最终大小就是 SpecSize 所指定的值,它对应于 LayoutParams 中的 match_parent 和设置具体的数值。

AT_MOST 测量模式:父容器指定了一个可用大小的空间即 SpecSize,View 的最终大小不能大于这个值,它对应于 LayoutParams 中的 wrap_content

MeasureSpec 和 LayoutParams 的关系

MeasureSpec 是用于测量 View 的规格,而我们在使用 View 的时候会设置 LayoutParams ,系统会根据设置的 LayoutParams 在父容器的约束下转换为 MeasureSpec。MeasureSpec 也不是唯一由 LayoutParams 决定的,LayoutParams 需要和父容器一起才能决定 View 的 MeasureSpec,从而进一步确定 View 的宽高。

  1. 当 View 采用固定宽高时,不管父容器的 MeasureSpec 是什么,View 的 MeasureSpec 都是 EXACTLY 模式,View 的大小遵循 LayoutParams 中的大小。
  2. 当 View 的宽高是 match_parent 时,如果父容器是 EXACTLY 模式,那么 View 也是 EXACTLY 模式且 View 的大小不会超过父容器的大小;如果父容器是 AT_MOST 模式,那么 View 也是 AT_MOST 模式且 View 的大小不会超过父容器的大小。
  3. 当 View 的宽高是 wrap_content 时,不管父容器是 EXACTLY 模式还是 AT_MOST 模式,View 的模式总是最大化并且不超过父容器的剩余空间

View 的工作流程

View 的工作流程主要是指 measure , layout, draw 。measure 确定 View 的测量宽 / 高,layout确定 View 的最终宽 / 高 和四个顶点,draw 将 View 绘制到屏幕上。

measure 过程

分为 View 和 ViewGroup 两种情况。

  • 原始 View

直接通过 measure 方法就完成了测量过程。而 measure 方法会去调用 View 的 onMeasure 方法。

onMeasure 方法源码:

1
2
3
4
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

setMeasuredDimension 方法会通过getDefaultSize方法得到宽高志并设置给 View 的宽高。

getDefaultSize 方法源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);

switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}

该方法主要是返回 measureSpec 中的 specSize,而这个specSize 就是 View 的测量大小,之所以说是测量大小是因为 View 的最终大小是在 layout 阶段确定的,但是几乎所有的情况下 View 的测量大小和最终的大小是相等的。而getDefaultSize 方法的第一个参数是getSuggestedMinimumWidth方法或者getSuggestedMinimumHeight方法,那么接下来看看该方法做了哪些操作。

getSuggestedMinimumWidth 和 getSuggestedMinimumHeight 方法源码:

1
2
3
4
5
6
7
8
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());

}

getSuggestedMinimumWidth 方法先判断 View 是否设置背景,如果没有设置背景,那么返回 mMinWidth 所指定的值,如果设置了背景,那么返回 mMinWidth 和背景的最小宽度 二者之间的最大值。getSuggestedMinimumHeight 方法同理。

  • ViewGroup

先完成 ViewGroup 的测量,然后去遍历 ViewGroup 中子 View 的测量。由于 ViewGroup 是一个抽象类public abstract class ViewGroup extends View implements ViewParent, ViewManager {},它没有重写 View 的 onMeasure 方法,但是它提供了一个叫 measureChildren 的方法

measureChildren 方法源码:

1
2
3
4
5
6
7
8
9
10
protected 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 方法源码:

1
2
3
4
5
6
7
8
9
10
11
protected 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);
}

从代码中可以看出主要是在 measureChild 方法中通过 getLayoutParams 方法 取出子控件的 LayoutParams ,然后再通过 getChildMeasureSpec 来获取子元素的 MeasureSpec , 接着将 MeasureSpec 直接传递给 View 的 measure 方法来进行测量。

Tip: View 的measure 过程和 Activity 的生命周期方法不是同步执行的。解决办法由四种:

  1. Activity / View 的 onWindowFocusChanged ,在该方法中 View 已经初始化完毕,需要注意该方法会被调用多次。
  2. view.post(runnable),通过 post 将一个 runnable 传递到消息队列的末尾,然后等待 Looper 调用此 runnable 的时候,View 也已经初始化好了。
  3. ViewTreeObserver ,使用 ViewTreeObserver 的中国回掉完成该功能,比如使用 OnGlobalLayoutListener 接口。
  4. view.measure(int widthMeasureSpec, int heightMeasureSpec),手动对 View 进行 measure ,该方式较复杂。

layout 过程

layout 方法确定 View 本身的位置,而 layout 方法中的 onLayout 方法会确定所有子元素的位置。

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
37
38
39
40
41
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);

if (shouldDrawRoundScrollbar()) {
if(mRoundScrollbarRenderer == null) {
mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
}
} else {
mRoundScrollbarRenderer = null;
}

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

mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}

该方法先通过 setFrame 方法来设定 View 的四个顶点的位置,接着调用 onLayout 方法去确定子元素的位置。接下来是判断了该 View 是显示在圆形可穿戴设备上,这块是为了适配可穿戴设备吧。当布局改变时调用 OnLayoutChangeListener 接口中的 onLayoutChange 方法。

draw 过程

draw 过程是将 View 绘制到屏幕上面,View 的绘制过程遵循如下几步:

  1. 绘制背景 background.draw(canvas)
  2. 绘制自己(onDraw)
  3. 绘制 children (dispatchDraw)
  4. 绘制装饰(onDrawScrollBars)

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
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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
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)
*/

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

/*
* Here we do the full fledged routine...
* (this is an uncommon case where speed matters less,
* this is why we repeat some of the tests that have been
* done above)
*/

boolean drawTop = false;
boolean drawBottom = false;
boolean drawLeft = false;
boolean drawRight = false;

float topFadeStrength = 0.0f;
float bottomFadeStrength = 0.0f;
float leftFadeStrength = 0.0f;
float rightFadeStrength = 0.0f;

// Step 2, save the canvas' layers
int paddingLeft = mPaddingLeft;

final boolean offsetRequired = isPaddingOffsetRequired();
if (offsetRequired) {
paddingLeft += getLeftPaddingOffset();
}

int left = mScrollX + paddingLeft;
int right = left + mRight - mLeft - mPaddingRight - paddingLeft;
int top = mScrollY + getFadeTop(offsetRequired);
int bottom = top + getFadeHeight(offsetRequired);

if (offsetRequired) {
right += getRightPaddingOffset();
bottom += getBottomPaddingOffset();
}

final ScrollabilityCache scrollabilityCache = mScrollCache;
final float fadeHeight = scrollabilityCache.fadingEdgeLength;
int length = (int) fadeHeight;

// clip the fade length if top and bottom fades overlap
// overlapping fades produce odd-looking artifacts
if (verticalEdges && (top + length > bottom - length)) {
length = (bottom - top) / 2;
}

// also clip horizontal fades if necessary
if (horizontalEdges && (left + length > right - length)) {
length = (right - left) / 2;
}

if (verticalEdges) {
topFadeStrength = Math.max(0.0f, Math.min(1.0f, getTopFadingEdgeStrength()));
drawTop = topFadeStrength * fadeHeight > 1.0f;
bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength()));
drawBottom = bottomFadeStrength * fadeHeight > 1.0f;
}

if (horizontalEdges) {
leftFadeStrength = Math.max(0.0f, Math.min(1.0f, getLeftFadingEdgeStrength()));
drawLeft = leftFadeStrength * fadeHeight > 1.0f;
rightFadeStrength = Math.max(0.0f, Math.min(1.0f, getRightFadingEdgeStrength()));
drawRight = rightFadeStrength * fadeHeight > 1.0f;
}

saveCount = canvas.getSaveCount();

int solidColor = getSolidColor();
if (solidColor == 0) {
final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;

if (drawTop) {
canvas.saveLayer(left, top, right, top + length, null, flags);
}

if (drawBottom) {
canvas.saveLayer(left, bottom - length, right, bottom, null, flags);
}

if (drawLeft) {
canvas.saveLayer(left, top, left + length, bottom, null, flags);
}

if (drawRight) {
canvas.saveLayer(right - length, top, right, bottom, null, flags);
}
} else {
scrollabilityCache.setFadeColor(solidColor);
}

// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);

// Step 4, draw the children
dispatchDraw(canvas);

// Step 5, draw the fade effect and restore layers
final Paint p = scrollabilityCache.paint;
final Matrix matrix = scrollabilityCache.matrix;
final Shader fade = scrollabilityCache.shader;

if (drawTop) {
matrix.setScale(1, fadeHeight * topFadeStrength);
matrix.postTranslate(left, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, top, right, top + length, p);
}

if (drawBottom) {
matrix.setScale(1, fadeHeight * bottomFadeStrength);
matrix.postRotate(180);
matrix.postTranslate(left, bottom);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, bottom - length, right, bottom, p);
}

if (drawLeft) {
matrix.setScale(1, fadeHeight * leftFadeStrength);
matrix.postRotate(-90);
matrix.postTranslate(left, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, top, left + length, bottom, p);
}

if (drawRight) {
matrix.setScale(1, fadeHeight * rightFadeStrength);
matrix.postRotate(90);
matrix.postTranslate(right, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(right - length, top, right, bottom, p);
}

canvas.restoreToCount(saveCount);

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

自定义 View

需要做出炫酷的效果,使用系统的控件是很难完成的,那么自定义 View 就是解决这个问题的。自定义 View 涉及到 View 的层次结构、事件分发、View 的工作原理等技术细节。

自定义的分类

  • 继承 View 重写 onDraw 方法
  • 继承 ViewGroup 派生特殊的 Layout
  • 继承特定的 View 比如(TextView)
  • 继承特定的 ViewGroup 比如(LinearLayout)

自定义 View 须知

  • 让 View 支持 warp_content

因为直接继承 View 或者 ViewGroup 控件,如果不在 onMeasure 方法中对wrap_content 做特殊处理,wrap_content 和 match_parent 效果是一样的。

  • 如果有必要,让你的 View 支持 padding

因为直接继承 View 或者 ViewGroup , 如果不在 draw 方法中对 View 的 padding 做处理,那么 padding 属性是不会起作用的。而且直接继承 ViewGroup 的控件还需要在 onMeasure 和 onLayout 中考虑 padding 和子元素 margin 对其造成的影响,不然将导致 padding 和子元素的 margin 都失效。

  • 尽量不要再 View 中使用 Handler ,因为没有必要

因为 View 内部本身提供了 post 系列的方法,完全可以替代 Handler 的作用。

  • View 中如果由线程或者动画,需要及时停止,否则会造成内存泄露
  • View 带有滑动嵌套情况时,需要处理好滑动个冲突

自定义 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
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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Shader;
import android.graphics.SweepGradient;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.support.v7.widget.AppCompatImageView;
import android.text.TextPaint;
import android.util.AttributeSet;

/**
* 圆形图片
* 圆角
* 设置边框
* 设置边框颜色
* 设置边框渐变
*/

public class CircleImageView extends AppCompatImageView {
private static final ScaleType SCALE_TYPE = ScaleType.CENTER_CROP;

private static final Bitmap.Config BITMAP_CONFIG = Bitmap.Config.ARGB_8888;
private static final int COLORDRAWABLE_DIMENSION = 2;

// 圆形边框的厚度默认值。
// 如果是0,则没有天蓝色渐变的边框。
private static final int DEFAULT_BORDER_WIDTH = 0;
// 默认的圆形边框颜色
private static final int DEFAULT_BORDER_COLOR = Color.BLACK;

private final RectF mDrawableRect = new RectF();
private final RectF mBorderRect = new RectF();

private final Matrix mShaderMatrix = new Matrix();
private final Paint mBitmapPaint = new Paint();
private final Paint mBorderPaint = new Paint();

private int mBorderColor = DEFAULT_BORDER_COLOR;
private int mBorderWidth = DEFAULT_BORDER_WIDTH;

private Bitmap mBitmap;
private BitmapShader mBitmapShader;
private int mBitmapWidth;
private int mBitmapHeight;

private float mDrawableRadius;
private float mBorderRadius;

private boolean mReady;
private boolean mSetupPending;
private final Paint mFlagBackgroundPaint = new Paint();
private final TextPaint mFlagTextPaint = new TextPaint();
private String mFlagText;
private boolean mShowFlag = false;
private Rect mFlagTextBounds = new Rect();

// 渐变----用于边框颜色的渐变
Shader mSweepGradient = null;

public CircleImageView(Context context) {
super(context);
init();
}

public CircleImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public CircleImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}

private void init() {
// 默认给图片设置了裁剪的样式
super.setScaleType(SCALE_TYPE);
mReady = true;
if (mSetupPending) {
setup();
mSetupPending = false;
}
}

@Override
public ScaleType getScaleType() {
return SCALE_TYPE;
}

@Override
public void setScaleType(ScaleType scaleType) {
if (scaleType != SCALE_TYPE) {
// ScaleType 设置只能为CENTER_CROP
throw new IllegalArgumentException(String.format(
"ScaleType %s not supported.", scaleType));
}
}

@Override
public void setAdjustViewBounds(boolean adjustViewBounds) {
if (adjustViewBounds) {
throw new IllegalArgumentException(
"adjustViewBounds not supported.");
}
}
/**
* 设置支持wrap_content
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(200,200);
}else if (widthSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(200, heightSpecSize);
}else if (heightMeasureSpec == MeasureSpec.AT_MOST){
setMeasuredDimension(widthSpecSize, 200);
}
}
/**
* 核心
* @param canvas
*/
@Override
protected void onDraw(Canvas canvas) {
if (getDrawable() == null) {
return;
}
// 绘制圆形图片
canvas.drawCircle(getWidth() / 2, getHeight() / 2, mDrawableRadius,
mBitmapPaint);
// 有描边
if (mBorderWidth != 0) {
canvas.save();
// 画布整体顺时针旋转20°
canvas.rotate(20, getWidth() / 2, getHeight() / 2);
// 绘制外圆描边
canvas.drawCircle(getWidth() / 2, getHeight() / 2, mBorderRadius,
mBorderPaint);
canvas.restore();
}
if (mShowFlag && mFlagText != null) {
canvas.drawArc(mBorderRect, 40, 100, false, mFlagBackgroundPaint);
mFlagTextPaint.getTextBounds(mFlagText, 0, mFlagText.length(),
mFlagTextBounds);
canvas.drawText(mFlagText, getWidth() / 2,
(float) ((3 + Math.cos((float) (Math.PI * 5 / 18)))
* getHeight() / 4 + mFlagTextBounds.height() / 3),
mFlagTextPaint);

}
}

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
setup();
}

/**
* 获取边框的颜色
* @return
*/
public int getBorderColor() {
return mBorderColor;
}

/**
* 设置边框的颜色
* @param borderColor
*/
public void setBorderColor(int borderColor) {
if (borderColor == mBorderColor) {
return;
}
mBorderColor = borderColor;
mBorderPaint.setColor(mBorderColor);
invalidate();
}

/**
* 获取边框的宽度
* @return
*/
public int getBorderWidth() {
return mBorderWidth;
}

/**
* @param borderWidth
* 圆形的边框厚度。
*/
public void setBorderWidth(int borderWidth) {
if (borderWidth == mBorderWidth) {
return;
}

mBorderWidth = borderWidth;
setup();
}

/**
* 设置bitmap
* @return
*/
@Override
public void setImageBitmap(Bitmap bm) {
super.setImageBitmap(bm);
mBitmap = bm;
setup();
}

/**
* 设置drawable
* @return
*/
@Override
public void setImageDrawable(Drawable drawable) {
super.setImageDrawable(drawable);
mBitmap = getBitmapFromDrawable(drawable);
setup();
}

@Override
public void setImageResource(int resId) {
super.setImageResource(resId);
mBitmap = getBitmapFromDrawable(getDrawable());
setup();
}
/**
* 设置路径
* @return
*/
@Override
public void setImageURI(Uri uri) {
super.setImageURI(uri);
mBitmap = getBitmapFromDrawable(getDrawable());
setup();
}

private Bitmap getBitmapFromDrawable(Drawable drawable) {
if (drawable == null) {
return null;
}

if (drawable instanceof BitmapDrawable) {
return ((BitmapDrawable) drawable).getBitmap();
}

try {
Bitmap bitmap;

if (drawable instanceof ColorDrawable) {
bitmap = Bitmap.createBitmap(COLORDRAWABLE_DIMENSION,
COLORDRAWABLE_DIMENSION, BITMAP_CONFIG);
} else {
bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
drawable.getIntrinsicHeight(), BITMAP_CONFIG);
}

Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return bitmap;
} catch (OutOfMemoryError e) {
return null;
}
}

/**
* 核心
* 在构造方法执行完毕之后执行该方法
*/
private void setup() {
if (!mReady) {
mSetupPending = true;
return;
}

if (mBitmap == null) {
return;
}
// BitmapShader类用来渲染头像
mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP,
Shader.TileMode.CLAMP);
// 图片画笔
mBitmapPaint.setAntiAlias(true);
mBitmapPaint.setShader(mBitmapShader);
// 边框画笔
mBorderPaint.setStyle(Paint.Style.STROKE);
mBorderPaint.setAntiAlias(true);
mBorderPaint.setColor(mBorderColor);
mBorderPaint.setStrokeWidth(mBorderWidth);
// 取的原图片的宽高
mBitmapHeight = mBitmap.getHeight();
mBitmapWidth = mBitmap.getWidth();
// 设置含边界显示区域,取的是CircleImageView的布局实际大小,为方形
mBorderRect.set(0, 0, getWidth(), getHeight());
// 计算外圆(包含描边)的半径
mBorderRadius = Math.min((mBorderRect.height() - mBorderWidth) / 2,
(mBorderRect.width() - mBorderWidth) / 2);
// 初始图片显示区域为mBorderRect
mDrawableRect.set(mBorderWidth, mBorderWidth, mBorderRect.width()
- mBorderWidth, mBorderRect.height() - mBorderWidth);
// 计算内圆的半径
mDrawableRadius = Math.min(mDrawableRect.height() / 2,
mDrawableRect.width() / 2);

mFlagBackgroundPaint.setColor(Color.BLACK & 0x66FFFFFF);
mFlagBackgroundPaint.setFlags(TextPaint.ANTI_ALIAS_FLAG);

mFlagTextPaint.setFlags(TextPaint.ANTI_ALIAS_FLAG);
mFlagTextPaint.setTextAlign(Paint.Align.CENTER);
mFlagTextPaint.setColor(Color.WHITE);
mFlagTextPaint
.setTextSize(getResources().getDisplayMetrics().density * 18);

// 边框内的颜色渐变
mSweepGradient = new SweepGradient(getWidth() / 2, getHeight() / 2,
new int[] { Color.rgb(255, 255, 255), Color.rgb(1, 209, 255) },
null);

mBorderPaint.setShader(mSweepGradient);

updateShaderMatrix();
invalidate();
}

/**
* 这个函数为设置BitmapShader的Matrix参数,设置最小缩放比例,平移参数。
* 作用:保证图片损失度最小和始终绘制图片正中央的那部分
*/
private void updateShaderMatrix() {
float scale;
float dx = 0;
float dy = 0;

mShaderMatrix.set(null);

if (mBitmapWidth * mDrawableRect.height() > mDrawableRect.width()
* mBitmapHeight) {
scale = mDrawableRect.height() / (float) mBitmapHeight;
dx = (mDrawableRect.width() - mBitmapWidth * scale) * 0.5f;
} else {
scale = mDrawableRect.width() / (float) mBitmapWidth;
dy = (mDrawableRect.height() - mBitmapHeight * scale) * 0.5f;
}
// 设置缩放
mShaderMatrix.setScale(scale, scale);
// 设置平移
mShaderMatrix.postTranslate((int) (dx + 0.5f) + mBorderWidth,
(int) (dy + 0.5f) + mBorderWidth);

mBitmapShader.setLocalMatrix(mShaderMatrix);
}

public void setShowFlag(boolean show) {
mShowFlag = show;
invalidate();
}

public void setFlagText(String text) {
mFlagText = text;
invalidate();
}
}
  • 类似于 ViewPager 控件的 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
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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
public class HorizontalScrollViewEx extends ViewGroup {
private static final String TAG = "HorizontalScrollViewEx";

private int mChildrenSize;
private int mChildWidth;
private int mChildIndex;

// 分别记录上次滑动的坐标
private int mLastX = 0;
private int mLastY = 0;
// 分别记录上次滑动的坐标(onInterceptTouchEvent)
private int mLastXIntercept = 0;
private int mLastYIntercept = 0;

private Scroller mScroller;
private VelocityTracker mVelocityTracker;

public HorizontalScrollViewEx(Context context) {
super(context);
init();
}

public HorizontalScrollViewEx(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}

public HorizontalScrollViewEx(Context context, AttributeSet attrs,
int defStyle) {
super(context, attrs, defStyle);
init();
}

private void init() {
if (mScroller == null) {
mScroller = new Scroller(getContext());
// VelocityTracker 是跟踪触摸事件滑动速度的帮助类,用于实现flinging以及其它类似的手势
mVelocityTracker = VelocityTracker.obtain();
}
}

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();

switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
// 采用外部拦截法处理滑动冲突
intercepted = false;
if (!mScroller.isFinished()) {
// 为了优化滑动体验而加入
mScroller.abortAnimation();
intercepted = true;
}
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;
if (Math.abs(deltaX) > Math.abs(deltaY)) {
// 父类需要处理该事件
intercepted = true;
} else {
intercepted = false;
}
break;
}
case MotionEvent.ACTION_UP: {
intercepted = false;
break;
}
default:
break;
}

Log.d(TAG, "intercepted=" + intercepted);
mLastX = x;
mLastY = y;
mLastXIntercept = x;
mLastYIntercept = y;

return intercepted;
}

@Override
public boolean onTouchEvent(MotionEvent event) {
mVelocityTracker.addMovement(event);
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
scrollBy(-deltaX, 0);
break;
}
case MotionEvent.ACTION_UP: {
// 当松开手的时候,会自动向两边滑动,具体向那边滑动要看当前所处的位置
int scrollX = getScrollX();
// 初始化滑动速率的单位
mVelocityTracker.computeCurrentVelocity(1000);
// 获得横向速率
float xVelocity = mVelocityTracker.getXVelocity();
if (Math.abs(xVelocity) >= 50) {
// 滑动方向
mChildIndex = xVelocity > 0 ? mChildIndex - 1 : mChildIndex + 1;
} else {
mChildIndex = (scrollX + mChildWidth / 2) / mChildWidth;
}
// 子元素最后的位置
mChildIndex = Math.max(0, Math.min(mChildIndex, mChildrenSize - 1));
int dx = mChildIndex * mChildWidth - scrollX;
// 带有动画的滑动
smoothScrollBy(dx, 0);
mVelocityTracker.clear();
break;
}
default:
break;
}

mLastX = x;
mLastY = y;
return true;
}

@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) {
// 没有子元素,则将自己的宽高设置为 0,0;此处应该是需要根据 LayoutParams 中的宽高来处理
setMeasuredDimension(0, 0);
} else if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
// 如果宽高为 wrap_content,这里还需要考虑父控件的 padding 以及子控件的 margin
final View childView = getChildAt(0);
// 所有子元素的宽的和就是父控件的宽
measuredWidth = childView.getMeasuredWidth() * childCount;
measuredHeight = childView.getMeasuredHeight();
setMeasuredDimension(measuredWidth, measuredHeight);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
// 如果高为 wrap_content
final View childView = getChildAt(0);
measuredHeight = childView.getMeasuredHeight();
setMeasuredDimension(widthSpaceSize, childView.getMeasuredHeight());
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
// 如果宽为 wrap_content
final View childView = getChildAt(0);
measuredWidth = childView.getMeasuredWidth() * childCount;
setMeasuredDimension(measuredWidth, heightSpaceSize);
}
}

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

private void smoothScrollBy(int dx, int dy) {
mScroller.startScroll(getScrollX(), 0, dx, 0, 500);
invalidate();
}

@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}

@Override
protected void onDetachedFromWindow() {
mVelocityTracker.recycle();
super.onDetachedFromWindow();
}
}
小额支持我写出更好的文章~