關於作者
郭孝星,程式設計師,吉他手,主要從事Android平臺基礎架構方面的工作,歡迎交流技術方面的問題,可以去我的Github提issue或者發郵件至guoxiaoxingse@163.com與我交流。
文章目錄
- 一 View的生命週期
- 二 View的測量流程
- 三 View的佈局流程
- 四 View的繪製流程
- 五 View事件分發機制
This class represents the basic building block for user interface components. A Viewoccupies a rectangular area on the screen and is
responsible for drawing and event handling.
View是螢幕上的一塊矩形區域,負責介面的繪製與觸控事件的處理,它是一種介面層控制元件的抽象,所有的控制元件都繼承自View。
View是Android顯示框架中較為複雜的一環,首先是它的生命週期會隨著Activity的生命週期進行變化,掌握View的生命週期對我們自定義View有著重要的意義。另一個方面View從ViewRoot.performTraversals()開始
經歷measure、layout、draw三個流程最終顯示在使用者面前,使用者在點選螢幕時,點選事件隨著Activity傳入Window,最終由ViewGroup/View進行分發處理。今天我們就圍繞著這些主題進行展開分析。
一 View生命週期
在View中有諸多回撥方法,它們在View的不同生命週期階段呼叫,常用的有以下方法。
我們寫一個簡單的自定義View來觀察View與Activity的生命週期變化。
public class CustomView extends View {
private static final String TAG = "View";
public CustomView(Context context) {
super(context);
Log.d(TAG, "CustomView()");
}
public CustomView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
Log.d(TAG, "CustomView()");
}
public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
Log.d(TAG, "CustomView()");
}
/**
* View在xml檔案里載入完成時呼叫
*/
@Override
protected void onFinishInflate() {
super.onFinishInflate();
Log.d(TAG, "View onFinishInflate()");
}
/**
* 測量View及其子View大小時呼叫
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Log.d(TAG, "View onMeasure()");
}
/**
* 佈局View及其子View大小時呼叫
*/
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
Log.d(TAG, "View onLayout() left = " + left + " top = " + top + " right = " + right + " bottom = " + bottom);
}
/**
* View大小發生改變時呼叫
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
Log.d(TAG, "View onSizeChanged() w = " + w + " h = " + h + " oldw = " + oldw + " oldh = " + oldh);
}
/**
* 繪製View及其子View大小時呼叫
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Log.d(TAG, "View onDraw()");
}
/**
* 物理按鍵事件發生時呼叫
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
Log.d(TAG, "View onKeyDown() event = " + event.getAction());
return super.onKeyDown(keyCode, event);
}
/**
* 物理按鍵事件發生時呼叫
*/
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
Log.d(TAG, "View onKeyUp() event = " + event.getAction());
return super.onKeyUp(keyCode, event);
}
/**
* 觸控事件發生時呼叫
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d(TAG, "View onTouchEvent() event = " + event.getAction());
return super.onTouchEvent(event);
}
/**
* View獲取焦點或者失去焦點時呼叫
*/
@Override
protected void onFocusChanged(boolean gainFocus, int direction, @Nullable Rect previouslyFocusedRect) {
super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
Log.d(TAG, "View onFocusChanged() gainFocus = " + gainFocus);
}
/**
* View所在視窗獲取焦點或者失去焦點時呼叫
*/
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
Log.d(TAG, "View onWindowFocusChanged() hasWindowFocus = " + hasWindowFocus);
}
/**
* View被關聯到視窗時呼叫
*/
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
Log.d(TAG, "View onAttachedToWindow()");
}
/**
* View從視窗分離時呼叫
*/
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
Log.d(TAG, "View onDetachedFromWindow()");
}
/**
* View的可見性發生變化時呼叫
*/
@Override
protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
super.onVisibilityChanged(changedView, visibility);
Log.d(TAG, "View onVisibilityChanged() visibility = " + visibility);
}
/**
* View所在視窗的可見性發生變化時呼叫
*/
@Override
protected void onWindowVisibilityChanged(int visibility) {
super.onWindowVisibilityChanged(visibility);
Log.d(TAG, "View onWindowVisibilityChanged() visibility = " + visibility);
}
}複製程式碼
Activity與View的生命週期變化一目瞭然。
Activity create
Activity pause
Activity resume
Activity destory
我們來總結一下View的宣告週期隨著Activity生命週期變化的情況。
我們瞭解這些生命週期方法有什麼作用呢??
其實這些方法在我們自定義View的時候發揮著很大的作用,我們來舉幾種應用場景。
場景1:在Activity啟動時獲取View的寬高,但是在onCreate、onStart和onResume均無法獲取正確的結果。這是因為在Activity的這些方法裡,Viewed繪製可能還沒有完成,我們可以在View的生命週期方法裡獲取。
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if(hasFocus){
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
}複製程式碼
場景2:在Activity生命週期發生變化時,View也要做響應的處理,典型的有VideoView儲存進度和恢復進度。
@Override
protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
super.onVisibilityChanged(changedView, visibility);
//TODO do something if activity lifecycle changed if necessary
//Activity onResume()
if(visibility == VISIBLE){
}
//Activity onPause()
else {
}
}
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
//TODO do something if activity lifecycle changed if necessary
//Activity onResume()
if (hasWindowFocus) {
}
//Activity onPause()
else {
}
}複製程式碼
場景3:釋放執行緒、資源
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
//TODO release resources, thread, animation
}複製程式碼
二 View的測量流程
View是一個矩形區域,它有自己的位置、大小與邊距。
View位置
View位置:有左上角座標(getLeft(), getTop())決定,該座標是以它的父View的左上角為座標原點,單位是pixels。
View大小
View大小:View的大小有兩對值來表示。getMeasuredWidth()/getMeasuredHeight()這組值表示了該View在它的父View裡期望的大小值,在measure()方法完成後可獲得。
getWidth()/getHeight()這組值表示了該View在螢幕上的實際大小,在draw()方法完成後可獲得。
View內邊距
View內邊距:View的內邊距用padding來表示,它表示View的內容距離View邊緣的距離。通過getPaddingXXX()方法獲取。需要注意的是我們在自定義View的時候需要單獨處理
padding,否則它不會生效,這一塊的內容我們會在View自定義實踐系列的文章中展開。
View外邊距
View內邊距:View的內邊距用margin來表示,它表示View的邊緣離它相鄰的View的距離。
Measure過程決定了View的寬高,該過程完成後,通常都可以通過getMeasuredWith()/getMeasuredHeight()獲得寬高。
理解了上面這些概念,我們接下來來看看詳細的測量流程。
View的測量流程看似複雜,實際遵循著簡單的邏輯。
在做測量的時候,measure()方法被父View呼叫,在measure()中做一些準備和優化工作後,呼叫onMeasure()來進行實際的自我測量。對於onMeasure(),View和ViewGroup有所區別:
- View:View 在 onMeasure() 中會計算出自己的尺寸然後儲存;
- ViewGroup:ViewGroup在onMeasure()中會呼叫所有子View的measure()讓它們進行自我測量,並根據子View計算出的期望尺寸來計算出它們的實際尺寸和位置然後儲存。同時,它也會
根據子View的尺寸和位置來計算出自己的尺寸然後儲存.
在介紹測量流程之前,我們先來介紹下MeasureSpec,它用來把測量要求從父View傳遞給子View。我們知道View的大小最終由子View的LayoutParams與父View的測量要求公共決定,測量要求指的
就是這個MeasureSpec,它是一個32位int值。
- 高2位:SpecMode,測量模式
- 低30位:SpecSize,在特定測量模式下的大小
測量模式有三種:
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
//父View不對子View做任何限制,需要多大給多大,這種情況一般用於系統內部,表示一種測量的狀態
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
//父View已經檢測出View所需要的精確大小,這個時候View的最終大小就是SpecSize所指定的值,它對應LayoutParams中的match_parent和具體數值這兩種模式
public static final int EXACTLY = 1 << MODE_SHIFT;
//父View給子VIew提供一個最大可用的大小,子View去自適應這個大小。
public static final int AT_MOST = 2 << MODE_SHIFT;
}複製程式碼
日常開發中我們接觸最多的不是MeasureSpec而是LayoutParams,在View測量的時候,LayoutParams會和父View的MeasureSpec相結合被換算成View的MeasureSpec,進而決定View的大小。
View的MeasureSpec計算原始碼如下所示:
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
public 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 = 0;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
}複製程式碼
該方法用來獲取子View的MeasureSpec,由引數我們就可以知道子View的MeasureSpec由父容器的spec,父容器中已佔用的的空間大小
padding,以及子View自身大小childDimension共同來決定的。
通過上述方法,我們可以總結出普通View的MeasureSpec的建立規則。
- 當View採用固定寬高的時候,不管父容器的MeasureSpec是什麼,resultSize都是指定的寬高,resultMode都是MeasureSpec.EXACTLY。
- 當View的寬高是match_parent,當父容器是MeasureSpec.EXACTLY,則View也是MeasureSpec.EXACTLY,並且其大小就是父容器的剩餘空間。當父容器是MeasureSpec.AT_MOST
則View也是MeasureSpec.AT_MOST,並且大小不會超過父容器的剩餘空間。 - 當View的寬高是wrap_content時,不管父容器的模式是MeasureSpec.EXACTLY還是MeasureSpec.AT_MOST,View的模式總是MeasureSpec.AT_MOST,並且大小都不會超過芙蓉的剩餘空間。
瞭解了MeasureSpec的概念之後,我就就可以開始分析測量流程了。
- 對於頂級View(DecorView)其MeasureSpec由視窗的尺寸和自身的LayoutParams共同確定的。
- 對於普通View其MeasureSpec由父容器的Measure和自身的LayoutParams共同確定的。
View的繪製會先呼叫View的measure()方法,measure()方法用來測量View的大小,實際的測量工作是由ziView的onMeasure()來完成的。我們來看看
onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法的實現。
關鍵點1:View.onMeasure(int widthMeasureSpec, int heightMeasureSpec)
public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
//設定View寬高的測量值
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;
measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
//measureSpec指的是View測量後的大小
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
//MeasureSpec.UNSPECIFIED一般用來系統的內部測量流程
case MeasureSpec.UNSPECIFIED:
result = size;
break;
//我們主要關注著兩種情況,它們返回的是View測量後的大小
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
//如果View沒有設定背景,那麼返回android:minWidth這個屬性的值,這個值可以為0
//如果View設定了背景,那麼返回android:minWidth和背景最小寬度兩者中的最大值。
protected int getSuggestedMinimumHeight() {
int suggestedMinHeight = mMinHeight;
if (mBGDrawable != null) {
final int bgMinHeight = mBGDrawable.getMinimumHeight();
if (suggestedMinHeight < bgMinHeight) {
suggestedMinHeight = bgMinHeight;
}
}
return suggestedMinHeight;
}
}複製程式碼
View的onMeasure()方法實現比較簡單,它呼叫setMeasuredDimension()方法來設定View的測量大小,測量的大小通過getDefaultSize()方法來獲取。
如果我們直接繼承View來自定義View時,需要重寫onMeasure()方法,並設定wrap_content時的大小。為什麼呢??
通過上面的描述我們知道,當LayoutParams為wrap_content時,SpecMode為AT_MOST,而在
關於getDefaultSize(int size, int measureSpec) 方法需要說明一下,通過上面的描述我們知道etDefaultSize()方法中AT_MOST與EXACTLY模式下,返回的
都是specSize,這個specSize是父View當前可以使用的大小,如果不處理,那wrap_content就相當於match_parent。
如何處理??
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Log.d(TAG, "widthMeasureSpec = " + widthMeasureSpec + " heightMeasureSpec = " + heightMeasureSpec);
//指定一組預設寬高,至於具體的值是多少,這就要看你希望在wrap_cotent模式下
//控制元件的大小應該設定多大了
int mWidth = 200;
int mHeight = 200;
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthSpecMode == MeasureSpec.AT_MOST && heightMeasureSpec == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, mHeight);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, mHeight);
}
}複製程式碼
注:你可以自己嘗試一下自定義一個View,然後不重寫onMeasure()方法,你會發現只有設定match_parent和wrap_content效果是一樣的,事實上TextView、ImageView
等系統元件都在wrap_content上有自己的處理,可以去翻一翻原始碼。
看完了View的measure過程,我們再來看看ViewGroup的measure過程。ViewGroup繼承於View,是一個抽象類,它並沒有重寫onMeasure()方法,因為不同佈局型別的測量
流程各不相同,因此onMeasure()方法由它的子類來實現。
我們來看個FrameLayout的onMeasure()方法的實現。
關鍵點2:FrameLayout.onMeasure(int widthMeasureSpec, int heightMeasureSpec)
View.onMeasure()方法的具體實現一般是由其子類來完成的,對於應用視窗的頂級檢視DecorView來說,它繼承於FrameLayout,我們來看看FrameLayout.onMeasure()
方法的實現。
public class FrameLayout extends ViewGroup {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int count = getChildCount();
int maxHeight = 0;
int maxWidth = 0;
// Find rightmost and bottommost child
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
maxWidth = Math.max(maxWidth, child.getMeasuredWidth());
maxHeight = Math.max(maxHeight, child.getMeasuredHeight());
}
}
// Account for padding too
maxWidth += mPaddingLeft + mPaddingRight + mForegroundPaddingLeft + mForegroundPaddingRight;
maxHeight += mPaddingTop + mPaddingBottom + mForegroundPaddingTop + mForegroundPaddingBottom;
// Check against our minimum height and width
maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
// Check against our foreground's minimum height and width
final Drawable drawable = getForeground();
if (drawable != null) {
maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
}
setMeasuredDimension(resolveSize(maxWidth, widthMeasureSpec),
resolveSize(maxHeight, heightMeasureSpec));
}
public static int resolveSize(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:
result = Math.min(size, specSize);
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
}複製程式碼
可以看到該方法主要做了以下事情:
- 呼叫measureChildWithMargins()去測量每一個子View的大小,找到最大高度和寬度儲存在maxWidth/maxHeigth中。
- 將上一步計算的maxWidth/maxHeigth加上padding值,mPaddingLeft,mPaddingRight,mPaddingTop ,mPaddingBottom表示當前內容區域的左右上下四條邊分別到當前檢視的左右上下四條邊的距離,
mForegroundPaddingLeft ,mForegroundPaddingRight,mForegroundPaddingTop ,mForegroundPaddingBottom表示當前檢視的各個子檢視所圍成的區域的左右上下四條邊到當前檢視前景區域的
左右上下四條邊的距離,經過計算獲得最終寬高。 - 當前檢視是否設定有最小寬度和高度。如果設定有的話,並且它們比前面計算得到的寬度maxWidth和高度maxHeight還要大,那麼就將它們作為當前檢視的寬度和高度值。
- 當前檢視是否設定有前景圖。如果設定有的話,並且它們比前面計算得到的寬度maxWidth和高度maxHeight還要大,那麼就將它們作為當前檢視的寬度和高度值。
- 經過以上的計算,就得到了正確的寬高,先呼叫resolveSize()方法,獲取MeasureSpec,接著呼叫父類的setMeasuredDimension()方法將它們作為當前檢視的大小。
我們再來看看resolveSize(int size, int measureSpec)方法是如果獲取MeasureSpec的?
這個方法的兩個引數:int size:前面計算出的最大寬/高,int measureSpec父檢視指定的MeasureSpec,它們按照:
- MeasureSpec.UNSPECIFIED: 取size
- MeasureSpec.AT_MOST: 取size, specSize的最小值
- MeasureSpec.EXACTLY: 取specSize
來生成最後的大小。
以上便是Measure的整個流程,該流程完成以後,我們可以通過getMeasuredWidth()與getMeasuredHeight()來獲得View的寬高。但是在某些情況下,系統需要經過多次Measure才能確定
最終的寬高,因此在onMeasure()方法中拿到的寬高很可能是不正確的,比較好的做法是在onLayout()方法中獲取View的寬高。
三 View的佈局流程
在進行佈局的時候,layout()方法被父View呼叫,在layout()中它會儲存父View傳進來的自己的位置和尺寸,並且呼叫onLayout()來進行實際的內部佈局。對於onLayout(),View和ViewGroup有所區別:
- View:由於沒有子 View,所以 View 的 onLayout() 什麼也不做。
- ViewGroup:ViewGroup在onLayout()中會呼叫自己的所有子View的layout()方法,把它們的尺寸和位置傳給它們,讓它們完成自我的內部佈局。
layout()方法用來確定View本身的位置,onLayout()方法用來確定子元素的位置。
public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {
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;
//1 呼叫setFrame()設定View四個頂點ed位置
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
//2 呼叫onLayout()確定View子元素的位置
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;
}
}複製程式碼
關鍵點1:View.invalidate()
public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {
public void invalidate() {
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.INVALIDATE);
}
//檢查mPrivateFlags的DRAWN位與HAS_BOUNDS是否被置1,說明上一次請求執行的UI繪製已經完成了,這個時候才能執行新的UI繪製操作
if ((mPrivateFlags & (DRAWN | HAS_BOUNDS)) == (DRAWN | HAS_BOUNDS)) {
//將mPrivateFlags的DRAWN位與HAS_BOUNDS是否被置0
mPrivateFlags &= ~DRAWN & ~DRAWING_CACHE_VALID;
final ViewParent p = mParent;
final AttachInfo ai = mAttachInfo;
if (p != null && ai != null) {
final Rect r = ai.mTmpInvalRect;
r.set(0, 0, mRight - mLeft, mBottom - mTop);
// Don't call invalidate -- we don't want to internally scroll
// our own bounds
p.invalidateChild(this, r);
}
}
}
}複製程式碼
該方法檢查mPrivateFlags的DRAWN位與HAS_BOUNDS是否被置1,說明上一次請求執行的UI繪製已經完成了,這個時候才能執行新的UI繪製操作,在執行新的UI繪製操作之前,還會將
這兩個標誌位置0,然後呼叫ViewParent.invalidateChild()方法來完成繪製操作,這個ViewParent指向的是ViewRoot物件。
關鍵點2:FrameLayout.onLayout(boolean changed, int left, int top, int right, int bottom)
onLayout的實現依賴於具體的佈局,所以View/ViewGroup並沒有實現這個方法,我們來看看FrameLayout的實現。
public class FrameLayout extends ViewGroup {
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
final int count = getChildCount();
final int parentLeft = mPaddingLeft + mForegroundPaddingLeft;
final int parentRight = right - left - mPaddingRight - mForegroundPaddingRight;
final int parentTop = mPaddingTop + mForegroundPaddingTop;
final int parentBottom = bottom - top - mPaddingBottom - mForegroundPaddingBottom;
mForegroundBoundsChanged = true;
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
int childLeft = parentLeft;
int childTop = parentTop;
final int gravity = lp.gravity;
if (gravity != -1) {
final int horizontalGravity = gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
switch (horizontalGravity) {
case Gravity.LEFT:
childLeft = parentLeft + lp.leftMargin;
break;
case Gravity.CENTER_HORIZONTAL:
childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
childLeft = parentRight - width - lp.rightMargin;
break;
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;
}
}
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}
}複製程式碼
我們先來解釋一下這個函式裡的變數的含義。
- int left, int top, int right, int bottom: 描述的是當前檢視的外邊距,即它與父視窗的邊距。
- mPaddingLeft,mPaddingTop,mPaddingRight,mPaddingBottom: 描述的當前檢視的內邊距。
通過這些引數,我們就可以得到當前檢視的子檢視所能佈局在的區域。
接著,該方法就會遍歷它的每一個子View,並獲取它的左上角的座標位置:childLeft,childTop。這兩個位置資訊會根據gravity來進行計算。
最後會呼叫子View的layout()方法迴圈佈局操作,直到所有的佈局都完成為止。
View的繪製流程
Draw過程最終將View繪製在螢幕上。
繪製從ViewRoot.draw()開始,它首先會建立一塊畫布,接著再在畫布上繪製Android上的UI,再把畫布的內容交給SurfaceFlinger服務來渲染。
關鍵點1:ViewRoot.draw(boolean fullRedrawNeeded)
public final class ViewRoot extends Handler implements ViewParent,
View.AttachInfo.Callbacks {
private void draw(boolean fullRedrawNeeded) {
//surface用來操作應用視窗的繪圖表面
Surface surface = mSurface;
if (surface == null || !surface.isValid()) {
return;
}
if (!sFirstDrawComplete) {
synchronized (sFirstDrawHandlers) {
sFirstDrawComplete = true;
for (int i=0; i<sFirstDrawHandlers.size(); i++) {
post(sFirstDrawHandlers.get(i));
}
}
}
scrollToRectOrFocus(null, false);
if (mAttachInfo.mViewScrollChanged) {
mAttachInfo.mViewScrollChanged = false;
mAttachInfo.mTreeObserver.dispatchOnScrollChanged();
}
int yoff;
//計算視窗是否處於滾動狀態
final boolean scrolling = mScroller != null && mScroller.computeScrollOffset();
if (scrolling) {
yoff = mScroller.getCurrY();
} else {
yoff = mScrollY;
}
if (mCurScrollY != yoff) {
mCurScrollY = yoff;
fullRedrawNeeded = true;
}
//描述視窗是否正在請求大小縮放
float appScale = mAttachInfo.mApplicationScale;
boolean scalingRequired = mAttachInfo.mScalingRequired;
//描述視窗的髒區域,即需要重新繪製的區域
Rect dirty = mDirty;
if (mSurfaceHolder != null) {
// The app owns the surface, we won't draw.
dirty.setEmpty();
return;
}
//用來描述是否需要用OpenGL介面來繪製UI,當應用視窗flag等於WindowManager.LayoutParams.MEMORY_TYPE_GPU
//則表示需要用OpenGL介面來繪製UI
if (mUseGL) {
if (!dirty.isEmpty()) {
Canvas canvas = mGlCanvas;
if (mGL != null && canvas != null) {
mGL.glDisable(GL_SCISSOR_TEST);
mGL.glClearColor(0, 0, 0, 0);
mGL.glClear(GL_COLOR_BUFFER_BIT);
mGL.glEnable(GL_SCISSOR_TEST);
mAttachInfo.mDrawingTime = SystemClock.uptimeMillis();
mAttachInfo.mIgnoreDirtyState = true;
mView.mPrivateFlags |= View.DRAWN;
int saveCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
try {
canvas.translate(0, -yoff);
if (mTranslator != null) {
mTranslator.translateCanvas(canvas);
}
canvas.setScreenDensity(scalingRequired
? DisplayMetrics.DENSITY_DEVICE : 0);
mView.draw(canvas);
if (Config.DEBUG && ViewDebug.consistencyCheckEnabled) {
mView.dispatchConsistencyCheck(ViewDebug.CONSISTENCY_DRAWING);
}
} finally {
canvas.restoreToCount(saveCount);
}
mAttachInfo.mIgnoreDirtyState = false;
mEgl.eglSwapBuffers(mEglDisplay, mEglSurface);
checkEglErrors();
if (SHOW_FPS || Config.DEBUG && ViewDebug.showFps) {
int now = (int)SystemClock.elapsedRealtime();
if (sDrawTime != 0) {
nativeShowFPS(canvas, now - sDrawTime);
}
sDrawTime = now;
}
}
}
//如果視窗處於滾動狀態,則應用視窗需要馬上進行下一次全部重繪,呼叫scheduleTraversals()方法
if (scrolling) {
mFullRedrawNeeded = true;
scheduleTraversals();
}
return;
}
//是否需要全部重繪,如果是則將視窗的髒區域設定為整個視窗區域,表示整個視窗曲雲都需要重繪
if (fullRedrawNeeded) {
mAttachInfo.mIgnoreDirtyState = true;
dirty.union(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
}
if (DEBUG_ORIENTATION || DEBUG_DRAW) {
Log.v(TAG, "Draw " + mView + "/"
+ mWindowAttributes.getTitle()
+ ": dirty={" + dirty.left + "," + dirty.top
+ "," + dirty.right + "," + dirty.bottom + "} surface="
+ surface + " surface.isValid()=" + surface.isValid() + ", appScale:" +
appScale + ", width=" + mWidth + ", height=" + mHeight);
}
if (!dirty.isEmpty() || mIsAnimating) {
Canvas canvas;
try {
int left = dirty.left;
int top = dirty.top;
int right = dirty.right;
int bottom = dirty.bottom;
//呼叫Surface.lockCanvas()來建立畫布
canvas = surface.lockCanvas(dirty);
if (left != dirty.left || top != dirty.top || right != dirty.right ||
bottom != dirty.bottom) {
mAttachInfo.mIgnoreDirtyState = true;
}
// TODO: Do this in native
canvas.setDensity(mDensity);
} catch (Surface.OutOfResourcesException e) {
Log.e(TAG, "OutOfResourcesException locking surface", e);
// TODO: we should ask the window manager to do something!
// for now we just do nothing
return;
} catch (IllegalArgumentException e) {
Log.e(TAG, "IllegalArgumentException locking surface", e);
// TODO: we should ask the window manager to do something!
// for now we just do nothing
return;
}
try {
if (!dirty.isEmpty() || mIsAnimating) {
long startTime = 0L;
if (DEBUG_ORIENTATION || DEBUG_DRAW) {
Log.v(TAG, "Surface " + surface + " drawing to bitmap w="
+ canvas.getWidth() + ", h=" + canvas.getHeight());
//canvas.drawARGB(255, 255, 0, 0);
}
if (Config.DEBUG && ViewDebug.profileDrawing) {
startTime = SystemClock.elapsedRealtime();
}
// If this bitmap's format includes an alpha channel, we
// need to clear it before drawing so that the child will
// properly re-composite its drawing on a transparent
// background. This automatically respects the clip/dirty region
// or
// If we are applying an offset, we need to clear the area
// where the offset doesn't appear to avoid having garbage
// left in the blank areas.
if (!canvas.isOpaque() || yoff != 0) {
canvas.drawColor(0, PorterDuff.Mode.CLEAR);
}
dirty.setEmpty();
mIsAnimating = false;
mAttachInfo.mDrawingTime = SystemClock.uptimeMillis();
mView.mPrivateFlags |= View.DRAWN;
if (DEBUG_DRAW) {
Context cxt = mView.getContext();
Log.i(TAG, "Drawing: package:" + cxt.getPackageName() +
", metrics=" + cxt.getResources().getDisplayMetrics() +
", compatibilityInfo=" + cxt.getResources().getCompatibilityInfo());
}
int saveCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
try {
canvas.translate(0, -yoff);
if (mTranslator != null) {
mTranslator.translateCanvas(canvas);
}
canvas.setScreenDensity(scalingRequired
? DisplayMetrics.DENSITY_DEVICE : 0);
mView.draw(canvas);
} finally {
mAttachInfo.mIgnoreDirtyState = false;
canvas.restoreToCount(saveCount);
}
if (Config.DEBUG && ViewDebug.consistencyCheckEnabled) {
mView.dispatchConsistencyCheck(ViewDebug.CONSISTENCY_DRAWING);
}
if (SHOW_FPS || Config.DEBUG && ViewDebug.showFps) {
int now = (int)SystemClock.elapsedRealtime();
if (sDrawTime != 0) {
nativeShowFPS(canvas, now - sDrawTime);
}
sDrawTime = now;
}
if (Config.DEBUG && ViewDebug.profileDrawing) {
EventLog.writeEvent(60000, SystemClock.elapsedRealtime() - startTime);
}
}
} finally {
//UI繪製完成後,呼叫urface.unlockCanvasAndPost(canvas)S來請求SurfaceFlinger進行UI的渲染
surface.unlockCanvasAndPost(canvas);
}
}
if (LOCAL_LOGV) {
Log.v(TAG, "Surface " + surface + " unlockCanvasAndPost");
}
if (scrolling) {
mFullRedrawNeeded = true;
scheduleTraversals();
}
}
}複製程式碼
這個函式主要做了以下事情:
- 呼叫Scroller.computeScrollOffset()方法計算應用是否處於滑動狀態,並獲得應用視窗在Y軸上的即時滑動位置yoff。
- 根據AttachInfo裡描述的資料,判斷視窗是否需要縮放。
- 根據成員變數React mDirty的描述來判斷視窗髒區域的大小,髒區域指的是需要全部重繪的視窗區域。
- 根據成員變數boolean mUserGL判斷是否需要用OpenGL介面來繪製UI,當應用視窗flag等於WindowManager.LayoutParams.MEMORY_TYPE_GPU則表示需要用OpenGL介面來繪製UI.
- 如果不是用OpenGL來繪製,則用Surface來繪製,先呼叫Surface.lockCanvas()來建立畫布,UI繪製完成後,再呼叫urface.unlockCanvasAndPost(canvas)S來請求SurfaceFlinger進行UI的渲染
注:這裡的Surface物件對應了C++層裡的Surface物件,真正的功能在C++層,關於C++層的實現,我們會在後續的文章進一步分析。
關鍵點2:View.draw(Canvas canvas)
public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {
public void draw(Canvas canvas) {
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW);
}
final int privateFlags = mPrivateFlags;
//dirtyOpaque用來描述當前繪製,它有兩種情況:1 檢查DIRTY_OPAQUE為是否為1,如果是則說明當前檢視某個子檢視請求了一個不透明的UI繪製操作,此時當前
//檢視會被子檢視覆蓋 2 如果mAttachInfo.mIgnoreDirtyState = true則表示忽略該標誌位
final boolean dirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
//將DIRTY_MASK與DRAWN置為1,表示開始繪製
mPrivateFlags = (privateFlags & ~DIRTY_MASK) | 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) {
//繪製當前檢視的背景
final Drawable background = mBGDrawable;
if (background != null) {
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if (mBackgroundSizeChanged) {
background.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
mBackgroundSizeChanged = false;
}
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
}
//檢查是否可以跳過第2步和第5步,也就是繪製變數,FADING_EDGE_HORIZONTAL == 1表示處於水平
//滑動狀態,則需要繪製水平邊框漸變效果,FADING_EDGE_VERTICAL == 1表示處於垂直滑動狀態,則
//需要繪製垂直邊框漸變效果。
// 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);
// Step 6, draw decorations (scrollbars)
onDrawScrollBars(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)
*/
//檢查失修需要儲存引數canvas所描述的一塊畫布的堆疊狀態,並且建立額外的圖層來繪製當前檢視
//在滑動時的邊框漸變效果
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;
int paddingTop = mPaddingTop;
final boolean offsetRequired = isPaddingOffsetRequired();
if (offsetRequired) {
paddingLeft += getLeftPaddingOffset();
paddingTop += getTopPaddingOffset();
}
//表示當前檢視可以用來繪製的內容區域,這個區域已經將內建的和擴充套件的內邊距排除之外
int left = mScrollX + paddingLeft;
int right = left + mRight - mLeft - mPaddingRight - paddingLeft;
int top = mScrollY + paddingTop;
int bottom = top + mBottom - mTop - mPaddingBottom - paddingTop;
if (offsetRequired) {
right += getRightPaddingOffset();
bottom += getBottomPaddingOffset();
}
final ScrollabilityCache scrollabilityCache = mScrollCache;
int length = scrollabilityCache.fadingEdgeLength;
// 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 >= 0.0f;
bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength()));
drawBottom = bottomFadeStrength >= 0.0f;
}
if (horizontalEdges) {
leftFadeStrength = Math.max(0.0f, Math.min(1.0f, getLeftFadingEdgeStrength()));
drawLeft = leftFadeStrength >= 0.0f;
rightFadeStrength = Math.max(0.0f, Math.min(1.0f, getRightFadingEdgeStrength()));
drawRight = rightFadeStrength >= 0.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;
final float fadeHeight = scrollabilityCache.fadingEdgeLength;
if (drawTop) {
matrix.setScale(1, fadeHeight * topFadeStrength);
matrix.postTranslate(left, top);
fade.setLocalMatrix(matrix);
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);
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);
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);
canvas.drawRect(right - length, top, right, bottom, p);
}
canvas.restoreToCount(saveCount);
//繪製當前檢視的滾動條
// Step 6, draw decorations (scrollbars)
onDrawScrollBars(canvas);
}
}複製程式碼
該方法主要完成了以下事情:
- 繪製當前檢視的背景
- 儲存當前畫布的狀態,並且在當前畫布建立額外的突出,以便接下來可以繪製檢視在滑動時的邊框漸變效果。
- 繪製當前檢視的內容
- 繪製當前檢視的子檢視
- 繪製當前檢視在滑動時的邊框漸變效果
- 繪製當前檢視的滾動條
關鍵點2:ViewGroup.dispatchDraw(Canvas canvas)
dispatchDraw用來迴圈繪製子View檢視。
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
protected void dispatchDraw(Canvas canvas) {
//當前檢視的子檢視個數
final int count = mChildrenCount;
final View[] children = mChildren;
int flags = mGroupFlags;
if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {
final boolean cache = (mGroupFlags & FLAG_ANIMATION_CACHE) == FLAG_ANIMATION_CACHE;
for (int i = 0; i < count; i++) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
final LayoutParams params = child.getLayoutParams();
attachLayoutAnimationParameters(child, params, i, count);
bindLayoutAnimation(child);
if (cache) {
child.setDrawingCacheEnabled(true);
child.buildDrawingCache(true);
}
}
}
final LayoutAnimationController controller = mLayoutAnimationController;
if (controller.willOverlap()) {
mGroupFlags |= FLAG_OPTIMIZE_INVALIDATE;
}
controller.start();
//檢查是否需要顯示動畫,即FLAG_RUN_ANIMATION == 1
mGroupFlags &= ~FLAG_RUN_ANIMATION;
mGroupFlags &= ~FLAG_ANIMATION_DONE;
if (cache) {
mGroupFlags |= FLAG_CHILDREN_DRAWN_WITH_CACHE;
}
//通知動畫監聽者動畫開始顯示了
if (mAnimationListener != null) {
mAnimationListener.onAnimationStart(controller.getAnimation());
}
}
int saveCount = 0;
//如果CLIP_TO_PADDING_MASK != 1,則說明引數canvas描述的是畫布的剪裁區域,該剪裁區域不包含當前檢視組的內邊距
final boolean clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK;
if (clipToPadding) {
saveCount = canvas.save();
//裁剪畫布
canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop,
mScrollX + mRight - mLeft - mPaddingRight,
mScrollY + mBottom - mTop - mPaddingBottom);
}
// We will draw our child's animation, let's reset the flag
mPrivateFlags &= ~DRAW_ANIMATION;
mGroupFlags &= ~FLAG_INVALIDATE_REQUIRED;
boolean more = false;
final long drawingTime = getDrawingTime();
//如果FLAG_USE_CHILD_DRAWING_ORDER == 0,則說明子檢視按照它們在children陣列裡順序進行繪製
//否則需要呼叫getChildDrawingOrder來判斷繪製順序
if ((flags & FLAG_USE_CHILD_DRAWING_ORDER) == 0) {
for (int i = 0; i < count; i++) {
final View child = children[i];
//如果子檢視可見,則開始繪製子檢視
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
} else {
for (int i = 0; i < count; i++) {
final View child = children[getChildDrawingOrder(count, i)];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
}
//mDisappearingChildren用來儲存哪些正在消失的子檢視,正在消失的子檢視也是需要繪製的
// Draw any disappearing views that have animations
if (mDisappearingChildren != null) {
final ArrayList<View> disappearingChildren = mDisappearingChildren;
final int disappearingCount = disappearingChildren.size() - 1;
// Go backwards -- we may delete as animations finish
for (int i = disappearingCount; i >= 0; i--) {
final View child = disappearingChildren.get(i);
more |= drawChild(canvas, child, drawingTime);
}
}
if (clipToPadding) {
canvas.restoreToCount(saveCount);
}
// mGroupFlags might have been updated by drawChild()
flags = mGroupFlags;
//如果FLAG_INVALIDATE_REQUIRED == 1,則說明需要進行重新繪製
if ((flags & FLAG_INVALIDATE_REQUIRED) == FLAG_INVALIDATE_REQUIRED) {
invalidate();
}
//通知動畫監聽者,動畫已經結束
if ((flags & FLAG_ANIMATION_DONE) == 0 && (flags & FLAG_NOTIFY_ANIMATION_LISTENER) == 0 &&
mLayoutAnimationController.isDone() && !more) {
// We want to erase the drawing cache and notify the listener after the
// next frame is drawn because one extra invalidate() is caused by
// drawChild() after the animation is over
mGroupFlags |= FLAG_NOTIFY_ANIMATION_LISTENER;
final Runnable end = new Runnable() {
public void run() {
notifyAnimationListener();
}
};
post(end);
}
}
}複製程式碼
dispatchDraw用來迴圈繪製子View檢視,它主要做了以下事情:
- 檢查是否需要顯示動畫,即FLAG_RUN_ANIMATION == 1,則開始顯示動畫,並通知動畫監聽者動畫已經開始。
- 如果FLAG_USE_CHILD_DRAWING_ORDER == 0,則說明子檢視按照它們在children陣列裡順序進行繪製否則需要呼叫getChildDrawingOrder來判斷繪製順序,最終呼叫drawChild()來完成
子檢視的繪製。 - 判斷是否需要進行重繪以及通知動畫監聽者動畫已經結束。
關鍵點3:ViewGroup.drawChild(Canvas canvas, View child, long drawingTime)
ViewGroup.drawChild(Canvas canvas, View child, long drawingTime)用來完成子檢視的繪製。
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
//表示子檢視child是否還在顯示動畫
boolean more = false;
//獲取子檢視的繪製區域以及標誌位
final int cl = child.mLeft;
final int ct = child.mTop;
final int cr = child.mRight;
final int cb = child.mBottom;
final int flags = mGroupFlags;
if ((flags & FLAG_CLEAR_TRANSFORMATION) == FLAG_CLEAR_TRANSFORMATION) {
if (mChildTransformation != null) {
mChildTransformation.clear();
}
mGroupFlags &= ~FLAG_CLEAR_TRANSFORMATION;
}
//獲取子檢視的變換矩陣transformToApply
Transformation transformToApply = null;
//獲取子檢視的動畫
final Animation a = child.getAnimation();
boolean concatMatrix = false;
if (a != null) {
if (mInvalidateRegion == null) {
mInvalidateRegion = new RectF();
}
final RectF region = mInvalidateRegion;
final boolean initialized = a.isInitialized();
if (!initialized) {
a.initialize(cr - cl, cb - ct, getWidth(), getHeight());
a.initializeInvalidateRegion(0, 0, cr - cl, cb - ct);
child.onAnimationStart();
}
if (mChildTransformation == null) {
mChildTransformation = new Transformation();
}
//如果子檢視需要播放動畫,則呼叫getTransformation開始執行動畫,如果動畫還需要繼續執行,則more == true,並且返回子檢視的
//變化矩陣mChildTransformation
more = a.getTransformation(drawingTime, mChildTransformation);
transformToApply = mChildTransformation;
concatMatrix = a.willChangeTransformationMatrix();
if (more) {
if (!a.willChangeBounds()) {
if ((flags & (FLAG_OPTIMIZE_INVALIDATE | FLAG_ANIMATION_DONE)) ==
FLAG_OPTIMIZE_INVALIDATE) {
mGroupFlags |= FLAG_INVALIDATE_REQUIRED;
} else if ((flags & FLAG_INVALIDATE_REQUIRED) == 0) {
// The child need to draw an animation, potentially offscreen, so
// make sure we do not cancel invalidate requests
mPrivateFlags |= DRAW_ANIMATION;
invalidate(cl, ct, cr, cb);
}
} else {
a.getInvalidateRegion(0, 0, cr - cl, cb - ct, region, transformToApply);
// The child need to draw an animation, potentially offscreen, so
// make sure we do not cancel invalidate requests
mPrivateFlags |= DRAW_ANIMATION;
final int left = cl + (int) region.left;
final int top = ct + (int) region.top;
invalidate(left, top, left + (int) region.width(), top + (int) region.height());
}
}
}
//如果FLAG_SUPPORT_STATIC_TRANSFORMATIONS == 1,呼叫getChildStaticTransformation()方法檢查子檢視是否被設定一個
//變換矩陣,如果設定了,即hasTransform == true,則mChildTransformation就是子檢視需要的變換矩陣
else if ((flags & FLAG_SUPPORT_STATIC_TRANSFORMATIONS) ==
FLAG_SUPPORT_STATIC_TRANSFORMATIONS) {
if (mChildTransformation == null) {
mChildTransformation = new Transformation();
}
final boolean hasTransform = getChildStaticTransformation(child, mChildTransformation);
if (hasTransform) {
final int transformType = mChildTransformation.getTransformationType();
transformToApply = transformType != Transformation.TYPE_IDENTITY ?
mChildTransformation : null;
concatMatrix = (transformType & Transformation.TYPE_MATRIX) != 0;
}
}
//設定mPrivateFlags的DRAWN標誌位為1,標明它要開始繪製了。
// Sets the flag as early as possible to allow draw() implementations
// to call invalidate() successfully when doing animations
child.mPrivateFlags |= DRAWN;
if (!concatMatrix && canvas.quickReject(cl, ct, cr, cb, Canvas.EdgeType.BW) &&
(child.mPrivateFlags & DRAW_ANIMATION) == 0) {
return more;
}
//呼叫computeScroll()計運算元檢視的滑動位置
child.computeScroll();
final int sx = child.mScrollX;
final int sy = child.mScrollY;
boolean scalingRequired = false;
Bitmap cache = null;
//如果FLAG_CHILDREN_DRAWN_WITH_CACHE或者FLAG_CHILDREN_DRAWN_WITH_CACHE為1,則表示它採用緩衝的方式進行
//繪製,它將自己的UI緩衝在一個Bitmap裡,可以呼叫getDrawingCache()方法來獲得這個Bitmap。
if ((flags & FLAG_CHILDREN_DRAWN_WITH_CACHE) == FLAG_CHILDREN_DRAWN_WITH_CACHE ||
(flags & FLAG_ALWAYS_DRAWN_WITH_CACHE) == FLAG_ALWAYS_DRAWN_WITH_CACHE) {
cache = child.getDrawingCache(true);
if (mAttachInfo != null) scalingRequired = mAttachInfo.mScalingRequired;
}
final boolean hasNoCache = cache == null;
//設定子檢視child的偏移、Alpha通道以及裁剪區域
final int restoreTo = canvas.save();
if (hasNoCache) {
canvas.translate(cl - sx, ct - sy);
} else {
canvas.translate(cl, ct);
if (scalingRequired) {
// mAttachInfo cannot be null, otherwise scalingRequired == false
final float scale = 1.0f / mAttachInfo.mApplicationScale;
canvas.scale(scale, scale);
}
}
float alpha = 1.0f;
if (transformToApply != null) {
if (concatMatrix) {
int transX = 0;
int transY = 0;
if (hasNoCache) {
transX = -sx;
transY = -sy;
}
// Undo the scroll translation, apply the transformation matrix,
// then redo the scroll translate to get the correct result.
canvas.translate(-transX, -transY);
canvas.concat(transformToApply.getMatrix());
canvas.translate(transX, transY);
mGroupFlags |= FLAG_CLEAR_TRANSFORMATION;
}
alpha = transformToApply.getAlpha();
if (alpha < 1.0f) {
mGroupFlags |= FLAG_CLEAR_TRANSFORMATION;
}
if (alpha < 1.0f && hasNoCache) {
final int multipliedAlpha = (int) (255 * alpha);
if (!child.onSetAlpha(multipliedAlpha)) {
canvas.saveLayerAlpha(sx, sy, sx + cr - cl, sy + cb - ct, multipliedAlpha,
Canvas.HAS_ALPHA_LAYER_SAVE_FLAG | Canvas.CLIP_TO_LAYER_SAVE_FLAG);
} else {
child.mPrivateFlags |= ALPHA_SET;
}
}
} else if ((child.mPrivateFlags & ALPHA_SET) == ALPHA_SET) {
child.onSetAlpha(255);
}
//如果FLAG_CLIP_CHILDREN == 1,則需要設定子檢視的裁剪區域
if ((flags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) {
if (hasNoCache) {
canvas.clipRect(sx, sy, sx + (cr - cl), sy + (cb - ct));
} else {
if (!scalingRequired) {
canvas.clipRect(0, 0, cr - cl, cb - ct);
} else {
canvas.clipRect(0, 0, cache.getWidth(), cache.getHeight());
}
}
}
//繪製子檢視的UI
if (hasNoCache) {
// Fast path for layouts with no backgrounds
if ((child.mPrivateFlags & SKIP_DRAW) == SKIP_DRAW) {
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW);
}
child.mPrivateFlags &= ~DIRTY_MASK;
child.dispatchDraw(canvas);
} else {
child.draw(canvas);
}
} else {
final Paint cachePaint = mCachePaint;
if (alpha < 1.0f) {
cachePaint.setAlpha((int) (alpha * 255));
mGroupFlags |= FLAG_ALPHA_LOWER_THAN_ONE;
} else if ((flags & FLAG_ALPHA_LOWER_THAN_ONE) == FLAG_ALPHA_LOWER_THAN_ONE) {
cachePaint.setAlpha(255);
mGroupFlags &= ~FLAG_ALPHA_LOWER_THAN_ONE;
}
if (Config.DEBUG && ViewDebug.profileDrawing) {
EventLog.writeEvent(60003, hashCode());
}
canvas.drawBitmap(cache, 0.0f, 0.0f, cachePaint);
}
//恢復畫布的堆疊狀態,以便在繪製完當前子檢視的UI後,可以繼續繪製其他子檢視的UI
canvas.restoreToCount(restoreTo);
if (a != null && !more) {
child.onSetAlpha(255);
finishAnimatingView(child, a);
}
return more;
}
}複製程式碼
ViewGroup.drawChild(Canvas canvas, View child, long drawingTime)用來完成子檢視的繪製,它主要完成了以下事情:
1 獲取子檢視的繪製區域以及標誌位
2 獲取子檢視的變換矩陣transformToApply,這個分兩種情況:
- 如果子檢視需要播放動畫,則呼叫getTransformation開始執行動畫,如果動畫還需要繼續執行,則more == true,並且返回子檢視的變化矩陣mChildTransformation
- 如果FLAG_SUPPORT_STATIC_TRANSFORMATIONS == 1,呼叫getChildStaticTransformation()方法檢查子檢視是否被設定一個變換矩陣,如果設定了,即hasTransform == true,則mChildTransformation就是子檢視需要的變換矩陣
3 如果FLAG_CHILDREN_DRAWN_WITH_CACHE或者FLAG_CHILDREN_DRAWN_WITH_CACHE為1,則表示它採用緩衝的方式進行繪製,它將自己的UI緩衝在一個Bitmap裡,可以呼叫getDrawingCache()方法來獲得這個Bitmap。
4 設定子檢視child的偏移、Alpha通道以及裁剪區域。
5 繪製子檢視的UI,這分為兩種情況:
- 如果以非緩衝的方式來繪製,如果SKIP_DRAW == 1,則說明需要跳過當前子檢視而去繪製它自己的子檢視,否則先繪製它的檢視,再繪製它的子檢視。繪製自身通過draw()函式來
完成,繪製它的子檢視則通過dispatchDraw()來完成的。 - 如果是以緩衝的方式來繪製,這種情況只需要將上一次的緩衝的Bitmap物件cache繪製到畫布canvas上
6 恢復畫布的堆疊狀態,以便在繪製完當前子檢視的UI後,可以繼續繪製其他子檢視的UI。
總結
至此,Android應用程式視窗的渲染流程就分析完了,我們再來總結一下。
- 渲染Android應用檢視的渲染流程,測量流程用來確定檢視的大小、佈局流程用來確定檢視的位置、繪製流程最終將檢視繪製在應用視窗上。
- Android應用程式視窗UI首先是使用Skia圖形庫API來繪製在一塊畫布上,實際地是繪製在這塊畫布裡面的一個圖形緩衝區中,這個圖形緩衝區最終會被交給SurfaceFlinger服
務,而SurfaceFlinger服務再使用OpenGL圖形庫API來將這個圖形緩衝區渲染到硬體幀緩衝區中。
五 View事件分發機制
在介紹View的事件分發機制之前,我們要先了解兩個概念。
- MotionEvent:Android中用來表示各種事件的物件,例如ACTION_DOWN、ACTION_MOVE等,我們還可以通過它獲取事件發生的座標,getX/getY獲取相對於當前View左上角的座標,getRawX/getRawY獲取相對於螢幕左上角的座標。
- TouchSlop:系統所能識別的最小滑動距離,通過ViewConfiguration.get(context).getScaledTouchSlop()方法獲取。
現在我們再來看看View裡的事件分發機制,概括來說,可以用下面程式碼表示:
public boolean dispatchTouchEvent(MotionEvent event){
boolean consume = false;
//父View決定是否攔截事件
if(onInterceptTouchEvent(event)){
//父View呼叫onTouchEvent(event)消費事件
consume = onTouchEvent(event);
}else{
//呼叫子View的dispatchTouchEvent(event)方法繼續分發事件
consume = child.dispatchTouchEvent(event);
}
return consume;
}複製程式碼
我們再來具體看看各個場景中的事件分發。
5.1 Activity的事件分發
當點選事件發生時,事件最先傳遞給Activity,Activity會首先將事件將誒所屬的Window進行處理,即呼叫superDispatchTouchEvent()方法。
通過觀察superDispatchTouchEvent()方法的呼叫鏈,我們可以發現事件的傳遞順序:
- PhoneWinodw.superDispatchTouchEvent()
- DecorView.dispatchTouchEvent(event)
- ViewGroup.dispatchTouchEvent(event)
事件一層層傳遞到了ViewGroup裡,關於ViewGroup對事件的處理,我們下面會說,如果superDispatchTouchEvent()方法返回false,即沒有
處理該事件,則會繼續呼叫Activity的onTouchEvent(ev)方法來處理該事件。可見Activity的onTouchEvent(ev)在事件處理的優先順序是最低的。
public class Activity extends ContextThemeWrapper
implements LayoutInflater.Factory2,
Window.Callback, KeyEvent.Callback,
OnCreateContextMenuListener, ComponentCallbacks2,
Window.OnWindowDismissedCallback, WindowControllerCallback {
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
}複製程式碼
5.2 ViewGroup的事件分發
ViewGroup作為View容器,它需要考慮自己的子View是否處理了該事件,具體說來:
- 如果ViewGroup攔截了事件,即它的onInterceptTouchEvent()返回true,則該事件由ViewGroup處理,如果ViewGroup呼叫了setOnTouchListener()則該介面的onTouch()方法會被呼叫
否則會呼叫onTouchEvent()方法。 - 如果ViewGroup沒有攔截事件,則該事件會傳遞給它的子View,子View的dispatchTouchEvent()會被呼叫,View.dispatchTouchEvent()的處理流程前面我們已經分析過。
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
public boolean dispatchTouchEvent(MotionEvent ev) {
...
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
//每當有ACTION_DOWN事件進來的時候,都重置成初始狀態
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// Check for interception.
final boolean intercepted;
//MotionEvent.ACTION_DOWN事件總是會被ViewGroup攔截
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//1. 判斷是否允許ViewGroup攔截除了ACTION_DOWN以外的其他事件,通過requestDisallowInterceptTouchEvent()方法設定
//FLAG_DISALLOW_INTERCEPT標誌位來完成的
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//2. 通過onInterceptTouchEvent(ev)方法判斷是否攔截該事件
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
//如果mFirstTouchTarget == null,即沒有接受
intercepted = true;
}
//ViewGroup以連結串列的形式儲存它的子View,mFirstTouchTarget表示連結串列中第一個
//被點選的子View
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}
// Check for cancelation.
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
// Update list of touch targets for pointer down, if needed.
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
...
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
// Clean up earlier touch targets for this pointer id in case they
// have become out of sync.
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
//3. 當ViewGroup不再攔截事件時,事件會向下分發給它的子View進行處理。
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
//4. 判斷子View是否能夠接受點選事件,判斷標準有兩點:① 子View是否可以獲取焦點
//② 點選的座標是否落在了子View的區域內
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
//5. dispatchTransformedTouchEvent()方法會去呼叫子View的dispatchTouchEvent()方法來處理事件
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
...
}
}
、
...
return handled;
}
}複製程式碼
5.3 View的事件分發
View沒有子元素,無法向下傳遞事件,它只能自己處理事件,所以View的事件傳遞比較簡單。
如果外界設定了OnTouchListener且OnTouchListener.onTouch(this, event)返回true,則表示該方法消費了該事件,則onTouchEvent(event)不再被呼叫。
可見OnTouchListener的優先順序高於onTouchEvent(event),這樣是為了便於外界處理事件。
public class View implements Drawable.Callback, KeyEvent.Callback,
AccessibilityEventSource {
public boolean dispatchTouchEvent(MotionEvent event) {
...
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//如果外界設定了OnTouchListener且OnTouchListener.onTouch(this, event)返回true,則
//表示該方法消費了該事件,則onTouchEvent(event)不再被呼叫
ListenerInfo li = mListenerInfo;
//如果外界呼叫了setOnTouchListener()方法且
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
...
return result;
}
}複製程式碼
我們再來看看View裡的onTouchEvent(event)方法的處理。
public class View implements Drawable.Callback, KeyEvent.Callback,
AccessibilityEventSource {
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
//1. View的disable屬性不會影響onTouchEvent()方法的返回值,哪怕View是disable的,只要
//View的clickable或者longClickable為true,onTouchEvent()方法還是會返回true
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
//2. 只要clickable或者longClickable為true,onTouchEvent()方法就會消費這個事件
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
// The button is being released before we actually
// showed it as pressed. Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
setPressed(true, x, y);
}
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
//3. 如果View設定了OnClickListener,則performClick()會呼叫它的onClick方法
if (!post(mPerformClick)) {
performClick();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_DOWN:
...
break;
case MotionEvent.ACTION_CANCEL:
...
break;
case MotionEvent.ACTION_MOVE:
...
break;
}
return true;
}
return false;
}
}複製程式碼
關於onTouchEvent(MotionEvent event),有兩點需要說明一下:
- View的disable屬性不會影響onTouchEvent()方法的返回值,哪怕View是disable的,只要
View的clickable或者longClickable為true,onTouchEvent()方法還是會返回true。 - 只要clickable或者longClickable為true,onTouchEvent()方法就會消費這個事件
- 如果View設定了OnClickListener,則performClick()會呼叫它的onClick方法。
上面我們提到了viewFlags裡的CLICKABLE與LONG_CLICKABLE,也就是xml或者程式碼裡可以設定的clickable與longClickable,View的LONG_CLICKABLE預設為
true,CLICKABLE預設為false,值得一提的是setOnClickListener()方法和setOnLongClickListener()會將這兩個值設定為true。
通過對原始碼的分析,我們已經掌握了各種場景下事件分發的規律,我們再來總結一下View事件分發的相關結論。
- 事件的傳遞是按照Activity -> Window -> View的順序進行的
- 一般情況下,一個事件序列只能由一個View攔截並消耗,一旦一個View攔截了該事件,則該事件序列的後續事件都會交由該View來處理。
- ViewGroup預設不攔截任何事件
- View沒有onInterceptTouchEvent()方法,一但有點選事件傳遞給它,它的ouTouchEvent()方法就會被呼叫。