lottie源碼分析

lottie簡介

Lottie是Airbnb開源的一個(gè)動畫渲染庫始锚,同時(shí)支持Android刽酱、IOS、React Native和Web平臺瞧捌,Lottie目前只支持渲染播放AE動畫棵里。Lottie使用bobymovin(After Effects插件)導(dǎo)出的json數(shù)據(jù)作為動畫數(shù)據(jù)源。

image
image
image

lottie的優(yōu)缺點(diǎn)

優(yōu)點(diǎn):

  • 相對與矢量動畫姐呐,lottie的操作更為簡單殿怜,生成文件的操作不需要程序猿完成,而且AE相對于一些矢量圖制作工具更加強(qiáng)大效果更好
    矢量圖在線制作工具 https://shapeshifter.design
  • 使用GIF曙砂,使用幀動畫占用空間大头谜,Android原生不支持GIF動畫的顯示。
  • 組合式動畫鸠澈,通過大量代碼實(shí)現(xiàn)復(fù)雜的動畫效果柱告,代碼復(fù)雜,不好調(diào)試笑陈,也會浪費(fèi)很多時(shí)間成本
  • Android, iOS, 和React Native多平臺支持
  • 降低動畫設(shè)計(jì)和開發(fā)成本
  • 完美解決設(shè)計(jì)提供動畫效果與實(shí)現(xiàn)不一致問題
  • 不需要ui適配

缺點(diǎn):依然有局限性际度,對于一些復(fù)雜的動畫特效,如高斯模糊等部分AE特效無法實(shí)現(xiàn)涵妥,可能是由于json文件不好描述

框架原理

使用AE工具生成一段json乖菱,Lottie使用json文件來作為動畫數(shù)據(jù)源,然后解析json數(shù)據(jù),根據(jù)解析后的數(shù)據(jù)建立合適的Drawable繪制到View上面窒所,然后不斷觸發(fā)view的繪制

使用

private void play(String name){
        // 取消播放
        mAnimationView.cancelAnimation();
        // 是否循環(huán)播放
        mAnimationView.loop(true);
        // 設(shè)置播放速率娜氏,例如:2代表播放速率是不設(shè)置時(shí)的二倍
        //mAnimationView.setSpeed(2f);
        // 開始播放
        mAnimationView.playAnimation();
        // 暫停播放
        mAnimationView.pauseAnimation();
        // 設(shè)置播放進(jìn)度
        //mAnimationView.setProgress(0.5f);
        // 判斷是否正在播放
       // mAnimationView.isAnimating();
        mAnimationView.setAnimation(name);
        mAnimationView.loop(false);
        mAnimationView.playAnimation();
    }

   /**
     * 自定義播放動畫和時(shí)長
     */
    private void playValueAnimator(){
        ValueAnimator valueAnimator = ValueAnimator
                .ofFloat(0f, 1f)
                .setDuration(5000);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mAnimationView.setProgress((Float) animation.getAnimatedValue());
            }
        });
        valueAnimator.start();
    }

動態(tài)屬性

在動畫播放的過程改變一些屬性,如動畫墩新,positon等

 /**
     * 設(shè)置顏色
     */
    private void setColor(){
        //Shirt,Group 5,Fill 1都是layer的名稱
        KeyPath shirt = new KeyPath("Shirt", "Group 5", "Fill 1");
        KeyPath leftArm = new KeyPath("LeftArmWave", "LeftArm", "Group 6", "Fill 1");
        KeyPath rightArm = new KeyPath("RightArm", "Group 6", "Fill 1");

        //關(guān)鍵path贸弥,需要改變的屬性
        mAnimationView.addValueCallback(shirt, LottieProperty.COLOR,
                new LottieValueCallback<Integer>(mColorArray[mIndex]){});

        mAnimationView.addValueCallback(leftArm, LottieProperty.COLOR,
                new LottieValueCallback<Integer>(mColorArray[mIndex]){});

        mAnimationView.addValueCallback(rightArm, LottieProperty.COLOR,
                new LottieValueCallback<Integer>(mColorArray[mIndex]){});
    }

    /**
     * 設(shè)置彈跳高度
     */
    private void setJumpHeight(){
        final PointF pointF = new PointF();
        mAnimationView.addValueCallback(new KeyPath("Body"), LottieProperty.TRANSFORM_POSITION, new SimpleLottieValueCallback<PointF>() {
            @Override
            public PointF getValue(LottieFrameInfo<PointF> frameInfo) {
                float startX = frameInfo.getStartValue().x;
                float startY = frameInfo.getStartValue().y;
                float endY = frameInfo.getEndValue().y;

                if (startY > endY) {
                    startY += mJmupArray[mIndex];
                } else if (endY > startY) {
                    endY += mJmupArray[mIndex];
                }
                pointF.set(startX, MiscUtils.lerp(startY, endY, frameInfo.getInterpolatedKeyframeProgress()));
                return pointF;
            }
        });
    }

事件綁定

與手勢事件綁定,但本質(zhì)上還是對positon做操作

private void initData() {
        final LottieRelativePointValueCallback largeValueCallback
                = new LottieRelativePointValueCallback(new PointF(0f, 0f));

        mAnimationView.addValueCallback(new KeyPath("First"), LottieProperty.TRANSFORM_POSITION,
                largeValueCallback);

        final LottieRelativePointValueCallback mediumValueCallback
                = new LottieRelativePointValueCallback(new PointF(0f, 0f));

        mAnimationView.addValueCallback(new KeyPath("Fourth"), LottieProperty.TRANSFORM_POSITION,
                mediumValueCallback);

        final LottieRelativePointValueCallback smallValueCallback
                = new LottieRelativePointValueCallback(new PointF(0f, 0f));

        mAnimationView.addValueCallback(new KeyPath("Seventh"), LottieProperty.TRANSFORM_POSITION,
                smallValueCallback);

        //mContainerView的點(diǎn)擊拖動事件委托給ViewDragHelper海渊,ViewDragHelper中對mTargetView做相應(yīng)處理
        ViewDragHelper viewDragHelper = ViewDragHelper.create(mContainerView, new ViewDragHelper.Callback() {

            /**
             * 捕獲拖動的這個(gè)View
             */
            @Override
            public boolean tryCaptureView(@NonNull View child, int pointerId) {
                return child == mTargetView;
            }

            /**
             * 拖動的這個(gè)View的位置發(fā)生變化
             *
             * @param changedView  當(dāng)前拖動的這個(gè)View
             * @param left         距離左邊的距離
             * @param top          距離右邊的距離
             * @param dx           x軸的變化量
             * @param dy           y軸的變化量
             */
            public void onViewPositionChanged(@NonNull View changedView, int left, int top, @Px int dx,
                                              @Px int dy) {
                totalDx += dx;
                totalDy += dy;
                //控制的是圓心然后觸發(fā)重新繪制,就是位置的距離轉(zhuǎn)換一下設(shè)置給新的圓心
                //這個(gè)觸摸綁定交互可能不具有參考意義绵疲,因?yàn)閯赢嫑]有特別復(fù)雜,直接canvas畫三個(gè)圓也能達(dá)到同樣的效果
                smallValueCallback.setValue(getPoint(totalDx, totalDy, 1.2f));
                mediumValueCallback.setValue(getPoint(totalDx, totalDy, 1f));
                largeValueCallback.setValue(getPoint(totalDx, totalDy, 0.75f));
            }

            public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
                return left;
            }

            public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
                return top;
            }

        });
        mContainerView.setViewDragHelper(viewDragHelper);
    }

    private PointF getPoint(float dx, float dy, float factor) {
        return new PointF(dx * factor, dy * factor);
    }

關(guān)于協(xié)議

{
    "v": "4.11.1",  //使用bodymovin的版本
    "fr": 60,       //幀率
    "ip": 0,        //起始關(guān)鍵幀
    "op": 180,      //結(jié)束關(guān)鍵幀
    "w": 300,       //視圖的寬度 寬高會根據(jù)屏幕密度做轉(zhuǎn)換成scaleWidth
    "h": 300,       //視圖的高度
    "nm": "Comp 1", //從源碼中未看到對此字段解析
    "ddd": 0,      
    "assets": [],  //圖片集合
    "layers": [    //圖層集合臣疑,為圖片的本地路徑(assert等等)
        {
            "ddd": 0,
            "ind": 1,     //layer的Id盔憨,唯一
            "ty": “sh",    //layer的類型
            "nm": "Shape Layer 1",  //layer的名稱,在ae中生成唯一
            "sr": 1,
            "ks": {},      //外觀信息
            "ao": 0,
            "shapes": [],  //矢量圖形圖層的數(shù)組
            "ip": 0,       //   該圖層的起始關(guān)鍵幀
            "op": 180,     //該圖層的結(jié)束關(guān)鍵幀
            "st": 0,       
            "bm": 0
        },
        {...},
        {...},
        {...},
    ]
}

ks中的字段

  • a 位置信息
  • p 位移信息
  • s 縮放信息
  • r 翻轉(zhuǎn)信息
  • o 不透明度
  • so 開始時(shí)不透明度
  • eo 結(jié)束時(shí)不透明度

源碼解析

一個(gè)動畫文件的播放過程大概可以分為三部分

  • 解析json文件
  • view繪制
  • 動畫播放

解析json文件

從setAnimation方法點(diǎn)進(jìn)來讯沈,看到在執(zhí)行解析asset文件夾下文件

public void setAnimation(final String assetName) {
    this.animationName = assetName;
    animationResId = 0; 
    setCompositionTask(LottieCompositionFactory.fromAsset
    (getContext(), assetName));
  }

LottieCompositionFactory這個(gè)類有很多解析方法包括raw郁岩,asset等文件夾下

public static LottieTask<LottieComposition> fromAsset(Context context, final String fileName) {
    // Prevent accidentally leaking an Activity.
   final Context appContext = context.getApplicationContext();
    //如果之前緩存過,取緩存缺狠,線程同步的方法问慎,會阻塞主線程
    return cache(fileName, new 
    Callable<LottieResult<LottieComposition>>() {  
      @Override public LottieResult<LottieComposition> call() {
         //該方法就是拿到了json文件的字節(jié)流
        return fromAssetSync(appContext, fileName);
      }
    });
  }

拿到文件的字節(jié)流后對內(nèi)容進(jìn)行解析

LottieComposition composition = LottieCompositionParser.parse(reader);

解析時(shí)會對LottieComposition進(jìn)行賦值,拿到以下很多的字段

    float scale = Utils.dpScale();
    float startFrame = 0f;
    float endFrame = 0f;
    float frameRate = 0f;
    final LongSparseArray<Layer> layerMap = new LongSparseArray<>();
    final List<Layer> layers = new ArrayList<>();
    int width = 0;
    int height = 0;
    Map<String, List<Layer>> precomps = new HashMap<>();
    Map<String, LottieImageAsset> images = new HashMap<>();
    Map<String, Font> fonts = new HashMap<>();
    List<Marker> markers = new ArrayList<>();
    SparseArrayCompat<FontCharacter> characters = new SparseArrayCompat<>();

LottieTask是一個(gè)線程池挤茄,LottieResult是LottieComposition的結(jié)果或者exception如叼,監(jiān)聽回調(diào)中得到解析后composition數(shù)據(jù)結(jié)構(gòu)

private final LottieListener<LottieComposition> loadedListener = new LottieListener<LottieComposition>() {
    @Override public void onResult(LottieComposition composition) {
      //得到解析后composition
      setComposition(composition);
    }
  };

drawable的繪制

比較核心的兩個(gè)類
LottieComposition和LottieDrawable將會在下面專門進(jìn)行分析,他們分別進(jìn)行了兩個(gè)重要的工作:json文件的解析和動畫的繪制穷劈。

LottieAnimationView中的setComposition講數(shù)據(jù)結(jié)構(gòu)交給了lottieDrawable

  public void setComposition(@NonNull LottieComposition composition) {
    if (L.DBG) {
      Log.v(TAG, "Set Composition \n" + composition);
    }
    lottieDrawable.setCallback(this);
    this.composition = composition;
    //lottieDrawable對解析后composition數(shù)據(jù)做了加工
    boolean isNewComposition = 
lottieDrawable.setComposition(composition);
    enableOrDisableHardwareLayer();
    if (getDrawable() == lottieDrawable && !isNewComposition) {
      return;
    }
    setImageDrawable(null);
    setImageDrawable(lottieDrawable);
    requestLayout();
    for (LottieOnCompositionLoadedListener lottieOnCompositionLoadedListener : lottieOnCompositionLoadedListeners) {     lottieOnCompositionLoadedListener.onCompositionLoaded(composition);
    }
  }

lottieDrawable中的setComposition方法中的buildCompositionLayer開始真正的解析layer和繪制
layer算是lottie原理中一個(gè)比較重要的概念笼恰,就是圖層
layer的類型與 AE中的圖層的對應(yīng)關(guān)系為:

  • ShapeLayer:形狀圖層
  • CompositionLayer:預(yù)合成圖層
  • SolidLayer:純色圖層
  • ImageLayer:圖片素材圖層
  • NullLayer:空圖層
  • TextLayer:文本圖層

在android層面可以理解為圖層就是view,在一個(gè)布局viewGroup中有很多的view歇终,就是不斷的繪制這些view來完成這些動畫的社证,LottieComposition對Layer進(jìn)行數(shù)據(jù)的映射,在CompositionLayer中為每一個(gè)layer生成一個(gè)對應(yīng)的LayerView
簡單說就是解析json->layer對象的映射->layer對象為layerview構(gòu)造出各種path等->數(shù)據(jù)全部準(zhǔn)備好就是不斷的驅(qū)使draw方法完成繪制

下載.jpeg

CompositionLayer中的構(gòu)造方法

public CompositionLayer(LottieDrawable lottieDrawable, Layer layerModel, List<Layer> layerModels,
      LottieComposition composition) {
    super(lottieDrawable, layerModel);

    AnimatableFloatValue timeRemapping = layerModel.getTimeRemapping();
    if (timeRemapping != null) {
      this.timeRemapping = timeRemapping.createAnimation();
      addAnimation(this.timeRemapping);
      //noinspection ConstantConditions
      this.timeRemapping.addUpdateListener(this);
    } else {
      this.timeRemapping = null;
    }

    //hashmap的優(yōu)化數(shù)據(jù)結(jié)構(gòu)
    LongSparseArray<BaseLayer> layerMap =
        new LongSparseArray<>(composition.getLayers().size());

    BaseLayer mattedLayer = null;
    //遍歷layer圖層
    for (int i = layerModels.size() - 1; i >= 0; i--) {
      Layer lm = layerModels.get(i);
      BaseLayer layer = BaseLayer.forModel(lm, lottieDrawable, composition);
      if (layer == null) {
        continue;
      }
      layerMap.put(layer.getLayerModel().getId(), layer);
      if (mattedLayer != null) {
        mattedLayer.setMatteLayer(layer);
        mattedLayer = null;
      } else {
        layers.add(0, layer);
        switch (lm.getMatteType()) {
          case ADD:
          case INVERT:
            mattedLayer = layer;
            break;
        }
      }
    }

    //將layer生成各種layerView完成繪制
    for (int i = 0; i < layerMap.size(); i++) {
      long key = layerMap.keyAt(i);
      BaseLayer layerView = layerMap.get(key);
      // This shouldn't happen but it appears as if sometimes on pre-lollipop devices when
      // compiled with d8, layerView is null sometimes.
      // https://github.com/airbnb/lottie-android/issues/524
      if (layerView == null) {
        continue;
      }
      BaseLayer parentLayer = layerMap.get(layerView.getLayerModel().getParentId());
      if (parentLayer != null) {
        layerView.setParentLayer(parentLayer);
      }
    }
  }

父類中根據(jù)不同類型评凝,繪制不同的圖層

 static BaseLayer forModel(
      Layer layerModel, LottieDrawable drawable, LottieComposition composition) {
    switch (layerModel.getLayerType()) {
      //形狀圖層,調(diào)用最頻繁
      case SHAPE:
        return new ShapeLayer(drawable, layerModel);
      //預(yù)合成圖層
      case PRE_COMP:
        return new CompositionLayer(drawable, layerModel,
            composition.getPrecomps(layerModel.getRefId()), composition);
      //純色圖層
      case SOLID:
        return new SolidLayer(drawable, layerModel);
      //有些會是zip壓縮包中會有圖片追葡,在這里解析成bitmap
      case IMAGE:
        return new ImageLayer(drawable, layerModel);
      //空圖層
      case NULL:
        return new NullLayer(drawable, layerModel);
      //文本圖層
      case TEXT:
        return new TextLayer(drawable, layerModel);
      case UNKNOWN:
      default:
        // Do nothing
        L.warn("Unknown layer type " + layerModel.getLayerType());
        return null;
    }
  }

然后就是通過setImageDrawable(lottieDrawable)將圖像顯示出來,顯示第一幀動畫肥哎。

動畫播放

LottieDrawable構(gòu)造方法中設(shè)置辽俗,animator的監(jiān)聽疾渣,在animator播放的時(shí)候篡诽,這個(gè)回調(diào)就會開始更新progress

public LottieDrawable() {
    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
      @Override
      public void onAnimationUpdate(ValueAnimator animation) {
        if (compositionLayer != null) {
          //根據(jù)animator的進(jìn)度,不斷調(diào)整compositionLayer的progress
          compositionLayer.setProgress(animator.getAnimatedValueAbsolute());
        }
      }
    });
  }

通過CompositionLayer將setProgress實(shí)現(xiàn)的顯示具體進(jìn)度動畫

@Override 
 public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
    super.setProgress(progress);
    if (timeRemapping != null) {
      float duration = lottieDrawable.getComposition().getDuration();
      long remappedTime = (long) (timeRemapping.getValue() * 1000);
      progress = remappedTime / duration;
    }
    if (layerModel.getTimeStretch() != 0) {
      progress /= layerModel.getTimeStretch();
    }

    progress -= layerModel.getStartProgress();
    for (int i = layers.size() - 1; i >= 0; i--) {
      layers.get(i).setProgress(progress);
    }
  }

父類中l(wèi)ayer通知進(jìn)度的改變

 void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
    // Time stretch should not be applied to the layer transform.
    transform.setProgress(progress);
    if (mask != null) {
      for (int i = 0; i < mask.getMaskAnimations().size(); i++) {
        mask.getMaskAnimations().get(i).setProgress(progress);
      }
    }
    if (layerModel.getTimeStretch() != 0) {
      progress /= layerModel.getTimeStretch();
    }
    if (matteLayer != null) {
      // The matte layer's time stretch is pre-calculated.
      float matteTimeStretch = matteLayer.layerModel.getTimeStretch();
      matteLayer.setProgress(progress * matteTimeStretch);
    }
    for (int i = 0; i < animations.size(); i++) {
      //animations會更新BaseKeyframeAnimation.AnimationListener回調(diào)onValueChanged觸發(fā)LottieDrawable重繪
      //會調(diào)用invalidateSelf()方法榴捡,該方法會觸發(fā)LottieAnimationView的invalidateDrawable杈女,然后
      animations.get(i).setProgress(progress);
    }
  }

BaseKeyframeAnimation.AnimationListener會粗發(fā)invalidateDrawable的方法

@Override 
public void invalidateDrawable(@NonNull Drawable dr) {
    if (getDrawable() == lottieDrawable) {
      // We always want to invalidate the root drawable so it redraws the whole drawable.
      // Eventually it would be great to be able to invalidate just the changed region.
      super.invalidateDrawable(lottieDrawable);
    } else {
      // Otherwise work as regular ImageView
      super.invalidateDrawable(dr);
    }
}

在LottieDrawable的setComposition()的方法中會開始執(zhí)行一個(gè)ValueAnimation動畫,這個(gè)動畫會驅(qū)使baseLayer的draw()方法不斷執(zhí)行

  @Override void drawLayer(Canvas canvas, Matrix parentMatrix, int parentAlpha) {
    L.beginSection("CompositionLayer#draw");
    canvas.save();
    newClipRect.set(0, 0, layerModel.getPreCompWidth(), layerModel.getPreCompHeight());
    parentMatrix.mapRect(newClipRect);
    
    for (int i = layers.size() - 1; i >= 0 ; i--) {
      boolean nonEmptyClip = true;
      if (!newClipRect.isEmpty()) {
        nonEmptyClip = canvas.clipRect(newClipRect);
      }
      if (nonEmptyClip) {
        BaseLayer layer = layers.get(i);
        layer.draw(canvas, parentMatrix, parentAlpha);
      }
    }
    canvas.restore();
    L.endSection("CompositionLayer#draw");
  }

總結(jié)

  1. 創(chuàng)建 LottieAnimationView
  2. 在LottieAnimationView中創(chuàng)建LottieDrawable
  3. 在LottieAnimationView中創(chuàng)建compositionLoader,進(jìn)行json文件解析得到LottieComposition达椰,完成數(shù)據(jù)到對象的映射翰蠢。
  4. 解析完后通過setComposition方法把LottieComposition給lottieDrawable,lottieDrawable在setComposition方法中轉(zhuǎn)換成各種Layer為繪制做準(zhǔn)備比如path啰劲,maritx梁沧,bitmap等等
  5. 在LottieAnimationView中把lottieDrawable設(shè)置setImageDrawable
  6. 然后開始動畫lottieDrawable.playAnimation()。

demo地址

源碼中添加了很多注釋
https://github.com/Johncuiqiang/LottieSource

參考

https://blog.csdn.net/weixin_37618354/article/details/84072783
https://blog.csdn.net/dcsff/article/details/80482841
https://blog.csdn.net/xiexiangyu92/article/details/78525456

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蝇裤,一起剝皮案震驚了整個(gè)濱河市廷支,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌栓辜,老刑警劉巖恋拍,帶你破解...
    沈念sama閱讀 222,183評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異藕甩,居然都是意外死亡施敢,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評論 3 399
  • 文/潘曉璐 我一進(jìn)店門狭莱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來僵娃,“玉大人,你說我怎么就攤上這事腋妙∶跣恚” “怎么了?”我有些...
    開封第一講書人閱讀 168,766評論 0 361
  • 文/不壞的土叔 我叫張陵辉阶,是天一觀的道長先壕。 經(jīng)常有香客問我,道長谆甜,這世上最難降的妖魔是什么垃僚? 我笑而不...
    開封第一講書人閱讀 59,854評論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮规辱,結(jié)果婚禮上谆棺,老公的妹妹穿的比我還像新娘。我一直安慰自己罕袋,他們只是感情好改淑,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,871評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著浴讯,像睡著了一般朵夏。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上榆纽,一...
    開封第一講書人閱讀 52,457評論 1 311
  • 那天仰猖,我揣著相機(jī)與錄音捏肢,去河邊找鬼。 笑死饥侵,一個(gè)胖子當(dāng)著我的面吹牛鸵赫,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播躏升,決...
    沈念sama閱讀 40,999評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼辩棒,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了膨疏?” 一聲冷哼從身側(cè)響起盗温,我...
    開封第一講書人閱讀 39,914評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎成肘,沒想到半個(gè)月后卖局,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,465評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡双霍,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,543評論 3 342
  • 正文 我和宋清朗相戀三年砚偶,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片洒闸。...
    茶點(diǎn)故事閱讀 40,675評論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡染坯,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出丘逸,到底是詐尸還是另有隱情单鹿,我是刑警寧澤,帶...
    沈念sama閱讀 36,354評論 5 351
  • 正文 年R本政府宣布仲锄,位于F島的核電站湃鹊,受9級特大地震影響币呵,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜芯义,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,029評論 3 335
  • 文/蒙蒙 一扛拨、第九天 我趴在偏房一處隱蔽的房頂上張望鬼癣。 院中可真熱鬧待秃,春花似錦章郁、人聲如沸志衍。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽肩钠。三九已至暂殖,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間呛每,已是汗流浹背晨横。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評論 1 274
  • 我被黑心中介騙來泰國打工手形, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人滤灯。 一個(gè)月前我還...
    沈念sama閱讀 49,091評論 3 378
  • 正文 我出身青樓,卻偏偏與公主長得像豫尽,于是被迫代替她去往敵國和親美旧。 傳聞我的和親對象是個(gè)殘疾皇子榴嗅,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,685評論 2 360

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