聽說懂Canvas的人運氣都不會太差朴译!

1.前言

逛街的時候井佑,看到一篇Android Canvas 方法總結(jié),這篇文章將Canvas一些基本操作介紹的很詳細眠寿。從零開始的朋友可以先去刷點經(jīng)驗躬翁,剩下的同學(xué)拿起手術(shù)刀,我們一起來將Canvas血腥解剖吧盯拱。

2.Canvas簡介

官方文檔介紹如下

The Canvas class holds the "draw" calls. To draw something, you need 4 basic components:
a Bitmap to hold the pixels, 
a Canvas to host the draw calls (writing into the bitmap), 
a drawing primitive (e.g. Rect, Path, text, Bitmap), 
a paint (to describe the colors and styles for the drawing).

用人話說大概是這樣

一個Canvas類對象有四大基本要素
1盒发、用來保存像素的Bitmap
2、用來在Bitmap上進行繪制操作的Canvas
3狡逢、要繪制的東西
4迹辐、繪制用的畫筆Paint

Bitmap和Canvas的關(guān)系類似于畫板與畫布,不理解沒有關(guān)系甚侣,后面還會詳細介紹的明吩。

3.Canvas基本繪制

一開始,先來點簡單的基礎(chǔ)題殷费。因為太懶了不想從頭寫起印荔,我就假設(shè)大家都看過了Android Canvas 方法總結(jié)低葫,來寫一些這里面沒有介紹的方法。

3.1 圓角矩形

畫筆初始化

mPaint = new Paint();
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(10);
        mPaint.setColor(Color.RED);

onDraw()中進行繪制仍律,參數(shù)二嘿悬、三越大,圓角半徑越大

 private void drawRoundRect(Canvas canvas) {
        RectF r = new RectF(100, 100, 400, 500);
        //x-radius ,y-radius圓角的半徑
        canvas.drawRoundRect(r, 80, 80, mPaint);
    }

效果圖如下

圓角矩形.png

3.2 圓角矩形路徑

那么有的時候水泉,我不需要那么整齊的圓角善涨,該怎么辦呢

private void drawRoundRectPath(Canvas canvas) {
        RectF r = new RectF(100, 100, 400, 500);
        Path path = new Path();
        float radii[] = {80, 80, 80, 80, 80, 80, 20, 20};
        path.addRoundRect(r, radii, Path.Direction.CCW);
        canvas.drawPath(path, mPaint);
    }

這里的radii就定義了四個角的弧度,接著使用Path就可以將其繪制出來草则。事實上钢拧,Path是無所不能的,我們將在以后單獨介紹他炕横。

圓角矩形路徑.png

3.3 Region區(qū)域

Region是區(qū)域的意思源内,它表示的Canvas圖層上的一塊封閉的區(qū)域。

來看看它的構(gòu)造方法

 /** Create an empty region
    */
    public Region() {
        this(nativeConstructor());
    }

    /** Return a copy of the specified region
    */
    public Region(Region region) {
        this(nativeConstructor());
        nativeSetRegion(mNativeRegion, region.mNativeRegion);
    }

    /** Return a region set to the specified rectangle
    */
    public Region(Rect r) {
        mNativeRegion = nativeConstructor();
        nativeSetRect(mNativeRegion, r.left, r.top, r.right, r.bottom);
    }

    /** Return a region set to the specified rectangle
    */
    public Region(int left, int top, int right, int bottom) {
        mNativeRegion = nativeConstructor();
        nativeSetRect(mNativeRegion, left, top, right, bottom);
    }

看上去挺萬金油的份殿,什么都能傳進去膜钓,那么Region能干嘛呢?我們可以用它來進行一些交并補集的操作,比如下面代碼就能展示兩個region的交集

region1.op(region2, Region.Op.INTERSECT);//交集部分 region1是調(diào)用者A卿嘲,region2是求交集的B

去源碼里看看這個Op颂斜,發(fā)現(xiàn)是個枚舉類

public enum Op {
        DIFFERENCE(0),
        INTERSECT(1),
        UNION(2),
        XOR(3),
        REVERSE_DIFFERENCE(4),
        REPLACE(5);
    ...
    }

可見交并補的類型還挺多,我們用一張圖來介紹吧


Region_op.png

那么這里就出現(xiàn)了一個問題拾枣,這些Op過后的region都是不規(guī)則的了沃疮,系統(tǒng)要如何將他們繪制出來呢?
嘿嘿嘿放前,請回憶起當(dāng)年被微積分支配的恐懼吧忿磅!

private void drawRegion(Canvas canvas){
        RectF r = new RectF(100, 100, 400, 500);
        Path path = new Path();
        float radii[] = {80, 80, 80, 80, 80, 80, 20, 20};
        path.addRoundRect(r, radii, Path.Direction.CCW);

        //創(chuàng)建一塊矩形的區(qū)域
        Region region = new Region(100, 100, 600, 800);
        Region region1 = new Region();
        region1.setPath(path, region);//path的橢圓區(qū)域和矩形區(qū)域進行交集

        //結(jié)合區(qū)域迭代器使用(得到圖形里面的所有的矩形區(qū)域)
        RegionIterator iterator = new RegionIterator(region1);

        Rect rect = new Rect();
        mPaint.setStrokeWidth(1);
        while (iterator.next(rect)) {
            canvas.drawRect(rect, mPaint);
        }
    }

看看代碼,這里通過迭代器用一個個小矩形填滿整個region控件凭语,為了方便展示葱她,我們把paint的類型設(shè)成stroke,效果圖如下

region繪制.png

解釋下似扔,這里首先繪制了最大的那個矩形吨些,然后在極限的距離上縮小矩形,再通過這些小矩形將剩下的部分都填充起來炒辉。

4.Canvas基本變換

4.1 Canavas坐標系

Canvas里面牽扯兩種坐標系:Canvas自己的坐標系豪墅、繪圖坐標系

Canvas的坐標系,它就在View的左上角黔寇,從坐標原點往右是X軸正半軸偶器,往下是Y軸的正半軸,有且只有一個,唯一不變

繪圖坐標系屏轰,它不是唯一不變的颊郎,它與Canvas的Matrix有關(guān)系,當(dāng)Matrix發(fā)生改變的時候霎苗,繪圖坐標系對應(yīng)的進行改變姆吭,同時這個過程是不可逆的(通過相反的矩陣還原),而Matrix又是通過我們設(shè)置translate唁盏、rotate内狸、scale、skew來進行改變的

上面這段話還是挺好理解的厘擂,我們用代碼來驗證下

 private void drawMatrix(Canvas canvas){
        // 繪制坐標系
        RectF r = new RectF(0, 0, 400, 500);
        mPaint.setColor(Color.GREEN);
        canvas.drawRect(r, mPaint);

        // 第一次繪制坐標軸
        canvas.drawLine(0,0,canvas.getWidth(),0,mPaint);// X 軸
        mPaint.setColor(Color.BLUE);
        canvas.drawLine(0,0,0,canvas.getHeight(),mPaint);// Y 軸

        //平移--即改變坐標原點
        canvas.translate(50, 50);
        // 第二次繪制坐標軸
        mPaint.setColor(Color.GREEN);
        canvas.drawLine(0,0,canvas.getWidth(),0,mPaint);// X 軸
        mPaint.setColor(Color.BLUE);
        canvas.drawLine(0,0,0,canvas.getHeight(),mPaint);// Y 軸

        canvas.rotate(45);
        // 第三次繪制坐標軸
        mPaint.setColor(Color.GREEN);
        canvas.drawLine(0,0,canvas.getWidth(),0,mPaint);// X 軸
        mPaint.setColor(Color.BLUE);
        canvas.drawLine(0,0,0,canvas.getHeight(),mPaint);// Y 軸
    }

運行結(jié)果如下

Canvas坐標系

剩下的translate昆淡、rotate、scale驴党、skew等方法瘪撇,在之前推薦的那篇文章里就有获茬,這里就不再重復(fù)勞動了

5.Canvas狀態(tài)保存

5.1 狀態(tài)棧

狀態(tài)棧通過save港庄、 restore方法來保存和還原變換操作Matrix以及Clip剪裁,也可以通過restoretoCount直接還原到對應(yīng)棧的保存狀態(tài)恕曲。需要注意的是鹏氧,一開始canvas就是在棧1的位置,執(zhí)行一次save就進棧一次(此時為2)佩谣,執(zhí)行一次restore就出棧一次把还。

 private void saveRestore(Canvas canvas){
        RectF r = new RectF(0, 0, 400, 500);
        mPaint.setColor(Color.GREEN);
        canvas.drawRect(r, mPaint);
        canvas.save();
        //平移
        canvas.translate(50, 50);
        mPaint.setColor(Color.BLUE);
        canvas.drawRect(r, mPaint);
        canvas.restore();
        mPaint.setColor(Color.YELLOW);
        r = new RectF(0, 0, 200, 200);
        canvas.drawRect(r, mPaint);
    }

效果圖如下

狀態(tài)棧

有的同學(xué)可能會把狀態(tài)棧理解為圖層,其實這是不對滴茸俭。我們來看下save方法的源碼

 /**
     * Saves the current matrix and clip onto a private stack.
     * <p>
     * Subsequent calls to translate,scale,rotate,skew,concat or clipRect,
     * clipPath will all operate as usual, but when the balancing call to
     * restore() is made, those calls will be forgotten, and the settings that
     * existed before the save() will be reinstated.
     *
     * @return The value to pass to restoreToCount() to balance this save()
     */
    public int save() {
        return native_save(mNativeCanvasWrapper, MATRIX_SAVE_FLAG | CLIP_SAVE_FLAG);
    }

注釋上說的很明白吊履,save只是保存了matrix和clip的狀態(tài),并沒有保存一個真正的圖層调鬓。真正的圖層是在Layer棧中保存的艇炎。

5.2 Layer棧

Layer棧通過saveLayer新建一個透明的圖層,并且會將saveLayer之前的一些Canvas操作延續(xù)過來腾窝,后續(xù)的繪圖操作都在新建的layer上面進行缀踪,當(dāng)我們調(diào)用restore或者 restoreToCount 時更新到對應(yīng)的圖層和畫布上。

下面這段代碼要仔細看

private void saveLayer(Canvas canvas) {
        RectF rectF = new RectF(0, 0, 400, 500);
        Paint paint = new Paint();
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(10);
        paint.setColor(Color.GREEN);

        canvas.drawRect(rectF, paint);
        canvas.translate(50, 50);

        canvas.saveLayer(0, 0, canvas.getWidth(), canvas.getHeight(), null, Canvas.ALL_SAVE_FLAG);
        canvas.drawColor(Color.BLUE);// 通過drawColor可以發(fā)現(xiàn)saveLayer是新建了一個圖層虹脯,
        paint.setColor(Color.YELLOW);
        canvas.drawRect(rectF, paint);
        canvas.restore();

        RectF rectF1 = new RectF(0, 0, 300, 400);
        paint.setColor(Color.RED);
        canvas.drawRect(rectF1, paint);

    }

先上效果圖

saveLayer

我們慢慢解釋驴娃。一開始畫了個綠色的框,之后移動了(50,50)的距離循集,再通過saveLayer新建了一個圖層唇敞。注意這里用到了Canvas.ALL_SAVE_FLAG,別的FLAG還有只保存Matrix的,只保存Clip的等等疆柔,大家可以自己去看蕉世。接著在新的圖層上畫了藍色背景和黃色的矩形,從結(jié)果可以看出之前的一些Canvas操作會被延續(xù)到新的圖層婆硬。調(diào)用restore后狠轻,兩個圖層合二為一,由于圖層2是藍色背景彬犯,因此就把圖層1的綠色邊框覆蓋了向楼。最后再繪制另一個紅色的框,此時只有一個圖層了谐区,所以就繪制在當(dāng)前圖層的最上方湖蜕。

文章開頭說Bitmap和Canvas的關(guān)系類似于畫板與畫布,就是因為每次Canvas執(zhí)行saveLayer時都會新建一個透明的圖層宋列,與之前的圖層疊加后更新到Bitmap上昭抒,從而將繪制的內(nèi)容展示出來。

這些大概就是save與saveLayer的區(qū)別所在了炼杖,如果覺得自己明白了灭返,就去看看給女朋友化妝系列的代碼,看看是否可以理解其中的save與saveLayer操作坤邪,以及為什么要這樣做熙含。

6.Drawable與Canvas

6.1 Drawable簡介

下面我們將用一個例子來加深學(xué)習(xí)效果,不過開始前還需要將Drawable這位兄弟介紹給大家艇纺。首先上一段官方注釋:

A Drawable is a general abstraction for "something that can be drawn."

顧名思義怎静,Drawable就是可以被畫出來的一個東西,這是一個抽象類黔衡,繼承自它的實現(xiàn)類如下(windows中查詢類繼承關(guān)系的快捷鍵是Ctrl+H)

drawable繼承結(jié)構(gòu).png

是不是發(fā)現(xiàn)有些熟悉的字眼蚓聘?比如layer、shape盟劫、color等等夜牡。對啦,就是可以在drawable文件夾中進行定義的xml資源文件捞高,系統(tǒng)會將這些xml轉(zhuǎn)換成都解析成相應(yīng)的drawable對象氯材。

由于drawable是可繪制的對象,canvas是繪制的畫紙硝岗,因此這兩位是密不可分的好基友氢哮。除去上圖中系統(tǒng)實現(xiàn)的drawable外,我們還可以根據(jù)需要自定義drawable型檀。事實上自定義drawable才是日常會用到的東西冗尤,下面一起來看看這種基本操作。

6.1 基本操作

這個自定義控件的功能不太好描述,先展示下效果圖

效果圖.png

最外層是一個HorizontalScrollView裂七,里面包裹著許多ImageView皆看,可以拖動,中間選中的區(qū)域呈現(xiàn)灰色背零,其余的顯示彩色腰吟。這些灰色、彩色的圖是兩套資源徙瓶。

要實現(xiàn)上述功能毛雇,我們需要兩個控件,一個是外層控制觸摸事件的ViewGroup侦镇,可以通過繼承HorizontalScrollView來完成灵疮。另一個是用來注入ImageView變換顏色的Drawable,這就需要自定義來實現(xiàn)了壳繁。

6.1.1 RevealDrawable

創(chuàng)建RevealDrawable繼承自Drawable震捣,需要重寫public void draw(@NonNull Canvas canvas)方法。

對于每一部分的圖片而言闹炉,會有以下幾種繪制情況:

1.灰色
2.彩色
3.左灰右彩
4.左彩右灰

灰色和彩色好辦蒿赢,直接將兩種圖片當(dāng)做參數(shù)傳入RevealDrawable中,在需要時通過draw(canvas)繪制出來即可剩胁。那么混合色該怎么做诉植?又要如何去判斷左右或者說灰彩各占的比例呢祥国?

對于問題一昵观,我們可以用之前所說的canvas裁剪來完成,需要注意save()restore()的調(diào)用舌稀;至于問題二啊犬,Drawable源碼中有這么一行參數(shù):private int mLevel = 0;,很顯然壁查,Google早已考慮到Drawable的這種使用場景觉至,而mLevel就是用來確定比例的撵幽,其值為0~10000办成,可以由我們在外層動態(tài)去設(shè)置惫搏。

總結(jié)一下突雪,整體思路就是HorizontalScrollView根據(jù)Scroll的距離為RevealDrawable動態(tài)設(shè)置level擦秽,而RevealDrawable則根據(jù)被設(shè)置的level展示出不同的圖像效果腌紧。剩下的就是數(shù)學(xué)問題了期丰。

這里就展示其核心的繪制方法欢瞪,注釋都有挂捻,完整的代碼等閑了整理下一起放到大型同性交友平臺上碉纺。

@Override
    public void draw(Canvas canvas) {
        // 繪制
        int level = getLevel();//from 0 (minimum) to 10000 
        //三個區(qū)間
        //右邊區(qū)間和左邊區(qū)間--設(shè)置成灰色
        if(level == 10000|| level == 0){
            mUnselectedDrawable.draw(canvas);
        }
        else if(level==5000){//全部選中--設(shè)置成彩色
            mSelectedDrawable.draw(canvas);
        }else{
            //混合效果的Drawable
            /**
             * 將畫板切割成兩塊-左邊和右邊
             */
            final Rect r = mTmpRect;
            //得到當(dāng)前自身Drawable的矩形區(qū)域
            Rect bounds = getBounds();
            {
                //1.先繪制灰色部分
                //level 0~5000~10000
                //比例
                float ratio = (level/5000f) - 1f;
                int w = bounds.width();
                if(mOrientation==HORIZONTAL){
                    w = (int) (w* Math.abs(ratio));
                }
                int h = bounds.height();
                if(mOrientation==VERTICAL){
                    h = (int) (h* Math.abs(ratio));
                }
                
                int gravity = ratio < 0 ? Gravity.LEFT : Gravity.RIGHT;
                //從一個已有的bounds矩形邊界范圍中摳出一個矩形r
                Gravity.apply(
                        gravity,//從左邊還是右邊開始摳
                        w,//目標矩形的寬 
                        h, //目標矩形的高
                        bounds, //被摳出來的rect
                        r);//目標rect
                
                canvas.save();//保存畫布
                canvas.clipRect(r);//切割
                mUnselectedDrawable.draw(canvas);//畫
                canvas.restore();//恢復(fù)之前保存的畫布
            }
            {
                //2.再繪制彩色部分
                //level 0~5000~10000
                //比例
                float ratio = (level/5000f) - 1f;
                int w = bounds.width();
                if(mOrientation==HORIZONTAL){
                    w -= (int) (w* Math.abs(ratio));
                }
                int h = bounds.height();
                if(mOrientation==VERTICAL){
                    h -= (int) (h* Math.abs(ratio));
                }
                
                int gravity = ratio < 0 ? Gravity.RIGHT : Gravity.LEFT;
                //從一個已有的bounds矩形邊界范圍中摳出一個矩形r
                Gravity.apply(
                        gravity,//從左邊還是右邊開始摳
                        w,//目標矩形的寬 
                        h, //目標矩形的高
                        bounds, //被摳出來的rect
                        r);//目標rect
                canvas.save();//保存畫布
                canvas.clipRect(r);//切割
                mSelectedDrawable.draw(canvas);//畫
                canvas.restore();//恢復(fù)之前保存的畫布
            }       
        }
    }
6.1.2 GalleryHorizontalScrollView

外層的GalleryHorizontalScrollView繼承自HorizontalScrollView,需要處理滑動事件與level設(shè)置,別忘了ScrollView的子View必須是ViewGroup

 private void init() {
        LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT
        );
        container = new LinearLayout(getContext());
        container.setLayoutParams(params);
        setOnScrollChangeListener(this);

    }

onLayout()的作用是在控件初始化時設(shè)置Padding值骨田,以便于一開始耿导,將第一個ImageView展示在正中間的位置(為了好看,沒什么特別的用處)

 @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        View v = container.getChildAt(0);
        icon_width = v.getWidth();//單個圖片的寬度
        centerX = getWidth() / 2;//整個sv的寬度
        centerX = centerX - icon_width/2;
        container.setPadding(centerX, 0, centerX, 0);
    }
起始圖.png

起初觸摸事件是在touch方法中完成的态贤,后來發(fā)現(xiàn)這個方法精度太高舱呻,滑動抖動明顯,因此換在scroll方法中執(zhí)行悠汽。

private void reveal() {
        // 漸變效果
        //得到hzv滑出去的距離
        int scrollX = getScrollX();
        Log.d(TAG, "reveal: "+scrollX);
        //找到兩張漸變的圖片的下標--左狮荔,右
        int index_left = scrollX/icon_width;
        int index_right = index_left + 1;
        //設(shè)置圖片的level
        for (int i = 0; i < container.getChildCount(); i++) {
            if(i==index_left||i==index_right){
                //變化
                //比例:
                float ratio = 5000f/icon_width;
                ImageView iv_left = (ImageView) container.getChildAt(index_left);
                //scrollX%icon_width:代表滑出去的距離
                //滑出去了icon_width/2  icon_width/2%icon_width
                iv_left.setImageLevel(
                        (int)(5000-scrollX%icon_width*ratio)
                );
                //右邊
                if(index_right<container.getChildCount()){
                    ImageView iv_right = (ImageView) container.getChildAt(index_right);
                    //scrollX%icon_width:代表滑出去的距離
                    //滑出去了icon_width/2  icon_width/2%icon_width
                    iv_right.setImageLevel(
                            (int)(10000-scrollX%icon_width*ratio)
                    );
                }
            }else{
                //灰色
                ImageView iv = (ImageView) container.getChildAt(i);
                iv.setImageLevel(0);
            }
        }
    }

最后是添加圖片的方法

public void addImageViews(Drawable[] revealDrawables){
        for (int i = 0; i < revealDrawables.length; i++) {
            ImageView img = new ImageView(getContext());
            img.setImageDrawable(revealDrawables[i]);
            container.addView(img);
            if(i==0){
                img.setImageLevel(5000);
            }
        }
        addView(container);
    }

7.Canvas緩存

在上面的例子中我們介紹了Canvas與自定義Drawable的配合使用,接下來我們上一個Canvas與Bitmap混合實現(xiàn)緩存的效果介粘。其實這句話是廢話殖氏,因為創(chuàng)建Canvas時就必須傳入Bitmap為參,畢竟Bitmap才是真正保存像素的地方姻采。

緩存的思想就是先將像素保存到CacheCanvas的CacheBitmap中雅采,再將這個CacheBitmap保存到View的Canvas上。

我們在初始化方法中創(chuàng)建緩存對象慨亲。

private void init() {
        //創(chuàng)建一個與該VIew相同大小的緩沖區(qū)
        cacheBitmap = Bitmap.createBitmap(VIEW_WIDTH, VIEW_HEIGHT, Bitmap.Config.ARGB_8888);
        //創(chuàng)建緩沖區(qū)Cache的Canvas對象
        cacheCanvas = new Canvas();
        path = new Path();
        //設(shè)置cacheCanvas將會繪制到內(nèi)存的bitmap上
        cacheCanvas.setBitmap(cacheBitmap);
        paint = new Paint();
        paint.setColor(Color.RED);
        paint.setFlags(Paint.DITHER_FLAG);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(5);
        paint.setAntiAlias(true);
        paint.setDither(true);//防抖動婚瓜,比較清晰
    }

接著onTouch()中根據(jù)手勢進行繪制,注意是繪制到緩存canvas上

 @Override
    public boolean onTouchEvent(MotionEvent event) {
        //獲取拖動時間的發(fā)生位置
        float x = event.getX();
        float y = event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                path.moveTo(x, y);
                preX = x;
                preY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                path.quadTo(preX, preY, x, y);//繪制圓滑曲線
                preX = x;
                preY = y;
                break;
            case MotionEvent.ACTION_UP:
                //這是是調(diào)用了cacheBitmap的Canvas在繪制
                cacheCanvas.drawPath(path, paint);
                path.reset();
                break;
        }
        invalidate();//在UI線程刷新VIew
        return true;
    }

invalidate()會回調(diào)draw()方法刑棵,此時再將緩存bitmap繪制到View的canvas中巴刻。

  @Override
    protected void onDraw(Canvas canvas) {
        Paint p = new Paint();
        //將cacheBitmap繪制到該View
        canvas.drawBitmap(cacheBitmap, 0, 0, p);
    }

這樣一來,手指滑動軌跡就會略有延遲后再繪制到用戶界面上蛉签,類似于寫字板的效果胡陪。

cache緩存.png

8.總結(jié)

關(guān)于canvas的介紹就到此為止,了解canvas的基本繪制碍舍,知道它的兩個坐標系柠座,學(xué)會和drawable、bitmap混合使用基本就差不多了片橡。希望能給大家?guī)砗眠\妈经。

前段時間秋招找工作實習(xí)什么的斷更好久,從今天開始捧书,立個FLAG在此吹泡,一天一篇!

最后編輯于
?著作權(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
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來敌卓,“玉大人慎式,你說我怎么就攤上這事√司叮” “怎么了瘪吏?”我有些...
    開封第一講書人閱讀 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)容