聽說懂Canvas的人運(yùn)氣都不會(huì)太差!

1.前言

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

2.Canvas簡(jiǎn)介

官方文檔介紹如下

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).

用人話說大概是這樣

一個(gè)Canvas類對(duì)象有四大基本要素
1、用來保存像素的Bitmap
2、用來在Bitmap上進(jìn)行繪制操作的Canvas
3、要繪制的東西
4、繪制用的畫筆Paint

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

3.Canvas基本繪制

一開始,先來點(diǎn)簡(jiǎn)單的基礎(chǔ)題。因?yàn)樘珣辛瞬幌霃念^寫起,我就假設(shè)大家都看過了Android Canvas 方法總結(jié),來寫一些這里面沒有介紹的方法。

3.1 圓角矩形

畫筆初始化

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

onDraw()中進(jìn)行繪制,參數(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 圓角矩形路徑

那么有的時(shí)候,我不需要那么整齊的圓角,該怎么辦呢

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就定義了四個(gè)角的弧度,接著使用Path就可以將其繪制出來。事實(shí)上,Path是無所不能的,我們將在以后單獨(dú)介紹他。

圓角矩形路徑.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);
    }

看上去挺萬金油的,什么都能傳進(jìn)去,那么Region能干嘛呢?我們可以用它來進(jìn)行一些交并補(bǔ)集的操作,比如下面代碼就能展示兩個(gè)region的交集

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

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

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

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


Region_op.png

那么這里就出現(xiàn)了一個(gè)問題,這些Op過后的region都是不規(guī)則的了,系統(tǒng)要如何將他們繪制出來呢?
嘿嘿嘿,請(qǐ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ū)域進(jìn)行交集

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

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

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

region繪制.png

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

4.Canvas基本變換

4.1 Canavas坐標(biāo)系

Canvas里面牽扯兩種坐標(biāo)系:Canvas自己的坐標(biāo)系、繪圖坐標(biāo)系

Canvas的坐標(biāo)系,它就在View的左上角,從坐標(biāo)原點(diǎn)往右是X軸正半軸,往下是Y軸的正半軸,有且只有一個(gè),唯一不變

繪圖坐標(biāo)系,它不是唯一不變的,它與Canvas的Matrix有關(guān)系,當(dāng)Matrix發(fā)生改變的時(shí)候,繪圖坐標(biāo)系對(duì)應(yīng)的進(jìn)行改變,同時(shí)這個(gè)過程是不可逆的(通過相反的矩陣還原),而Matrix又是通過我們?cè)O(shè)置translate、rotate、scale、skew來進(jìn)行改變的

上面這段話還是挺好理解的,我們用代碼來驗(yàn)證下

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

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

        //平移--即改變坐標(biāo)原點(diǎn)
        canvas.translate(50, 50);
        // 第二次繪制坐標(biāo)軸
        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);
        // 第三次繪制坐標(biāo)軸
        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 軸
    }

運(yùn)行結(jié)果如下

Canvas坐標(biāo)系

剩下的translate、rotate、scale、skew等方法,在之前推薦的那篇文章里就有,這里就不再重復(fù)勞動(dòng)了

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

5.1 狀態(tài)棧

狀態(tài)棧通過save、 restore方法來保存和還原變換操作Matrix以及Clip剪裁,也可以通過restoretoCount直接還原到對(duì)應(yīng)棧的保存狀態(tài)。需要注意的是,一開始canvas就是在棧1的位置,執(zhí)行一次save就進(jìn)棧一次(此時(shí)為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é)可能會(huì)把狀態(tài)棧理解為圖層,其實(shí)這是不對(duì)滴。我們來看下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),并沒有保存一個(gè)真正的圖層。真正的圖層是在Layer棧中保存的。

5.2 Layer棧

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

下面這段代碼要仔細(xì)看

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是新建了一個(gè)圖層,
        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

我們慢慢解釋。一開始畫了個(gè)綠色的框,之后移動(dòng)了(50,50)的距離,再通過saveLayer新建了一個(gè)圖層。注意這里用到了Canvas.ALL_SAVE_FLAG,別的FLAG還有只保存Matrix的,只保存Clip的等等,大家可以自己去看。接著在新的圖層上畫了藍(lán)色背景和黃色的矩形,從結(jié)果可以看出之前的一些Canvas操作會(huì)被延續(xù)到新的圖層。調(diào)用restore后,兩個(gè)圖層合二為一,由于圖層2是藍(lán)色背景,因此就把圖層1的綠色邊框覆蓋了。最后再繪制另一個(gè)紅色的框,此時(shí)只有一個(gè)圖層了,所以就繪制在當(dāng)前圖層的最上方。

文章開頭說Bitmap和Canvas的關(guān)系類似于畫板與畫布,就是因?yàn)槊看蜟anvas執(zhí)行saveLayer時(shí)都會(huì)新建一個(gè)透明的圖層,與之前的圖層疊加后更新到Bitmap上,從而將繪制的內(nèi)容展示出來。

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

6.Drawable與Canvas

6.1 Drawable簡(jiǎn)介

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

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

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

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

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

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

6.1 基本操作

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

效果圖.png

最外層是一個(gè)HorizontalScrollView,里面包裹著許多ImageView,可以拖動(dòng),中間選中的區(qū)域呈現(xiàn)灰色,其余的顯示彩色。這些灰色、彩色的圖是兩套資源。

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

6.1.1 RevealDrawable

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

對(duì)于每一部分的圖片而言,會(huì)有以下幾種繪制情況:

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

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

對(duì)于問題一,我們可以用之前所說的canvas裁剪來完成,需要注意save()restore()的調(diào)用;至于問題二,Drawable源碼中有這么一行參數(shù):private int mLevel = 0;,很顯然,Google早已考慮到Drawable的這種使用場(chǎng)景,而mLevel就是用來確定比例的,其值為0~10000,可以由我們?cè)谕鈱觿?dòng)態(tài)去設(shè)置。

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

這里就展示其核心的繪制方法,注釋都有,完整的代碼等閑了整理下一起放到大型同性交友平臺(tái)上。

@Override
    public void draw(Canvas canvas) {
        // 繪制
        int level = getLevel();//from 0 (minimum) to 10000 
        //三個(gè)區(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;
                //從一個(gè)已有的bounds矩形邊界范圍中摳出一個(gè)矩形r
                Gravity.apply(
                        gravity,//從左邊還是右邊開始摳
                        w,//目標(biāo)矩形的寬 
                        h, //目標(biāo)矩形的高
                        bounds, //被摳出來的rect
                        r);//目標(biāo)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;
                //從一個(gè)已有的bounds矩形邊界范圍中摳出一個(gè)矩形r
                Gravity.apply(
                        gravity,//從左邊還是右邊開始摳
                        w,//目標(biāo)矩形的寬 
                        h, //目標(biāo)矩形的高
                        bounds, //被摳出來的rect
                        r);//目標(biāo)rect
                canvas.save();//保存畫布
                canvas.clipRect(r);//切割
                mSelectedDrawable.draw(canvas);//畫
                canvas.restore();//恢復(fù)之前保存的畫布
            }       
        }
    }
6.1.2 GalleryHorizontalScrollView

外層的GalleryHorizontalScrollView繼承自HorizontalScrollView,需要處理滑動(dòng)事件與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í)設(shè)置Padding值,以便于一開始,將第一個(gè)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();//單個(gè)圖片的寬度
        centerX = getWidth() / 2;//整個(gè)sv的寬度
        centerX = centerX - icon_width/2;
        container.setPadding(centerX, 0, centerX, 0);
    }
起始圖.png

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

private void reveal() {
        // 漸變效果
        //得到hzv滑出去的距離
        int scrollX = getScrollX();
        Log.d(TAG, "reveal: "+scrollX);
        //找到兩張漸變的圖片的下標(biāo)--左,右
        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的配合使用,接下來我們上一個(gè)Canvas與Bitmap混合實(shí)現(xiàn)緩存的效果。其實(shí)這句話是廢話,因?yàn)閯?chuàng)建Canvas時(shí)就必須傳入Bitmap為參,畢竟Bitmap才是真正保存像素的地方。

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

我們?cè)诔跏蓟椒ㄖ袆?chuàng)建緩存對(duì)象。

private void init() {
        //創(chuàng)建一個(gè)與該VIew相同大小的緩沖區(qū)
        cacheBitmap = Bitmap.createBitmap(VIEW_WIDTH, VIEW_HEIGHT, Bitmap.Config.ARGB_8888);
        //創(chuàng)建緩沖區(qū)Cache的Canvas對(duì)象
        cacheCanvas = new Canvas();
        path = new Path();
        //設(shè)置cacheCanvas將會(huì)繪制到內(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);//防抖動(dòng),比較清晰
    }

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

 @Override
    public boolean onTouchEvent(MotionEvent event) {
        //獲取拖動(dòng)時(shí)間的發(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()會(huì)回調(diào)draw()方法,此時(shí)再將緩存bitmap繪制到View的canvas中。

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

這樣一來,手指滑動(dòng)軌跡就會(huì)略有延遲后再繪制到用戶界面上,類似于寫字板的效果。

cache緩存.png

8.總結(jié)

關(guān)于canvas的介紹就到此為止,了解canvas的基本繪制,知道它的兩個(gè)坐標(biāo)系,學(xué)會(huì)和drawable、bitmap混合使用基本就差不多了。希望能給大家?guī)砗眠\(yùn)。

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,002評(píng)論 6 542
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,400評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,136評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,714評(píng)論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,452評(píng)論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,818評(píng)論 1 328
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,812評(píng)論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,997評(píng)論 0 290
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,552評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,292評(píng)論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,510評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,035評(píng)論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,721評(píng)論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,121評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,429評(píng)論 1 294
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,235評(píng)論 3 398
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,480評(píng)論 2 379

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