自定義View

Andtoid中的顏色

ViewRootImpl的performTraversals()

private void performTraversals() {
        // cache mView since it is used so much below...
        //mView就是DecorView根布局
        final View host = mView;
        //在Step3 成員變量mAdded賦值為true,因此條件不成立
        if (host == null || !mAdded)
            return;
        //是否正在遍歷
        mIsInTraversal = true;
        //是否馬上繪制View
        mWillDrawSoon = true;
         ...
        //頂層視圖DecorView所需要窗口的寬度和高度
        int desiredWindowWidth;
        int desiredWindowHeight;

         ...
        //在構(gòu)造方法中mFirst已經(jīng)設(shè)置為true,表示是否是第一次繪制DecorView
        if (mFirst) {
            mFullRedrawNeeded = true;
            mLayoutRequested = true;
            //如果窗口的類型是有狀態(tài)欄的,那么頂層視圖DecorView所需要窗口的寬度和高度就是除了狀態(tài)欄
            if (lp.type == WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL
                    || lp.type == WindowManager.LayoutParams.TYPE_INPUT_METHOD) {
                // NOTE -- system code, won't try to do compat mode.
                Point size = new Point();
                mDisplay.getRealSize(size);
                desiredWindowWidth = size.x;
                desiredWindowHeight = size.y;
            } else {//否則頂層視圖DecorView所需要窗口的寬度和高度就是整個屏幕的寬高
                DisplayMetrics packageMetrics =
                    mView.getContext().getResources().getDisplayMetrics();
                desiredWindowWidth = packageMetrics.widthPixels;
                desiredWindowHeight = packageMetrics.heightPixels;
            }
    }
  ...
//獲得view寬高的測量規(guī)格,mWidth和mHeight表示窗口的寬高,lp.widthhe和lp.height表示DecorView根布局寬和高
 int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
 int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

  // Ask host how big it wants to be
  //執(zhí)行測量操作
 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
  ...
  //執(zhí)行布局操作
 performLayout(lp, desiredWindowWidth, desiredWindowHeight);
  ...
  //執(zhí)行繪制操作
 performDraw();
}

啟動Activity后,視圖添加,繪制。是在ViewRootImpl.setView(),這方法中會丟一個Runnable給Choreographer,在下次Vsync信號來的時候會調(diào)用performTraversals(),然后會調(diào)用relaywindow(),請求WMS關(guān)聯(lián)Java層Surface和native層Surface,然后執(zhí)行View的繪制流程performMeasure(),performLayout(),performDraw()

Measure

MeasureSpec

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
        try {
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
}
 public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int oWidth  = insets.left + insets.right;
            int oHeight = insets.top  + insets.bottom;
            //根據(jù)原有寬高計算獲取不同模式下的具體寬高值
            widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);
            heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
        }
        ...
        if (forceLayout || needsLayout) {
            // first clears the measured dimension flag
            mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

            resolveRtlPropertiesIfNeeded();

            int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                //在該方法中子控件完成具體的測量
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                ...
            } 
         ...
    }

MeasureSpec是View的內(nèi)部類,內(nèi)部封裝了View的規(guī)格尺寸,以及View的寬高信息。在Measure的流程中,系統(tǒng)會將View的LayoutParams根據(jù)父容器是施加的規(guī)則轉(zhuǎn)換為MeasureSpec,然后在onMeasure()方法中具體確定控件的寬高信息

MeasureSpec的常量中指定了兩種內(nèi)容,一種為尺寸模式,一種為具體的寬高信息。其中高2位表示尺寸測量模式,低30位表示具體的寬高信息。

①UNSPECIFIED:未指定模式,父容器沒有對當(dāng)前View有任何限制,當(dāng)前View可以任意取尺寸。一般用于系統(tǒng)內(nèi)部的測量
②AT_MOST:最大模式,當(dāng)前尺寸是當(dāng)前View能取的最大尺寸,對應(yīng)于在xml文件中指定控件大小為wrap_content屬性,子View的最終大小是父View指定的大小值,并且子View的大小不能大于這個值
③EXACTLY :精確模式,當(dāng)前的尺寸就是當(dāng)前View應(yīng)該取的尺寸,對應(yīng)于在xml文件中指定控件為match_parent屬性或者是具體的數(shù)值,父容器測量出View所需的具體大小

match_parent--->EXACTLY。怎么理解呢?match_parent就是要利用父View給我們提供的所有剩余空間,而父View剩余空間是確定的,也就是這個測量模式的整數(shù)里面存放的尺寸。

wrap_content--->AT_MOST。怎么理解:就是我們想要將大小設(shè)置為包裹我們的view內(nèi)容,那么尺寸大小就是父View給我們作為參考的尺寸,只要不超過這個尺寸就可以啦,具體尺寸就根據(jù)我們的需求去設(shè)定。

固定尺寸(如100dp)--->EXACTLY。用戶自己指定了尺寸大小,我們就不用再去干涉了,當(dāng)然是以指定的大小為主啦。

對于每一個View,都持有一個MeasureSpec,MeasureSpec保存了該View的尺寸測量模式以及具體的寬高信息,MeasureSpec受自身的LayoutParams和父容器的MeasureSpec共同影響。

measure流程

View的Measure

   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
   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);
    }
 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;
    }

通過上文對MeasureSpec的分析,在這里我們就能明確,getDefaultSize實質(zhì)上就是根據(jù)測繪模式確定子View的具體大小,而對于自定義View而言,子View的寬高信息不僅由自身決定,如果它被包裹在ViewGroup中就需要具體測量得到其精確值。

View的Measure過程中遇到的問題以及解決方案

View 的measure過程和Activity的生命周期方法不是同步執(zhí)行的,因此無法保證Activity執(zhí)行了onCreate、onStart、onResume時某個View已經(jīng)測量完畢了。如果View還沒有測量完畢,那么獲得的寬和高都是0。下面是3種解決該問題的方法:
①Activity/View的onWindowsChanged()方法
onWindowFocusChanged()方法表示 View 已經(jīng)初始化完畢了,寬高已經(jīng)準(zhǔn)備好了,這個時候去獲取是沒問題的。這個方法會被調(diào)用多次,當(dāng)Activity繼續(xù)執(zhí)行或者暫停執(zhí)行的時候,這個方法都會被調(diào)用,代碼如下:

 public void onWindowFocusChanged(boolean hasWindowFocus) {
         super.onWindowFocusChanged(hasWindowFocus);
       if(hasWindowFocus){
       int width=view.getMeasuredWidth();
       int height=view.getMeasuredHeight();
      }      
  }

②View.post(runnable)方法
通過post將一個 Runnable投遞到消息隊列的尾部,然后等待Looper調(diào)用此runnable的時候View也已經(jīng)初始化好了

@Override  
protected void onStart() {  
    super.onStart();  
    view.post(new Runnable() {  
        @Override  
        public void run() {  
            int width=view.getMeasuredWidth();  
            int height=view.getMeasuredHeight();  
        }  
    });  
}  

③ViewTreeObsever
使用 ViewTreeObserver 的眾多回調(diào)方法可以完成這個功能,比如使用onGlobalLayoutListener 接口,當(dāng) View樹的狀態(tài)發(fā)生改變或者View樹內(nèi)部的View的可見性發(fā)生改變時,onGlobalLayout 方法將被回調(diào)。伴隨著View樹的變化,這個方法也會被多次調(diào)用。

 @Override  
  protected void onStart() {  
    super.onStart();  
    ViewTreeObserver viewTreeObserver=view.getViewTreeObserver();  
    viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {  
        @Override  
        public void onGlobalLayout() {  
            view.getViewTreeObserver().removeOnGlobalLayoutListener(this);  
            int width=view.getMeasuredWidth();  
            int height=view.getMeasuredHeight();  
        }  
    });  
} 

Layout

layout過程

layout 的作用是ViewGroup來確定子元素的位置,當(dāng) ViewGroup 的位置被確定后,在layout中會調(diào)用onLayout ,在onLayout中會遍歷所有的子元素并調(diào)用子元素的 layout 方法。
在代碼中設(shè)置View的成員變量 mLeft,mTop,mRight,mBottom 的值,這幾個值是在屏幕上構(gòu)成矩形區(qū)域的四個坐標(biāo)點,就是該View顯示的位置,不過這里的具體位置都是相對與父視圖的位置而言,而 onLayout 方法則會確定所有子元素位置,ViewGroup在onLayout函數(shù)中通過調(diào)用其children的layout函數(shù)來設(shè)置子視圖相對與父視圖中的位置,具體位置由函數(shù) layout 的參數(shù)決定。下面我們先看View的layout 方法(只展示關(guān)鍵性代碼)如下:

/*  
 *@param l view 左邊緣相對于父布局左邊緣距離 
 *@param t view 上邊緣相對于父布局上邊緣位置 
 *@param r view 右邊緣相對于父布局左邊緣距離 
 *@param b view 下邊緣相對于父布局上邊緣距離 
 */  
    public void layout(int l, int t, int r, int b) {
        ...
        //記錄 view 原始位置  
        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;
        //調(diào)用 setFrame 方法 設(shè)置新的 mLeft、mTop、mBottom、mRight 值,  
        //設(shè)置 View 本身四個頂點位置  
        //并返回 changed 用于判斷 view 布局是否改變  
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
        //第二步,如果 view 位置改變那么調(diào)用 onLayout 方法設(shè)置子 view 位置  
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        //調(diào)用 onLayout  
            onLayout(changed, l, t, r, b);
            ...
        }
        ...
    }

Draw

Draw流程

在View的draw()方法的注釋中,說明了繪制流程中具體每一步的作用,源碼中對于draw()方法的注釋如下,我們在這里重點分析注釋中除第2、第5步外的其他步驟。

  /*
         * 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(繪制View的內(nèi)容)
         *      4. Draw children(繪制子View)
         *      5. If necessary, draw the fading edges and restore layers(如果需要的話,繪制漸變邊緣并恢復(fù)畫布圖層。)
         *      6. Draw decorations (scrollbars for instance)(繪制裝飾(例如滾動條scrollbar))
         */

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

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

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

一般來說,如果View確定自身不再適合當(dāng)前區(qū)域,比如說它的LayoutParams發(fā)生了改變,需要父布局對其進行重新測量、擺放、繪制這三個流程,往往使用requestLayout。而invalidate則是刷新當(dāng)前View,使當(dāng)前View進行重繪,不會進行測量、布局流程,因此如果View只需要重繪而不需要測量,布局的時候,使用invalidate方法往往比requestLayout方法更高效。

參考
自定義View心法——View工作流程
自定義View,有這一篇就夠了
Android自定義View全解
教你步步為營掌握自定義View
Android自定義控件,你們是如何系統(tǒng)學(xué)習(xí)的?
安卓自定義View教程目錄

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

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