第4章?View體系與自定義View

4.1 View的事件體系

一、View的基礎知識

1、View的位置參數

1.1、兩種坐標系

Android坐標系:以屏幕左上角點作為坐標系原點。
View坐標系:以View的左上角點作為坐標系原點。

1.2、View的位置屬性

View的位置主要由四個屬性決定:top、left、right、bottom。從Android3.0開始,還增加了x、y、translationX、translationY。這幾個參數都是相對于父容器坐標系而言。

width = right - left
height = bottom - top
x = left + translationX   //left不會變
y = top + translationY   //top不會變

x、y是View的左上角坐標
translationX、translationY是View的左上角相對于父容器的偏移量,默認值是0

2、MotionEvent

典型的事件類型

  • ACTION_DOWN 手指剛接觸屏幕
  • ACTION_MOVE 手指在屏幕上移動
  • ACTION_UP 手指從屏幕上松開

MotionEvent的getX()getY()是相對于發生事件的View本身坐標系而言的,getRawX()getRawY()是相對于Android坐標系而言的。

若在View處按下,View接收到了MotionEvent對象,移到View上方時,getY()返回負數,移到View下方時,getY()將返回的值大于getHeight(),getX()也是類似的。

3、TouchSlop

系統所能識別出的被認為是滑動的最小距離,這是一個常量,與設備有關,可通過以下方法獲得

ViewConfiguration.get(getContext()).getScaledTouchSloup()

當我們處理滑動時,比如滑動距離小于這個值,我們就可以過濾這個事件(系統會默認過濾),從而有更好的用戶體驗。

4、VelocityTracker

速度追蹤,用于追蹤手指在滑動過程中的速度,包括水平放向速度和豎直方向速度。使用方法:

  1. 在View的onTouchEvent方法中追蹤當前事件的速度
VelocityRracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);
  1. 計算速度,獲得水平速度和豎直速度
velocityTracker.computeCurrentVelocity(1000);//計算速度。獲取速度之前,必須調用。
int xVelocity = (int)velocityTracker.getXVelocity();
int yVelocity = (int)velocityTracker.getYVelocity();

這里的速度是指一段時間內手指滑過的像素數,1000指的是1000ms,得到的是1000ms內滑過的像素數。速度可正可負:速度 = ( 終點位置 - 起點位置) / 時間段

  1. 當不需要使用的時候,需要調用clear()方法重置并回收內存
velocityTracker.clear();
velocityTracker.recycle();

5、GestureDetector

手勢檢測,用于輔助檢測用戶的單擊、滑動、長按、雙擊等行為。
使用過程

  1. 創建一個GestureDetector對象并實現OnGestureListener(或OnDoubleTapListener)接口:
GestureDetector mGestureDetector = new GestureDetector(this);
//解決長按屏幕后無法拖動的現象
mGestureDetector.setIsLongpressEnabled(false);

2.接管目標View的onTouchEvent方法

boolean consume = mGestureDetector.onTouchEvent(event);
return consume;

OnGestureListener和OnDoubleTapListener接口中的方法:


其中常用的方法有:onSingleTapUp(單擊)、onFling(快速滑動)、onScroll(拖動)、onLongPress(長按)和onDoubleTap( 雙擊)。建議:如果只是監聽滑動相關的,可以自己在onTouchEvent中實現,如果要監聽雙擊這種行為,那么就使用GestureDetector

2、View的滑動

三種方式實現滑動:①通過View本身提供的scrollTo/scrollBy方法。②通過動畫對View施加平移效果。③通過改變View的LayoutParams使得View重新布局來實現滑動。

2.1、使用scrollTo/scrollBy

View的兩個屬性:mScrollXmScollY
mScrollX = View布局x(左邊緣) - View內容x(內容左邊緣)可能為負數
scrollTo/scrollBy 只能改變View內容的位置而不能改變View在布局中的位置。
View內容:若View是一個ViewGroup,指的就是其子元素。若View如Buttom,那么指的就是text值。

scrollTo(int x, int y)
scrollBy(int x, int y)
getScrollX()
getScrollY()

2.2、使用動畫

使用動畫移動View,主要是操作View的translationX和translationY屬性,既可以采用傳統的View動畫,也可以采用屬性動畫,如果使用屬性動畫,為了能夠兼容3.0以下的版本,需要采用開源動畫庫nineolddandroids。

2.3、改變參數布局

LinearLayout.MarginLayoutParams params //取決于button的父容器是什么布局
    = (LinearLayout.MarginLayoutParams) button.getLayoutParams();
params.width = 100;
params.height = 200;
params.leftMargin = 100;
button.requestLayout();//或者 button.setLayoutParams(params)

ViewParent
View需要與其父ViewGroup進行交互時的API,基本所有的View都實現了這個接口
重要方法:
View的getParent() ViewParent
ViewParent的requestLayout()

requeLayout() : 子View調用requestLayout方法,會標記當前View及父容器,同時逐層向上提交,直到ViewRootImpl處理該事件,ViewRootImpl會調用三大流程,從measure開始,對于每一個含有標記位的view及其子View都會進行測量、布局、繪制。

invalidate() :當子View調用了invalidate方法后,會為該View添加一個標記位,同時不斷向父容器請求刷新,父容器通過計算得出自身需要重繪的區域,直到傳遞到ViewRootImpl中,最終觸發performTraversals方法,進行開始View樹重繪流程(只繪制需要重繪的視圖)。

postInvalidate():這個方法與invalidate方法的作用是一樣的,都是使View樹重繪,但兩者的使用條件不同,postInvalidate是在非UI線程中調用,invalidate則是在UI線程中調用。

layout():對控件進行重新定位執行onLayout()這個方法,比如要做一個可回彈的ScrollView,思路就是隨著手勢的滑動子控件滑動,那么我們可以將ScrollView的子控件調用layout(l,t,r,b)這個方法就行了。
Android View 深度分析requestLayout、invalidate與postInvalidate

2.4、各種滑動方式的對比

  • scrollTo/scrollBy:操作簡單,適合對View內容的滑動;
  • 動畫:操作簡單,主要適用于沒有交互的View和實現復雜的動畫效果;
  • 改變布局參數:操作稍微復雜,適用于有交互的View。

3、彈性滑動

共同思想:將一次大的滑動分成若干次小的滑動,并在一定時間段內完成。

3.1、使用Scroller

使用Scroller實現彈性滑動的典型使用方法如下

Scroller scroller = new Scroller(mContext);
//緩慢移動到指定位置
private void smoothScrollTo(int destX,int dextY){
    int scrollX = getScrollX();
    int deltaX = destX - scrollX;
    //1000ms內滑向destX,效果就是緩慢滑動
    mScroller.startSscroll(scrollX,0,deltaX,0,1000);//僅僅保存了傳遞的參數,并不會滑動
    invalidate();//View會進行重繪
} 
@override
public void computeScroll(){
    if(mScroller.computeScrollOffset()){
    scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
    postInvalidate();
    }
}

invalidate()導致View重繪,在View的draw方法中調用了computeScroll()computeScroll()在View中是一個空的實現,需要我們自己去實現。computeScrollOffset()會根據時間流逝去計算當前的mScrollX和mScrollY,并調用scrollTo方法實現滑動,接著又調用postInvalidate()進行第二次重繪。如此反復,直到繪制結束。

Scroller方法:

  • startScroll(int startX, int startY, int dx, int dy, int duration)
  • boolean computeScrollOffset() //返回true,代表滑動未結束
  • int getCurrX() //當前時刻應該所處的位置

3.2、通過動畫

方法一

ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start()

方法二

//當然,我們也可以利用動畫來模仿Scroller實現View彈性滑動的過程:
final int startX = 0;
final int deltaX = 100;
ValueAnimator animator = ValueAnimator.ofInt(0,1).setDuration(1000);
animator.addUpdateListener(new AnimatorUpdateListener(){
    @override
    public void onAnimationUpdate(ValueAnimator animator){
    float fraction = animator.getAnimatedFraction();
    mButton1.scrollTo(startX + (int) (deltaX * fraction) , 0);
    }
});
animator.start();

3.3、使用延時策略

延時策略的核心思想是通過發送一系列延時信息從而達到一種漸近式的效果,具體可以通過Hander和View的postDelayed方法,也可以使用線程的sleep方法。 下面以Handler為例:

private static final int MESSAGE_SCROLL_TO = 1;
private static final int FRAME_COUNT = 30;
private static final int DELATED_TIME = 33;
private int mCount = 0;
@suppressLint("HandlerLeak")
private Handler handler = new handler(){
    public void handleMessage(Message msg){
    switch(msg.what){
        case MESSAGE_SCROLL_TO:
        mCount ++ ;
        if (mCount <= FRAME_COUNT){
            float fraction = mCount / (float) FRAME_COUNT;
            int scrollX = (int) (fraction * 100);
            mButton1.scrollTo(scrollX,0);
            mHandelr.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO , DELAYED_TIME);
            } 
        break;
        default : break;
        }
    }
}

四、事件的分發機制

1、基礎認知

當用戶觸摸屏幕時將產生MotionEvent對象

典型的事件類型:
MotionEvent.ACTION_DOWN:按下View(所有事件的開始)
MotionEvent.ACTION_MOVE:滑動View
MotionEvent.ACTION_CANCEL:非人為原因結束本次事件
MotionEvent.ACTION_UP:抬起View(與DOWN對應)

事件分發的本質:即當一個點擊事件發生后,系統需要將這個事件傳遞給一個具體的View去處理。這個事件傳遞的過程就是分發過程。由三個重要方法來共同完成。
boolean dispatchTouchEvent(MotionEvent event) 用來進行事件的分發
boolean onInterceptTouchEvent(MotionEvent ev) 用來判斷是否攔截事件
boolean onTouchEvent(MotionEvent event) 用來處理事件

他們之間的關系,可以用如下偽代碼表示:

public boolean dispatchTouchEvent (MotionEvent ev){
boolean consume = false;
if (onInterceptTouchEvnet(ev)){
    consume = onTouchEvent(ev);
} else {
    consume = child.dispatchTouchEnvet(ev);
} 
return consume;
}
事件傳遞

事件分發機制的重要結論:

  1. 同一個事件序列以down事件開始,中間包含數量不定的move事件,最終以up事件結束。
  2. 正常情況下,一個事件序列只能由一個View攔截并消耗。
  3. 某個View攔截了事件后,該事件序列只能由它去處理,并且它的onInterceptTouchEvent不會再被調用。
  4. 某個View一旦開始處理事件,如果它不消耗ACTION_DOWN事件( onTouchEvnet返回false) ,那么同一事件序列中的其他事件都不會交給他處理,并且事件將重新交由他的父元素去處理,即父元素的onTouchEvent被調用。好比一個程序員,如果這件事沒有處理好,短期內上級不會再把事情交給他處理。
  5. 如果View不消耗ACTION_DOWN以外的其他事件,那么這個事件將會消失,此時父元素的onTouchEvent并不會被調用,并且當前View可以持續收到后續的事件,最終消失的點擊事件會傳遞給Activity去處理。
  6. ViewGroup默認不攔截任何事件。
  7. View沒有onInterceptTouchEvent方法。一旦事件傳遞給它,它的onTouchEvent方法會被調用。
  8. View的onTouchEvent默認消耗事件,除非他是不可點擊的( clickable和longClickable同時為false) 。View的longClickable屬性默認false,clickable默認屬性分情況(如TextView為false,button為true)。
  9. View的enable屬性不影響onTouchEvent的默認返回值。
  10. onClick會發生的前提是當前View是可點擊的,并且收到了down和up事件。
  11. 事件傳遞過程總是由外向內的,即事件總是先傳遞給父元素,然后由父元素分發給子View,通過requestDisallowInterceptTouchEvent方法可以在子元素中干預父元素的分發過程,但是ACTION_DOWN事件除外。
  12. onTouch(dispatchTouchEvent中調用)優先于onTouchEvent執行,onClick優先級最低。onTouch能夠得到執行需要兩個前提條件,第一mOnTouchListener的值不能為空,第二當前點擊的控件必須是enable的。因此如果你有一個控件是非enable的,那么給它注冊onTouch事件將永遠得不到執行。對于這一類控件,如果我們想要監聽它的touch事件,就必須通過在該控件中重寫onTouchEvent方法來實現。

參考文獻:
OnFling和onSingleTapUp不執行的問題的一種解決方法

4.2 View的工作原理

一、解析Activity的構成

1、DecorView的創建

當我們調用startActivity方法時,最終調用ActivityThread#handleLaunchActivity,該方法中會首先會調用Activity的onCreate方法。在onCreate方法中,會調用Activity#setContentViewsetContentView內部會調用Activity的成員變量mWindow的(Window是抽象類,其實現類是PhoneWindow,mWindow是PhoneWindow的一個實例)setContentView。其setContentView方法中,首先new一個DecorView對象,然后DecorView對象會根據不同的情況(主題,Window的feature等)加載不同的布局資源。DecorView是Activity中的根View,繼承了FrameLayout。至此DecorView創建完成。

2、添加DecorView到Window

完成DecorView的創建之后,接著調用ActivityThread#handleResumeActivity方法。在handleResumeActivity方法中,首先調用Activity#onResume方法,handleResumeActivity方法接著會得到一個DecorView對象和一個WindowManager對象(接口,實現類是WindowManagerImpl),然后調用WindowManagerImpl#addView方法,DecorView對象作為入參傳入。在WindowManager#addView中,創建了一個ViewRootImpl對象(ViewRoot的實現類),并調用了ViewRootImpl#setView,DecorView對象作為入參。在ViewRootImpl#setView方法內部,會通過跨進程的方式向WMS(WindowManagerService)發起一個調用,從而將DecorView最終添加到Window上,才能真正顯示出來。在這個過程中,ViewRootImpl、DecorView和WMS會彼此關聯,最后通過WMS調用ViewRootImpl#performTraverals方法開始View的測量、布局、繪制流程。

Window是一個抽象類,具體是實現是PhoneWindow,Activity、Dialog等的視圖都需要附加到Window上來呈現。
WindowManager是外界訪問Window的入口,實現類是WindowManagerImpl,Window的具體實現是在WindowManagerService中,WindowManager和WindowManagerService的交互是一個IPC過程。。
DecorView是頂級View,是一個FrameLayout布局,代表了整個應用的界面。內部有titlebar和contentParent兩個子元素,contentParent的id是content,而我們設置的main.xml布局則是contentParent里面的一個子元素。
ViewRoot的實現類是ViewRootImpl,在WindowManager中創建,用于將DecorView添加到Window中。

二、理解MeasureSpec

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;//11000000 0000...000
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;
        public static final int EXACTLY     = 1 << MODE_SHIFT;
        public static final int AT_MOST     = 2 << MODE_SHIFT;
       
        public static int makeMeasureSpec(int size,int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }

        public static int getMode(int measureSpec) {
            return (measureSpec & MODE_MASK);
        }

        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }
}

MeasureSpec通過將SpecSize和SpecMode打包成了一個int值來避免過多對象的內存分配。
SpecMode有三類:

UNSPECIFIED :父容器不對View進行任何限制,要多大給多大,一般用于系統內部。
EXACTLY:父容器檢測到View所需要的精確大小,這時候View的最終大小就是SpecSize所指定的值,對應LayoutParams中的match_parent和具體數值這兩種模式(也不一定,還受父容器影響,詳見下面的表格)。
AT_MOST:父容器指定了一個可用大小即SpecSize,View的大小不能大于這個值,對LayoutParams中的wrap_content。
說明:上面描述的是理論上應該有的邏輯。

對于頂級DecorView,其MeasureSpec是由窗口尺寸和自身的LayoutParams共同確定。對于普通的View,其MeasureSpec由父容器和自身的LayoutParams共同確定。一旦MeasureSpec確定,onMeasure中就可以確定View的測量寬/高。

三、View的工作流程

主要指measure、layout、draw這三大流程。measure確定View的測量寬/高,layout確定View的最終寬/高和四個頂點的位置,而draw則將View繪制到屏幕上。

ViewRootImpl#performTraversals會依次調用performMeasureperformLayoutperformDraw三個方法,這三個方法分別開啟頂級View的measure、layout和draw這三大流程。

其中performMeasure中會調用頂級View#measure 方法,measure調用onMeasure,在onMeasure 方法中則會測量自身并調用所有子元素measure方法,這樣就完成了一次measure過程;子元素會重復父容器的measure過程,如此反復完成了整個View樹的遍歷。另外兩個過程同理。

1、ViewGroup的Measure流程

對于ViewGroup既要測量自身,也要遍歷子元素的measure方法(通過實現onMeasure方法)
在performMeasure方法中,調用了DecorView#measure(繼承自View,其實調用的是View#measure),measure會調用onMeasure方法。ViewGroup并沒有定義onMeasure,這個方法需要子類去實現,主要需要實現兩個功能:①測量自身②測量子View。

ViewGroup提供了measureChildWithMarginsmeasureChildren方法。

1.1、measureChildWithMargins方法
protectedvoidmeasureChildWithMargins(Viewchild,
intparentWidthMeasureSpec,intwidthUsed,
intparentHeightMeasureSpec,intheightUsed){
finalMarginLayoutParamslp=(MarginLayoutParams)child.getLayoutParams();
    //入參:父容器的MeasureSpec;父的padding和自身的margin(剩下為子元素可用空間);自身的寬度。
finalintchildWidthMeasureSpec=getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft+mPaddingRight+lp.leftMargin+lp.rightMargin
+widthUsed,lp.width);
finalintchildHeightMeasureSpec=getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop+mPaddingBottom+lp.topMargin+lp.bottomMargin
+heightUsed,lp.height);
//注意:此時的入參是自身的MeasureSpec。measure又會調用child#onMeasure方法
child.measure(childWidthMeasureSpec,childHeightMeasureSpec);
}

從上面的方法可以看出,View的MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同決定,MeasureSpec一旦確定,onMeasure中就可以確定View的測量寬/高。getChildMeasureSpec(int spec, int padding, int childDimension)方法的邏輯整理出如下表格:


表中的parentSize是指父容器目前可以使用的大小,即父容器的specSize減去入參padding

ViewGroup并沒有定義onMeasure,需要其子類去實現,為什么ViewGroup不像View一樣對其onMeasure做統一呢?因為不同的ViewGroup子類有不同的布局特征,導致測量細節各不相同,無法統一。

根據上面的表格,我們發現父容器的MeasureSpec屬性為AT_MOST,子元素的LayoutParams為WRAP_CONTENT的時候,子元素的測量模式為AT_MOST,它的SpecSize為父容器的SpecSize減去padding(入參),也就是說子元素WRAP_CONTENT和MATCH_PARENT一樣的。為了解決這個問題,需要在WRAP_CONTENT時指定一下默認的寬高。

1.2、measureChildren方法

measureChildren中會循環調用measureChild方法,在measureChild中,首先會調用getChildMeasureSpec方法,入參和上面類似,區別在于padding入參僅僅為自身的padding,然后會調用子元素的measure方法(和measureChildWithMargins非常類似)。

2、View的Measure過程

View的measure方法是一個final方法,會調用onMeasure方法,因此只需要關注onMeasure方法,入參為自己的measureSpec

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

setMeasuredDimension用于設置測量的寬高,測量好之后,必須調用

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

簡單理解,getDefaultSize返回的就是measureSpec中的specSize,這就是View測量后的大小。在AT_MOST和EXACTLY模式下,都返回了specSize。也就是說對于一個直接繼承View的自定義View,它的wrap_content和match_parent效果一樣,因此如果要實現自定義View的wrap_content,則要重寫onMeasure方法。解決問題:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
      // 在 MeasureSpec.AT_MOST 模式下,給定一個默認值mWidth,mHeight。默認寬高靈活指定
      //參考TextView、ImageView的處理方式
      //其他情況下沿用系統測量規則即可
    if (widthSpecMode == MeasureSpec.AT_MOST
            && heightSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(mWith, mHeight);
    } else if (widthSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(mWith, heightSpecSize);
    } else if (heightSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(widthSpecSize, mHeight);
    }
}

getSuggestedMinimumWidth()方法就是:如果View沒有設置背景,就返回minWidth屬性值(可以為0);如果設置了背景,就返回minWidth和背景的最小寬度之間的最大值。

View的measure過程是三大流程中最復雜的一個,measure完成以后,通過 getMeasuredWidth/Height 方法就可以正確獲取到View的測量后寬/高。在某些情況下,系統可能需要多次measure才能確定最終的測量寬/高,所以在onMeasure中拿到的寬/高很可能不是準確的。一個較好的習慣是在onLayout方法中,去獲取View測量寬高或最終寬高。

3、如何正確獲得寬高

如果我們想要在Activity啟動的時候就獲取一個View的寬高,怎么操作呢?因為View的measure過程和Activity的生命周期并不是同步執行,無法保證在Activity的 onCreate、onStart、onResume 時某個View就已經測量完畢。所以有以下四種方式來獲取View的寬高:

3.1、Activity/View#onWindowFocusChanged

onWindowFocusChanged這個方法的含義是:VieW已經初始化完畢了,寬高已經準備好了,需要注意:它會被調用多次,當Activity的窗口得到焦點和失去焦點均會被調用。

3.2、view.post(runnable)

通過post將一個runnable投遞到消息隊列的尾部,當Looper調用此runnable的時候,View也初始化好了。

3.3、ViewTreeObserver

使用 ViewTreeObserver 的眾多回調可以完成這個功能,比如OnGlobalLayoutListener 這個接口,當View樹的狀態發送改變或View樹內部的View的可見性發生改變時,onGlobalLayout 方法會被回調,這是獲取View寬高的好時機。需要注意的是,伴隨著View樹狀態的改變, onGlobalLayout 會被回調多次。

3.4、view.measure(int widthMeasureSpec,intheightMeasureSpec)

手動對view進行measure。需要根據View的layoutParams分情況處理:

  • match_parent:直接放棄。根據上表所示,需要知道parentSize,即父容器剩余空間,而此時無法知道這個值。
  • 具體的數值( dp/px):
  int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
  int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
  view.measure(widthMeasureSpec,heightMeasureSpec);
  • wrap_content:
  int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST);
  // View的尺寸使用30位二進制表示,最大值30個1,在AT_MOST模式下,我們用View理論上能支持的最大值去構造MeasureSpec是合理的
  int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST);
  view.measure(widthMeasureSpec,heightMeasureSpec);

四、layout過程

layout方法確定View本身的位置,會調用onLayout方法。onLayout確定所有子元素的位置,通過遍歷所有的子View并調用其layout方法。

View#layout中,setFrame確定View的四個頂點位置,即初始化mLeft,mRight,mTop,mBottom這四個值(確定了最終的寬高),也就確定了View在父容器中的位置。接著調用onLayout方法,確定所有子View的位置,和onMeasure一樣,onLayout的具體實現和布局有關,因此View和ViewGroup均沒有真正實現onLayout方法。

View的測量寬高和最終寬高的區別:
在View的默認實現中,View的測量寬高和最終寬高相等,只不過測量寬高形成于measure過程,最終寬高形成于layout過程。即便View需要多次測量才能確定自己的測量寬高,但最終來說,測量寬高和最終寬高還是一致。

五、draw過程

View的繪制過程遵循如下幾步:

  • 繪制背景 drawBackground(canvas)
  • 繪制自己 onDraw
  • 繪制children dispatchDraw 遍歷所有子View的 draw 方法
  • 繪制裝飾 onDrawScrollBars

View#setWillNotDraw,如果一個View不需要繪制任何內容,那么置為ture,系統會進行相應的優化。默認情況下,View為false,ViewGroup為true。所以自定義ViewGroup需要通過onDraw來繪制內容時,必須顯式的關閉 WILL_NOT_DRAW 這個優化標記位,即調用 setWillNotDraw(false)。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容