自定義View心法——View工作流程

前言

本文的目的有兩個(gè):

  1. 給對(duì)自定義View感興趣的人一些入門的指引
  2. 給正在使用自定義View的人一些更深入的解析
    自定義View一直都被認(rèn)為是Android開發(fā)高手的必備技能,而穩(wěn)中帶皮的學(xué)習(xí)View的基礎(chǔ)體系,這是自定義View的必經(jīng)之路,如果自定義View如果設(shè)計(jì)的不好或者不考慮性能的話會(huì)造成很大的問(wèn)題。所以我們進(jìn)入View工作流程的分析。

一、Android的UI層級(jí)繪制體系

Android中的Activity是作為應(yīng)用程序的載體存在的,它代表一個(gè)完整的用戶界面并提供了窗口進(jìn)行視圖繪制。

  • 在這里,我們這里所說(shuō)的視圖繪制,實(shí)質(zhì)上就是在對(duì)View及其子類進(jìn)行操作。而View作為視圖控件的頂層父類,在本文中會(huì)對(duì)其進(jìn)行詳細(xì)分析。我們以Android的UI層級(jí)繪制體系為切入點(diǎn)對(duì)View進(jìn)行探究。
    圖1 View的層級(jí)結(jié)構(gòu)

Android的UI層級(jí)繪制體系如圖1所示。

繪制體系中做了這些事情
①當(dāng)調(diào)用 Activity 的setContentView 方法后會(huì)調(diào)用PhoneWindow 類的setContentView方法(PhoneWindow是抽象類Windiw的實(shí)現(xiàn)類,Window用來(lái)描述Activity視圖最頂端的窗口的顯示內(nèi)容和行為動(dòng)作)。
②PhoneWindow類的setContentView方法中最終會(huì)生成一個(gè)DecorView對(duì)象(DectorView是是PhoneWindow的內(nèi)部類,繼承自FrameLayout)。
③DecorView容器中包含根布局,根布局中包含一個(gè)id為content的FrameLayout布局,Activity加載布局的xml最后通過(guò)LayoutInflater將xml文件中的內(nèi)容解析成View層級(jí)體系,最后填加到id為content的FrameLayout布局中。

至此,View最終就會(huì)顯示到手機(jī)屏幕上。

二、View的視圖繪制流程剖析

1、DecorView被加載到Window中

DecorView被加載到Window的過(guò)程中,WindowManager起到了關(guān)鍵性的作用,最后交給ViewRootImpl做詳細(xì)處理,通過(guò)如下的局部ActivityThread的源碼分析這一點(diǎn)可以得到印證(在這里我只展示核心源碼,詳細(xì)源碼可以在代碼中查看)。

final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
        ActivityClientRecord r = mActivities.get(token);
       ...
       //在這里執(zhí)行performResumeActivity的方法中會(huì)執(zhí)行Activity的onResume()方法
        r = performResumeActivity(token, clearHide, reason);
       ...
       if (r.window == null && !a.mFinished && willBeVisible) {
          //PhoneWindow在這里獲取到
          r.window = r.activity.getWindow();
          //DecorView在這里獲取到
          View decor = r.window.getDecorView();
          decor.setVisibility(View.INVISIBLE);
          //獲取ViewManager對(duì)象,在這里getWindowManager()實(shí)質(zhì)上獲取的是ViewManager的子類對(duì)象WindowManager
          ViewManager wm = a.getWindowManager();
          ...
          if (r.mPreserveWindow) {
          ...
          //獲取ViewRootImpl對(duì)象
          ViewRootImpl impl = decor.getViewRootImpl();
           ...
          }
          if (a.mVisibleFromClient) {
              if (!a.mWindowAdded) {
                   a.mWindowAdded = true;
                   //在這里WindowManager將DecorView添加到PhoneWindow中
                   wm.addView(decor, l);
                   } 
                   ...
          }
          ...
    }

WindowManager將DecorView添加到PhoneWindow中,即addView()方法執(zhí)行時(shí)將視圖添加的動(dòng)作交給了ViewRoot,ViewRoot作為接口,其實(shí)現(xiàn)類ViewRootImpl具體實(shí)現(xiàn)了addView()方法,最后,視圖的具體繪制在performTraversals()中展開,如下圖2.1所示:

圖2.1 View繪制的代碼層級(jí)分析

2、ViewRootImpl的performTraversals()方法完成具體的視圖繪制流程

在源碼中ViewRootImpl中視圖具體繪制的流程如下:

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所需要窗口的寬度和高度就是整個(gè)屏幕的寬高
                DisplayMetrics packageMetrics =
                    mView.getContext().getResources().getDisplayMetrics();
                desiredWindowWidth = packageMetrics.widthPixels;
                desiredWindowHeight = packageMetrics.heightPixels;
            }
    }
  ...
//獲得view寬高的測(cè)量規(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í)行測(cè)量操作
 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
  ...
  //執(zhí)行布局操作
 performLayout(lp, desiredWindowWidth, desiredWindowHeight);
  ...
  //執(zhí)行繪制操作
 performDraw();
}

該方法主要流程就體現(xiàn)了View繪制渲染的三個(gè)主要步驟,分別是測(cè)量,擺放,繪制三個(gè)階段。流程圖如下圖2.2所示:
圖2.2 View的繪制流程

接下來(lái),我們對(duì)于 performMeasure()、performLayout()、 performDraw()完成具體拆解分析。實(shí)質(zhì)上最后就需要定位到View的onMeasure()、onLayout()、onDraw()方法中。

三、MeasureSpec在View體系中的作用

1、MeasureSpec的作用

首先我們從performMeasure()入手分析,在上面的內(nèi)容中,我們通過(guò)源碼可以看到 performMeasure()方法中傳入了childWidthMeasureSpec、childHeightMeasureSpec兩個(gè)int類型的值,performMeasure方法的源碼如下所示:

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

這兩個(gè)值又傳遞到mView.measure(childWidthMeasureSpec, childHeightMeasureSpec)方法中,其中measure方法的核心源碼如下:

 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ù)原有寬高計(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
                //在該方法中子控件完成具體的測(cè)量
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                ...
            } 
         ...
    }

到這里我們應(yīng)該明確,childWidthMeasureSpec, childHeightMeasureSpec是MeasureSpec根據(jù)原有寬高計(jì)算獲取不同模式下的具體寬高值。

2、MeasureSpec剖析

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

public static class MeasureSpec {
        //int類型占4個(gè)字節(jié),其中高2位表示尺寸測(cè)量模式,低30位表示具體的寬高信息
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

        /** @hide */
        @IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
        @Retention(RetentionPolicy.SOURCE)
        public @interface MeasureSpecMode {}
        //如下所示是MeasureSpec中的三種模式:UNSPECIFIED、EXACTLY、AT_MOST                  

        /**
         * Measure specification mode: The parent has not imposed any constraint
         * on the child. It can be whatever size it wants.
         */
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        /**
         * Measure specification mode: The parent has determined an exact size
         * for the child. The child is going to be given those bounds regardless
         * of how big it wants to be.
         */
        public static final int EXACTLY     = 1 << MODE_SHIFT;

        /**
         * Measure specification mode: The child can be as large as it wants up
         * to the specified size.
         */
        public static final int AT_MOST     = 2 << MODE_SHIFT;

        //根據(jù)尺寸測(cè)量模式跟寬高具體確定控件的具體寬高
        public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                          @MeasureSpecMode int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }

        /**
         * Like {@link #makeMeasureSpec(int, int)}, but any spec with a mode of UNSPECIFIED
         * will automatically get a size of 0. Older apps expect this.
         *
         * @hide internal use only for compatibility with system widgets and older apps
         */
        public static int makeSafeMeasureSpec(int size, int mode) {
            if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
                return 0;
            }
            return makeMeasureSpec(size, mode);
        }

        //獲取尺寸模式
        /**
         * Extracts the mode from the supplied measure specification.
         *
         * @param measureSpec the measure specification to extract the mode from
         * @return {@link android.view.View.MeasureSpec#UNSPECIFIED},
         *         {@link android.view.View.MeasureSpec#AT_MOST} or
         *         {@link android.view.View.MeasureSpec#EXACTLY}
         */
        @MeasureSpecMode
        public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }

        //獲取寬高信息
        /**
         * Extracts the size from the supplied measure specification.
         *
         * @param measureSpec the measure specification to extract the size from
         * @return the size in pixels defined in the supplied measure specification
         */
        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }

        //將控件的尺寸模式、寬高信息進(jìn)行拆解查看,并對(duì)不同模式下的寬高信息進(jìn)行不同的處理
        static int adjust(int measureSpec, int delta) {
            final int mode = getMode(measureSpec);
            int size = getSize(measureSpec);
            if (mode == UNSPECIFIED) {
                // No need to adjust size for UNSPECIFIED mode.
                return makeMeasureSpec(size, UNSPECIFIED);
            }
            size += delta;
            if (size < 0) {
                Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +
                        ") spec: " + toString(measureSpec) + " delta: " + delta);
                size = 0;
            }
            return makeMeasureSpec(size, mode);
        }

        /**
         * Returns a String representation of the specified measure
         * specification.
         *
         * @param measureSpec the measure specification to convert to a String
         * @return a String with the following format: "MeasureSpec: MODE SIZE"
         */
        public static String toString(int measureSpec) {
            int mode = getMode(measureSpec);
            int size = getSize(measureSpec);

            StringBuilder sb = new StringBuilder("MeasureSpec: ");

            if (mode == UNSPECIFIED)
                sb.append("UNSPECIFIED ");
            else if (mode == EXACTLY)
                sb.append("EXACTLY ");
            else if (mode == AT_MOST)
                sb.append("AT_MOST ");
            else
                sb.append(mode).append(" ");

            sb.append(size);
            return sb.toString();
        }
    }

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

尺寸測(cè)量模式的3種類型
①UNSPECIFIED:未指定模式,父容器不限制View的大小,一般用于系統(tǒng)內(nèi)部的測(cè)量
②AT_MOST:最大模式,對(duì)應(yīng)于在xml文件中指定控件大小為wrap_content屬性,子View的最終大小是父View指定的大小值,并且子View的大小不能大于這個(gè)值
③EXACTLY :精確模式,對(duì)應(yīng)于在xml文件中指定控件為match_parent屬性或者是具體的數(shù)值,父容器測(cè)量出View所需的具體大小

我?guī)湍憧偨Y(jié)一下
對(duì)于每一個(gè)View,都持有一個(gè)MeasureSpec,MeasureSpec保存了該View的尺寸測(cè)量模式以及具體的寬高信息,MeasureSpec受自身的LayoutParams和父容器的MeasureSpec共同影響。

四、View的Measure流程分析

1、View樹的Measure測(cè)量流程邏輯圖

2、View的Measure流程分析

那么在上文3.1的分析中,我們能夠明確在measure方法中最后調(diào)用onMeasure()方法完成子View的具體測(cè)量,onMeasure()方法的源碼如下所示:

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

setMeasuredDimension()方法在onMeasure()中被調(diào)用,被用于存儲(chǔ)測(cè)繪的寬度、高度,而不這樣做的話會(huì)觸發(fā)測(cè)繪時(shí)的異常。

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

在setMeasuredDimension()方法中傳入的是getDefaultSize(),接著分析getDefaultSize()中做了哪些操作:

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

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

3、View的Measure過(guò)程中遇到的問(wèn)題以及解決方案

View 的measure過(guò)程和Activity的生命周期方法不是同步執(zhí)行的,因此無(wú)法保證Activity執(zhí)行了onCreate、onStart、onResume時(shí)某個(gè)View已經(jīng)測(cè)量完畢了。如果View還沒有測(cè)量完畢,那么獲得的寬和高都是0。下面是3種解決該問(wèn)題的方法:

①Activity/View的onWindowsChanged()方法

onWindowFocusChanged()方法表示 View 已經(jīng)初始化完畢了,寬高已經(jīng)準(zhǔn)備好了,這個(gè)時(shí)候去獲取是沒問(wèn)題的。這個(gè)方法會(huì)被調(diào)用多次,當(dāng)Activity繼續(xù)執(zhí)行或者暫停執(zhí)行的時(shí)候,這個(gè)方法都會(huì)被調(diào)用,代碼如下:

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

②View.post(runnable)方法

通過(guò)post將一個(gè) Runnable投遞到消息隊(duì)列的尾部,然后等待Looper調(diào)用此runnable的時(shí)候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)方法可以完成這個(gè)功能,比如使用onGlobalLayoutListener 接口,當(dāng) View樹的狀態(tài)發(fā)生改變或者View樹內(nèi)部的View的可見性發(fā)生改變時(shí),onGlobalLayout 方法將被回調(diào)。伴隨著View樹的變化,這個(gè)方法也會(huì)被多次調(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();  
        }  
    });  
} 

當(dāng)然,在這里你可以通過(guò)setMeasuredDimension()方法對(duì)子View的具體寬高以及測(cè)量模式進(jìn)行指定。

五、View的layout流程分析

1、View樹的layout擺放流程邏輯圖

2、View的layout流程分析

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

/*  
 *@param l view 左邊緣相對(duì)于父布局左邊緣距離 
 *@param t view 上邊緣相對(duì)于父布局上邊緣位置 
 *@param r view 右邊緣相對(duì)于父布局左邊緣距離 
 *@param b view 下邊緣相對(duì)于父布局上邊緣距離 
 */  
    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 本身四個(gè)頂點(diǎn)位置  
        //并返回 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);
            ...
        }
        ...
    }

六、View的draw流程分析

1、View樹的draw繪制流程邏輯圖

2、View的draw流程分析

在View的draw()方法的注釋中,說(shuō)明了繪制流程中具體每一步的作用,源碼中對(duì)于draw()方法的注釋如下,我們?cè)谶@里重點(diǎn)分析注釋中除第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)(繪制裝飾(例如滾動(dòng)條scrollbar))
         */

①View中的drawBackground()繪制背景

核心源碼如下:

  private void drawBackground(Canvas canvas) {
        final Drawable background = mBackground;
        if (background == null) {
            return;
        }
        ...
        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        if ((scrollX | scrollY) == 0) {
            background.draw(canvas);
        } else {
            canvas.translate(scrollX, scrollY);
            background.draw(canvas);
            canvas.translate(-scrollX, -scrollY);
        }
    }

如果背景有偏移,實(shí)質(zhì)上對(duì)畫布首先做偏移處理,然后在其上進(jìn)行繪制。

②View內(nèi)容的繪制

View內(nèi)容的繪制源碼如下所示:

    protected void onDraw(Canvas canvas) {
    }

該方法是空實(shí)現(xiàn),就根據(jù)不同的內(nèi)容進(jìn)行不同的設(shè)置,自定義View中就需要重寫該方法加入我們自己的業(yè)務(wù)邏輯。

③子View的繪制

子View的繪制源碼如下所示:

    protected void dispatchDraw(Canvas canvas) {

    }

該方法同樣為空實(shí)現(xiàn),而對(duì)于ViewGroup而言對(duì)子View進(jìn)行遍歷,并最終調(diào)用子View的onDraw方法進(jìn)行繪制。

④裝飾繪制

裝飾繪制的源碼如下所示(只展示核心源碼):

    public void onDrawForeground(Canvas canvas) {
        //繪制前景裝飾
        onDrawScrollIndicators(canvas);
        onDrawScrollBars(canvas);
       ...
            foreground.draw(canvas);
    }

很明顯,在這里onDrawForeground()方法用于繪制例如ScrollBar等其他裝飾,并將它們顯示在視圖的最上層。

七、視圖重繪

1、requestLayout重新繪制視圖

子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è)量、布局、繪制。

2、invalidate在UI線程中重新繪制視圖

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

3、postInvalidate在非UI線程中重新繪制視圖

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


我要總結(jié)了
  • 總結(jié)一下
    一般來(lái)說(shuō),如果View確定自身不再適合當(dāng)前區(qū)域,比如說(shuō)它的LayoutParams發(fā)生了改變,需要父布局對(duì)其進(jìn)行重新測(cè)量、擺放、繪制這三個(gè)流程,往往使用requestLayout。而invalidate則是刷新當(dāng)前View,使當(dāng)前View進(jìn)行重繪,不會(huì)進(jìn)行測(cè)量、布局流程,因此如果View只需要重繪而不需要測(cè)量,布局的時(shí)候,使用invalidate方法往往比requestLayout方法更高效。
最后編輯于
?著作權(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ù)。

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