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

4.1 View的事件體系

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

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

1.1买鸽、兩種坐標系

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

1.2眼五、View的位置屬性

View的位置主要由四個屬性決定:top妆艘、left、right弹砚、bottom双仍。從Android3.0開始,還增加了x桌吃、y朱沃、translationX、translationY茅诱。這幾個參數(shù)都是相對于父容器坐標系而言逗物。

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()是相對于發(fā)生事件的View本身坐標系而言的摆寄,getRawX()getRawY()是相對于Android坐標系而言的失暴。

若在View處按下,View接收到了MotionEvent對象微饥,移到View上方時逗扒,getY()返回負數(shù),移到View下方時欠橘,getY()將返回的值大于getHeight()矩肩,getX()也是類似的。

3肃续、TouchSlop

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

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

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

4、VelocityTracker

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

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

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

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

5、GestureDetector

手勢檢測新锈,用于輔助檢測用戶的單擊甲脏、滑動、長按妹笆、雙擊等行為块请。
使用過程

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

2.接管目標View的onTouchEvent方法

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

OnGestureListener和OnDoubleTapListener接口中的方法:


其中常用的方法有:onSingleTapUp(單擊)、onFling(快速滑動)拳缠、onScroll(拖動)墩新、onLongPress(長按)和onDoubleTap( 雙擊)。建議:如果只是監(jiān)聽滑動相關(guān)的窟坐,可以自己在onTouchEvent中實現(xiàn)海渊,如果要監(jiān)聽雙擊這種行為,那么就使用GestureDetector哲鸳。

2臣疑、View的滑動

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

2.1、使用scrollTo/scrollBy

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

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

2.2驮樊、使用動畫

使用動畫移動View,主要是操作View的translationX和translationY屬性片酝,既可以采用傳統(tǒng)的View動畫囚衔,也可以采用屬性動畫,如果使用屬性動畫雕沿,為了能夠兼容3.0以下的版本练湿,需要采用開源動畫庫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進行交互時的API肥哎,基本所有的View都實現(xiàn)了這個接口
重要方法:
View的getParent() ViewParent
ViewParent的requestLayout()

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

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

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

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

2.4贩毕、各種滑動方式的對比

  • scrollTo/scrollBy:操作簡單悯许,適合對View內(nèi)容的滑動;
  • 動畫:操作簡單辉阶,主要適用于沒有交互的View和實現(xiàn)復雜的動畫效果先壕;
  • 改變布局參數(shù):操作稍微復雜,適用于有交互的View谆甜。

3垃僚、彈性滑動

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

3.1冈在、使用Scroller

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

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

invalidate()導致View重繪包券,在View的draw方法中調(diào)用了computeScroll()computeScroll()在View中是一個空的實現(xiàn)炫贤,需要我們自己去實現(xiàn)溅固。computeScrollOffset()會根據(jù)時間流逝去計算當前的mScrollX和mScrollY,并調(diào)用scrollTo方法實現(xiàn)滑動兰珍,接著又調(diào)用postInvalidate()進行第二次重繪侍郭。如此反復,直到繪制結(jié)束掠河。

Scroller方法:

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

3.2、通過動畫

方法一

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

方法二

//當然唠摹,我們也可以利用動畫來模仿Scroller實現(xiàn)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爆捞、使用延時策略

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

1藕赞、基礎(chǔ)認知

當用戶觸摸屏幕時將產(chǎn)生MotionEvent對象

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

事件分發(fā)的本質(zhì):即當一個點擊事件發(fā)生后成肘,系統(tǒng)需要將這個事件傳遞給一個具體的View去處理。這個事件傳遞的過程就是分發(fā)過程斧蜕。由三個重要方法來共同完成双霍。
boolean dispatchTouchEvent(MotionEvent event) 用來進行事件的分發(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ā)機制的重要結(jié)論:

  1. 同一個事件序列以down事件開始批销,中間包含數(shù)量不定的move事件洒闸,最終以up事件結(jié)束。
  2. 正常情況下风钻,一個事件序列只能由一個View攔截并消耗顷蟀。
  3. 某個View攔截了事件后,該事件序列只能由它去處理骡技,并且它的onInterceptTouchEvent不會再被調(diào)用鸣个。
  4. 某個View一旦開始處理事件,如果它不消耗ACTION_DOWN事件( onTouchEvnet返回false) 布朦,那么同一事件序列中的其他事件都不會交給他處理囤萤,并且事件將重新交由他的父元素去處理,即父元素的onTouchEvent被調(diào)用是趴。好比一個程序員涛舍,如果這件事沒有處理好,短期內(nèi)上級不會再把事情交給他處理唆途。
  5. 如果View不消耗ACTION_DOWN以外的其他事件富雅,那么這個事件將會消失掸驱,此時父元素的onTouchEvent并不會被調(diào)用,并且當前View可以持續(xù)收到后續(xù)的事件没佑,最終消失的點擊事件會傳遞給Activity去處理毕贼。
  6. ViewGroup默認不攔截任何事件。
  7. View沒有onInterceptTouchEvent方法蛤奢。一旦事件傳遞給它鬼癣,它的onTouchEvent方法會被調(diào)用。
  8. View的onTouchEvent默認消耗事件啤贩,除非他是不可點擊的( clickable和longClickable同時為false) 待秃。View的longClickable屬性默認false,clickable默認屬性分情況(如TextView為false痹屹,button為true)章郁。
  9. View的enable屬性不影響onTouchEvent的默認返回值。
  10. onClick會發(fā)生的前提是當前View是可點擊的痢掠,并且收到了down和up事件驱犹。
  11. 事件傳遞過程總是由外向內(nèi)的,即事件總是先傳遞給父元素足画,然后由父元素分發(fā)給子View雄驹,通過requestDisallowInterceptTouchEvent方法可以在子元素中干預父元素的分發(fā)過程,但是ACTION_DOWN事件除外淹辞。
  12. onTouch(dispatchTouchEvent中調(diào)用)優(yōu)先于onTouchEvent執(zhí)行医舆,onClick優(yōu)先級最低。onTouch能夠得到執(zhí)行需要兩個前提條件象缀,第一mOnTouchListener的值不能為空蔬将,第二當前點擊的控件必須是enable的。因此如果你有一個控件是非enable的央星,那么給它注冊onTouch事件將永遠得不到執(zhí)行霞怀。對于這一類控件,如果我們想要監(jiān)聽它的touch事件莉给,就必須通過在該控件中重寫onTouchEvent方法來實現(xiàn)毙石。

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

4.2 View的工作原理

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

1颓遏、DecorView的創(chuàng)建

當我們調(diào)用startActivity方法時徐矩,最終調(diào)用ActivityThread#handleLaunchActivity,該方法中會首先會調(diào)用Activity的onCreate方法叁幢。在onCreate方法中滤灯,會調(diào)用Activity#setContentViewsetContentView內(nèi)部會調(diào)用Activity的成員變量mWindow的(Window是抽象類,其實現(xiàn)類是PhoneWindow鳞骤,mWindow是PhoneWindow的一個實例)setContentView窒百。其setContentView方法中,首先new一個DecorView對象豫尽,然后DecorView對象會根據(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方法接著會得到一個DecorView對象和一個WindowManager對象(接口吊说,實現(xiàn)類是WindowManagerImpl)论咏,然后調(diào)用WindowManagerImpl#addView方法,DecorView對象作為入?yún)魅氚渚T赪indowManager#addView中厅贪,創(chuàng)建了一個ViewRootImpl對象(ViewRoot的實現(xiàn)類),并調(diào)用了ViewRootImpl#setView雅宾,DecorView對象作為入?yún)⒀獭T赩iewRootImpl#setView方法內(nèi)部,會通過跨進程的方式向WMS(WindowManagerService)發(fā)起一個調(diào)用眉抬,從而將DecorView最終添加到Window上贯吓,才能真正顯示出來。在這個過程中蜀变,ViewRootImpl悄谐、DecorView和WMS會彼此關(guān)聯(lián),最后通過WMS調(diào)用ViewRootImpl#performTraverals方法開始View的測量库北、布局爬舰、繪制流程。

Window是一個抽象類贤惯,具體是實現(xiàn)是PhoneWindow洼专,Activity、Dialog等的視圖都需要附加到Window上來呈現(xiàn)孵构。
WindowManager是外界訪問Window的入口屁商,實現(xiàn)類是WindowManagerImpl,Window的具體實現(xiàn)是在WindowManagerService中,WindowManager和WindowManagerService的交互是一個IPC過程蜡镶。雾袱。
DecorView是頂級View,是一個FrameLayout布局官还,代表了整個應用的界面芹橡。內(nèi)部有titlebar和contentParent兩個子元素,contentParent的id是content望伦,而我們設(shè)置的main.xml布局則是contentParent里面的一個子元素林说。
ViewRoot的實現(xiàn)類是ViewRootImpl,在WindowManager中創(chuàng)建屯伞,用于將DecorView添加到Window中腿箩。

二、理解MeasureSpec

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

//主要理解 & ~ | 位運算的作用末融,體會這樣設(shè)計的妙處
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值來避免過多對象的內(nèi)存分配钧惧。
SpecMode有三類:

UNSPECIFIED :父容器不對View進行任何限制,要多大給多大勾习,一般用于系統(tǒng)內(nèi)部浓瞪。
EXACTLY:父容器檢測到View所需要的精確大小,這時候View的最終大小就是SpecSize所指定的值语卤,對應LayoutParams中的match_parent和具體數(shù)值這兩種模式(也不一定追逮,還受父容器影響,詳見下面的表格)粹舵。
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會依次調(diào)用performMeasureperformLayoutperformDraw三個方法科乎,這三個方法分別開啟頂級View的measure壁畸、layout和draw這三大流程。

其中performMeasure中會調(diào)用頂級View#measure 方法茅茂,measure調(diào)用onMeasure捏萍,在onMeasure 方法中則會測量自身并調(diào)用所有子元素measure方法,這樣就完成了一次measure過程空闲;子元素會重復父容器的measure過程照弥,如此反復完成了整個View樹的遍歷。另外兩個過程同理进副。

1、ViewGroup的Measure流程

對于ViewGroup既要測量自身悔常,也要遍歷子元素的measure方法(通過實現(xiàn)onMeasure方法)影斑。
在performMeasure方法中,調(diào)用了DecorView#measure(繼承自View机打,其實調(diào)用的是View#measure)矫户,measure會調(diào)用onMeasure方法。ViewGroup并沒有定義onMeasure残邀,這個方法需要子類去實現(xiàn)皆辽,主要需要實現(xiàn)兩個功能:①測量自身②測量子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);
//注意:此時的入?yún)⑹亲陨淼腗easureSpec。measure又會調(diào)用child#onMeasure方法
child.measure(childWidthMeasureSpec,childHeightMeasureSpec);
}

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


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

ViewGroup并沒有定義onMeasure墨坚,需要其子類去實現(xiàn)秧饮,為什么ViewGroup不像View一樣對其onMeasure做統(tǒng)一呢?因為不同的ViewGroup子類有不同的布局特征,導致測量細節(jié)各不相同浦楣,無法統(tǒng)一袖肥。

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

1.2融求、measureChildren方法

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

2檬嘀、View的Measure過程

View的measure方法是一個final方法,會調(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è)置測量的寬高,測量好之后罕拂,必須調(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;
}

簡單理解,getDefaultSize返回的就是measureSpec中的specSize爆班,這就是View測量后的大小衷掷。在AT_MOST和EXACTLY模式下,都返回了specSize柿菩。也就是說對于一個直接繼承View的自定義View棍鳖,它的wrap_content和match_parent效果一樣,因此如果要實現(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 模式下,給定一個默認值mWidth,mHeight祟辟。默認寬高靈活指定
      //參考TextView医瘫、ImageView的處理方式
      //其他情況下沿用系統(tǒng)測量規(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過程是三大流程中最復雜的一個,measure完成以后僚纷,通過 getMeasuredWidth/Height 方法就可以正確獲取到View的測量后寬/高矩距。在某些情況下,系統(tǒng)可能需要多次measure才能確定最終的測量寬/高怖竭,所以在onMeasure中拿到的寬/高很可能不是準確的锥债。一個較好的習慣是在onLayout方法中,去獲取View測量寬高或最終寬高痊臭。

3哮肚、如何正確獲得寬高

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

3.1、Activity/View#onWindowFocusChanged

onWindowFocusChanged這個方法的含義是:VieW已經(jīng)初始化完畢了分唾,寬高已經(jīng)準備好了鲁纠,需要注意:它會被調(diào)用多次,當Activity的窗口得到焦點和失去焦點均會被調(diào)用鳍寂。

3.2、view.post(runnable)

通過post將一個runnable投遞到消息隊列的尾部情龄,當Looper調(diào)用此runnable的時候迄汛,View也初始化好了。

3.3骤视、ViewTreeObserver

使用 ViewTreeObserver 的眾多回調(diào)可以完成這個功能鞍爱,比如OnGlobalLayoutListener 這個接口,當View樹的狀態(tài)發(fā)送改變或View樹內(nèi)部的View的可見性發(fā)生改變時专酗,onGlobalLayout 方法會被回調(diào)睹逃,這是獲取View寬高的好時機。需要注意的是祷肯,伴隨著View樹狀態(tài)的改變沉填, onGlobalLayout 會被回調(diào)多次。

3.4佑笋、view.measure(int widthMeasureSpec,intheightMeasureSpec)

手動對view進行measure翼闹。需要根據(jù)View的layoutParams分情況處理:

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

四输虱、layout過程

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

View#layout中,setFrame確定View的四個頂點位置横堡,即初始化mLeft埋市,mRight,mTop命贴,mBottom這四個值(確定了最終的寬高)道宅,也就確定了View在父容器中的位置。接著調(diào)用onLayout方法胸蛛,確定所有子View的位置污茵,和onMeasure一樣,onLayout的具體實現(xiàn)和布局有關(guān)葬项,因此View和ViewGroup均沒有真正實現(xiàn)onLayout方法泞当。

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

五蝶溶、draw過程

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

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

View#setWillNotDraw嗜历,如果一個View不需要繪制任何內(nèi)容,那么置為ture抖所,系統(tǒng)會進行相應的優(yōu)化梨州。默認情況下,View為false田轧,ViewGroup為true摊唇。所以自定義ViewGroup需要通過onDraw來繪制內(nèi)容時,必須顯式的關(guān)閉 WILL_NOT_DRAW 這個優(yōu)化標記位涯鲁,即調(diào)用 setWillNotDraw(false)巷查。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末有序,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子岛请,更是在濱河造成了極大的恐慌旭寿,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,013評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件崇败,死亡現(xiàn)場離奇詭異盅称,居然都是意外死亡,警方通過查閱死者的電腦和手機后室,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,205評論 2 382
  • 文/潘曉璐 我一進店門缩膝,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人岸霹,你說我怎么就攤上這事疾层。” “怎么了贡避?”我有些...
    開封第一講書人閱讀 152,370評論 0 342
  • 文/不壞的土叔 我叫張陵痛黎,是天一觀的道長。 經(jīng)常有香客問我刮吧,道長湖饱,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,168評論 1 278
  • 正文 為了忘掉前任杀捻,我火速辦了婚禮井厌,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘致讥。我一直安慰自己仅仆,他們只是感情好,可當我...
    茶點故事閱讀 64,153評論 5 371
  • 文/花漫 我一把揭開白布拄踪。 她就那樣靜靜地躺著,像睡著了一般拳魁。 火紅的嫁衣襯著肌膚如雪惶桐。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 48,954評論 1 283
  • 那天潘懊,我揣著相機與錄音姚糊,去河邊找鬼。 笑死授舟,一個胖子當著我的面吹牛救恨,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播释树,決...
    沈念sama閱讀 38,271評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼肠槽,長吁一口氣:“原來是場噩夢啊……” “哼擎淤!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起秸仙,我...
    開封第一講書人閱讀 36,916評論 0 259
  • 序言:老撾萬榮一對情侶失蹤嘴拢,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后寂纪,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體席吴,經(jīng)...
    沈念sama閱讀 43,382評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,877評論 2 323
  • 正文 我和宋清朗相戀三年捞蛋,在試婚紗的時候發(fā)現(xiàn)自己被綠了孝冒。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 37,989評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡拟杉,死狀恐怖庄涡,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情捣域,我是刑警寧澤啼染,帶...
    沈念sama閱讀 33,624評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站焕梅,受9級特大地震影響迹鹅,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜贞言,卻給世界環(huán)境...
    茶點故事閱讀 39,209評論 3 307
  • 文/蒙蒙 一斜棚、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧该窗,春花似錦弟蚀、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,199評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至规肴,卻和暖如春捶闸,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背拖刃。 一陣腳步聲響...
    開封第一講書人閱讀 31,418評論 1 260
  • 我被黑心中介騙來泰國打工删壮, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人兑牡。 一個月前我還...
    沈念sama閱讀 45,401評論 2 352
  • 正文 我出身青樓央碟,卻偏偏與公主長得像,于是被迫代替她去往敵國和親均函。 傳聞我的和親對象是個殘疾皇子亿虽,可洞房花燭夜當晚...
    茶點故事閱讀 42,700評論 2 345

推薦閱讀更多精彩內(nèi)容