Android 面試題總結之Android 進階(二)
Android 之美 從0到1 之Android 進階(二)
在上一章節中《Android 之美 從0到1 之Android 進階(一)》中我們已經理解了一些View的基本知識並且知道如何自定義View。那麼本章節將繼續深入理解View,關於View的繪製流程,View的事件分發。重新整理機制等等。
掌握
- Window是什麼?
- View的繪製流程
- View的事件分發機制
- View 與SurfaceView,GLSurfaceView
View的繪製流程
上一文章中我們已經自定義View以及View的三大過程,基本操作由三個函式完成:measure()、layout()、draw(),其內部又分別包含了onMeasure()、onLayout()、onDraw()三個子方法
在理解View的繪製流程之前我們應該知道這幾個類:
- View:最基本的UI元件,表示螢幕上的一個矩形區域。
- : 是一個抽象基類,作用於外觀使用者介面和行為策略表示一個視窗,它包含一個View tree和視窗的layout 引數。View tree的root View可以通過getDecorView得到。還可以設定Window的Content View。其實現類是PhoneWindow。Activity,Dialog,Toast,都包含一個Window,該Window在Activity的attach()函式中
mWindow = new PhoneWindow(this);
建立。 - DecorView
- PhoneWindow:PhoneWindow物件幫我們建立了一個PhoneWindow內部類DecorView(父類為FrameLayout)視窗頂層檢視
- ViewRootImpl:ViewRootImpl是連線WindowManager與DecorView的紐帶,View的整個繪製流程的三大步(measure、layout、draw)以及我們一些addView()的操作,都是通過ViewRootImpl完成的。
:應用程式介面和視窗管理器
在ActivityonCreate
使用的setContentView()就是設定的ContentView,通過LayoutInflater將xml內容佈局解析成View樹形結構新增到DecorView頂層檢視中id為content的FrameLayout父容器上面。那麼DecorView是如何繪製的呢?我們分兩個步驟來理解:
- DecorView新增到Window的過程
- DecorView的繪製過程
DecorView新增到Window的過程
我們根據下圖步驟來解析DecorView新增到Window的過程,以便讓我們更容易的理解。
- **Activity初始化:**Activity 啟動,關於Activity的建立過程啊或者其他細節,因為不是本篇幅重點故不做詳細討論。我們儘量簡化理解View的繪製流程。
PhoneWindow的建立:
Activity物件建立完成後,初始化了PhoneWindow物件,該Window在Activity的attach()函式中mWindow = new PhoneWindow(this);
建立,相關程式碼塊如下:final void attach(Context context, ActivityThread aThread..){ .......... mFragments.attachHost(null /*parent*/); //建立PhoneWindow物件 mWindow = new PhoneWindow(this); mWindow.setCallback(this); mWindow.setOnWindowDismissedCallback(this); mWindow.getLayoutInflater().setPrivateFactory(this); .......... }
DecorView新增Window:
ActivityThread.java
類會呼叫handleResumeActivity方法將頂層檢視DecorView新增到PhoneWindow視窗,因此通過PhoneWindow的setContentView將Activity與Window進行關聯了。final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume) { //獲得當前Activity的PhoneWindow物件 r.window = r.activity.getWindow(); //獲得當前PhoneWindow內部類DecorView物件 View decor = r.window.getDecorView(); //設定DecorView為可見 decor.setVisibility(View.INVISIBLE); //獲取Activity的WindowManager ViewManager wm = a.getWindowManager(); WindowManager.LayoutParams l = r.window.getAttributes(); a.mDecor = decor; l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION; l.softInputMode |= forwardBit; if (a.mVisibleFromClient) { //標記已新增至Window a.mWindowAdded = true; //新增DecorView到Window wm.addView(decor, l); } }
接著DecorView通過WindowManager設定到ViewRootImpl中,然後就是下面DecorView的繪製流程了。
因此我們知道在Activity的onCreate和onResume方法中呼叫View.getWidth()和View.getMeasuredHeight()返回值是0,因為View 還沒有開始繪製。
View的繪製過程
ViewRootImpl是連線WindowManager與DecorView的紐帶,View的整個繪製流程的三大步(measure、layout、draw)都是通過ViewRootImpl完成的,
繪製是從根節點開始,對佈局樹進行 measure 和 draw 。整個 View 樹的繪圖流程在 ViewRootImpl.java 類的 performTraversals() 函式展開,該函式所做 的工作可簡單概括為是否需要重新計算檢視大小(measure)、是否需要重新安置檢視的位置(layout)、以及是否需要重繪(draw),結合DecorView新增至Window過程,整體大概的流程圖如下:
那麼我們圍繞圖上過程來分析View的繪製流程,首先我們進入ViewRootImpl.java
中,檢視performTraversals
函式,這個函式非常長,View的繪製三大流程將在此展開。
“` private void performTraversals() {
// 快取DecorView ,因為在下面用的比較多
final View host = mView;
…..
if (measureAgain) {
if (DEBUG_LAYOUT) Log.v(TAG,
“And hey let’s measure once more: width=” + width
+ ” height=” + height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
…..
//獲得view寬高的測量規格,mWidth和mHeight表示視窗的寬高,lp.width和lp.height表示DecorView根佈局寬和高
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
//執行測量操作
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
.....
//執行佈局操作
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
......
//執行繪製操作
performDraw();
}
“`
主要分下面三大步驟。
measure
measure操作主要用於計算檢視的大小
在前面文章 Android 之美 從0到1 Android 進階(一)中我們知道View的MeasureSpec由父容器的MeasureSpec和其自身的LayoutParams共同確定,而對於DecorView是由它的MeasureSpec由視窗尺寸和其自身的LayoutParams共同確定。
在ViewRootImpl的performTraversals方法中,完成了建立DecorView的MeasureSpec的過程,相應的程式碼片段如下:
//獲得view寬高的測量規格,mWidth和mHeight表示視窗的寬高,lp.width和lp.height表示DecorView根佈局寬和高
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
我們知道Activity的根檢視總是全屏的,因為ViewRootImpl 在建立DecorView的MeasureSpec的過程 測量模式是EXACTLY,而Size是windowSize,相應的程式碼片段如下:
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
//匹配父容器時,測量模式為MeasureSpec.EXACTLY,測量大小直接為螢幕的大小,也就是充滿真個螢幕
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
......
}
return measureSpec;
}
View 的measure過程
measure在performMeasure開始的,該函式在view中定義為final型別,要求子類不能修改。measure()函式中又會呼叫onMeasure()函式,相應的程式碼片段如下:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...........
//如果上一次的測量規格和這次不一樣,重新測量檢視View的大小
if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
widthMeasureSpec != mOldWidthMeasureSpec ||
heightMeasureSpec != mOldHeightMeasureSpec) {
// first clears the measured dimension flag
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
resolveRtlPropertiesIfNeeded();
int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
long value = mMeasureCache.valueAt(cacheIndex);
// Casting a long to int drops the high 32 bits, no mask needed
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} ...........
}
實際為整個View tree計算大小是onMeasure()函式,裡面直接呼叫setMeasuredDimension()提供一個預設模式View計算大小,相應的程式碼片段如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(),widthMeasureSpec),getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
其中預設使用getDefaultSize() 獲取預設尺寸大小,如果自定義View不重寫onMesure(),在佈局中使用wrap_content就相當於使用match_parent的效果相應的程式碼片段如下:
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
//獲得測量模式
int specMode = MeasureSpec.getMode(measureSpec);
//獲得父親容器留給子檢視View的大小
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;
}
普通View的measure()函式是由ViewGroup在measureChild方法中呼叫的,ViewGroup呼叫其子View的measure時即傳入了該子View的widthMeasureSpec和heightMeasureSpec,共同決定了View的大小。而DecorView是繼承自FrameLayout的,所以我們看下面ViewGroup的measure過程。
ViewGroup 的measure過程
ViewGroup需要先完成子View的measure過程,才能完成自身的measure過程,在ViewGroup的onMeasure()函式中,不同的佈局(LinearLayout、RelativeLayout、FrameLayout等等)有不同的實現。FrameLayout的onMeasure()方法程式碼如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//獲取子View的個數
int count = getChildCount();
final boolean measureMatchParentChildren =
MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
mMatchParentChildren.clear();
int maxHeight = 0;
int maxWidth = 0;
int childState = 0;
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
//測量FrameLayout下每個子檢視View的寬和高
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
childState = combineMeasuredStates(childState, child.getMeasuredState());
if (measureMatchParentChildren) {
if (lp.width == LayoutParams.MATCH_PARENT ||
lp.height == LayoutParams.MATCH_PARENT) {
mMatchParentChildren.add(child);
}
}
}
}
.............
}
歸納總結一張圖大致理解View 的 measure過程
至此View的measure 過程大致清楚了,下面是View的layout過程。
layout
layout在view中定義為final型別,要求子類不能修改,用於設定子View的位置,因而是由父容器獲取子View的位置引數後,呼叫child.layout方法並傳入已獲取的位置引數,從而完成對子View的layout。相應的程式碼片段如下:
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的方法,該方法由ViewGroup實現
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);
}
}
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}
通過上面程式碼我們知道layout主要完成兩個操作:setFrame(l,t,r,b),l,t,r,b即子檢視在父檢視中的具體位置,該函式用於將這些引數儲存起來,onLayout() 是空方法由ViewGroup實現,在ViewGroup中,onLayout是一個抽象方法,因為對於不同的佈局管理器類,對子元素的佈局方式是不同的。而DecorView是繼承自FrameLayout的,所以我們看下面DecorView的onLayout程式碼片段:
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}
void layoutChildren(int left, int top, int right, int bottom,
boolean forceLeftGravity) {
final int count = getChildCount();
final int parentLeft = getPaddingLeftWithForeground();
final int parentRight = right - left - getPaddingRightWithForeground();
final int parentTop = getPaddingTopWithForeground();
final int parentBottom = bottom - top - getPaddingBottomWithForeground();
//遍歷每一個子View 進行佈局
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
//當子檢視View可見度設定為GONE時,不進行當前子檢視View的佈局
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
int childLeft;
int childTop;
int gravity = lp.gravity;
if (gravity == -1) {
gravity = DEFAULT_CHILD_GRAVITY;
}
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
//獲取子View的位置
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
if (!forceLeftGravity) {
childLeft = parentRight - width - lp.rightMargin;
break;
}
case Gravity.LEFT:
default:
childLeft = parentLeft + lp.leftMargin;
}
switch (verticalGravity) {
case Gravity.TOP:
childTop = parentTop + lp.topMargin;
break;
case Gravity.CENTER_VERTICAL:
childTop = parentTop + (parentBottom - parentTop - height) / 2 +
lp.topMargin - lp.bottomMargin;
break;
case Gravity.BOTTOM:
childTop = parentBottom - height - lp.bottomMargin;
break;
default:
childTop = parentTop + lp.topMargin;
}
//子View佈局
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}
歸納總結一張圖大致理解View 的 layout過程
通過上面程式碼我們知道主要是遍歷子View 獲取位置,進行佈局,至此View的layout 過程大致清楚了,下面是View的draw過程
draw
View檢視繪製流程中的最後一步繪製draw是由ViewRootImpl中的performDraw成員方法開始的,用於繪製View內容到畫布上,每次發起繪圖時,並不會重新繪製每個View樹的檢視,而只會重新繪製那些“需要重繪”的檢視,View類內部變數包含了一個標誌位DRAWN,當該檢視需要重繪時,就會為該View新增該標誌位。(View不需要繪製任何內容,可通過這個方法將相應標記設為true,系統會進行相應優化。ViewGroup預設開啟這個標記,View預設不開啟)相應程式碼片段如下:
//設定是否需要重繪
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
繪製開始
private void performDraw() {
....
if (!mAttachInfo.mScreenOn && !mReportNextDraw) {
return;
}
final boolean fullRedrawNeeded = mFullRedrawNeeded;
mFullRedrawNeeded = false;
mIsDrawing = true;
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw");
try {
//呼叫下面draw方法
draw(fullRedrawNeeded);
} finally {
mIsDrawing = false;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
.....
}
接著會在ViewRootImpl類中的drawSoftware方法繪製View,然後呼叫View的成員方法draw開始繪製,相應程式碼塊如下:
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int yoff,
boolean scalingRequired, Rect dirty) {
............
try {
canvas.translate(0, -yoff);
if (mTranslator != null) {
mTranslator.translateCanvas(canvas);
}
canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0);
attachInfo.mSetIgnoreDirtyState = false;
mView.draw(canvas);
............
return true;
}
接著我們看mView.draw() 開始繪製,主要做了以下6件事:
- 繪製該View的背景
- 如果要檢視顯示漸變框,這裡會做一些準備工作
- 呼叫onDraw()方法繪製檢視本身 ,每個View都需要override該方法,ViewGroup不需要實現該方法,因為ViewGroup沒有內容,但是ViewGroup需要通知View 呼叫onDraw函式,也就是下面的dispatchDraw();
- 繪製子檢視的內容,dispatchDraw()函式。在View中這是個空函式,具體的檢視不需要實現該方法,ViewGroup類已經為我們重寫了dispatchDraw()的功能實現,該方法內部會遍歷每個子檢視,呼叫drawChild()去重新回撥每個子檢視的draw()方法。
- 如果需要, 繪製當前檢視在滑動時的邊框漸變效果
- 繪製滾動條
理解相應程式碼塊如下:
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();
}
.........
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;
}
.........
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;
........
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 的 draw過程
至此View的繪製流程就大致清楚了,通過ViewRootImpl完成的,
繪製是從根節點開始,對佈局樹進行 measure 和 layout、draw,接下來在最後面我們小結一下一些View比較重要的問題。
View的事件分發機制
理解View的事件機制有助於解決開發過程中經常會遇到滑動、點選事件衝突問題、View事件機制已經是android開發者必不可少的知識。那麼
之前《Android 面試題總結之Android 基礎 (六)》一文中我們已經熟悉了View 和ViewGroup之間的關係,為我們理解View的事件分發機制奠定了基礎。
View 事件構成
在Android中,事件主要包括onClick、onLongClick、onScroll、onFling等,onClick又包括單擊和雙擊,另外還包括單指操作和多指操作。
使用者在手指與螢幕接觸過程中通過MotionEvent物件產生一系列事件,它有四種狀態:
- 按下(ACTION_DOWN)
- 移動(ACTION_MOVE)
- 擡起(ACTION_UP)
- 退出 (ACTION_CANCEL) 一般由程式產生,不會由使用者產生
所有這些都構成了Android中的事件響應,Touch事件由 Action_Down、Action_Move、Aciton_UP 組成,其中一次完整的Touch事件中,Down 和 Up 都只 有一個,Move 有若干個,可以為 0 個。
View的事件分發
我們先來了解三個常見的函式的作用。
View事件機制三個過程
事件分發的過程
在這個事件分發過程,我們分為ViewGroup的事件分發過程和View的事件分發過程這兩個方面。
public boolean dispatchTouchEvent(MotionEvent event)
Android中所有的事件都必須經過這個方法的分發,然後決定是自身消費當前事件還是繼續往下分發給子控制元件處理進行事件分發,dispatchTouchEvent 的事件分發邏輯如下:- 如果 return true,事件會分發給當前 View 並由 dispatchTouchEvent 方法進行消費,同時事件會停止向下傳遞;
- 如果 return false,事件分發給父View的onTouchEvent 進行消費(如果在最外層是Activity 則是返回給Activity的onTouchEvent):
- 如果返回系統預設的 super.dispatchTouchEvent(ev),事件會自動的分發給當前 ViewGroup 的 onInterceptTouchEvent方法 ,如果是View 就繼續往下分發。
ViewGroup分發過程
當事件分發到ViewGroup的dispatchTouchEvent方法,如果返回系統預設的 super.dispatchTouchEvent(ev),事件會自動的分發給當前 ViewGroup 的 onInterceptTouchEvent方法,如果onInterceptTouchEvent 返回true,則呼叫onTouchEvent方法進行事件處理,否則繼續向child View.dispatchTouchEvent分發。
Android中事件傳遞按照從上到下進行層級傳遞,事件處理從Activity開始到ViewGroup再到View,舉個下面的例子:
當一個 Touch 事件(觸控事件為例)到達根節點,即 Acitivty 的 DecorView 時,它會依次下發,下發的過程是呼叫子 View(ViewGroup)的 dispatchTouchEvent 方法實現的。簡單來說,就是 ViewGroup 遍歷它包含著的子 View,再進行判斷當前的x,y座標是否落在子View身上,如果在,那麼呼叫每個 View 的 dispatchTouchEvent 方法,而當子 View 為 ViewGroup 時,又會通過呼叫 ViwGroup 的dispatchTouchEvent 方法繼續呼叫其內部的 View的 dispatchTouchEvent 方法。上述例子中的訊息下發順序是這樣的:①-②-⑤-⑥-⑦-③-④。dispatchTouchEvent 方法只負責事件的分發,它擁有 boolean 型別的返回值,當返回為 true 時,順序下發會中斷。在上述例子中如果⑤的 dispatchTouchEvent 返回結果為 true,那麼⑥-⑦-③-④將都接收不到本次 Touch 事件,標誌著本次事件取消。
View的事件分發
下面通過點選Activity上的一個Button小例子來分析View的事件流程:
先來一張簡化的流程圖
當點選這個Button,首先執行到的是MainActivity的dispatchTouchEvent方法,這將是事件分發的開始。
- 如果MainActivity 不進行攔截,那麼繼續分發給Button,詢問Button是否分發,如果不進行分發,回撥onTouchEvent是否進行事件消費。
- 如果MainActivity 進行攔截,子View就分發不到。
總結一張圖理解事件分發流程(紅色箭頭流向):
事件響應的過程
響應的過程是子View 回傳遞到父View的過程
還是用這張圖來理解事件響應流程(綠色箭頭):
- 如果子View(Button)消費了事件,所以開始回傳,一層一層往上告訴父View他已經消費了事件。
- 如果子View(Button)沒有消費事件,也開始回傳,一層一層往上告訴父View他沒有消費事件,問ViewGroup2是否消費事件,如果ViewGroup1也不消費事件,繼續回傳到ViewGroup1,問ViewGroup1是否消費事件,如果也不消費事件,最終回傳到Activity,讓Activity去消費。
事件處理的過程
- 如果事件傳遞到當前 View(Btuuon) 的 onTouchEvent 方法,如果返回false,那麼這個事件會從當前 View(Bttuon) 向上ViewGroup1傳遞。
- 如果返回了 true 接收並消費該事件。
- 如果返回 super.onTouchEvent(ev) 預設處理事件的邏輯和返回 false 時相同。
最終當一個View接收到了觸碰事件時,會呼叫其onTouchEvent方法.相關程式碼塊如下:
/**
* Implement this method to handle touch screen motion events.
*
* @param event The motion event.
* @return True if the event was handled, false otherwise.
*/
public boolean onTouchEvent(MotionEvent event) {}
如果此view被禁用了 返回的是false;相關程式碼塊如下:
// 如果View被禁用的話,則返回它是否可以點選。 if ((viewFlags & ENABLED_MASK) == DISABLED) { if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } return (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)); }
如果此View有觸碰事件處理代理,那麼將此事件交給mTouchDelegate,相關程式碼塊如下:
// 如果該View的mTouchDelegate不為null的話,將觸控訊息分發給mTouchDelegate。 // mTouchDelegate的預設值是null。 if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } }
如果View不可點選則直接返回false,如果可以點選進入處理點選,更新View狀態等。
if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) { switch (event.getAction()) { if (!post(mPerformClick)) { performClick(); } }
View 與SurfaceView,GLSurfaceView
SurfaceView是從View基類中派生出來的顯示類。android遊戲開發中常用的三種檢視是:view、SurfaceView和GLSurfaceView
- View:顯示檢視,內建畫布,提供圖形繪製函式、觸屏事件、按鍵事件函式等;必須在UI主執行緒內更新畫面,速度較慢。
- SurfaceView:基於view檢視進行拓展的檢視類,更適合2D遊戲的開發;是view的子類,類似使用雙緩機制,在新的執行緒中更新畫面所以重新整理介面速度比view快,缺點 非常消耗cpu和記憶體的開銷
- GLSurfaceView:基於SurfaceView檢視再次進行拓展的檢視類,專用於3D遊戲開發的檢視;是SurfaceView的子類,openGL專用,。
問題總結
View的繪製流程分幾步,從哪開始?哪個過程結束以後能看到view?
從ViewRootImpl
的performTraversals
開始,經過measure,layout,draw 三個流程。draw流程結束以後就可以在螢幕上看到view了。view的測量寬高和實際寬高有區別嗎?
基本上百分之99的情況下都是可以認為沒有區別的。有兩種情況,有區別。第一種 就是有的時候會因為某些原因 view會多次測量,那第一次測量的寬高 肯定和最後實際的寬高 是不一定相等的,但是在這種情況下最後一次測量的寬高和實際寬高是一致的。此外,實際寬高是在layout流程裡確定的,我們可以在layout流程裡 將實際寬高寫死 寫成硬編碼,這樣測量的寬高和實際寬高就肯定不一樣了,雖然這麼做沒有意義 而且也不好。
view的measureSpec 由誰決定?頂級view呢?
View的MeasureSpec由父容器的MeasureSpec和其自身的LayoutParams共同確定,而對於DecorView是由它的MeasureSpec由視窗尺寸和其自身的LayoutParams共同確定。
在ViewRootImpl的performTraversals方法中,完成了建立DecorView的MeasureSpec的過程,一旦確定了spec,onMeasure中就可以確定view的寬高了。對於普通view來說,他的measure過程中,與父view有關嗎?如果有關,這個父view也就是viewgroup扮演了什麼角色?
對於普通view的measure來說 是由這個view的 父view ,也就是viewgroup來觸發的。
通過前面歸納總結一張圖大致理解View 的 measure過程view的meaure和onMeasure有什麼關係?
view的measure是final 方法 我們子類無法修改的,是在measure方法裡呼叫了onMeasure方法。自定義view中 如果onMeasure方法 沒有對wrap_content 做處理 會發生什麼?為什麼?怎麼解決?
如果沒有對wrap_content做處理 ,那即使你在xml裡設定為wrap_content.其效果也和match_parent相同。
解決方式就是在onMeasure裡 針對wrap 來做特殊處理 比如指定一個預設的寬高,當發現是wrap_content 就設定這個預設寬高即可。ViewGroup有onMeasure方法嗎?為什麼?
沒有,這個方法是交給子類自己實現的。不同的viewgroup子類 肯定佈局都不一樣,那onMeasure索性就全部交給他們自己實現好了。
為什麼在activity的生命週期裡無法獲得測量寬高?有什麼方法可以解決這個問題嗎?
因為measure的過程和activity的生命週期 沒有任何關係。你無法確定在哪個生命週期執行完畢以後 view的measure過程一定走完。可以嘗試如下幾種方法 獲取view的測量寬高。
//重寫activity的這個方法 public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); if (hasFocus) { int width = tv.getMeasuredWidth(); int height = tv.getMeasuredHeight(); Log.v("burning", "width==" + width); Log.v("burning", "height==" + height); } }
延時一段時間,等待控制元件測量、佈局完成後再獲取
//延時一段時間,等待控制元件測量、佈局完成後再獲取 @Override protected void onStart() { super.onStart(); tv.post(new Runnable() { @Override public void run() { int width = tv.getMeasuredWidth(); int height = tv.getMeasuredHeight(); } }); }
監聽onlayout方法執行完成之後,就可以獲取控制元件大小了
@Override protected void onStart() { super.onStart(); ViewTreeObserver observer = tv.getViewTreeObserver(); observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { int width = tv.getMeasuredWidth(); int height = tv.getMeasuredHeight(); tv.getViewTreeObserver().removeOnGlobalLayoutListener(this); } }); }
draw方法 大概有幾個步驟?
主要分為6個步驟:- 繪製該View的背景
- 如果要檢視顯示漸變框,這裡會做一些準備工作
- 呼叫onDraw()方法繪製檢視本身 ,每個View都需要override該方法,ViewGroup不需要實現該方法,因為ViewGroup沒有內容,但是ViewGroup需要通知View 呼叫onDraw函式,也就是下面的dispatchDraw();
- 繪製子檢視的內容,dispatchDraw()函式。在View中這是個空函式,具體的檢視不需要實現該方法,ViewGroup類已經為我們重寫了dispatchDraw()的功能實現,該方法內部會遍歷每個子檢視,呼叫drawChild()去重新回撥每個子檢視的draw()方法。
- 如果需要, 繪製當前檢視在滑動時的邊框漸變效果
- 繪製滾動條
- 繪製該View的背景
View的重新整理機制?
當子View需要重新整理時會呼叫子View的invalidate()來重新繪製。View的重新整理機制,是通過父View負責重新整理、佈局顯示子View;而當子View需要重新整理時,則是通知父View來完成,我們可通過下圖更容易理解之間的關係。
事件分發中的 onTouch 和 onTouchEvent 有什麼區別,又該如何使用?
這兩個方法都是在 View 的 dispatchTouchEvent 中呼叫的,onTouch 優先於 onTouchEvent 執行。如果在 onTouch 方法中通過返回 true 將事件消費掉,onTouchEvent 將不會再執行。
onTouch 執行需要滿足兩個條件:- mOnTouchListener 的值不能為空
- 當前點選的控制元件必須是 enable 的。因此如果你有一個控制元件是非 enable 的,那麼給它註冊 onTouch 事件將永遠得不到 執行。對於這一類控制元件,如果我們想要監聽它的 touch 事件,就必須通過在該控制元件中重寫 onTouchEvent 方法來實現,相關程式碼塊如下:
if (onFilterTouchEventForSecurity(event)) { //noinspection SimplifiableIfStatement ListenerInfo li = mListenerInfo; if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; } if (!result && onTouchEvent(event)) { result = true; } }
onTouch 和onClick有什麼區別,?
onTouch事件要先於onClick事件執行,onTouch在事件分發方法dispatchTouchEvent中呼叫,而onClick在事件處理方法onTouchEvent中被呼叫,onTouchEvent要後於dispatchTouchEvent方法的呼叫。