LoadingDrawable源碼分析

項目地址:LoadingDrawable糯笙,本文分析版本: 979291e

1.簡介

LoadingDrawable是一個使用Drawable來繪制Loading動畫的項目稽屏,由于使用Drawable的原因可以結(jié)合任何View使用欠动,并且替換方便。目前已經(jīng)實現(xiàn)了8種動畫,而且項目作者dinuscxj表示后期會維護至20多種動畫权均。對于動畫項目,我們之前有分析過:HTextView锅锨、JJSearchViewAnim叽赊。此類項目的結(jié)構(gòu)大家應該比較熟悉了。那么我們就來一起看看LoadingDrawable是如何使用與實現(xiàn)的必搞。

2.使用方法

如果在ImageView上使用:

ImageView mIvGear = (ImageView) findViewById(R.id.gear_view);
LoadingDrawable mGearDrawable = new LoadingDrawable(new GearLoadingRenderer(this));
mIvGear.setImageDrawable(mGearDrawable);

mGearDrawable.start();

mGearDrawable.stop();

如果在View上使用:

View mIvGear = findViewById(R.id.gear_view);
LoadingDrawable mGearDrawable = new LoadingDrawable(new GearLoadingRenderer(this));
mIvGear.setBackground(mGearDrawable);

mGearDrawable.start();

mGearDrawable.stop();

LoadingDrawable的使用方法非常簡單必指,我們只需要在setImageDrawable()方法或者setBackground()傳入LoadingDrawable對象并在構(gòu)造方法中初始化對應的動畫實現(xiàn)類就可以了。另外開啟動畫與停止動畫分別對應LoadingDrawable中的start()stop()方法即可恕洲。

3.類關(guān)系圖

LoadingDrawable.png

類關(guān)系圖很清晰塔橡,就不再多說了,我們直接來看源碼:

4.源碼分析

1.LoadingDrawable的實現(xiàn):

首先我們來看看LoadingDrawable是如何實現(xiàn)的研侣。代碼如下:


public class LoadingDrawable extends Drawable implements Animatable {
  //LoadingRenderer負責具體動畫的繪制
  private LoadingRenderer mLoadingRender;

  //Drawable.CallBack這里是負責更新Drawable谱邪。
  private final Callback mCallback = new Callback() {
    @Override
    public void invalidateDrawable(Drawable d) {
      invalidateSelf();
    }

    @Override
    public void scheduleDrawable(Drawable d, Runnable what, long when) {
      scheduleSelf(what, when);
    }

    @Override
    public void unscheduleDrawable(Drawable d, Runnable what) {
      unscheduleSelf(what);
    }
  };

  //構(gòu)造方法
  public LoadingDrawable(LoadingRenderer loadingRender) {
    this.mLoadingRender = loadingRender;
    this.mLoadingRender.setCallback(mCallback);
  }

  @Override
  public void draw(Canvas canvas) {
    //直接交給mLoadingRender的draw()方法
    mLoadingRender.draw(canvas, getBounds());
  }

  @Override
  public void setAlpha(int alpha) {
    mLoadingRender.setAlpha(alpha);
  }

  @Override
  public void setColorFilter(ColorFilter cf) {
    mLoadingRender.setColorFilter(cf);
  }

  @Override
  public int getOpacity() {
    //返回透明的像素格式
    return PixelFormat.TRANSLUCENT;
  }

  @Override
  public void start() {
    mLoadingRender.start();
  }

  @Override
  public void stop() {
    mLoadingRender.stop();
  }

  @Override
  public boolean isRunning() {
    return mLoadingRender.isRunning();
  }

  @Override
  public int getIntrinsicHeight() {
    //返回Drawable的高度
    return (int) (mLoadingRender.getHeight() + 1);
  }

  @Override
  public int getIntrinsicWidth() {
    //返回Drawable的寬度
    return (int) (mLoadingRender.getWidth() + 1);
  }
}

LoadingDrawable是繼承自Drawable的并且實現(xiàn)了Animatable接口,Drawable簡單的來說就是可以通過Canvas來繪制出圖形或者圖像的類庶诡。通俗的抽象就是:一些能被畫出來的東西惦银。想必大家也都很熟悉了。對于Animatable來說,其實就是Android提供給需要實現(xiàn)動畫的Drawable需要實現(xiàn)的接口扯俱,它分別有start()书蚪、stop()isRunning()方法很顯然應該是控制動畫的開始和停止。
對于自定義的Drawable迅栅,draw()方法應該是最重要的了殊校。這里可以看出draw()方法中直接交給了mLoadingRender來處理《链妫看過我們以前分析的幾篇動畫庫的同學應該知道为流,mLoadingRender肯定就是所有動畫的父類了。然后根據(jù)父類的抽象方法來分別做具體的實現(xiàn)让簿,從而實現(xiàn)不同的動畫敬察。所以我們接著來看LoadingRenderer的實現(xiàn):

2.LoadingRenderer的實現(xiàn):

LoadingRenderer的主要代碼如下:


public abstract class LoadingRenderer {
  protected float mWidth;
  protected float mHeight;
  protected float mStrokeWidth;
  protected float mCenterRadius;

  private long mDuration;
  private Drawable.Callback mCallback;
  private ValueAnimator mRenderAnimator;

  public LoadingRenderer(Context context) {
    setupDefaultParams(context);
    setupAnimators();
  }

  //抽象方法交給子類去實現(xiàn)
  public abstract void draw(Canvas canvas, Rect bounds);
  public abstract void computeRender(float renderProgress);
  public abstract void setAlpha(int alpha);
  public abstract void setColorFilter(ColorFilter cf);
  public abstract void reset();

  public void start() {
    reset();
    setDuration(mDuration);
    mRenderAnimator.start();
  }

  public void stop() {
    mRenderAnimator.cancel();
  }

  public boolean isRunning() {
    return mRenderAnimator.isRunning();
  }

  public void setCallback(Drawable.Callback callback) {
    this.mCallback = callback;
  }

  //invalidate方法,重繪當前Drawable
  protected void invalidateSelf() {
    mCallback.invalidateDrawable(null);
  }

  //設(shè)置寬度,高度,線條寬度以及圓的半徑等默認參數(shù)
  private void setupDefaultParams(Context context) {
    final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
    final float screenDensity = metrics.density;

    mWidth = DEFAULT_SIZE * screenDensity;
    mHeight = DEFAULT_SIZE * screenDensity;
    mStrokeWidth = DEFAULT_STROKE_WIDTH * screenDensity;
    mCenterRadius = DEFAULT_CENTER_RADIUS * screenDensity;

    mDuration = ANIMATION_DURATION;
  }

  //設(shè)置ValueAnimator的參數(shù)
  private void setupAnimators() {
    mRenderAnimator = ValueAnimator.ofFloat(0, 1);
    mRenderAnimator.setRepeatCount(Animation.INFINITE);
    mRenderAnimator.setRepeatMode(Animation.RESTART);
    mRenderAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
      @Override
      public void onAnimationUpdate(ValueAnimator animation) {
        computeRender((float) animation.getAnimatedValue());
        invalidateSelf();
      }
    });
  }
}

果然和我們的猜想是一樣的LoadingRenderer是一個抽象類尔当,首先在構(gòu)造方法里定義了一些參數(shù)的默認值莲祸,例如:寬高,描邊寬度椭迎,圓的默認半徑锐帜。以及初始化了一個ValueAnimator,并在onAnimationUpdate()方法里調(diào)用了computeRender()以及invalidateSelf()方法畜号。
其中computeRender(float renderProgress)方法里的renderProgress就是0到1不斷變化的值缴阎,這個方法是用來計算當前動畫繪制需要的參數(shù),在實現(xiàn)動畫的時候我們通常會使用這種方法根據(jù)當前的renderProgress的值來計算當前需要繪制圖像的參數(shù)简软,從而完成繪制药蜻。一般情況下我們都會在draw()里直接根據(jù)renderProgress來做計算然后直接進行繪制,但是這樣寫出的代碼的可讀性就不太好了替饿,因為計算和繪制都寫在了一起。LoadingDrawable在這一點上就做的非常好贸典。它通過computeRender(float renderProgress);方法來計算好能直接被draw();方法使用的參數(shù)视卢。同時draw()方法里就只負責繪制的邏輯。在計算比較復雜的場景這樣做能極大的提高代碼的可讀性廊驼。這一點非常值得我們學習据过。
看完了LoadingRenderer的實現(xiàn),接下來我們就來看看其中一個具體的實現(xiàn)類到底是如何實現(xiàn)的妒挎。LoadingRenderer目前實現(xiàn)了8種不同的動畫绳锅,具體在LoadingRenderergithub主頁上都可以看到。大家也可以選自己喜歡的動畫去分析酝掩。(由于我的數(shù)學很渣鳞芙。。)我們這次就分析一個較為簡單的動畫WhorlLoadingRenderer。就是下圖左上角的這個動畫:

3.WhorlLoadingRenderer 的實現(xiàn):

首先我們先來分解一下這個動畫:

  1. 首先是不斷旋轉(zhuǎn)的原朝。
  2. 先從初始點繪制出一個弧形驯嘱,然后弧形再不斷的縮短直至重新變?yōu)橐粋€點,最終不斷循環(huán)喳坠。

其中還有一些細節(jié)比如三條弧線的位置以及線條寬度等等鞠评,我們來看看WhorlLoadingRenderer是怎樣實現(xiàn)的:


public class WhorlLoadingRenderer extends LoadingRenderer {
  private static final Interpolator MATERIAL_INTERPOLATOR = new FastOutSlowInInterpolator();

  private static final float FULL_ROTATION = 1080.0f;
  private static final float ROTATION_FACTOR = 0.25f;
  private static final float MAX_PROGRESS_ARC = 0.6f;

  private static final float START_TRIM_DURATION_OFFSET = 0.5f;
  private static final float END_TRIM_DURATION_OFFSET = 1.0f;

  private static final int DEGREE_180 = 180;
  private static final int DEGREE_360 = 360;
  private static final int NUM_POINTS = 5;

  private int[] mColors;
  private float mStrokeInset;
  private float mEndTrim;
  private float mRotation;
  private float mStartTrim;
  private float mRotationCount;
  private float mGroupRotation;
  private float mOriginEndTrim;
  private float mOriginRotation;
  private float mOriginStartTrim;

  private static final int[] DEFAULT_COLORS = new int[] {
      Color.RED, Color.GREEN, Color.BLUE
  };

  private final Paint mPaint = new Paint();
  private final RectF mTempBounds = new RectF();

  private final Animator.AnimatorListener mAnimatorListener = new AnimatorListenerAdapter() {
    @Override
    public void onAnimationRepeat(Animator animator) {
      super.onAnimationRepeat(animator);
      //儲存初始點
      storeOriginals();

      mStartTrim = mEndTrim;
      mRotationCount = (mRotationCount + 1) % (NUM_POINTS);
    }

    @Override
    public void onAnimationStart(Animator animation) {
      super.onAnimationStart(animation);
      mRotationCount = 0;
    }
  };

  public WhorlLoadingRenderer(Context context) {
    super(context);
    //設(shè)置Paint的參數(shù)
    setupPaint();
    //添加動畫監(jiān)聽
    addRenderListener(mAnimatorListener);
  }

  private void setupPaint() {
    mColors = DEFAULT_COLORS;

    mPaint.setAntiAlias(true);
    mPaint.setStrokeWidth(getStrokeWidth());
    mPaint.setStyle(Paint.Style.STROKE);
    mPaint.setStrokeCap(Paint.Cap.ROUND);
    //設(shè)置內(nèi)邊距
    setInsets((int) getWidth(), (int) getHeight());
  }

  @Override
  public void draw(Canvas canvas, Rect bounds) {
    //保存當前畫布狀態(tài)
    int saveCount = canvas.save();

    //旋轉(zhuǎn)畫布
    canvas.rotate(mGroupRotation, bounds.exactCenterX(), bounds.exactCenterY());
    //設(shè)置繪制的RectF
    RectF arcBounds = mTempBounds;
    arcBounds.set(bounds);
    //設(shè)置內(nèi)邊距
    arcBounds.inset(mStrokeInset, mStrokeInset);

    if (mStartTrim == mEndTrim) {
      mStartTrim = mEndTrim + getMinProgressArc();
    }

    //開始的角度
    float startAngle = (mStartTrim + mRotation) * DEGREE_360;
    //結(jié)束的角度
    float endAngle = (mEndTrim + mRotation) * DEGREE_360;
    //角度范圍
    float sweepAngle = endAngle - startAngle;

    //根據(jù)mColors.length來繪制曲線
    for (int i = 0; i < mColors.length; i++) {
      mPaint.setStrokeWidth(getStrokeWidth() / (i + 1));
      mPaint.setColor(mColors[i]);
      //繪制弧線
      canvas.drawArc(createArcBounds(arcBounds, i), startAngle + DEGREE_180 * (i % 2), sweepAngle, false, mPaint);
    }

    //恢復畫布狀態(tài)
    canvas.restoreToCount(saveCount);
  }

  // 根據(jù)需要繪制弧線的index 來計算不同的繪制范圍即arcBounds
  private RectF createArcBounds(RectF sourceArcBounds, int index) {
    RectF arcBounds = new RectF();
    int intervalWidth = 0;

    for (int i = 0; i < index; i++) {
      intervalWidth += getStrokeWidth() / (i + 1.0f) * 1.5f;
    }

    int arcBoundsLeft = (int) (sourceArcBounds.left + intervalWidth);
    int arcBoundsTop = (int) (sourceArcBounds.top + intervalWidth);
    int arcBoundsRight = (int) (sourceArcBounds.right - intervalWidth);
    int arcBoundsBottom = (int) (sourceArcBounds.bottom - intervalWidth);
    arcBounds.set(arcBoundsLeft, arcBoundsTop, arcBoundsRight, arcBoundsBottom);

    return arcBounds;
  }

  @Override
  public void computeRender(float renderProgress) {
    //獲得最小的弧度
    final float minProgressArc = getMinProgressArc();
    final float originEndTrim = mOriginEndTrim;
    final float originStartTrim = mOriginStartTrim;
    final float originRotation = mOriginRotation;

    // Moving the start trim only occurs in the first 50% of a
    // single ring animation
    // 當renderProgress < 0.5時,不斷增加mStartTrim的值
    if (renderProgress <= START_TRIM_DURATION_OFFSET) {
      float startTrimProgress = (renderProgress) / (1.0f - START_TRIM_DURATION_OFFSET);
      mStartTrim = originStartTrim + ((MAX_PROGRESS_ARC - minProgressArc) * MATERIAL_INTERPOLATOR.getInterpolation(startTrimProgress));
    }

    // Moving the end trim starts after 50% of a single ring
    // animation completes
    // 當renderProgress > 0.5時壕鹉,不斷增加mEndTrim的值
    if (renderProgress > START_TRIM_DURATION_OFFSET) {
      float endTrimProgress = (renderProgress - START_TRIM_DURATION_OFFSET) / (END_TRIM_DURATION_OFFSET - START_TRIM_DURATION_OFFSET);
      mEndTrim = originEndTrim + ((MAX_PROGRESS_ARC - minProgressArc) * MATERIAL_INTERPOLATOR.getInterpolation(endTrimProgress));
    }

    //計算畫布整體的旋轉(zhuǎn)角度
    mGroupRotation = ((FULL_ROTATION / NUM_POINTS) * renderProgress) + (FULL_ROTATION * (mRotationCount / NUM_POINTS));
    //計算弧線點的旋轉(zhuǎn)
    mRotation = originRotation + (ROTATION_FACTOR * renderProgress);

    invalidateSelf();
  }

  @Override
  public void reset() {
    resetOriginals();
  }

  //設(shè)置內(nèi)邊距剃幌,為了使動畫的繪制居中
  public void setInsets(int width, int height) {
    final float minEdge = (float) Math.min(width, height);
    float insets;
    if (getCenterRadius() <= 0 || minEdge < 0) {
      insets = (float) Math.ceil(getStrokeWidth() / 2.0f);
    } else {
      insets = minEdge / 2.0f - getCenterRadius();
    }
    mStrokeInset = insets;
  }

  private void storeOriginals() {
    mOriginStartTrim = mStartTrim;
    mOriginEndTrim = mEndTrim;
    mOriginRotation = mRotation;
  }

  //重置初始參數(shù)
  private void resetOriginals() {
    mOriginStartTrim = 0;
    mOriginEndTrim = 0;
    mOriginRotation = 0;
    setStartTrim(0);
    setEndTrim(0);
    setRotation(0);
  }

  //獲得最小弧度,
  private float getMinProgressArc() {
    //Math.toRadians將角度轉(zhuǎn)換為弧度,這里的角度是描邊的寬度 / 周長。所以繪制出的初始是一個點
    return (float) Math.toRadians(getStrokeWidth() / (2 * Math.PI * getCenterRadius()));
  }
}

WhorlLoadingRenderer的代碼中可以看出晾浴,首先在構(gòu)造方法里設(shè)置了Paint的參數(shù)以及添加了動畫監(jiān)聽mAnimatorListener负乡。然后當start()方法觸發(fā)時,會不斷的調(diào)用computeRender()draw()方法怠肋。其中computeRender()負責計算mStartTrim敬鬓、mEndTrimmGroupRotation等參數(shù)笙各。draw()方法根據(jù)計算得到的值來繪制出對應的圖形钉答。從而最終形成動畫。

5.注意事項

在使用LoadingDrawable過程中我們會發(fā)現(xiàn)使用ImageViewsetImageDrawable()方法與使用ViewsetBackground()效果并不一樣:

使用ImageViewsetImageDrawable()方法效果如下:

setImageDrawable()
setImageDrawable()

而使用ViewsetBackground()方法效果如下:

setBackground()

同樣的實現(xiàn)代碼杈抢,兩種調(diào)用方法的效果竟然不一樣数尿。setBackground()中的圓環(huán)變大了,而線條變細了惶楼,那這到底是因為什么呢右蹦?我們只能從這兩個方法的源碼中找答案了。我們來看看setImageDrawable()setBackground()是如何實現(xiàn)的(源碼對應Android API 23):

1.ImageView的setImageDrawable()方法的實現(xiàn):

    public void setImageDrawable(@Nullable Drawable drawable) {
        if (mDrawable != drawable) {
            mResource = 0;
            mUri = null;

            final int oldWidth = mDrawableWidth;
            final int oldHeight = mDrawableHeight;

            updateDrawable(drawable);

            if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) {
                requestLayout();
            }
            invalidate();
        }
    }

首先判斷mDrawable是否為空歼捐,然后賦值了寬高何陆,緊接著調(diào)用了updateDrawable(drawable)

    private void updateDrawable(Drawable d) {
        if (d != mRecycleableBitmapDrawable && mRecycleableBitmapDrawable != null) {
            mRecycleableBitmapDrawable.setBitmap(null);
        }

        if (mDrawable != null) {
            mDrawable.setCallback(null);
            unscheduleDrawable(mDrawable);
        }

        mDrawable = d;

        if (d != null) {
            d.setCallback(this);
            d.setLayoutDirection(getLayoutDirection());
            if (d.isStateful()) {
                d.setState(getDrawableState());
            }
            d.setVisible(getVisibility() == VISIBLE, true);
            d.setLevel(mLevel);
            mDrawableWidth = d.getIntrinsicWidth();
            mDrawableHeight = d.getIntrinsicHeight();
            applyImageTint();
            applyColorMod();

            configureBounds();
        } else {
            mDrawableWidth = mDrawableHeight = -1;
        }
    }

設(shè)置Callback以及一些參數(shù),這里是把ImageView設(shè)置成了mDrawableCallback豹储,所以當調(diào)用LoadingDrawablemCallbackinvalidateSelf();方法時其實是調(diào)用了ImageViewinvalidateDrawable()方法從而更新drawable贷盲。這里我們的重點是看configureBounds()方法:


    private void configureBounds() {
        if (mDrawable == null || !mHaveFrame) {
            return;
        }

        int dwidth = mDrawableWidth;
        int dheight = mDrawableHeight;

        int vwidth = getWidth() - mPaddingLeft - mPaddingRight;
        int vheight = getHeight() - mPaddingTop - mPaddingBottom;

        boolean fits = (dwidth < 0 || vwidth == dwidth) &&
                       (dheight < 0 || vheight == dheight);

        if (dwidth <= 0 || dheight <= 0 || ScaleType.FIT_XY == mScaleType) {
            /* If the drawable has no intrinsic size, or we're told to
                scaletofit, then we just fill our entire view.
            */
            mDrawable.setBounds(0, 0, vwidth, vheight);
            mDrawMatrix = null;
        } else {
            // We need to do the scaling ourself, so have the drawable
            // use its native size.
            //我們需要自身縮放。所以drawable使用它已有的尺寸
            mDrawable.setBounds(0, 0, dwidth, dheight);
            ......
        }
    }

省略了部分代碼剥扣,這里我們注意看注釋巩剖。當drawable的寬高為0或者ImageViewScaleTypeScaleType.FIT_XY時。直接把當前View去除padding的寬高設(shè)置給drawable钠怯。如果drawable有寬高的話佳魔,那么ImageView則會自身縮放來適應drawable。具體的縮放是通過Matrix來做的晦炊。有興趣的同學可以自行研究鞠鲜。其實這里設(shè)置了mDrawable的寬高宁脊,所以在LoadingDrawable類里的draw()方法:


  @Override
  public void draw(Canvas canvas) {
    //直接交給mLoadingRender的draw()方法
    mLoadingRender.draw(canvas, getBounds());
  }

中的getBounds()有可能會被ImageView重新設(shè)置。所以如果把ImageViewScaleType設(shè)置成ScaleType.FIT_XY那么結(jié)果就會和setBackground()一樣镊尺。接下來我們看看setBackground()是怎么實現(xiàn)的:

2.ViewsetBackground()方法實現(xiàn)


    public void setBackground(Drawable background) {
        //noinspection deprecation
        setBackgroundDrawable(background);
    }

    /**
     * @deprecated use {@link #setBackground(Drawable)} instead
     */
    @Deprecated
    public void setBackgroundDrawable(Drawable background) {
        computeOpaqueFlags();

        if (background == mBackground) {
            return;
        }

        boolean requestLayout = false;
        mBackgroundResource = 0;

        if (mBackground != null) {
            mBackground.setCallback(null);
            unscheduleDrawable(mBackground);
        }

        if (background != null) {
            ......
            mBackground = background;
            ......
        }
        ......
        computeOpaqueFlags();

        if (requestLayout) {
            requestLayout();
        }

        mBackgroundSizeChanged = true;
        invalidate(true);
    }

我們發(fā)現(xiàn)方法里就只把drawable賦值給了mBackground并沒有操作drawable的大小朦佩。省略的部分代碼也沒有相關(guān)邏輯。但是我們知道最終mBackground是要被繪制出來的庐氮。我們?nèi)?code>View的draw()方法看看:


    public void draw(Canvas canvas) {
        ...
        if (!dirtyOpaque) {
            drawBackground(canvas);
        }
        ...
    }

果然有drawBackground(canvas)


    private void drawBackground(Canvas canvas) {
        final Drawable background = mBackground;
        if (background == null) {
            return;
        }

        setBackgroundBounds();
        ...    
    }

    void setBackgroundBounds() {
        if (mBackgroundSizeChanged && mBackground != null) {
            mBackground.setBounds(0, 0,  mRight - mLeft, mBottom - mTop);
            mBackgroundSizeChanged = false;
            rebuildOutline();
        }
    }

省略部分繪制背景的代碼语稠,最終我們發(fā)現(xiàn)會將View的寬高設(shè)置給mBackground。所以我們就找出了為什么setBackground()方法會繪制出不一樣的效果弄砍。所以這里推薦LoadingDrawableImageView配合使用仙畦。如果配合View使用可能還需要自己去手動調(diào)整一些參數(shù)。

6.個人評價

LoadingDrawable實現(xiàn)了多種實用的Loading動畫音婶,并且在一些特定的業(yè)務(wù)場景下慨畸,Drawable使用起來更加方便。除了需要注意上一條的注意事項之外衣式。LoadingDrawable非常適合在項目中使用寸士。而且LoadingDrawable的代碼相當規(guī)范。如果你的項目里有類似動畫的需求碴卧,結(jié)合LoadingDrawable一定能讓你事半功倍!

我每周會寫一篇源代碼分析的文章,以后也可能會有其他主題.
如果你喜歡我寫的文章的話,歡迎關(guān)注我的新浪微博@達達達達sky
地址: http://weibo.com/u/2030683111
每周我會第一時間在微博分享我寫的文章,也會積極轉(zhuǎn)發(fā)更多有用的知識給大家.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末弱卡,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子住册,更是在濱河造成了極大的恐慌婶博,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件荧飞,死亡現(xiàn)場離奇詭異凡人,居然都是意外死亡,警方通過查閱死者的電腦和手機叹阔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門挠轴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人耳幢,你說我怎么就攤上這事忠荞。” “怎么了帅掘?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長堂油。 經(jīng)常有香客問我修档,道長,這世上最難降的妖魔是什么府框? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任吱窝,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘院峡。我一直安慰自己兴使,他們只是感情好,可當我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布照激。 她就那樣靜靜地躺著发魄,像睡著了一般。 火紅的嫁衣襯著肌膚如雪俩垃。 梳的紋絲不亂的頭發(fā)上励幼,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天,我揣著相機與錄音口柳,去河邊找鬼苹粟。 笑死,一個胖子當著我的面吹牛跃闹,可吹牛的內(nèi)容都是我干的嵌削。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼望艺,長吁一口氣:“原來是場噩夢啊……” “哼苛秕!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起荣茫,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤想帅,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后啡莉,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體港准,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年咧欣,在試婚紗的時候發(fā)現(xiàn)自己被綠了浅缸。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡魄咕,死狀恐怖衩椒,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情哮兰,我是刑警寧澤毛萌,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站喝滞,受9級特大地震影響阁将,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜右遭,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一做盅、第九天 我趴在偏房一處隱蔽的房頂上張望缤削。 院中可真熱鬧,春花似錦吹榴、人聲如沸亭敢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽帅刀。三九已至,卻和暖如春婿斥,著一層夾襖步出監(jiān)牢的瞬間劝篷,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工民宿, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留娇妓,地道東北人。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓活鹰,卻偏偏與公主長得像哈恰,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子志群,可洞房花燭夜當晚...
    茶點故事閱讀 42,877評論 2 345

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

  • 在iOS中隨處都可以看到絢麗的動畫效果着绷,實現(xiàn)這些動畫的過程并不復雜,今天將帶大家一窺iOS動畫全貌锌云。在這里你可以看...
    F麥子閱讀 5,094評論 5 13
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,520評論 25 707
  • 1. 概述 對于Drawable荠医,相信大家都不陌生,而且用起來非常方便桑涎。在Android中Drawable代表可以...
    小蕓論閱讀 3,719評論 1 30
  • 注意事項: 布局優(yōu)化彬向;盡量使用include、merge攻冷、ViewStub標簽娃胆,盡量不存在冗余嵌套及過于復雜布局(...
    HarryXR閱讀 5,149評論 1 19
  • 月亮延續(xù)了陽光, 讓回家的路通暢等曼。 不起眼的紅磚墻里烦, 誰畫的白色夢想。
    趙著急_閱讀 160評論 0 0