高級(jí) UI 成長(zhǎng)之路 (三) 理解 View 工作原理并帶你入自定義 View 門(mén)

前言

該篇分為上下結(jié)構(gòu)夯辖,上部分主要講解 View 的工作原理,下部分主要以案例的形式講解自定義 View董饰。

ps:該篇文章大部份內(nèi)容會(huì)參考 Android 開(kāi)發(fā)藝術(shù)探索蒿褂。

初識(shí) ViewRootImpl 和 DecorView

在介紹 View 繪制的三大流程之前我們有必要先了解下 ViewRootImplDecorView 基本概念,ViewRootImpl 它是連接 WindowManager 和 DecorView 的紐帶卒暂,View 的三大繪制流程也是在 ViewRootImpl 中完成的啄栓。在 ActivityThread 中,當(dāng) Activity 創(chuàng)建完成并執(zhí)行 onCreate 生命周期的調(diào)用也祠,在用戶(hù)主動(dòng)調(diào)用 setContentView 之后昙楚,會(huì)初始化 DecorView 實(shí)例,DecorView 相當(dāng)于是整個(gè) Activity 中 View 的父類(lèi)诈嘿。ViewRootImpl 是在 ActivityThread 執(zhí)行 handleResumeActivity 函數(shù)之后的 WindowManagerGlobal 中進(jìn)行初始化的堪旧,可以把它理解為是 Activity 的 View 樹(shù)管理者,最后并將 ViewRootImpl 和 DecorView 建立關(guān)聯(lián)永淌,這個(gè)過(guò)程可以參考下面源碼崎场,代碼如下:

//ActivityThread#handleResumeActivity -> ViewManager#addView -> WindowManagerGlobal#andView
//WindowManagerGlobal.java
    /**
     *
     * @param view DecorView
     * @param params
     * @param display
     * @param parentWindow
     */
    public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
                ...

        ViewRootImpl root;
        View panelParentView = null;

        synchronized (mLock) {
            ...

            /**
             * 1. 實(shí)例化 ViewRootImpl 對(duì)象,并賦值給 root 變量
             */
            root = new ViewRootImpl(view.getContext(), display);
          ...
            try {
                /**
                 * 2. 將 DecorView 和窗口的參數(shù)通過(guò)ViewRootImpl setView 方法設(shè)置到 ViewRootImpl                                *      中
                 */
                root.setView(view, wparams, panelParentView);
            } catch (RuntimeException e) {
             ...
            }
        }
    }

通過(guò)上面代碼我們可以知道在 Activity 執(zhí)行 onResume 生命周期之后遂蛀,首先會(huì)執(zhí)行上面的代碼邏輯谭跨,進(jìn)行關(guān)聯(lián)。

View 的繪制流程是從 ViewRootImpl 的 performTraversals 方法開(kāi)始的李滴,它經(jīng)過(guò)了 measure 螃宙、layout、draw 三個(gè)過(guò)程才能最終將一個(gè) View 繪制出來(lái)所坯,其中 measure 用來(lái)測(cè)量 View 的寬高谆扎,layout 確定 View 在父容器中的放置位置,draw 就是根據(jù)前面 measure 和 layout 的步驟把 View 繪制在屏幕上芹助。針對(duì) performTraversals 的大致流程堂湖,可以看下圖:

image

通過(guò)上圖我們知道 ViewRootImpl#performTraversals 會(huì)依次調(diào)用 performMeasure, performLayout , performDraw 三個(gè)方法闲先,這三個(gè)方法會(huì)分別調(diào)用頂級(jí) View 的 measure, layout,draw 方法以完成 View 的繪制工作。由于在 ViewRootImpl # performTraversals 中的三大方法會(huì)在 onMeasure 无蜂,onLayout伺糠,onDraw 中分別對(duì)所有子 View 進(jìn)行操作,由于每一步都相似斥季,這里就拿 onMeasure 測(cè)量舉例 ,通過(guò)上圖我們知道在 performMeasure 中會(huì)調(diào)用 measure 方法训桶,在 measure 方法中又會(huì)調(diào)用 onMeasure 方法,在 onMeasure 方法中則會(huì)對(duì)所有的子元素進(jìn)行 measure 測(cè)量過(guò)程酣倾,這個(gè)時(shí)候 measure 流程就從父容器傳遞到子元素中了舵揭,這樣就完成了一次 measure 過(guò)程。接著子元素會(huì)重復(fù)父容器的 measure 過(guò)程躁锡,如此反復(fù)就完成了整個(gè) View 樹(shù)的遍歷午绳。

measure: measure 過(guò)程決定了 View 的期望寬高, Measure 測(cè)量完了之后稚铣,可以通過(guò) getMeasuredWidth/getMeasuredHeight 方法來(lái)獲取到 View 測(cè)量后的寬高箱叁,在幾乎所有的情況下它都等同于 View 的最終寬高,但是有一種情況下除外惕医,比如在 Activity 中的 onCreate ,onStart, onResume 中如果直接獲取 View 的寬高耕漱,你會(huì)發(fā)現(xiàn)獲取都是 0,0抬伺,為什么會(huì)這樣呢螟够?這個(gè)后面會(huì)進(jìn)行說(shuō)明。

Layout: layout 過(guò)程決定了 View 的四個(gè)頂點(diǎn)的坐標(biāo)和實(shí)際的 View 的寬高峡钓,完成以后妓笙,可以通過(guò) getTop,getBottom,getLeft,和 getRight 分別獲取對(duì)應(yīng)的值能岩,并通過(guò) getWidth 和 getHeight 方法來(lái)拿到 View 最終寬高寞宫。

Draw: draw 過(guò)程則決定了 View 的顯示,只有 draw 方法完成以后 View 的內(nèi)容才能顯示到屏幕上拉鹃。

前面我們講解了 ViewRootImpl 的基本概念辈赋,下面我們來(lái)看下 DecorView 在 Activity 中的作用,先來(lái)看一張圖:

image

DecorView 是一個(gè)應(yīng)用窗口的根容器,它本質(zhì)上是一個(gè) FrameLayout膏燕。DecorView 有唯一一個(gè)子 View钥屈,它是一個(gè)垂直 LinearLayout,包含兩個(gè)子元素坝辫,一個(gè)是 TitleView(ActionBar的容器)篷就,另一個(gè)是 ContentView(窗口內(nèi)容的容器)。關(guān)于 ContentView近忙,它是一個(gè) FrameLayout(android.R.id.content)竭业,我們平常用的 setContentView 就是設(shè)置它的子 View智润。上圖還表達(dá)了每個(gè) Activity 都與一個(gè) Window(具體來(lái)說(shuō)是PhoneWindow)相關(guān)聯(lián),用戶(hù)界面則由 Window 所承載未辆。View 層的事件傳遞也都要先經(jīng)過(guò) DecorView ,然后才傳遞給我們的 View做鹰。

理解 MeasureSpec

為了更好的理解 View 的測(cè)量過(guò)程,我們還需要理解 MeasureSpec 它的含義鼎姐,從名字上得出的解譯,看起來(lái)像是 “測(cè)量規(guī)則” 更振,其實(shí)它就是決定 View 在測(cè)量的時(shí)候的一個(gè)尺寸規(guī)格炕桨。

MeasureSpec

MeasureSpec 代表一個(gè) 32 位 int 值,高 2 位代表 SpecMode ,低 30 位代表 SpecSize ,SpecMode 是指測(cè)量模式肯腕,而 SpecSize 是指在某種測(cè)量代碼模式下的規(guī)格大小献宫,下面先看一下 MeasureSpec 的定義,代碼如下:

   public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
     
        @IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
        @Retention(RetentionPolicy.SOURCE)
        public @interface MeasureSpecMode {}

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


        public static int makeSafeMeasureSpec(int size, int mode) {
            if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
                return 0;
            }
            return makeMeasureSpec(size, mode);
        }

        @MeasureSpecMode
        public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }

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

MeasureSpec 通過(guò)將 SpecMode 和 SpecSize 打包成一個(gè) int 值來(lái)避免過(guò)多的對(duì)象內(nèi)存分配实撒,為了方便操作姊途,其提供了打包和解包方法。SpecMode 和 SpecSize 也是一個(gè) int 值知态,一組 SpecMode 和 SpecSize 可以打包為一個(gè) MeasureSpec , 而一個(gè) MeasureSpec 可以通過(guò)解包的形式來(lái)解出其原始的 SpecMode 和 SpceSize 捷兰,需要注意的是這里提到的 MeasureSpec 是指 MeasureSpec 所代表的 int 值,而并非 MeasureSpec 本身负敏。

SpecMode 有三類(lèi)贡茅,每一類(lèi)都表示特殊的含義,如下所示其做。

UNSPECIFIED: 父容器不對(duì) View 有任何的限制顶考,原始多大就是多大。

EXACTLY: 父容器已經(jīng)檢測(cè)出 View 所需要的精確大小妖泄,這個(gè)時(shí)候 View 的最終大小就是 SpecSize 所指定的值驹沿。它對(duì)應(yīng)于 LayoutParams 中的 match_parent 和 具體數(shù)值 這 2 中模式。

AT_MOST: 父容器指定了一個(gè)可用大小即 SpecSize 蹈胡, View 的大小不能大于這個(gè)值渊季,具體是什么值看不同 View 的具體實(shí)現(xiàn)。它對(duì)應(yīng)于 LayoutParams 中的 wrap_content 审残。

MeasureSpec 和 LayoutParams 對(duì)應(yīng)關(guān)系

在上面提到梭域,系統(tǒng)內(nèi)部是通過(guò) MeasureSpec 來(lái)進(jìn)行 View 的測(cè)量,但是正常情況下我們使用 View 指定 MeasureSpec, 盡管如此搅轿,但是我們可以給 View 設(shè)置 LayoutParams 病涨。在 View 測(cè)量的時(shí)候,系統(tǒng)會(huì)將 LayoutParams 在父容器的約束下轉(zhuǎn)換成對(duì)應(yīng)的 MeasureSpec 璧坟,然后在根據(jù)這個(gè) MeasureSpec 來(lái)確定 View 的測(cè)量后的寬高既穆,需要注意的是赎懦,MeasureSpec 不是唯一由 LayoutParams 決定的,LayoutParams 需要和父容器一起才能決定 View 的 MeasureSpec 幻工,從而進(jìn)一步?jīng)Q定 View 的寬高励两。另外,對(duì)于頂級(jí) View (DecorView) 和普通 View 來(lái)說(shuō)囊颅,MeasureSpec 的轉(zhuǎn)換過(guò)程略有不同当悔。對(duì)于 DecorView ,其 MeasureSpec 由窗口的尺寸和其自身的 LayoutParams 來(lái)共同決定踢代;對(duì)于普通 View 盲憎,其 MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 來(lái)共同決定,MeasureSpec 一旦確定后胳挎,onMeasure 中就可以確定 View 的測(cè)量寬饼疙、高。

對(duì)于 DecorView 來(lái)說(shuō)慕爬,在 ViewRootImpl 中的 measureHierarchy 方法中有如下一段代碼窑眯,它展示了 DecorView 的 MeasureSpec 的創(chuàng)建過(guò)程,其中 desiredWindowWidth 和 desiredWindowHeight 是屏幕的尺寸医窿;

//ViewRootImpl.java
private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
            final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
int childWidthMeasureSpec;
int childHeightMeasureSpec;
 ...
   
  /**
    * 根據(jù)父容器的 size 和 LayoutParsms 寬高來(lái)得到子 View 的測(cè)量規(guī)格
    */
   childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
   childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
   performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);  
 ...
  
  
}

我們?cè)诶^續(xù)看下 getRootMeasureSpec 的源碼實(shí)現(xiàn)磅甩,代碼如下:

//ViewRootImpl.java
    private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {
            /**
             *其實(shí)就是 XML 布局中定義的 MATCH_PARENT
             */
        case ViewGroup.LayoutParams.MATCH_PARENT:
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
            /**
             *其實(shí)就是 XML 布局中定義的 WRAP_CONTENT
             */
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            /**
             *其實(shí)就是 XML 布局中定義的 絕對(duì) px
             */
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }

通過(guò)上面代碼,DecorView 的 MeasureSpec 的產(chǎn)生過(guò)程就很明確了姥卢,具體其準(zhǔn)守如下規(guī)則更胖,根據(jù)它的 LayoutParams 中的寬高的參數(shù)來(lái)劃分。

  • LayoutParams.MATCH_PARENT: 精確模式隔显,大小就是窗口的大小;
  • LayoutParams.WRAP_CONTENT: 最大模式却妨,大小不定,此模式一般是子 View 決定,但是不能超過(guò)父容器的大小;
  • XML 中寬高 px/dp 固定: 精確模式括眠,大小為 LayoutParams 中指定的大小彪标。

那么對(duì)于普通 View 也就是 XML 布局中的 View 是怎么測(cè)量的呢?我們先來(lái)看一下 ViewGroup 的 measureChildWithMargins 方法掷豺,因?yàn)?View 的 measure 過(guò)程就是由它給傳遞過(guò)來(lái)的捞烟,代碼如下:

//ViewGroup.java
    protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
                //得到子元素的 MeasureSpec 
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);
                //對(duì)子View 開(kāi)始進(jìn)行 measure
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

上述方法首先會(huì)拿到子 View 的 LayoutParams 布局中定義的參數(shù),然后根據(jù)父容器的 MeasureSpec 当船、 子元素的 LayoutParams 题画、子元素的 padding 等然后拿到對(duì)子元素的測(cè)量規(guī)格,可以看 getChildMeasureSpec 代碼具體實(shí)現(xiàn):

//ViewGroup.java
        //padding:指父容器中已占用的大小
    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);
                //拿到在父容器中可用的最大的尺寸值
        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;
                //根據(jù)父容器的測(cè)量規(guī)格和 View 本身的 LayoutParams 來(lái)確定子元素的 MeasureSpec 
        switch (specMode) {
        
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {

                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {

                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;


        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {

                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {

                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }

        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

這一段代碼雖然比較多德频,但是還是比較容易理解苍息,它主要的工作就是先拿到在父容器中可用的尺寸,然后根據(jù)父元素的測(cè)量規(guī)格和子元素中 LayoutParams 參數(shù)來(lái)決定當(dāng)前子 View 的 MeasureSpec。

上面的代碼竞思,如果用一張表格來(lái)表示的話表谊,應(yīng)該更好理解,請(qǐng)看下表:

parentSpecMode / childLaoutParams EXACTLY(精準(zhǔn)模式) AT_MOST(最大模式) UNSPECIFIED(精準(zhǔn)模式)
dp/px EXACTLY childSize EXACTLY childSize EXACTLY childSize
match_parent EXACTLY parentSize AT_MOST parentSize UNSPECIFIED 0
Warap_content AT_MOST parentSize AT_MOST parentSize UNSPECIFIED 0

通過(guò)此表可以更加清晰的看出盖喷,只要提供父容器的 MeasureSpec 和子元素的 LayoutParams, 就可以快速的確定出子元素的 MeasureSpec 了爆办,有了 MeasureSpec 就可以進(jìn)一步確定出子元素測(cè)量后的大小了成洗。

View 工作流程

View 的工作流程主要是指 measure锨推、layout 、draw 這三個(gè)流程蔽氨,即 測(cè)量 -> 布局 -> 繪制暮刃,其中 measure 確定 View 測(cè)量寬高挑格,layout 確定 View 的最終寬高和四個(gè)頂點(diǎn)的位置,而 draw 則將 View 繪制到屏幕上沾歪。

在講解 View 的繪制流程之前,我們有必要知道 View 的 measure 何時(shí)觸發(fā)雾消,其實(shí)如果對(duì) Activity 生命周期源碼有所了解的應(yīng)該知道灾搏,在 onCreate 生命周期中,我們做了 setContentView 把 XML 中的節(jié)點(diǎn)轉(zhuǎn)為 View 樹(shù)的過(guò)程立润,然后在 onResume 可以交互的狀態(tài)狂窑,開(kāi)始觸發(fā)繪制工作,可以說(shuō) Activity 的 onResume 是開(kāi)始繪制 View 的入口也不為過(guò)桑腮,下面看入口代碼:

//ActivityThread.java
    final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
        ...

        /**
         * 1. 最終會(huì)調(diào)用 Activity onResume 生命周期函數(shù)
         */
        r = performResumeActivity(token, clearHide, reason);
                    ...
                if (a.mVisibleFromClient) {
                    if (!a.mWindowAdded) {
                        a.mWindowAdded = true;
                        /**
                         * 2. 調(diào)用 ViewManager 的 addView 方法
                         */
                        wm.addView(decor, l);
                    } else {
                       ...

        } else {
           ...
        }
    }

通過(guò)上面代碼我們知道泉哈,首先會(huì)調(diào)用注釋 1 performResumeActivity 方法,其內(nèi)部會(huì)執(zhí)行 Activity onResume 生命周期方法, 然后會(huì)執(zhí)行將 Activity 所有 View 的父類(lèi) DecorView 添加到 Window 的過(guò)程破讨,我們看注釋 2 代碼它調(diào)用的是 ViewManager#addView 方法丛晦,在講解 WindowManager 源碼的時(shí)候,我們知道了 WindowManager 繼承了 ViewManager 然后它們的實(shí)現(xiàn)類(lèi)就是 WindowManagerImpl 所以我們直接看它內(nèi)部 addView 實(shí)現(xiàn):

//WindowManagerImpl.java
    @Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        /**
         * 委托給 WindowManagerGlobal 來(lái)處理 addView
         */
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }

內(nèi)部處理又交給了 WindowManagerGlobal 對(duì)象提陶,我們繼續(xù)跟蹤烫沙,代碼如下:

//WindowManagerGlobal.java
   /**
     *
     * @param view DecorView
     * @param params
     * @param display
     * @param parentWindow
     */
    public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        if (view == null) {
            throw new IllegalArgumentException("view must not be null");
        }
        if (display == null) {
            throw new IllegalArgumentException("display must not be null");
        }
        if (!(params instanceof WindowManager.LayoutParams)) {
            throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
        }

        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
        if (parentWindow != null) {
            /**
             * 1. 根據(jù) WindowManager.LayoutParams 的參數(shù)來(lái)對(duì)添加的子窗口進(jìn)行相應(yīng)的調(diào)整
             */
            parentWindow.adjustLayoutParamsForSubWindow(wparams);
        } else {
            ...
        }

        ViewRootImpl root;
        View panelParentView = null;

        synchronized (mLock) {
           ...

            /**
             * 2. 實(shí)例化 ViewRootImpl 對(duì)象,并賦值給 root 變量
             */
            root = new ViewRootImpl(view.getContext(), display);

            view.setLayoutParams(wparams);

            /**
             * 3. 添加 view 到 mViews 列表中
             */
            mViews.add(view);
            /**
             * 4. 將 root 存儲(chǔ)在 ViewRootImp 列表中
             */
            mRoots.add(root);
            /**
             * 5. 將窗口的參數(shù)保存到布局參數(shù)列表中隙笆。
             */
            mParams.add(wparams);

            // do this last because it fires off messages to start doing things
            try {
                /**
                 * 6. 將窗口和窗口的參數(shù)通過(guò) setView 方法設(shè)置到 ViewRootImpl 中
                 */
                root.setView(view, wparams, panelParentView);
            } catch (RuntimeException e) {
                // BadTokenException or InvalidDisplayException, clean up.
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
                throw e;
            }
        }
    }

我們直接注釋 6 ViewRootImpl#setView 方法锌蓄,代碼如下:

//ViewRootImpl.java
    /**
     * We have one child
     */
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
...

    //1. 請(qǐng)求刷新布局
    requestLayout();

...


}

該類(lèi) setView 代碼比較多,我們直接直接找我們需要的核心代碼注釋 1 撑柔,我們看它內(nèi)部實(shí)現(xiàn)瘸爽,代碼如下:

//ViewRootImpl.java
    @Override
    public void requestLayout() {
        //如果 onMeasure 和 onLayout 工作還沒(méi)完成,那么就不允許調(diào)用 執(zhí)行
        if (!mHandlingLayoutInLayoutRequest) {
            //檢查線程,是否是主線程
            checkThread();
            mLayoutRequested = true;
            //開(kāi)始遍歷
            scheduleTraversals();
        }
    }

上面代碼首先是檢查是否可以開(kāi)始進(jìn)入繪制流程铅忿,我們看 checkThread 方法實(shí)現(xiàn)剪决,代碼如下:

//ViewRootImpl.java
    /**
     * 檢查當(dāng)前線程,如果是子線程就拋出異常。
     */
void checkThread() {
        if (mThread != Thread.currentThread()) {
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }

是不是在程序中這個(gè)異常經(jīng)常遇見(jiàn)昼捍?現(xiàn)在知道它是從哪里拋出來(lái)的了吧识虚,下面我們接著看 scheduleTraversals 方法的實(shí)現(xiàn),代碼如下:

   //ViewRootImpl.java  
   void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            /**
             * mChoreographer:用于接收顯示系統(tǒng)的 VSync 信號(hào)妒茬,在下一幀渲染時(shí)控制執(zhí)行一些操作担锤,
             * 用于發(fā)起添加回調(diào) 在 mTraversalRunnable 的 run 中具體實(shí)現(xiàn)
             */
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

這里代碼的意思就是如果收到系統(tǒng) VSYNC 信號(hào),那么就會(huì)在 mTraversalRunnable run 方法執(zhí)行乍钻,代碼如下:

//ViewRootImpl.java
    void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            //1. 刪除當(dāng)前接收 SYNCBarrier 信號(hào)的回調(diào)
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

            if (mProfile) {
                Debug.startMethodTracing("ViewAncestor");
            }
                        //2. 繪制入口
            performTraversals();

            if (mProfile) {
                Debug.stopMethodTracing();
                mProfile = false;
            }
        }
    }

在文章的開(kāi)始部分我們知道 performTraversals 就是測(cè)量 layout draw 入口肛循,那么我們繼續(xù)看它的實(shí)現(xiàn),代碼如下:

//ViewRootImpl.java
private void performTraversals() {
    ...
    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
    ...
    //1. 執(zhí)行測(cè)量流程
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    ...
    //2. 執(zhí)行布局流程
    performLayout(lp, desiredWindowWidth, desiredWindowHeight);
    ...
    //3. 執(zhí)行繪制流程
    performDraw();
}

//說(shuō)明 1.
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        if (mView == null) {
            return;
        }
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
        try {
            //調(diào)用 View 的 measure 方法 
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }

//說(shuō)明 2.
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) {
 ...
  final View host = mView;//代表 DecorView
  ...
  //內(nèi)部在調(diào)用 View onLayout 
  host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
 ...
  
}

//說(shuō)明 3.
private void performDraw() {
...
  //內(nèi)部通過(guò)調(diào)用 GPU/CPU 來(lái)繪制
   draw(fullRedrawNeeded);
...    
}

到這里上面對(duì)應(yīng)的函數(shù)會(huì)對(duì)應(yīng)調(diào)用 View 的 onMeasure -> onLayout -> ondraw 方法银择,下面我們就具體來(lái)說(shuō)明下繪制過(guò)程多糠。

measure 過(guò)程

measure 過(guò)程要分情況來(lái)看,如果一個(gè)原始的 View 浩考,那么通過(guò) measure 方法就完成了其測(cè)量過(guò)程夹孔,如果是一個(gè) ViewGroup ,除了完成自己的測(cè)量過(guò)程外析孽,還會(huì)遍歷它所有的子 View 的 measure 方法搭伤,各個(gè)子元素在遞歸去執(zhí)行這個(gè)流程(有子 View 的情況),下面針對(duì)這兩種情況分別討論袜瞬。

View 的 measure 過(guò)程

//View.java
    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        boolean optical = isLayoutModeOptical(this);
        ...
        onMeasure(widthMeasureSpec, heightMeasureSpec);

            ....
    }

View 的 measure 過(guò)程由其 measure 方法來(lái)完成怜俐,通過(guò) View#measure 源碼可以知道 它是被 final 修飾的,那么就代表了子類(lèi)不能重寫(xiě)邓尤,通過(guò)上面源碼我們知道在 View#measure 內(nèi)部又會(huì)去調(diào)用 onMeasure 方法拍鲤,我們接著看它的源碼實(shí)現(xiàn),代碼如下:

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

上面的代碼就做了一件事兒,就是設(shè)置測(cè)量之后的寬高值汞扎,我們先來(lái)看看 getDefaultSize 方法季稳,代碼如下:

//View.java
    public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        //根據(jù) measureSpec 拿到當(dāng)前 View 的 specMode
        int specMode = MeasureSpec.getMode(measureSpec);
        //根據(jù) measureSpec 拿到當(dāng)前 View 的 specSize
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED: //一般用于系統(tǒng)內(nèi)部的測(cè)量過(guò)程
            result = size;//直接返回傳遞進(jìn)來(lái)的 size
            break;
        case MeasureSpec.AT_MOST:// wrap_content 模式,大小由子類(lèi)決定但是不能超過(guò)父類(lèi)
        case MeasureSpec.EXACTLY://精準(zhǔn)模式
            result = specSize;//返回測(cè)量之后的大小
            break;
        }
        return result;
    }

通過(guò)上面代碼可以看出澈魄,getDefaultSize 內(nèi)部邏輯不多绞幌,也比較簡(jiǎn)單,對(duì)于我們來(lái)說(shuō)只需要關(guān)心 AT_MOST, EXACTLY 這兩種情況就行一忱,其最終就是返回測(cè)量之后的大小莲蜘。這里要注意的是這里測(cè)量之后的大小并不是最終 View 的大小,最終大小是在 layout 階段確定的帘营,所以這里一定要注意票渠。

我們來(lái)看一下 getSuggestedMinimumXXXX() 源碼實(shí)現(xiàn):

//View.java
    protected int getSuggestedMinimumHeight() {
        return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());

    }
    protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

可以看到 getSuggestedMinimumXXX 內(nèi)部的代碼意思就是,如果 View 沒(méi)有設(shè)置背景芬迄,那么返回 android:minWidth 這個(gè)屬性所指定的值问顷,這個(gè)值可以為 0,如果 View 設(shè)置了背景,則返回 android:minWidth 和背景的 最小寬度/最小高度 這兩者的者中的最大值杜窄,它們返回的就是 UNSPECIFIED 情況下的寬高肠骆。

從 getDefaultSize 方法實(shí)現(xiàn)來(lái)看, View 的寬高由 specSize 決定塞耕,所以我們可以得到如下結(jié)論:既然 measure 被 final 修飾不能重寫(xiě)蚀腿,可是我們?cè)谒鼉?nèi)部也發(fā)現(xiàn)了新大陸 onMeasure 方法,我們可以直接繼承 View 然后重寫(xiě) onMeasure 方法并設(shè)置自身大小扫外。

這里在重寫(xiě) onMeasure 方法的時(shí)候設(shè)置自身寬高需要注意一下,如果在 View 在布局中使用 wrap_content 莉钙,那么它的 specMode 是 AT_MOST 模式,在這種模式下筛谚,它的寬高等于 specSize,也就是父類(lèi)控件空剩余可以使用的空間大小磁玉,這種效果和在布局中使用 match_parent 完全一致,那么如何解決這個(gè)問(wèn)題勒驾讲,可以參考下面代碼:

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        val widthMode = MeasureSpec.getMode(widthMeasureSpec)
        val heightMode = MeasureSpec.getMode(heightMeasureSpec)       
        
        val widthSize = MeasureSpec.getSize(widthMeasureSpec)
        val heightSize = MeasureSpec.getSize(heightMeasureSpec)

        /**
         * 說(shuō)明在布局中使用了 wrap_content 模式
         */
        if (widthMeasureSpec == MeasureSpec.AT_MOST && heightMeasureSpec == MeasureSpec.AT_MOST){
            setMeasuredDimension(mWidth,mHeight)
        }else if (widthMeasureSpec == MeasureSpec.AT_MOST){
            setMeasuredDimension(mWidth,heightSize)
        }else if (heightMeasureSpec == MeasureSpec.AT_MOST){
            setMeasuredDimension(widthSize,mHeight)
        }else {
           setMeasuredDimension(widthSize,heightSize)
        }
    }

在上面代碼中蚊伞,我們只需要給 View 指定一個(gè)默認(rèn)的內(nèi)部寬高(mWidth、mHeight)吮铭,并在 wrap_content 的時(shí)候設(shè)置此寬高即可时迫。對(duì)于非 wrap_content 情形,我們就沿用系統(tǒng)的測(cè)量值即可沐兵。

ViewGroup 的 measure 過(guò)程

對(duì)于 ViewGroup 來(lái)說(shuō),初了完成自己的 measure 過(guò)程以外便监,還會(huì)去遍歷調(diào)用所有的子 View 的 measure 方法扎谎,各個(gè)元素遞歸去執(zhí)行這個(gè)過(guò)程,和 View 不同的是 ViewGroup 是一個(gè)抽象類(lèi)烧董,因此它沒(méi)有重寫(xiě) View 的 onMeasure 方法毁靶,但是它定義了一個(gè) measureChild 方法,代碼如下:

//ViewGroup.java
    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        //拿到所有子 View
        final View[] children = mChildren;
        //遍歷子 View 
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                //依次對(duì)子 View 進(jìn)行測(cè)量
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

上面代碼也很簡(jiǎn)單逊移,ViewGroup 在 measure 時(shí)预吆,會(huì)對(duì)每一個(gè)子元素進(jìn)行 measure ,如下代碼所示:

//ViewGroup.java
    protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        //拿到該子 View 在布局XML中或者代碼中定義的屬性
        final LayoutParams lp = child.getLayoutParams();
                //通過(guò) getChildMeasureSpec 方法胳泉,根據(jù)父元素的寬高測(cè)量規(guī)則拿到子元素的測(cè)量規(guī)則
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);
                //拿到子元素的測(cè)量規(guī)則之后傳遞到 View 中拐叉,開(kāi)始 measure 流程
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

measureChild 代碼邏輯也很容易理解,首先取出設(shè)置的 LayoutParams 參數(shù)扇商,然后通過(guò) getChildMeasureSpec 方法凤瘦,根據(jù)父元素的寬高測(cè)量規(guī)格拿到子元素的測(cè)量規(guī)格,最后將拿到的測(cè)量規(guī)格直接傳遞給 View#measure 來(lái)進(jìn)行測(cè)量案铺。

measure 小總結(jié):

  1. 獲取 View 最終寬高蔬芥,需要在 onLayout 中獲取,因?yàn)?measure 在某些極端的情況下需要測(cè)量多次。
  2. 在 Activity 中獲取 View 的寬高需要使用 Activity/View#onWindowFocusChanged笔诵、view.post(runnable)返吻、ViewTreeObserver 的 onGlobalLayoutListener 回調(diào)手動(dòng)調(diào)用 view.measure(int width,int height) ,最終使用哪個(gè)以實(shí)際情況來(lái)定乎婿。

layout 過(guò)程

measure 完之后就是 layout 確定子 View 的位置测僵,當(dāng) ViewGroup 位置確定以后,它在 onLayout 中會(huì)遍歷所有的子元素并調(diào)用其 layout 方法次酌,在 layout 方法中 onLayout 方法又會(huì)被調(diào)用恨课,我們直接看 View 的 layout 方法,代碼如下:

//View.java
    @SuppressWarnings({"unchecked"})
    public void layout(int l, int t, int r, int b) {
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;

        /**
         * 1. 通過(guò) setFrame 來(lái)初始化四個(gè)點(diǎn)的位置
         */
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            /**
             * 2. 確定 子View 位置
             */
            onLayout(changed, l, t, r, b);

            ...
        }
    }

layout 方法大致流程首先會(huì)通過(guò) setFrame 方法來(lái)設(shè)定 View 四個(gè)頂點(diǎn)位置岳服,View 的四個(gè)頂點(diǎn)一旦確認(rèn)了那么就會(huì)接著調(diào)用 onLayout 方法剂公,這個(gè)方法的用途是父容器確定子元素的位置。

draw 過(guò)程

measure 和 layout 過(guò)程確定了之后就該執(zhí)行繪制的最后一個(gè)流程了 draw,它的作用就是將 View 繪制到屏幕上面吊宋,View 的繪制過(guò)程遵循以下幾點(diǎn):

  1. 繪制背景 backgroud.draw(canvas)
  2. 繪制自己(onDraw)
  3. 繪制 children (dispatchDraw)
  4. 繪制裝飾 (onDrawScrollBars)

下面我們從源碼的角度來(lái)看一下 draw 實(shí)現(xiàn)纲辽,代碼如下:

//ViewRootImpl.java
private void performDraw() {
    ...
    draw(fullRefrawNeeded);
    ...
}

private void draw(boolean fullRedrawNeeded) {
  ....
    
  //使用硬件加速繪制,mView -> DecorView
  mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);
 ...
 //使用 CPU 繪制
 if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
     return;
    }
}

這里我們直接看 CPU 繪制

//ViewRootImpl.java
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty) {
...
 mView.draw(canvas);  
...
}

上面代碼內(nèi)部會(huì)調(diào)用 View#draw 方法,我們直接看內(nèi)部實(shí)現(xiàn)璃搜,代碼如下:

//View.java
public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

        int saveCount;

        if (!dirtyOpaque) {
            /**
             * 1. 繪制背景
             */
            drawBackground(canvas);
        }

        
        final int viewFlags = mViewFlags;
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
        if (!verticalEdges && !horizontalEdges) {
            /**
             * 2. 調(diào)用 ondraw 繪制 View 內(nèi)容
             */
            if (!dirtyOpaque) onDraw(canvas);

           
            /**
             *3. 繪制 View 的子 View
             */
            dispatchDraw(canvas);

            drawAutofilledHighlight(canvas);

            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }

           
            /**
             * 4. 繪制 View 的裝飾
             */
            onDrawForeground(canvas);

         
            drawDefaultFocusHighlight(canvas);

            if (debugDraw()) {
                debugDrawFocus(canvas);
            }

            // we're done...
            return;
        }

        ...
    }

到目前為止拖吼,View 的繪制流程就介紹完了。根節(jié)點(diǎn)是 DecorView这吻,整個(gè) View 體系就是一棵以 DecorView 為根的View 樹(shù)吊档,依次通過(guò)遍歷來(lái)完成 measure、layout 和 draw 過(guò)程唾糯。而如果要自定義 view 怠硼,一般都是通過(guò)重寫(xiě)onMeasure(),onLayout()移怯,onDraw() 來(lái)完成要自定義的部分香璃,整個(gè)繪制流程也基本上是圍繞著這幾個(gè)核心的地方來(lái)展開(kāi)的。

自定義 View

下面我們將詳細(xì)介紹自定義 View 舟误。自定義 View 的作用不用多說(shuō)葡秒,這個(gè)大家應(yīng)該都比較清楚,如果你想做出比較絢麗華彩的 UI 那么僅僅依靠系統(tǒng)的控件是遠(yuǎn)遠(yuǎn)不夠的嵌溢,這個(gè)時(shí)候就必須通過(guò)自定義 View 來(lái)實(shí)現(xiàn)這個(gè)絢麗的效果眯牧。自定義 View 是一個(gè)綜合性技術(shù)體系,它涉及 View 的層次結(jié)構(gòu)赖草、事件分發(fā)炸站、和 View 工作原理等,這些技術(shù)每一項(xiàng)又都是初學(xué)者難以掌握的疚顷,所以前面 2 篇文章我們分別講解了 View 基礎(chǔ)旱易,事件分發(fā)以及該篇文章的 View 工作原理等知識(shí)禁偎,有了這些知識(shí)之后再來(lái)學(xué)習(xí)自定義 View 那將面對(duì)復(fù)雜的 UI 效果也能一一應(yīng)對(duì)了,下面我們就來(lái)認(rèn)識(shí)自定義 View 在該小節(jié)末尾也會(huì)給出實(shí)際例子阀坏,以供大家參考如暖。

自定義 View 分類(lèi)

自定義 View 的分類(lèi)標(biāo)準(zhǔn)不唯一,這里則把它分為四大類(lèi)忌堂,請(qǐng)看下面:

  1. 繼承 View 重寫(xiě) onDraw 方法

這個(gè)方法主要用于實(shí)現(xiàn)一些不規(guī)則的效果盒至,即這種效果不方便通過(guò)布局的組合方式達(dá)到,往往需要靜態(tài)或者動(dòng)態(tài)地顯示一些不規(guī)則的圖形士修。很顯然這需要通過(guò)繪制的方式實(shí)現(xiàn)枷遂,即重寫(xiě) onDraw 方法,采用這種方式需要自己支持 wrap_content 棋嘲,并且 padding 也需要自己處理酒唉。

  1. 繼承 ViewGroup 派生特殊的 Layout

這種方式主要用于實(shí)現(xiàn)自定義的布局,即除了基本系統(tǒng)布局之外沸移,我們重新定義一種新的布局痪伦,當(dāng)某種效果看起來(lái)像幾種 View 組合在一起的時(shí)候,可以采用這種方式來(lái)實(shí)現(xiàn)雹锣。采用這種方式稍微復(fù)雜一些网沾,需要合適的處理 ViewGroup 的 measure 、布局這兩個(gè)過(guò)程蕊爵,并同時(shí)處理子元素的測(cè)量和布局過(guò)程辉哥。

  1. 繼承特定的 View (比如 TextView)

這種方式比較常見(jiàn),一般是用于擴(kuò)展某種已有的 View 的功能攒射,比如 TextView ,這種方法比較容易實(shí)現(xiàn)醋旦。也不需要自己支持 wrap_content 和 padding 等。

  1. 繼承特定的 ViewGroup(比如 LinearLayout)

    這種方式也比較常見(jiàn)匆篓,當(dāng)某種效果看起來(lái)很像幾種 View 組合在一起的時(shí)候浑度,可以采用這種方法來(lái)實(shí)現(xiàn)寇窑。采用這種方法不需要自己處理 ViewGroup 的測(cè)量和布局這 兩個(gè)過(guò)程鸦概,需要注意的這種方法和方法 2 的區(qū)別,一般來(lái)說(shuō)方法 2 能實(shí)現(xiàn)的效果方式 4 也能實(shí)現(xiàn)甩骏,兩則的區(qū)別在于方法 2 更接近 View 的底層窗市。

自定義 View 注意事項(xiàng)

該小節(jié)主要介紹 自定義 View 過(guò)程中的一些注意事項(xiàng),這些問(wèn)題如果處理不好有可能直接導(dǎo)致 View 的正常使用饮笛,具體事項(xiàng)如下:

  1. 讓 View 支持 wrap_content

這是因?yàn)橹苯永^承 View 或者 ViewGroup 的控件咨察,如果不在 onMeasure 中對(duì) wrap_content 做特殊處理,那么當(dāng)外界在布局中使用 wrap_content 屬性時(shí)就無(wú)法達(dá)到預(yù)期的效果福青,具體處理可以參考該篇文章的 View 的 measure 過(guò)程摄狱。

  1. 如果有必要脓诡,讓你的 View 支持 padding

這是因?yàn)橹苯永^承 View 的控件,如果不在 draw 方法中處理 padding ,那么 padding 屬性時(shí)無(wú)法起作用的媒役。另外直接繼承 ViewGroup 的控件需要在 onMeasure 和 onLayout 中考慮 padding 和子元素的 margin 對(duì)其造成的影響祝谚,不然將導(dǎo)致 padding 和 子元素的 margin 失效。

  1. 盡量不要在 View 中使用 Handler

這是因?yàn)?View 內(nèi)部本身就提供了 post 系列的方法酣衷,完全可以替代 Handler 的作用交惯,當(dāng)然除非你很明確需要使用 handler 來(lái)發(fā)送消息。

  1. View 中如果有線程或者動(dòng)畫(huà)穿仪,需要及時(shí)停止席爽,參考 View#onDetachedFromWindow

這一條也很好理解,如果有線程或者動(dòng)畫(huà)需要停止時(shí)啊片,那么 onDetachedFromWindow 是一個(gè)很好的時(shí)機(jī)只锻。當(dāng)包含此 View 的 Activity 退出或者當(dāng)前 View 被 remove 時(shí),View 的 onDetachedFromWindow 方法會(huì)被調(diào)用钠龙,和此方法對(duì)應(yīng)的是 onAttachedToWindow 炬藤,當(dāng)包含此 View 的 Activity 啟動(dòng)時(shí),View 的 onAttachedToWindow 方法會(huì)被調(diào)用碴里,同時(shí)沈矿,當(dāng) View 變得不可見(jiàn)時(shí)我們也需要停止線程和動(dòng)畫(huà),如果不及時(shí)處理這種問(wèn)題咬腋,將有可能會(huì)造成內(nèi)存泄漏羹膳。

  1. View 帶有滑動(dòng)嵌套情形時(shí),需要處理好滑動(dòng)沖突

如果有滑動(dòng)沖突的話根竿,那么就需要合適的處理滑動(dòng)沖突陵像,否則將嚴(yán)重影響 View 的效果。

自定義 View 示例

下面通過(guò)示例代碼來(lái)一起學(xué)習(xí)自定義 View, 下面還是以自定義分類(lèi)來(lái)具體體現(xiàn)寇壳。

  1. 繼承 View 重寫(xiě) onDraw 方法

為了更好的展示一些平時(shí)不容易注意到的問(wèn)題醒颖,這里先實(shí)現(xiàn)一個(gè)很簡(jiǎn)單的自定義控件,我們先繪制一個(gè)圓壳炎,盡管如此泞歉,需要注意的細(xì)節(jié)還是很多的,為了實(shí)現(xiàn)一個(gè)規(guī)范控件匿辩,在實(shí)現(xiàn)過(guò)程必須考慮 wrap_content 模式以及 padding ,同時(shí)為了便捷性腰耙,還要對(duì)外提供自定義屬性,我們先來(lái)看一下代碼實(shí)現(xiàn)铲球,如下:

image
    class CircleView2: View {
        val color = Color.RED
        val paint = Paint(Paint.ANTI_ALIAS_FLAG)
        constructor(context: Context) : super(context) {
            init()
        }
        constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
            init()
        }
        constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
            init()
        }
    
        override fun draw(canvas: Canvas) {
            super.draw(canvas)
            val height = height
            val width = width
            val radius = Math.min(width, height) / 2f
            canvas.drawCircle(width/2f,height/2f,radius,paint)
        }
        private fun init() {
            paint.setColor(color)
            paint.isAntiAlias = true
        }
    }    
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                  android:orientation="vertical"
                  android:layout_width="match_parent"
                  android:layout_height="match_parent">
    
        <com.devyk.customview.sample_1.CircleView2
                android:layout_width="match_parent"
                android:layout_height="100dp"/>
    
    </LinearLayout>

上面簡(jiǎn)單繪制了一個(gè)以當(dāng)前寬高的一半的最小值在自己的中心點(diǎn)繪制一個(gè)紅色的實(shí)心圓挺庞,其實(shí)上面并不是一個(gè)規(guī)范的自定義控件為什么這么說(shuō)呢?我們通過(guò)調(diào)整布局參數(shù)再來(lái)看一下

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                  android:orientation="vertical"
                  android:layout_width="match_parent"
                  android:layout_height="match_parent">
    
        <com.devyk.customview.sample_1.CircleView2
                android:layout_width="match_parent"
                android:background="#000"
                android:layout_height="100dp"/>
    
        <com.devyk.customview.sample_1.CircleView2
                android:layout_width="match_parent"
                android:background="#9C27B0"
                android:layout_margin="20dp"
                android:layout_height="100dp"/>
    </LinearLayout> 
image

運(yùn)行效果如上稼病,可以看到 margin 屬性是有效果的选侨,這是因?yàn)?margin 屬性是由父容器控制的掖鱼,因此不需要再 CircleView2 中做特殊處理。我們現(xiàn)在在來(lái)調(diào)整它的布局參數(shù)援制,為其設(shè)置 20dp 的 padding,如下所示:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                  android:orientation="vertical"
                  android:background="#FF9800"
                  android:layout_width="match_parent"
                  android:layout_height="match_parent">
    
        <com.devyk.customview.sample_1.CircleView2
                android:layout_width="match_parent"
                android:background="#000"
                android:layout_height="100dp"/>
    
        <com.devyk.customview.sample_1.CircleView2
                android:layout_width="match_parent"
                android:background="#9C27B0"
                android:layout_margin="20dp"
                android:layout_height="100dp"/>
    
        <com.devyk.customview.sample_1.CircleView2
                android:layout_width="match_parent"
                android:background="#2196F3"
                android:layout_margin="20dp"
                android:padding="20dp"
                android:layout_height="100dp"/>
    </LinearLayout>    

運(yùn)行效果如下:

image

可以看到 第三個(gè)圓 我們?cè)诓季种性O(shè)置了 padding 結(jié)果根本沒(méi)有無(wú)效锨用,這就是我們?cè)谇懊嫣岬降闹苯永^承自 View 和 ViewGroup 的控件,padding 是默認(rèn)無(wú)法生效的隘谣,需要自己處理增拥,我們?cè)趯⑵鋵挾仍O(shè)置為 wrap_content ,如下:

        <com.devyk.customview.sample_1.CircleView2
                android:layout_width="wrap_content"
                android:background="#8BC34A"
                android:layout_margin="20dp"
                android:padding="20dp"
                android:layout_height="100dp"/> 

運(yùn)行效果如下:

image

結(jié)果發(fā)現(xiàn) wrap_content 并沒(méi)有達(dá)到預(yù)期的效果寻歧,對(duì)比圖上其它的 MATCH_PARENT 屬性繪制的圓其實(shí)跟 wrap_content 一樣掌栅,其實(shí)的確是這樣,這一點(diǎn)在源碼中也講解到了码泛,可以看下面:

        public static int getDefaultSize(int size, int measureSpec) {
            int result = size;
            //根據(jù) measureSpec 拿到當(dāng)前 View 的 specMode
            int specMode = MeasureSpec.getMode(measureSpec);
            //根據(jù) measureSpec 拿到當(dāng)前 View 的 specSize
            int specSize = MeasureSpec.getSize(measureSpec);
    
            switch (specMode) {
            case MeasureSpec.UNSPECIFIED: //一般用于系統(tǒng)內(nèi)部的測(cè)量過(guò)程
                result = size;
                break;
            case MeasureSpec.AT_MOST:// wrap_content 模式猾封,大小由子類(lèi)決定但是不能超過(guò)父類(lèi)
            case MeasureSpec.EXACTLY://精準(zhǔn)模式
                result = specSize;
                break;
            }
            return result;
        }

其實(shí)不管是 AT_MOST 或者 EXACTLY 都是按照 specSize 賦值,大小都是一樣的噪珊,所以為了解決這個(gè)問(wèn)題晌缘,我們需要重寫(xiě) onMeasure 并且在 draw 方法中拿到 padding 然后減去該值 ,先來(lái)看代碼實(shí)現(xiàn):

    
        /**
         * 解決 wrap_content 
         */
        override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec)
            val widthSpecMode = View.MeasureSpec.getMode(widthMeasureSpec)
            val widthSpecSize = View.MeasureSpec.getSize(widthMeasureSpec)
            val heightSpecMode = View.MeasureSpec.getMode(heightMeasureSpec)
            val heightSpecSize = View.MeasureSpec.getSize(heightMeasureSpec)
            if (widthSpecMode == View.MeasureSpec.AT_MOST && heightSpecMode == View.MeasureSpec.AT_MOST) {
                setMeasuredDimension(200, 200)
            } else if (widthSpecMode == View.MeasureSpec.AT_MOST) {
                setMeasuredDimension(200, heightSpecSize)
            } else if (heightSpecMode == View.MeasureSpec.AT_MOST) {
                setMeasuredDimension(widthSpecSize, 200)
            }
        }
    
    
        /**
         * 解決 padding
         */
        override fun draw(canvas: Canvas) {
            super.draw(canvas)
    
            val paddingLeft = paddingLeft
            val paddingRight = paddingRight
            val paddingBottom = paddingBottom
            val paddingTop = paddingTop
            val height = height - paddingBottom - paddingTop
            val width = width - paddingLeft - paddingRight
            val radius = Math.min(width, height) / 2f
    
            canvas.drawCircle(paddingLeft + width/2f,paddingTop + height/2f,radius,paint)
    
    
        } 
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                  android:orientation="vertical"
                  android:background="#FF9800"
                  android:layout_width="match_parent"
                  android:layout_height="match_parent">
    
        <com.devyk.customview.sample_1.CircleView2
                android:layout_width="match_parent"
                android:background="#000"
                android:layout_height="100dp"/>
    
        <com.devyk.customview.sample_1.CircleView2
                android:layout_width="match_parent"
                android:background="#9C27B0"
                android:layout_margin="20dp"
                android:layout_height="100dp"/>
    
        <com.devyk.customview.sample_1.CircleView2
                android:layout_width="match_parent"
                android:background="#2196F3"
                android:layout_margin="20dp"
                android:padding="20dp"
                android:layout_height="100dp"/>
    
        <com.devyk.customview.sample_1.CircleView3
                android:layout_width="wrap_content"
                android:background="#8BC34A"
                android:layout_margin="20dp"
                android:padding="30dp"
                android:layout_height="100dp"/>
    </LinearLayout> 

運(yùn)行效果如下:

image

通過(guò)我們?cè)?draw 中減去了 各自的 padding 解決了 padding 的問(wèn)題痢站,通過(guò)重寫(xiě) onMeasure 對(duì)該 View 設(shè)置寬高磷箕,解決了 wrap_content 屬性的效果。

最后為了我們的自定義控件的擴(kuò)展性阵难,我們需要給它實(shí)現(xiàn)自定義屬性岳枷,步驟如下所示:

    //1. 我們?cè)?values 目錄下創(chuàng)建一個(gè) attrs 開(kāi)頭的文件夾,然后在創(chuàng)建一個(gè) attrs_circle 的文件呜叫,
    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <declare-styleable name="CircleView">
            <attr name="circle_view_color" format="color"></attr>
        </declare-styleable>
    </resources>
    
    //2. 代碼中進(jìn)行解析屬性值
        private fun initTypedrray(context: Context, attrs: AttributeSet) {
            //拿到自定義屬性組
            val obtainStyledAttributes = context.obtainStyledAttributes(attrs, R.styleable.CircleView)
            color = obtainStyledAttributes.getColor(R.styleable.CircleView_circle_view_color, Color.RED)
            obtainStyledAttributes.recycle()
    
        }
    
    //3. 布局中聲明自定義屬性的空間空繁,在根布局中添加如下屬性
     xmlns:app="http://schemas.android.com/apk/res-auto"
    
    //4. 在自定義 View 中配置該屬性值
        <com.devyk.customview.sample_1.CircleView3
                android:layout_width="wrap_content"
                android:background="#8BC34A"
                android:layout_margin="20dp"
                app:circle_view_color = "#3F51B5"
                android:padding="20dp"
                android:layout_height="100dp"/>  

運(yùn)行效果如下:

image

自定義屬性也配置完成了。這樣做的好處是在不修改原始代碼的情況下朱庆,可以讓用戶(hù)自定義顏色值盛泡,擴(kuò)展性比較強(qiáng)。

  1. 繼承 ViewGroup 派生特殊的 Layout

我們先看一下我們需要實(shí)現(xiàn)的效果->流式布局

image
  1. 定義用于裝 x 軸 View娱颊,y 軸 height, 容器中所有子 View 的容器
            /**
             * 定義一個(gè)裝所有子 View 的容器
             */
            protected var mAllViews: MutableList<List<View>> = ArrayList<List<View>>()
            /**
             * 定義行高
             */
            protected var mLineHeight: MutableList<Int> = ArrayList()
            /**
             * 定義行寬
             */
            protected var mLineWidth: MutableList<Int> = ArrayList()
            /**
             * 當(dāng)前行上的子 View 控件
             */
            protected var mLinViews: MutableList<View> = ArrayList<View>()  

  1. 重寫(xiě) View onMeasure 方法傲诵,測(cè)量每一個(gè)子 View 的寬高,并且計(jì)算 X 軸上每一個(gè)子 View 的寬度是否超出總的 width
            /**
             * 1. 確定所有子 View 的寬高
             */
            override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
                super.onMeasure(widthMeasureSpec, heightMeasureSpec)
                //根據(jù)當(dāng)前寬高的測(cè)量模式维蒙,拿到寬高和當(dāng)前模式
                val widthMode = MeasureSpec.getMode(widthMeasureSpec)
                val heightMode = MeasureSpec.getMode(heightMeasureSpec)
                val widthSize = MeasureSpec.getSize(widthMeasureSpec)
                val heightsize = MeasureSpec.getSize(heightMeasureSpec)
        
                //如果當(dāng)前容器 XML 布局定義的 wrap_content 那么就需要自己解決實(shí)際測(cè)量高度
                var width = 0
                var height = 0
        
                //當(dāng)前行高/寬
                var lineWidth = 0
                var lineHeight = 0
        
                //拿到所有子 View 總數(shù)
                val allViewCount = childCount
        
                //遍歷進(jìn)行對(duì)子 View 進(jìn)行測(cè)量
                for (child in 0..allViewCount -1 ){
                    //拿到當(dāng)前 View
                    var childView = getChildAt(child)
                    //判斷當(dāng)前 view 是否隱藏狀態(tài)
                    if (childView.visibility == View.GONE) {
                        //如果是最后一個(gè),拿到當(dāng)前行高
                        if (child == allViewCount - 1){
                            width = Math.max(lineWidth,width)
                            height += lineHeight
                        }
                        continue
                    }
                    //對(duì) childView 進(jìn)行測(cè)量
                    measureChild(childView,widthMeasureSpec,heightMeasureSpec)
                    //拿到當(dāng)前子 View 布局參數(shù)
                    val marginLayoutParams = childView.layoutParams as MarginLayoutParams
                    //拿到測(cè)量之后的寬掰吕、高 + 設(shè)置的 margin
                    val childWidth = childView.measuredWidth + marginLayoutParams.leftMargin + marginLayoutParams.rightMargin
                    val childHeight = childView.measuredHeight + marginLayoutParams.topMargin + marginLayoutParams.bottomMargin
        
                    //說(shuō)明已經(jīng)放不下
                    if (lineWidth + childWidth > widthSize - paddingLeft - paddingRight){
                        //拿到當(dāng)前行最大的寬值
                        width = Math.max(width,lineWidth)
                        //當(dāng)前行的寬度
                        lineWidth = childWidth
                        //子 View 總高度
                        height += lineHeight
                        //當(dāng)前行的高度
                        lineHeight = childHeight
                    }else{
                        //將子 View 的寬度累計(jì)相加
                        lineWidth += childWidth
                        //拿到當(dāng)前行最大的高度
                        lineHeight = Math.max(lineHeight,childHeight)
        
                    }
                }
                //設(shè)置當(dāng)前容器的寬高
                setMeasuredDimension(
                    //判斷是否是 match——parent 模式如果不是,那么就是 wrap_content 或者 精準(zhǔn) dp 模式果覆,需要所有子 View 寬/高 相加
                    if (widthMode === MeasureSpec.EXACTLY) widthSize else width + paddingLeft + paddingRight,
                    if (heightMode === MeasureSpec.EXACTLY) heightsize else height + paddingTop + paddingBottom
                )
            }
  1. 將測(cè)量好的子 View 開(kāi)始放入 ViewGroup 中
            /**
             * 2. 確定所有子 View 的位置
             */
            override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
                //清空容器里面的數(shù)據(jù)
                mAllViews.clear()
                mLineHeight.clear()
                mLineWidth.clear()
                mLinViews.clear()
        
                //拿到控件的寬
                var width  = width
                //當(dāng)前行寬
                var lineWidth = 0
                //當(dāng)前行高
                var lineHeight = 0
                //當(dāng)前 childCount
                val childCount = childCount
                //遍歷子 View
                for (childIndex in 0..childCount-1){
                    var childView  = getChildAt(childIndex)
                    if(childView.visibility == View.GONE)continue
                    val marginLayoutParams = childView.layoutParams as MarginLayoutParams
                    //拿到最后 View 真實(shí)寬高
                    val measuredWidth = childView.measuredWidth
                    val measuredHeight = childView.measuredHeight
        
                    //當(dāng)前子 View 的寬+ 當(dāng)前行寬再加當(dāng)前 margin 如果大于當(dāng)前總寬的話 說(shuō)明放不下了颅痊,需要換行
                    if (measuredWidth + lineWidth + marginLayoutParams.leftMargin + marginLayoutParams.rightMargin > width - paddingRight-paddingLeft){
                        //當(dāng)前行的最大的高
                        mLineHeight.add(lineHeight)
                        //當(dāng)前行總寬度
                        mLineWidth.add(lineWidth)
                        //這里面裝的是每一行所有的子View,該容器的 size 取決于 有多少行
                        mAllViews.add(mLinViews)
                        //將下一行的寬設(shè)置為 0 初始高度
                        lineWidth = 0
                        //將下一行的高度初始為第一個(gè)子 View 的高度
                        lineHeight = measuredHeight
                        //初始化一個(gè)容器局待,用于裝下一行所有的子 View
                        mLinViews = ArrayList<View>()
        
                        Log.d(TAG,"lineWidth:$lineWidth lineHeight:$lineHeight")
                    }
        
                    //依次加當(dāng)前 View 占用的寬
                    lineWidth += measuredWidth
                    //找出當(dāng)前子 View 最大的height
                    lineHeight = Math.max(lineHeight,measuredHeight + marginLayoutParams.bottomMargin + marginLayoutParams.topMargin)
                    //將行上的 VIew 添加到容器里面
                    mLinViews.add(childView)
                    Log.d(TAG,"--- lineWidth:$lineWidth lineHeight:$lineHeight")
                }
        
        
                mLineHeight.add(lineHeight)
                mLineWidth.add(lineWidth)
                mAllViews.add(mLinViews)
        
                var left = paddingLeft
                var top  = paddingTop
        
                //拿到當(dāng)前所有的子 VIew
                for (curAllView in 0..mAllViews.size -1){
                    mLinViews = mAllViews.get(curAllView) as ArrayList
                    lineHeight = mLineHeight.get(curAllView)
        
                    val curLinewidth = mLineHeight.get(curAllView)
                    when(mGravity){
                        LEFT -> left = paddingLeft
                        CENTER -> (width - curLinewidth)/2 + paddingLeft
                        RIGHT -> {
                            left = width - (curLinewidth + paddingLeft) - paddingRight
                            Collections.reverse(mLinViews)
                        }
                    }
        
                    mLinViews.forEach lit@{
                        if (it.visibility == View.GONE)return@lit
        
                        val lp = it.layoutParams as MarginLayoutParams
                        var lc = left + lp.leftMargin
                        var tc = top + lp.topMargin
                        var rc = lc + it.measuredWidth
                        var bc = tc + it.measuredHeight
        
                        Log.d(TAG,"lc:$lc tc:$tc rc:$rc bc:$bc");
        
                        //開(kāi)始放入子 VIew
                        it.layout(lc,tc,rc,bc)
        
                        left += it.measuredWidth + lp.leftMargin + lp.rightMargin
                    }
                    top += lineHeight
                }
            }    
  1. 添加子 View
                mFlowLayout.setAdapter(object : TagAdapter<String>(mVals) {
        
                    override fun getView(parent: FlowLayout, position: Int, s: String): View {
                        val tv = LayoutInflater.from(applicationContext).inflate(
                            R.layout.tv,
                            mFlowLayout, false
                        ) as TextView
                        tv.text = s
                        Log.d(TAG,s);
                        return tv
                    }
                })   

到這里就已經(jīng)將流式布局繪制出來(lái)了斑响,該源碼我參考的是 hongyangAndroid/FlowLayout 上面代碼跟源碼略有不同菱属,我這個(gè)是 kotlin 版本,需要看所有代碼舰罚,請(qǐng)移步源代碼倉(cāng)庫(kù)纽门。

通過(guò)學(xué)習(xí)該案例你將學(xué)習(xí)到自定義 ViewGroup 的流程和如果測(cè)量子 View 及如何放置子 View.建議不會(huì)都一定要敲一遍,才能加深對(duì)自定義 View 的認(rèn)識(shí)营罢。

總結(jié)

到這里赏陵,自定義 View 相關(guān)的知識(shí)都已經(jīng)介紹完了饲漾,在閱讀該篇文章之前首先要對(duì) View 有一個(gè)整體的認(rèn)識(shí)蝙搔,比如如果在 View 、ViewGroup 中進(jìn)行 measure 考传,如何解決 xml 中定義的 wrap_content 和 padding 邊距吃型。繪制流程是如何進(jìn)行的。我相信看完該篇文章你對(duì) View 的認(rèn)識(shí)會(huì)更加深刻僚楞。

感謝你的閱讀勤晚,謝謝!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末泉褐,一起剝皮案震驚了整個(gè)濱河市赐写,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌膜赃,老刑警劉巖血淌,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異财剖,居然都是意外死亡悠夯,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)躺坟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)沦补,“玉大人,你說(shuō)我怎么就攤上這事咪橙∠Π颍” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵美侦,是天一觀的道長(zhǎng)产舞。 經(jīng)常有香客問(wèn)我,道長(zhǎng)菠剩,這世上最難降的妖魔是什么宏蛉? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任佛玄,我火速辦了婚禮,結(jié)果婚禮上餐抢,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著炮赦,像睡著了一般。 火紅的嫁衣襯著肌膚如雪样勃。 梳的紋絲不亂的頭發(fā)上吠勘,一...
    開(kāi)封第一講書(shū)人閱讀 51,125評(píng)論 1 297
  • 那天,我揣著相機(jī)與錄音峡眶,去河邊找鬼看幼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛幌陕,可吹牛的內(nèi)容都是我干的诵姜。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼搏熄,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼棚唆!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起心例,我...
    開(kāi)封第一講書(shū)人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤宵凌,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后止后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體瞎惫,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年译株,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了瓜喇。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡歉糜,死狀恐怖乘寒,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情匪补,我是刑警寧澤伞辛,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站夯缺,受9級(jí)特大地震影響蚤氏,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜踊兜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一竿滨、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦姐呐、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至骏掀,卻和暖如春鸠澈,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背截驮。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工笑陈, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人葵袭。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓涵妥,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親坡锡。 傳聞我的和親對(duì)象是個(gè)殘疾皇子蓬网,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

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