自定義控件三部曲之視圖篇

該文為匯總網(wǎng)上優(yōu)秀的資源所得虏肾,喜歡請關(guān)注原作者,我記錄下來主要是為了以后我自己查找用的
ref 參考

啟艦

Android自定義控件三部曲文章索引

HenCoder Android UI 部分 2-1 布局基礎(chǔ)

一才漆、測量與布局

1. ViewGroup繪制流程

注意,View及ViewGroup基本相同佛点,只是在ViewGroup中不僅要繪制自己還是繪制其中的子控件醇滥,而View則只需要繪制自己就可以了,所以我們這里就以ViewGroup為例來講述整個繪制流程超营。

繪制流程分為三步:測量鸳玩、布局、繪制
分別對應(yīng):onMeasure() -> onLayout() -> onDraw()

image.png

其中演闭,他們?nèi)齻€的作用分別如下:

onMeasure():測量自己的大小不跟,為正式布局提供建議。(注意米碰,只是建議窝革,至于用不用,要看onLayout);
onLayout():使用layout()函數(shù)對所有子控件布局吕座;
onDraw():根據(jù)布局的位置繪圖虐译;

2. onMeasure與MeasureSpec

布局繪畫涉及兩個過程:測量過程布局過程
測量過程通過measure方法實現(xiàn)吴趴,是View樹自頂向下的遍歷漆诽,每個View在循環(huán)過程中將尺寸細(xì)節(jié)往下傳遞,當(dāng)測量過程完成之后,所有的View都存儲了自己的尺寸厢拭。第二個過程則是通過方法layout來實現(xiàn)的兰英,也是自頂向下的。在這個過程中蚪腐,每個父View負(fù)責(zé)通過計算好的尺寸放置它的子View箭昵。
前面講過税朴,onMeasure()是用來測量當(dāng)前控件大小的回季,給onLayout() 提供數(shù)值參考,需要特別注意的是:測量完成以后通過setMeasuredDimension(int,int)設(shè)置給系統(tǒng)正林。

2.1 onMeasure

首先泡一,看一下onMeasure()的聲明:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)

這里我們主要關(guān)注傳進(jìn)來的兩個參數(shù):int widthMeasureSpec, int heightMeasureSpec

他們的意義:

他們是父類傳遞過來給當(dāng)前view的一個建議值,即想把當(dāng)前view的尺寸設(shè)置為寬widthMeasureSpec,高h(yuǎn)eightMeasureSpec

有關(guān)他們的組成觅廓,我們就直接轉(zhuǎn)到MeasureSpec部分鼻忠。

2.2 MeasureSpec

雖然表面上看起來他們是int類型的數(shù)字,其實他們是由mode+size兩部分組成的杈绸。

widthMeasureSpec和heightMeasureSpec轉(zhuǎn)化成二進(jìn)制數(shù)字表示帖蔓,他們都是32位的。

前兩位代表mode(測量模式)瞳脓,后面30位才是他們的實際數(shù)值(size)塑娇。

(1)模式分類

它有三種模式:

①、UNSPECIFIED(未指定)劫侧,父元素不對子元素施加任何束縛埋酬,子元素可以得到任意想要的大小烧栋;
②写妥、EXACTLY(完全),父元素決定自元素的確切大小审姓,子元素將被限定在給定的邊界里而忽略它本身大小珍特;
③、AT_MOST(至多)魔吐,子元素至多達(dá)到指定大小的值扎筒。

他們對應(yīng)的二進(jìn)制值分別是:

UNSPECIFIED=00000000000000000000000000000000

EXACTLY =01000000000000000000000000000000

AT_MOST =10000000000000000000000000000000

由于最前面兩位代表模式,所以他們分別對應(yīng)十進(jìn)制的0画畅,1砸琅,2;

(2)模式提取

現(xiàn)在我們知道了widthMeasureSpec和heightMeasureSpec是由模式和數(shù)值組成的轴踱,而且二進(jìn)制的前兩位代表模式症脂,后28位代表數(shù)字。

andorid系統(tǒng)提供提取模式和數(shù)值的類,MeasureSpec

下面兩個函數(shù)就可以實現(xiàn)這個功能:

//MODE的取值為
MeasureSpec.AT_MOST

MeasureSpec.EXACTLY

MeasureSpec.UNSPECIFIED
//獲取MODE
MeasureSpec.getMode(int spec) 
//獲取數(shù)值
MeasureSpec.getSize(int spec) 

通過下面的代碼就可以分別獲取widthMeasureSpec和heightMeasureSpec的MODE和數(shù)值

int measureWidth = MeasureSpec.getSize(widthMeasureSpec);

int measureHeight = MeasureSpec.getSize(heightMeasureSpec);

int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);

int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);

(3)模式有什么用呢

我們知道有三個模式:EXACTLY诱篷、AT_MOST壶唤、UNSPECIFIED

需要注意的是widthMeasureSpec和heightMeasureSpec各自都有它對應(yīng)的模式,模式的由來分別來自于XML定義:

簡單來說棕所,XML布局和模式有如下對應(yīng)關(guān)系:

  • wrap_content-> MeasureSpec.AT_MOST
  • match_parent -> MeasureSpec.EXACTLY
  • 具體值 -> MeasureSpec.EXACTLY

例如闸盔,下面這個XML

<com.example.harvic.myapplication.FlowLayout
     android:layout_width="match_parent"
     android:layout_height="wrap_content">
</com.example.harvic.myapplication.FlowLayout>

那FlowLayout在onMeasure()中傳值時widthMeasureSpec的模式就是 MeasureSpec.EXACTLY,即父窗口寬度值琳省。heightMeasureSpec的模式就是 MeasureSpec.AT_MOST迎吵,即不確定的。

一定要注意是针贬,當(dāng)模式是MeasureSpec.EXACTLY時击费,我們就不必要設(shè)定我們計算的大小了,因為這個大小是用戶指定的桦他,我們不應(yīng)更改蔫巩。但當(dāng)模式是MeasureSpec.AT_MOST時,也就是說用戶將布局設(shè)置成了wrap_content快压,我們就需要將大小設(shè)定為我們計算的數(shù)值圆仔,因為用戶根本沒有設(shè)置具體值是多少,需要我們自己計算蔫劣。

即坪郭,假如width和height是我們經(jīng)過計算的控件所占的寬度和高度。那在onMeasure()中使用setMeasuredDimension()最后設(shè)置時截粗,代碼應(yīng)該是這樣的:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
    int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
    
    int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
    int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);

    //經(jīng)過計算,控件所占的寬和高分別對應(yīng)width和height
    //計算過程鸵隧,我們會在下篇細(xì)講
    …………
    setMeasuredDimension((measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth: width, (measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight: height);

}

3. onLayout()

3.1 概述

上面說了绸罗,onLayout()是實現(xiàn)所有子控件布局的函數(shù)。注意豆瘫,是所有子控件I后啊!外驱!那它自己的布局怎么辦育灸?后面我們再講,先講講在onLayout()中我們應(yīng)該做什么昵宇。

我們先看看ViewGroup的onLayout()函數(shù)的默認(rèn)行為是什么

在ViewGroup.java中

@Override
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);

是一個抽象方法磅崭,說明凡是派生自ViewGroup的類都必須自己去實現(xiàn)這個方法。像LinearLayout瓦哎、RelativeLayout等布局砸喻,都是重寫了這個方法柔逼,然后在內(nèi)部按照各自的規(guī)則對子視圖進(jìn)行布局的。

3.2 實例

下面我們就舉個例子來看一下有關(guān)onMeasure()和onLayout()的具體使用:

//com.richy.kotlindemo.customview.MyLinLayout
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        val measureWidth = View.MeasureSpec.getSize(widthMeasureSpec)
        val measureHeight = View.MeasureSpec.getSize(heightMeasureSpec)
        val measureWidthMode = View.MeasureSpec.getMode(widthMeasureSpec)
        val measureHeightMode = View.MeasureSpec.getMode(heightMeasureSpec)

        var height = 0
        var width = 0

        for (i in 0 until childCount) {
            //測量子控件
            val child = getChildAt(i)
            //measureChild 測量子控件
            measureChild(child, widthMeasureSpec, heightMeasureSpec)
            //獲得子控件的高度和寬度
            val childHeight = child.measuredHeight
            val childWidth = child.measuredWidth
            //得到最大寬度割岛,并且累加高度
            height += childHeight
            width = Math.max(childWidth, width)
        }

        //設(shè)置測量結(jié)果
        setMeasuredDimension(if (measureWidthMode === View.MeasureSpec.EXACTLY) measureWidth else width, if (measureHeightMode === View.MeasureSpec.EXACTLY) measureHeight else height)

        /*
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
        示例中的這里的measureWidthMode應(yīng)該是MeasureSpec.EXACTLY,measureHeightMode應(yīng)該是MeasureSpec.AT_MOST愉适;

        * */
    }

總體來講,onMeasure()中計算出的width和height癣漆,就是當(dāng)XML布局設(shè)置為layout_width="wrap_content"维咸、layout_height="wrap_content"時所占的寬和高;即整個container所占的最小矩形 (因為 match_parent -> MeasureSpec.EXACTLY惠爽,具體值 -> MeasureSpec.EXACTLY)

override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
    var top = 0;
    for (i in 0 until childCount) {
        val child = getChildAt(i)

        val childWidth = child.measuredWidth
        val childHeight = child.measuredHeight

        child.layout(0, top, childWidth, top + childHeight)
        top += childHeight
    }
}

(1)癌蓖、getMeasuredWidth()與getWidth()

趁熱打鐵,就這個例子疆股,我們講一個很容易出錯的問題:getMeasuredWidth()與getWidth()的區(qū)別费坊。他們的值大部分時間都是相同的倒槐,但意義確是根本不一樣的旬痹,我們就來簡單分析一下。

區(qū)別主要體現(xiàn)在下面幾點:
- 首先getMeasureWidth()方法在measure()過程結(jié)束后就可以獲取到了讨越,而getWidth()方法要在layout()過程結(jié)束后才能獲取到两残。
- getMeasureWidth()方法中的值是通過setMeasuredDimension()方法來進(jìn)行設(shè)置的,而getWidth()方法中的值則是通過layout(left,top,right,bottom)方法設(shè)置的把跨。

還記得嗎人弓,我們前面講過,setMeasuredDimension()提供的測量結(jié)果只是為布局提供建議着逐,最終的取用與否要看layout()函數(shù)崔赌。大家再看看我們上面重寫的MyLinLayout,是不是我們自己使用child.layout(left,top,right,bottom)來定義了各個子控件所應(yīng)在的位置:

val childWidth = child.measuredWidth
val childHeight = child.measuredHeight

child.layout(0, top, childWidth, top + childHeight)
top += childHeight

從代碼中可以看到耸别,我們使用child.layout(0, top, childWidth, top + childHeight);來布局控件的位置健芭,其中g(shù)etWidth()的取值就是這里的右坐標(biāo)減去左坐標(biāo)的寬度;因為我們這里的寬度是直接使用的child.getMeasuredWidth()的值秀姐,當(dāng)然會導(dǎo)致getMeasuredWidth()與getWidth()的值是一樣的慈迈。如果我們在調(diào)用layout()的時候傳進(jìn)去的寬度值不與getMeasuredWidth()相同,那必然getMeasuredWidth()與getWidth()的值就不再一樣了省有。

一定要注意的一點是:getMeasureWidth()方法在measure()過程結(jié)束后就可以獲取到了痒留,而getWidth()方法要在layout()過程結(jié)束后才能獲取到。再重申一遍4姥亍I焱贰!O象啊恤磷!

3.3 疑問:container自己什么時候被布局

前面我們說了弧轧,在派生自ViewGroup的container中,比如我們上面的MyLinLayout碗殷,在onLayout()中布局它所有的子控件精绎。那它自己什么時候被布局呢?

它當(dāng)然也有父控件锌妻,它的布局也是在父控件中由它的父控件完成的代乃,就這樣一層一層地向上由各自的父控件完成對自己的布局。直到所有控件的最頂層結(jié)點仿粹,在所有的控件的最頂部有一個ViewRoot搁吓,ViewRootImpl是View中的最高層級,屬于所有View的根(但ViewRootImpl不是View吭历,只是實現(xiàn)了ViewParent接口)堕仔,實現(xiàn)了View和WindowManager之間的通信協(xié)議。那讓我們來看看它是怎么來做的吧晌区。

每個Activity中都包含一個Window對象摩骨,通常,Android中的Window是由PhoneWindow實現(xiàn)的朗若。而PhoneWindow又將一個DecorView設(shè)置為整個窗口的根View(DecorView是一個ViewGroup)恼五。

在它布局里,會調(diào)用一個layout()函數(shù)(不能被重載哭懈,代碼位于View.java):

/* final 標(biāo)識符 灾馒, 不能被重載 , 參數(shù)為每個視圖位于父視圖的坐標(biāo)軸 
 * @param l Left position, relative to parent 

 * @param t Top position, relative to parent 

 * @param r Right position, relative to parent 

 * @param b Bottom position, relative to parent

 */  
public final void layout(int l, int t, int r, int b) {  
    boolean changed = setFrame(l, t, r, b); //設(shè)置每個視圖位于父視圖的坐標(biāo)軸  
    if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {  
        onLayout(changed, l, t, r, b);//回調(diào)onLayout函數(shù) 遣总,設(shè)置每個子視圖的布局  
        mPrivateFlags &= ~LAYOUT_REQUIRED;  
    }  

    mPrivateFlags &= ~FORCE_LAYOUT;  

在SetFrame(l,t,r,b)就是設(shè)置自己的位置睬罗,設(shè)置結(jié)束以后才會調(diào)用onLayout(changed, l, t, r, b)來設(shè)置內(nèi)部所有子控件的位置。

OK啦旭斥,到這里有關(guān)onMeasure()和onLayout()的內(nèi)容就講完啦容达,想必大家應(yīng)該也對整個布局流程有了一個清楚的認(rèn)識了,下面我們再看一個緊要的問題:如何得到自定義控件的左右間距margin值琉预。

4. 獲取子控件Margin的方法

4.1 獲取方法及示例

如果要自定義ViewGroup支持子控件的layout_margin參數(shù)董饰,則自定義的ViewGroup類必須重載generateLayoutParams()函數(shù),并且在該函數(shù)中返回一個ViewGroup.MarginLayoutParams派生類對象圆米,這樣才能使用margin參數(shù)卒暂。

需要特別注意的是,如果我們在onLayout()中根據(jù)margin來布局的話娄帖,那么我們在onMeasure()中計算container的大小時也祠,也要加上margin,不然會導(dǎo)致container太小近速,而控件顯示不全的問題诈嘿。


    override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams {
        return MarginLayoutParams(context, attrs)//注意這里返回的是MarginLayoutParams堪旧,而不是LayoutParams
    }

    override fun generateLayoutParams(p: LayoutParams?): LayoutParams {
        return MarginLayoutParams(p)
    }

/*
*如果要使用默認(rèn)的構(gòu)造方法,就生成layout_width="match_parent"奖亚、layout_height="match_parent"對應(yīng)的參數(shù)
*/
    override fun generateDefaultLayoutParams(): LayoutParams {
        return MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
    }


override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        ...
        for (i in 0 until childCount) {
            //測量子控件
            val child = getChildAt(i)
            //measureChild 測量子控件
            measureChild(child, widthMeasureSpec, heightMeasureSpec)
            //獲得子控件的高度和寬度
//            val childHeight = child.measuredHeight
//            val childWidth = child.measuredWidth

            val lp =  child.layoutParams as ViewGroup.MarginLayoutParams
            val childHeight = child.measuredHeight + lp.topMargin + lp.bottomMargin
            val childWidth = child.measuredWidth + lp.leftMargin + lp.rightMargin

            //得到最大寬度淳梦,并且累加高度
            height += childHeight
            width = Math.max(childWidth, width)
        }
    
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        var top = 0
        for (i in 0 until childCount) {
            val child = getChildAt(i)

//            val childWidth = child.measuredWidth
//            val childHeight = child.measuredHeight
//            child.layout(0, top, childWidth, top + childHeight)
            val lp = child.layoutParams as ViewGroup.MarginLayoutParams//generateLayoutParams()返回的是MarginLayoutParams
            val childHeight = child.measuredHeight + lp.topMargin + lp.bottomMargin
            val childWidth = child.measuredWidth + lp.leftMargin + lp.rightMargin
            child.layout(lp.leftMargin, top + lp.topMargin, child.measuredWidth + lp.leftMargin, top + child.measuredHeight + lp.topMargin);
            top += childHeight
        }
        
    }

4.2 原理

上面我們看了要重寫generateDefaultLayoutParams()函數(shù)才能獲取控件的margin間距。那為什么要重寫呢昔字?下面這句就為什么非要強轉(zhuǎn)呢爆袍?

MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

下面我們來看看這么做的原因。

/**
*從指定的XML中獲取對應(yīng)的layout_width和layout_height值
*/
public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new LayoutParams(getContext(), attrs);//默認(rèn)返回LayoutParams
}
/*
*如果要使用默認(rèn)的構(gòu)造方法作郭,就生成layout_width="wrap_content"陨囊、layout_height="wrap_content"對應(yīng)的參數(shù)
*/
protected LayoutParams generateDefaultLayoutParams() {
     return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}

首先,在container在初始化子控件時夹攒,會調(diào)用LayoutParams#generateLayoutParams(LayoutParams p)來為子控件生成對應(yīng)的布局屬性蜘醋,但默認(rèn)只是生成layout_width和layout_height所以對應(yīng)的布局參數(shù),即在正常情況下的generateLayoutParams()函數(shù)生成的LayoutParams實例是不能夠取到margin值的咏尝。即:

(1)generateLayoutParams()的默認(rèn)實現(xiàn)

//位于ViewGrop.java中
public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new LayoutParams(getContext(), attrs);
}
public LayoutParams(Context c, AttributeSet attrs) {
    TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
    setBaseAttributes(a,
            R.styleable.ViewGroup_Layout_layout_width,
            R.styleable.ViewGroup_Layout_layout_height);
    a.recycle();
}
protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
    width = a.getLayoutDimension(widthAttr, "layout_width");
    height = a.getLayoutDimension(heightAttr, "layout_height");
}

從上面的代碼中明顯可以看出压语,generateLayoutParams()調(diào)用LayoutParams()產(chǎn)生布局信息,而LayoutParams()最終調(diào)用setBaseAttributes()來獲得對應(yīng)的寬状土,高屬性无蜂。
這里是通過TypedArray對自定義的XML進(jìn)行值提取的過程。從這里也可以看到蒙谓,generateLayoutParams生成的LayoutParams屬性只有l(wèi)ayout_width和layout_height的屬性值。

(2)MarginLayoutParams實現(xiàn)

下面再來看看MarginLayoutParams的具體實現(xiàn)训桶,其實通過上面的過程累驮,大家也應(yīng)該想到,它也是通過TypeArray來解析自定義屬性來獲得用戶的定義值的舵揭。

public MarginLayoutParams(Context c, AttributeSet attrs) {
    super();
 
    TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_MarginLayout);
    int margin = a.getDimensionPixelSize(
            com.android.internal.R.styleable.ViewGroup_MarginLayout_layout_margin, -1);
//第一部分:提取layout_margin的值并設(shè)置
    if (margin >= 0) {
        leftMargin = margin;
        topMargin = margin;
        rightMargin= margin;
        bottomMargin = margin;
    } else {
//第二部分:如果用戶沒有設(shè)置layout_margin谤专,而是單個設(shè)置的,那么就一個個提取
       leftMargin = a.getDimensionPixelSize(
               R.styleable.ViewGroup_MarginLayout_layout_marginLeft,
               UNDEFINED_MARGIN);
       rightMargin = a.getDimensionPixelSize(
               R.styleable.ViewGroup_MarginLayout_layout_marginRight,
               UNDEFINED_MARGIN);
 
       topMargin = a.getDimensionPixelSize(
               R.styleable.ViewGroup_MarginLayout_layout_marginTop,
               DEFAULT_MARGIN_RESOLVED);
 
       startMargin = a.getDimensionPixelSize(
               R.styleable.ViewGroup_MarginLayout_layout_marginStart,
               DEFAULT_MARGIN_RELATIVE);
       endMargin = a.getDimensionPixelSize(
               R.styleable.ViewGroup_MarginLayout_layout_marginEnd,
               DEFAULT_MARGIN_RELATIVE);
    }
    a.recycle();
}

這里大家也可以看到為什么非要重寫generateLayoutParams()函數(shù)了午绳,就是因為默認(rèn)的generateLayoutParams()函數(shù)只會提取layout_width置侍、layout_height的值,只有MarginLayoutParams()才具有提取margin間距的功能拦焚!

二蜡坊、 繪制順序

Android 里面的繪制都是按順序的,先繪制的內(nèi)容會被后繪制的蓋住赎败。比如你在重疊的位置先畫圓再畫方秕衙,和先畫方再畫圓所呈現(xiàn)出來的結(jié)果肯定是不同的:

img

1 super.onDraw() 前 or 后?

前幾期我寫的自定義繪制僵刮,全都是直接繼承 View 類据忘,然后重寫它的 onDraw() 方法鹦牛,把繪制代碼寫在里面,就像這樣:

public class AppView extends View {  
    ...
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        ... // 自定義繪制代碼
    }
    ...
}

這是自定義繪制最基本的形態(tài):繼承 View 類勇吊,在 onDraw() 中完全自定義它的繪制曼追。

在之前的樣例中,我把繪制代碼全都寫在了 super.onDraw() 的下面汉规。不過其實拉鹃,繪制代碼寫在 super.onDraw() 的上面還是下面都無所謂,甚至鲫忍,你把 super.onDraw() 這行代碼刪掉都沒關(guān)系膏燕,效果都是一樣的——因為在 View 這個類里,onDraw() 本來就是空實現(xiàn):

// 在 View.java 的源碼中悟民,onDraw() 是空的
// 所以直接繼承 View 的類坝辫,它們的 super.onDraw() 什么也不會做
public class View implements Drawable.Callback,  
        KeyEvent.Callback, AccessibilityEventSource {
    ...
    /**
     * Implement this to do your drawing.
     *
     * @param canvas the canvas on which the background will be drawn
     */
    protected void onDraw(Canvas canvas) {
    }
    ...
}

然而,除了繼承 View 類射亏,自定義繪制更為常見的情況是近忙,繼承一個具有某種功能的控件,去重寫它的 onDraw() 智润,在里面添加一些繪制代碼及舍,做出一個「進(jìn)化版」的控件:

img

基于 EditText,在它的基礎(chǔ)上增加了頂部的 Hint Text 和底部的字符計數(shù)窟绷。

而這種基于已有控件的自定義繪制锯玛,就不能不考慮 super.onDraw() 了:你需要根據(jù)自己的需求,判斷出你繪制的內(nèi)容需要蓋住控件原有的內(nèi)容還是需要被控件原有的內(nèi)容蓋住兼蜈,從而確定你的繪制代碼是應(yīng)該寫在 super.onDraw() 的上面還是下面攘残。

1.1 寫在 super.onDraw() 的下面

把繪制代碼寫在 super.onDraw() 的下面,由于繪制代碼會在原有內(nèi)容繪制結(jié)束之后才執(zhí)行为狸,所以繪制內(nèi)容就會蓋住控件原來的內(nèi)容歼郭。

這是最為常見的情況:為控件增加點綴性內(nèi)容。比如辐棒,在 Debug 模式下繪制出 ImageView 的圖像尺寸信息:

public class AppImageView extends ImageView {  
    ...

    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        if (DEBUG) {
            // 在 debug 模式下繪制出 drawable 的尺寸信息
            ...
        }
    }
}
img

這招很好用的病曾,試過嗎?

當(dāng)然漾根,除此之外還有其他的很多用法泰涂,具體怎么用就取決于你的需求、經(jīng)驗和想象力了立叛。

1.2 寫在 super.onDraw() 的上面

如果把繪制代碼寫在 super.onDraw() 的上面负敏,由于繪制代碼會執(zhí)行在原有內(nèi)容的繪制之前,所以繪制的內(nèi)容會被控件的原內(nèi)容蓋住秘蛇。

相對來說其做,這種用法的場景就會少一些顶考。不過只是少一些而不是沒有,比如你可以通過在文字的下層繪制純色矩形來作為「強調(diào)色」:

public class AppTextView extends TextView {  
    ...

    protected void onDraw(Canvas canvas) {
        ... // 在 super.onDraw() 繪制文字之前妖泄,先繪制出被強調(diào)的文字的背景

        super.onDraw(canvas);
    }
}
img

2 dispatchDraw():繪制子 View 的方法

講了這幾期驹沿,到目前為止我只提到了 onDraw() 這一個繪制方法。但其實繪制方法不是只有一個的蹈胡,而是有好幾個渊季,其中 onDraw() 只是負(fù)責(zé)自身主體內(nèi)容繪制的。而有的時候罚渐,你想要的遮蓋關(guān)系無法通過 onDraw() 來實現(xiàn)却汉,而是需要通過別的繪制方法。

例如荷并,你繼承了一個 LinearLayout合砂,重寫了它的 onDraw() 方法,在 super.onDraw() 中插入了你自己的繪制代碼源织,使它能夠在內(nèi)部繪制一些斑點作為點綴:

public class SpottedLinearLayout extends LinearLayout {  
    ...

    protected void onDraw(Canvas canvas) {
       super.onDraw(canvas);

       ... // 繪制斑點
    }
}
img

看起來沒問題對吧翩伪?

但是你會發(fā)現(xiàn),當(dāng)你添加了子 View 之后谈息,你的斑點不見了:

<SpottedLinearLayout  
    android:orientation="vertical"
    ... >

    <ImageView ... />

    <TextView ... />

</SpottedLinearLayout>  
img

造成這種情況的原因是 Android 的繪制順序:在繪制過程中缘屹,每一個 ViewGroup 會先調(diào)用自己的 onDraw() 來繪制完自己的主體之后再去繪制它的子 View。對于上面這個例子來說侠仇,就是你的 LinearLayout 會在繪制完斑點后再去繪制它的子 View轻姿。那么在子 View 繪制完成之后,先前繪制的斑點就被子 View 蓋住了傅瞻。

具體來講踢代,這里說的「繪制子 View」是通過另一個繪制方法的調(diào)用來發(fā)生的,這個繪制方法叫做:dispatchDraw()嗅骄。也就是說,在繪制過程中饼疙,每個 View 和 ViewGroup 都會先調(diào)用 onDraw() 方法來繪制主體溺森,再調(diào)用 dispatchDraw() 方法來繪制子 View。

注:雖然 ViewViewGroup 都有 dispatchDraw() 方法窑眯,不過由于 View 是沒有子 View 的屏积,所以一般來說 dispatchDraw() 這個方法只對 ViewGroup(以及它的子類)有意義。

img

回到剛才的問題:怎樣才能讓 LinearLayout 的繪制內(nèi)容蓋住子 View 呢磅甩?只要讓它的繪制代碼在子 View 的繪制之后再執(zhí)行就好了炊林。

2.1 寫在 super.dispatchDraw() 的下面

只要重寫 dispatchDraw(),并在 super.dispatchDraw() 的下面寫上你的繪制代碼卷要,這段繪制代碼就會發(fā)生在子 View 的繪制之后渣聚,從而讓繪制內(nèi)容蓋住子 View 了独榴。

public class SpottedLinearLayout extends LinearLayout {  
    ...

    // 把 onDraw() 換成了 dispatchDraw()
    protected void dispatchDraw(Canvas canvas) {
       super.dispatchDraw(canvas);

       ... // 繪制斑點
    }
}
img

好萌的蝙蝠俠啊

2.2 寫在 super.dispatchDraw() 的上面

同理,把繪制代碼寫在 super.dispatchDraw() 的上面奕枝,這段繪制就會在 onDraw() 之后棺榔、 super.dispatchDraw() 之前發(fā)生,也就是繪制內(nèi)容會出現(xiàn)在主體內(nèi)容和子 View 之間隘道。而這個……

其實和前面 1.1 講的症歇,重寫 onDraw() 并把繪制代碼寫在 super.onDraw() 之后的做法,效果是一樣的谭梗。

能想明白為什么吧忘晤?圖就不上了。

3 繪制過程簡述

繪制過程中最典型的兩個部分是上面講到的主體和子 View激捏,但它們并不是繪制過程的全部设塔。除此之外,繪制過程還包含一些其他內(nèi)容的繪制缩幸。具體來講壹置,一個完整的繪制過程會依次繪制以下幾個內(nèi)容:

  1. 背景
  2. 主體(onDraw()
  3. 子 View(dispatchDraw()
  4. 滑動邊緣漸變和滑動條
  5. 前景

一般來說,一個 View(或 ViewGroup)的繪制不會這幾項全都包含表谊,但必然逃不出這幾項娇斩,并且一定會嚴(yán)格遵守這個順序。例如通常一個 LinearLayout 只有背景和子 View炮沐,那么它會先繪制背景再繪制子 View泛烙;一個 ImageView 有主體,有可能會再加上一層半透明的前景作為遮罩距辆,那么它的前景也會在主體之后進(jìn)行繪制余佃。需要注意,前景的支持是在 Android 6.0(也就是 API 23)才加入的跨算;之前其實也有爆土,不過只支持 FrameLayout,而直到 6.0 才把這個支持放進(jìn)了 View 類里诸蚕。

這其中的第 2步势、3 兩步,前面已經(jīng)講過了背犯;第 1 步——背景坏瘩,它的繪制發(fā)生在一個叫 drawBackground()的方法里,但這個方法是 private 的漠魏,不能重寫倔矾,你如果要設(shè)置背景,只能用自帶的 API 去設(shè)置(xml 布局文件的 android:background 屬性以及 Java 代碼的 View.setBackgroundXxx() 方法,這個每個人都用得很 6 了)哪自,而不能自定義繪制丰包;而第 4、5 兩步——滑動邊緣漸變和滑動條以及前景提陶,這兩部分被合在一起放在了 onDrawForeground() 方法里烫沙,這個方法是可以重寫的。

img

滑動邊緣漸變和滑動條可以通過 xml 的 android:scrollbarXXX 系列屬性或 Java 代碼的 View.setXXXScrollbarXXX() 系列方法來設(shè)置隙笆;前景可以通過 xml 的 android:foreground 屬性或 Java 代碼的 View.setForeground() 方法來設(shè)置锌蓄。而重寫 onDrawForeground() 方法,并在它的 super.onDrawForeground() 方法的上面或下面插入繪制代碼撑柔,則可以控制繪制內(nèi)容和滑動邊緣漸變瘸爽、滑動條以及前景的遮蓋關(guān)系。

4 onDrawForeground()

首先铅忿,再說一遍剪决,這個方法是 API 23 才引入的,所以在重寫這個方法的時候要確認(rèn)你的 minSdk 達(dá)到了 23檀训,不然低版本的手機裝上你的軟件會沒有效果柑潦。

onDrawForeground() 中,會依次繪制滑動邊緣漸變峻凫、滑動條和前景渗鬼。所以如果你重寫 onDrawForeground()

4.1 寫在 super.onDrawForeground() 的下面

如果你把繪制代碼寫在了 super.onDrawForeground() 的下面,繪制代碼會在滑動邊緣漸變荧琼、滑動條和前景之后被執(zhí)行譬胎,那么繪制內(nèi)容將會蓋住滑動邊緣漸變、滑動條和前景命锄。

public class AppImageView extends ImageView {  
    ...
    public void onDrawForeground(Canvas canvas) {
       super.onDrawForeground(canvas);
       ... // 繪制「New」標(biāo)簽
    }
}
<!-- 使用半透明的黑色作為前景堰乔,這是一種很常見的處理 -->  
<AppImageView  
    ...
    android:foreground="#88000000" />
img

左上角的標(biāo)簽并沒有被黑色遮罩蓋住,而是保持了原有的顏色脐恩。

4.2 寫在 super.onDrawForeground() 的上面

如果你把繪制代碼寫在了 super.onDrawForeground() 的上面镐侯,繪制內(nèi)容就會在 dispatchDraw()super.onDrawForeground() 之間執(zhí)行,那么繪制內(nèi)容會蓋住子 View驶冒,但被滑動邊緣漸變析孽、滑動條以及前景蓋住:

public class AppImageView extends ImageView {  
    ...

    public void onDrawForeground(Canvas canvas) {
       ... // 繪制「New」標(biāo)簽

       super.onDrawForeground(canvas);
    }
}
img

由于被半透明黑色遮罩蓋住只怎,左上角的標(biāo)簽明顯變暗了。

這種寫法怜俐,和前面 2.1 講的身堡,重寫 dispatchDraw() 并把繪制代碼寫在 super.dispatchDraw() 的下面的效果是一樣的:繪制內(nèi)容都會蓋住子 View,但被滑動邊緣漸變拍鲤、滑動條以及前景蓋住贴谎。

4.3 想在滑動邊緣漸變汞扎、滑動條和前景之間插入繪制代碼?

很簡單:不行擅这。

雖然這三部分是依次繪制的澈魄,但它們被一起寫進(jìn)了 onDrawForeground() 方法里,所以你要么把繪制內(nèi)容插在它們之前仲翎,要么把繪制內(nèi)容插在它們之后痹扇。而想往它們之間插入繪制,是做不到的溯香。

5 draw() 總調(diào)度方法

除了 onDraw() dispatchDraw()onDrawForeground() 之外鲫构,還有一個可以用來實現(xiàn)自定義繪制的方法: draw()

draw() 是繪制過程的總調(diào)度方法玫坛。一個 View 的整個繪制過程都發(fā)生在 draw() 方法里结笨。前面講到的背景、主體湿镀、子 View 炕吸、滑動相關(guān)以及前景的繪制,它們其實都是在 draw() 方法里的勉痴。

// View.java 的 draw() 方法的簡化版大致結(jié)構(gòu)(是大致結(jié)構(gòu)赫模,不是源碼哦):

public void draw(Canvas canvas) {  
    ...

    drawBackground(Canvas); // 繪制背景(不能重寫)
    onDraw(Canvas); // 繪制主體
    dispatchDraw(Canvas); // 繪制子 View
    onDrawForeground(Canvas); // 繪制滑動相關(guān)和前景

    ...
}

從上面的代碼可以看出,onDraw() dispatchDraw() onDrawForeground() 這三個方法在 draw() 中被依次調(diào)用蚀腿,因此它們的遮蓋關(guān)系也就像前面所說的——dispatchDraw() 繪制的內(nèi)容蓋住 onDraw() 繪制的內(nèi)容嘴瓤;onDrawForeground() 繪制的內(nèi)容蓋住 dispatchDraw() 繪制的內(nèi)容。而在它們的外部莉钙,則是由 draw() 這個方法作為總的調(diào)度廓脆。所以,你也可以重寫 draw() 方法來做自定義的繪制磁玉。

img

5.1 寫在 super.draw() 的下面

由于 draw() 是總調(diào)度方法停忿,所以如果把繪制代碼寫在 super.draw() 的下面,那么這段代碼會在其他所有繪制完成之后再執(zhí)行蚊伞,也就是說席赂,它的繪制內(nèi)容會蓋住其他的所有繪制內(nèi)容。

它的效果和重寫 onDrawForeground()时迫,并把繪制代碼寫在 super.onDrawForeground() 下面時的效果是一樣的:都會蓋住其他的所有內(nèi)容颅停。

當(dāng)然了,雖說它們效果一樣掠拳,但如果你既重寫 draw() 又重寫 onDrawForeground() 癞揉,那么 draw()里的內(nèi)容還是會蓋住 onDrawForeground() 里的內(nèi)容的。所以嚴(yán)格來講,它們的效果還是有一點點不一樣的喊熟。

但這屬于抬杠……

5.2 寫在 super.draw() 的上面

同理柏肪,由于 draw() 是總調(diào)度方法,所以如果把繪制代碼寫在 super.draw() 的上面芥牌,那么這段代碼會在其他所有繪制之前被執(zhí)行烦味,所以這部分繪制內(nèi)容會被其他所有的內(nèi)容蓋住,包括背景壁拉。是的谬俄,背景也會蓋住它。

是不是覺得沒用扇商?覺得怎么可能會有誰想要在背景的下面繪制內(nèi)容凤瘦?別這么想,有的時候它還真的有用案铺。

例如我有一個 EditText

img

它下面的那條橫線蔬芥,是 EditText 的背景。所以如果我想給這個 EditText 加一個綠色的底控汉,我不能使用給它設(shè)置綠色背景色的方式笔诵,因為這就相當(dāng)于是把它的背景替換掉,從而會導(dǎo)致下面的那條橫線消失:

<EditText  
    ...
    android:background="#66BB6A" />
img

EditText:我到底是個 EditText 還是個 TextView姑子?傻傻分不清楚乎婿。

在這種時候,你就可以重寫它的 draw() 方法街佑,然后在 super.draw() 的上方插入代碼谢翎,以此來在所有內(nèi)容的底部涂上一片綠色:

public class SpottedLinearLayout extends LinearLayout {  
    ...

    // 把 onDraw() 換成了 dispatchDraw()
    protected void dispatchDraw(Canvas canvas) {
       super.dispatchDraw(canvas);

       ... // 繪制斑點
    }
}
img

當(dāng)然,這種用法并不常見沐旨,事實上我也并沒有在項目中寫過這樣的代碼森逮。但我想說的是,我們作為工程師磁携,是無法預(yù)知將來會遇到怎樣的需求的褒侧。我們能做的只能是盡量地去多學(xué)習(xí)一些、多掌握一些谊迄,盡量地了解我們能夠做什么闷供、怎么做,然后在需求到來的時候统诺,就可以多一些自如歪脏,少一些束手無策。

注意

關(guān)于繪制方法粮呢,有兩點需要注意一下:

  1. 出于效率的考慮唾糯,ViewGroup 默認(rèn)會繞過 draw() 方法怠硼,換而直接執(zhí)行 dispatchDraw(),以此來簡化繪制流程移怯。所以如果你自定義了某個 ViewGroup 的子類(比如 LinearLayout)并且需要在它的除 dispatchDraw() 以外的任何一個繪制方法內(nèi)繪制內(nèi)容,你可能會需要調(diào)用 View.setWillNotDraw(false) 這行代碼來切換到完整的繪制流程(是「可能」而不是「必須」的原因是这难,有些 ViewGroup 是已經(jīng)調(diào)用過 setWillNotDraw(false) 了的舟误,例如 ScrollView)。
  2. 有的時候姻乓,一段繪制代碼寫在不同的繪制方法中效果是一樣的嵌溢,這時你可以選一個自己喜歡或者習(xí)慣的繪制方法來重寫。但有一個例外:如果繪制代碼既可以寫在 onDraw() 里蹋岩,也可以寫在其他繪制方法里赖草,那么優(yōu)先寫在 onDraw() ,因為 Android 有相關(guān)的優(yōu)化剪个,可以在不需要重繪的時候自動跳過 onDraw() 的重復(fù)執(zhí)行秧骑,以提升開發(fā)效率。享受這種優(yōu)化的只有 onDraw() 一個方法扣囊。

總結(jié)

今天的內(nèi)容就是這些:使用不同的繪制方法乎折,以及在重寫的時候把繪制代碼放在 super.繪制方法() 的上面或下面不同的位置,以此來實現(xiàn)需要的遮蓋關(guān)系侵歇。下面用一張圖和一個表格總結(jié)一下:

img

嗯骂澄,上面這張圖在前面已經(jīng)貼過了,不用比較了完全一樣的惕虑。

img

另外別忘了上面提到的那兩個注意事項:

  1. ViewGroup 的子類中重寫除 dispatchDraw() 以外的繪制方法時坟冲,可能需要調(diào)用 setWillNotDraw(false)
  2. 在重寫的方法有多個選擇時溃蔫,優(yōu)先選擇 onDraw()健提。

練習(xí):FlowLayout自適應(yīng)容器實現(xiàn)

XML布局

Flowlayout

提取margin與onMeasure()重寫

提取margin

要提取margin,就一定要重寫generateLayoutParams

重寫onMeasure()——計算當(dāng)前FlowLayout所占的寬高

這里就要重寫onMeasure()函數(shù)酒唉,在其中計算所有當(dāng)前container所占的大小矩桂。

要做FlowLayout,首先涉及下面幾個問題:

  1. 何時換行

從效果圖中可以看到,F(xiàn)lowLayout的布局是一行行的痪伦,如果當(dāng)前行已經(jīng)放不下 下一個 控件侄榴,那就把這個控件移到下一行顯示。所以我們要有個變量來計算當(dāng)前行已經(jīng)占據(jù)的寬度网沾,以判斷剩下的空間是否還能容得下 下一個 控件癞蚕。

  1. 如何得到FlowLayout的寬度

FlowLayout的寬度是所有行寬度的最大值,所以我們要記錄下每一行的所占據(jù)的寬度值辉哥,進(jìn)而找到所有值中的最大值桦山。

  1. 如何得到FlowLayout的高度

很顯然攒射,F(xiàn)lowLayout的高度是每一行高度的總和,而每一行的高度則是取該行中所有控件高度的最大值恒水。

重寫onLayout()——布局所有子控件

代碼和注釋在com.richy.kotlindemo.customview.FlowLayout

class FlowLayout : ViewGroup {

    override fun generateLayoutParams(p: ViewGroup.LayoutParams): ViewGroup.LayoutParams {
        return ViewGroup.MarginLayoutParams(p)
    }

    override fun generateLayoutParams(attrs: AttributeSet): ViewGroup.LayoutParams {
        return ViewGroup.MarginLayoutParams(context, attrs)
    }

    override fun generateDefaultLayoutParams(): ViewGroup.LayoutParams {
        return ViewGroup.MarginLayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT)
    }

    constructor(context: Context?) : super(context)
    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
//        (1)首先会放,剛進(jìn)來的時候是利用MeasureSpec獲取系統(tǒng)建議的數(shù)值的模式
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        val measureWidth = View.MeasureSpec.getSize(widthMeasureSpec)
        val measureHeight = View.MeasureSpec.getSize(heightMeasureSpec)
        val measureWidthMode = View.MeasureSpec.getMode(widthMeasureSpec)
        val measureHeightMode = View.MeasureSpec.getMode(heightMeasureSpec)

//        (2)然后,是計算FlowLayout所占用的空間大小
        var lineWidth = 0//記錄每一行的寬度
        var lineHeight = 0//記錄每一行的高度
        var height = 0//記錄整個FlowLayout所占高度
        var width = 0//記錄整個FlowLayout所占寬度

        //計算
        val count = childCount
        for (i in 0 until count) {
            val child = getChildAt(i)
            //這里一定要注意的是:在調(diào)用child.getMeasuredWidth()钉凌、child.getMeasuredHeight()之前咧最,
            // 一定要調(diào)用measureChild(child,widthMeasureSpec,heightMeasureSpec);
            // 在onMeasure()之后才能調(diào)用getMeasuredWidth()獲得值;同樣御雕,只有調(diào)用onLayout()后,getWidth()才能獲取值
            measureChild(child, widthMeasureSpec, heightMeasureSpec)

            val lp = child.layoutParams as ViewGroup.MarginLayoutParams
            val childWidth = child.measuredWidth + lp.leftMargin + lp.rightMargin
            val childHeight = child.measuredHeight + lp.topMargin + lp.bottomMargin

            if (lineWidth + childWidth > measureWidth) {
                //換行
                //FlowLayout的寬度是所有行寬度的最大值酸纲,所以我們要記錄下每一行的所占據(jù)的寬度值捣鲸,進(jìn)而找到所有值中的最大值
                width = Math.max(lineWidth, width)
                height += lineHeight
                //因為由于盛不下當(dāng)前控件,而將此控件調(diào)到下一行闽坡,所以將此控件的高度和寬度初始化給lineHeight栽惶、lineWidth
                lineHeight = childHeight
                lineWidth = childWidth

            } else {
                //累加
                lineHeight = Math.max(lineHeight, childHeight)
                lineWidth += childWidth
            }

            //最后一行是不會超出width范圍的,所以要單獨處理
            if (i == count - 1) {
                height += lineHeight
                width = Math.max(width, lineWidth)
            }

        }
        //當(dāng)屬性是MeasureSpec.EXACTLY時无午,那么它的高度就是確定的媒役,
        // 只有當(dāng)是wrap_content時,根據(jù)內(nèi)部控件的大小來確定它的大小時宪迟,大小是不確定的酣衷,屬性是AT_MOST,
        // 此時,就需要我們自己計算它的應(yīng)當(dāng)?shù)拇笮〈卧螅⒃O(shè)置進(jìn)去
        setMeasuredDimension(if (measureWidthMode == View.MeasureSpec.EXACTLY) measureWidth else width,
                if (measureHeightMode == View.MeasureSpec.EXACTLY) measureHeight else height)
    }

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        var lineWidth = 0
        var lineHeight = 0
        var top = 0
        var left = 0
        for (i in 0 until childCount) {
            val child = getChildAt(i)
            val lp = child.layoutParams as ViewGroup.MarginLayoutParams
            //這樣做其實不好穿仪,這樣把margin當(dāng)做了padding處理了,包含在了child里面了
            val childWidth = child.measuredWidth + lp.leftMargin + lp.rightMargin
            val childHeight = child.measuredHeight + lp.topMargin + lp.bottomMargin

            if (childWidth + lineWidth > measuredWidth) {
                //如果換行,當(dāng)前控件將跑到下一行意荤,從最左邊開始啊片,所以left就是0,而top則需要加上上一行的行高玖像,才是這個控件的top點;
                top += lineHeight
                left = 0
                //同樣紫谷,重新初始化lineHeight和lineWidth
                lineHeight = childHeight
                lineWidth = childWidth
            } else {
                //不換行
                lineHeight = Math.max(lineHeight, childHeight)
                lineWidth += childWidth
            }
            //計算childView的left,top,right,bottom
            val lc = left + lp.leftMargin
            val tc = top + lp.topMargin
            val rc = lc + child.measuredWidth
            val bc = tc + child.measuredHeight
            child.layout(lc, tc, rc, bc)
            //將left置為下一子控件的起始點
            left += childWidth
        }

    }

}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市捐寥,隨后出現(xiàn)的幾起案子笤昨,更是在濱河造成了極大的恐慌,老刑警劉巖握恳,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件瞒窒,死亡現(xiàn)場離奇詭異,居然都是意外死亡乡洼,警方通過查閱死者的電腦和手機崇裁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門匕坯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人拔稳,你說我怎么就攤上這事葛峻。” “怎么了壳炎?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵泞歉,是天一觀的道長。 經(jīng)常有香客問我匿辩,道長,這世上最難降的妖魔是什么榛丢? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任铲球,我火速辦了婚禮,結(jié)果婚禮上晰赞,老公的妹妹穿的比我還像新娘稼病。我一直安慰自己,他們只是感情好掖鱼,可當(dāng)我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布然走。 她就那樣靜靜地躺著,像睡著了一般戏挡。 火紅的嫁衣襯著肌膚如雪芍瑞。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天褐墅,我揣著相機與錄音拆檬,去河邊找鬼。 笑死妥凳,一個胖子當(dāng)著我的面吹牛竟贯,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播逝钥,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼屑那,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了艘款?” 一聲冷哼從身側(cè)響起持际,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎磷箕,沒想到半個月后选酗,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡岳枷,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年芒填,在試婚紗的時候發(fā)現(xiàn)自己被綠了呜叫。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡殿衰,死狀恐怖朱庆,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情闷祥,我是刑警寧澤娱颊,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站凯砍,受9級特大地震影響箱硕,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜悟衩,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一剧罩、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧座泳,春花似錦惠昔、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至潮饱,卻和暖如春来氧,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背饼齿。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工饲漾, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人缕溉。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓考传,卻偏偏與公主長得像,于是被迫代替她去往敵國和親证鸥。 傳聞我的和親對象是個殘疾皇子僚楞,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,916評論 2 344

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