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

4.1 View的事件體系

一、View的基礎(chǔ)知識(shí)

1、View的位置參數(shù)

1.1、兩種坐標(biāo)系

Android坐標(biāo)系:以屏幕左上角點(diǎn)作為坐標(biāo)系原點(diǎn)。
View坐標(biāo)系:以View的左上角點(diǎn)作為坐標(biāo)系原點(diǎn)。

1.2、View的位置屬性

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

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

x、y是View的左上角坐標(biāo)
translationX、translationY是View的左上角相對(duì)于父容器的偏移量,默認(rèn)值是0

2、MotionEvent

典型的事件類型

  • ACTION_DOWN 手指剛接觸屏幕
  • ACTION_MOVE 手指在屏幕上移動(dòng)
  • ACTION_UP 手指從屏幕上松開

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

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

3、TouchSlop

系統(tǒng)所能識(shí)別出的被認(rèn)為是滑動(dòng)的最小距離,這是一個(gè)常量,與設(shè)備有關(guān),可通過以下方法獲得

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

當(dāng)我們處理滑動(dòng)時(shí),比如滑動(dòng)距離小于這個(gè)值,我們就可以過濾這個(gè)事件(系統(tǒng)會(huì)默認(rèn)過濾),從而有更好的用戶體驗(yàn)。

4、VelocityTracker

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

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

這里的速度是指一段時(shí)間內(nèi)手指滑過的像素?cái)?shù),1000指的是1000ms,得到的是1000ms內(nèi)滑過的像素?cái)?shù)。速度可正可負(fù):速度 = ( 終點(diǎn)位置 - 起點(diǎn)位置) / 時(shí)間段

  1. 當(dāng)不需要使用的時(shí)候,需要調(diào)用clear()方法重置并回收內(nèi)存
velocityTracker.clear();
velocityTracker.recycle();

5、GestureDetector

手勢(shì)檢測(cè),用于輔助檢測(cè)用戶的單擊、滑動(dòng)、長(zhǎng)按、雙擊等行為。
使用過程

  1. 創(chuàng)建一個(gè)GestureDetector對(duì)象并實(shí)現(xiàn)OnGestureListener(或OnDoubleTapListener)接口:
GestureDetector mGestureDetector = new GestureDetector(this);
//解決長(zhǎng)按屏幕后無法拖動(dòng)的現(xiàn)象
mGestureDetector.setIsLongpressEnabled(false);

2.接管目標(biāo)View的onTouchEvent方法

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

OnGestureListener和OnDoubleTapListener接口中的方法:


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

2、View的滑動(dòng)

三種方式實(shí)現(xiàn)滑動(dòng):①通過View本身提供的scrollTo/scrollBy方法。②通過動(dòng)畫對(duì)View施加平移效果。③通過改變View的LayoutParams使得View重新布局來實(shí)現(xiàn)滑動(dòng)。

2.1、使用scrollTo/scrollBy

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

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

2.2、使用動(dòng)畫

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

2.3、改變參數(shù)布局

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進(jìn)行交互時(shí)的API,基本所有的View都實(shí)現(xiàn)了這個(gè)接口
重要方法:
View的getParent() ViewParent
ViewParent的requestLayout()

requeLayout() : 子View調(diào)用requestLayout方法,會(huì)標(biāo)記當(dāng)前View及父容器,同時(shí)逐層向上提交,直到ViewRootImpl處理該事件,ViewRootImpl會(huì)調(diào)用三大流程,從measure開始,對(duì)于每一個(gè)含有標(biāo)記位的view及其子View都會(huì)進(jìn)行測(cè)量、布局、繪制。

invalidate() :當(dāng)子View調(diào)用了invalidate方法后,會(huì)為該View添加一個(gè)標(biāo)記位,同時(shí)不斷向父容器請(qǐng)求刷新,父容器通過計(jì)算得出自身需要重繪的區(qū)域,直到傳遞到ViewRootImpl中,最終觸發(fā)performTraversals方法,進(jìn)行開始View樹重繪流程(只繪制需要重繪的視圖)。

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

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

2.4、各種滑動(dòng)方式的對(duì)比

  • scrollTo/scrollBy:操作簡(jiǎn)單,適合對(duì)View內(nèi)容的滑動(dòng);
  • 動(dòng)畫:操作簡(jiǎn)單,主要適用于沒有交互的View和實(shí)現(xiàn)復(fù)雜的動(dòng)畫效果;
  • 改變布局參數(shù):操作稍微復(fù)雜,適用于有交互的View。

3、彈性滑動(dòng)

共同思想:將一次大的滑動(dòng)分成若干次小的滑動(dòng),并在一定時(shí)間段內(nèi)完成。

3.1、使用Scroller

使用Scroller實(shí)現(xiàn)彈性滑動(dòng)的典型使用方法如下

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

invalidate()導(dǎo)致View重繪,在View的draw方法中調(diào)用了computeScroll(),computeScroll()在View中是一個(gè)空的實(shí)現(xiàn),需要我們自己去實(shí)現(xiàn)。computeScrollOffset()會(huì)根據(jù)時(shí)間流逝去計(jì)算當(dāng)前的mScrollX和mScrollY,并調(diào)用scrollTo方法實(shí)現(xiàn)滑動(dòng),接著又調(diào)用postInvalidate()進(jìn)行第二次重繪。如此反復(fù),直到繪制結(jié)束。

Scroller方法:

  • startScroll(int startX, int startY, int dx, int dy, int duration)
  • boolean computeScrollOffset() //返回true,代表滑動(dòng)未結(jié)束
  • int getCurrX() //當(dāng)前時(shí)刻應(yīng)該所處的位置

3.2、通過動(dòng)畫

方法一

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

方法二

//當(dāng)然,我們也可以利用動(dòng)畫來模仿Scroller實(shí)現(xiàn)View彈性滑動(dòng)的過程:
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、使用延時(shí)策略

延時(shí)策略的核心思想是通過發(fā)送一系列延時(shí)信息從而達(dá)到一種漸近式的效果,具體可以通過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;
        }
    }
}

四、事件的分發(fā)機(jī)制

1、基礎(chǔ)認(rèn)知

當(dāng)用戶觸摸屏幕時(shí)將產(chǎn)生MotionEvent對(duì)象

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

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

他們之間的關(guān)系,可以用如下偽代碼表示:

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

事件分發(fā)機(jī)制的重要結(jié)論:

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

參考文獻(xiàn):
OnFling和onSingleTapUp不執(zhí)行的問題的一種解決方法

4.2 View的工作原理

一、解析Activity的構(gòu)成

1、DecorView的創(chuàng)建

當(dāng)我們調(diào)用startActivity方法時(shí),最終調(diào)用ActivityThread#handleLaunchActivity,該方法中會(huì)首先會(huì)調(diào)用Activity的onCreate方法。在onCreate方法中,會(huì)調(diào)用Activity#setContentView,setContentView內(nèi)部會(huì)調(diào)用Activity的成員變量mWindow的(Window是抽象類,其實(shí)現(xiàn)類是PhoneWindow,mWindow是PhoneWindow的一個(gè)實(shí)例)setContentView。其setContentView方法中,首先new一個(gè)DecorView對(duì)象,然后DecorView對(duì)象會(huì)根據(jù)不同的情況(主題,Window的feature等)加載不同的布局資源。DecorView是Activity中的根View,繼承了FrameLayout。至此DecorView創(chuàng)建完成。

2、添加DecorView到Window

完成DecorView的創(chuàng)建之后,接著調(diào)用ActivityThread#handleResumeActivity方法。在handleResumeActivity方法中,首先調(diào)用Activity#onResume方法,handleResumeActivity方法接著會(huì)得到一個(gè)DecorView對(duì)象和一個(gè)WindowManager對(duì)象(接口,實(shí)現(xiàn)類是WindowManagerImpl),然后調(diào)用WindowManagerImpl#addView方法,DecorView對(duì)象作為入?yún)魅?。在WindowManager#addView中,創(chuàng)建了一個(gè)ViewRootImpl對(duì)象(ViewRoot的實(shí)現(xiàn)類),并調(diào)用了ViewRootImpl#setView,DecorView對(duì)象作為入?yún)?。在ViewRootImpl#setView方法內(nèi)部,會(huì)通過跨進(jìn)程的方式向WMS(WindowManagerService)發(fā)起一個(gè)調(diào)用,從而將DecorView最終添加到Window上,才能真正顯示出來。在這個(gè)過程中,ViewRootImpl、DecorView和WMS會(huì)彼此關(guān)聯(lián),最后通過WMS調(diào)用ViewRootImpl#performTraverals方法開始View的測(cè)量、布局、繪制流程。

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

二、理解MeasureSpec

MeasureSpec代表一個(gè)32位int值,高2位代表SpecMode(測(cè)量模式),低30位代表SpecSize(某種測(cè)量模式下的規(guī)格大小)。

//主要理解 & ~ | 位運(yùn)算的作用,體會(huì)這樣設(shè)計(jì)的妙處
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打包成了一個(gè)int值來避免過多對(duì)象的內(nèi)存分配。
SpecMode有三類:

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

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

三、View的工作流程

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

ViewRootImpl#performTraversals會(huì)依次調(diào)用performMeasureperformLayoutperformDraw三個(gè)方法,這三個(gè)方法分別開啟頂級(jí)View的measure、layout和draw這三大流程。

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

1、ViewGroup的Measure流程

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

ViewGroup提供了measureChildWithMarginsmeasureChildren方法。

1.1、measureChildWithMargins方法
protectedvoidmeasureChildWithMargins(Viewchild,
intparentWidthMeasureSpec,intwidthUsed,
intparentHeightMeasureSpec,intheightUsed){
finalMarginLayoutParamslp=(MarginLayoutParams)child.getLayoutParams();
    //入?yún)ⅲ焊溉萜鞯腗easureSpec;父的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);
//注意:此時(shí)的入?yún)⑹亲陨淼腗easureSpec。measure又會(huì)調(diào)用child#onMeasure方法
child.measure(childWidthMeasureSpec,childHeightMeasureSpec);
}

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


表中的parentSize是指父容器目前可以使用的大小,即父容器的specSize減去入?yún)adding。

ViewGroup并沒有定義onMeasure,需要其子類去實(shí)現(xiàn),為什么ViewGroup不像View一樣對(duì)其onMeasure做統(tǒng)一呢?因?yàn)椴煌腣iewGroup子類有不同的布局特征,導(dǎo)致測(cè)量細(xì)節(jié)各不相同,無法統(tǒng)一。

根據(jù)上面的表格,我們發(fā)現(xiàn)父容器的MeasureSpec屬性為AT_MOST,子元素的LayoutParams為WRAP_CONTENT的時(shí)候,子元素的測(cè)量模式為AT_MOST,它的SpecSize為父容器的SpecSize減去padding(入?yún)ⅲ簿褪钦f子元素WRAP_CONTENT和MATCH_PARENT一樣的。為了解決這個(gè)問題,需要在WRAP_CONTENT時(shí)指定一下默認(rèn)的寬高。

1.2、measureChildren方法

measureChildren中會(huì)循環(huán)調(diào)用measureChild方法,在measureChild中,首先會(huì)調(diào)用getChildMeasureSpec方法,入?yún)⒑蜕厦骖愃?,區(qū)別在于padding入?yún)H僅為自身的padding,然后會(huì)調(diào)用子元素的measure方法(和measureChildWithMargins非常類似)。

2、View的Measure過程

View的measure方法是一個(gè)final方法,會(huì)調(diào)用onMeasure方法,因此只需要關(guān)注onMeasure方法,入?yún)樽约旱膍easureSpec

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

setMeasuredDimension用于設(shè)置測(cè)量的寬高,測(cè)量好之后,必須調(diào)用。

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

簡(jiǎn)單理解,getDefaultSize返回的就是measureSpec中的specSize,這就是View測(cè)量后的大小。在AT_MOST和EXACTLY模式下,都返回了specSize。也就是說對(duì)于一個(gè)直接繼承View的自定義View,它的wrap_content和match_parent效果一樣,因此如果要實(shí)現(xiàn)自定義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 模式下,給定一個(gè)默認(rèn)值mWidth,mHeight。默認(rèn)寬高靈活指定
      //參考TextView、ImageView的處理方式
      //其他情況下沿用系統(tǒng)測(cè)量規(guī)則即可
    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沒有設(shè)置背景,就返回minWidth屬性值(可以為0);如果設(shè)置了背景,就返回minWidth和背景的最小寬度之間的最大值。

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

3、如何正確獲得寬高

如果我們想要在Activity啟動(dòng)的時(shí)候就獲取一個(gè)View的寬高,怎么操作呢?因?yàn)閂iew的measure過程和Activity的生命周期并不是同步執(zhí)行,無法保證在Activity的 onCreate、onStart、onResume 時(shí)某個(gè)View就已經(jīng)測(cè)量完畢。所以有以下四種方式來獲取View的寬高:

3.1、Activity/View#onWindowFocusChanged

onWindowFocusChanged這個(gè)方法的含義是:VieW已經(jīng)初始化完畢了,寬高已經(jīng)準(zhǔn)備好了,需要注意:它會(huì)被調(diào)用多次,當(dāng)Activity的窗口得到焦點(diǎn)和失去焦點(diǎn)均會(huì)被調(diào)用。

3.2、view.post(runnable)

通過post將一個(gè)runnable投遞到消息隊(duì)列的尾部,當(dāng)Looper調(diào)用此runnable的時(shí)候,View也初始化好了。

3.3、ViewTreeObserver

使用 ViewTreeObserver 的眾多回調(diào)可以完成這個(gè)功能,比如OnGlobalLayoutListener 這個(gè)接口,當(dāng)View樹的狀態(tài)發(fā)送改變或View樹內(nèi)部的View的可見性發(fā)生改變時(shí),onGlobalLayout 方法會(huì)被回調(diào),這是獲取View寬高的好時(shí)機(jī)。需要注意的是,伴隨著View樹狀態(tài)的改變, onGlobalLayout 會(huì)被回調(diào)多次。

3.4、view.measure(int widthMeasureSpec,intheightMeasureSpec)

手動(dòng)對(duì)view進(jìn)行measure。需要根據(jù)View的layoutParams分情況處理:

  • match_parent:直接放棄。根據(jù)上表所示,需要知道parentSize,即父容器剩余空間,而此時(shí)無法知道這個(gè)值。
  • 具體的數(shù)值( 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位二進(jìn)制表示,最大值30個(gè)1,在AT_MOST模式下,我們用View理論上能支持的最大值去構(gòu)造MeasureSpec是合理的
  int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST);
  view.measure(widthMeasureSpec,heightMeasureSpec);

四、layout過程

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

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

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

五、draw過程

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

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

View#setWillNotDraw,如果一個(gè)View不需要繪制任何內(nèi)容,那么置為ture,系統(tǒng)會(huì)進(jìn)行相應(yīng)的優(yōu)化。默認(rèn)情況下,View為false,ViewGroup為true。所以自定義ViewGroup需要通過onDraw來繪制內(nèi)容時(shí),必須顯式的關(guān)閉 WILL_NOT_DRAW 這個(gè)優(yōu)化標(biāo)記位,即調(diào)用 setWillNotDraw(false)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容