lottie-android 框架使用及源碼解析

安卓動(dòng)畫(huà)

最近業(yè)務(wù)太多,好久沒(méi)更新。。花了兩個(gè)晚上研究了一些lottie框架的實(shí)現(xiàn),學(xué)到了一些思路,有機(jī)會(huì)可以把view繪制深入學(xué)習(xí)一下,ok開(kāi)始。

https://github.com/airbnb/lottie-android

Lottie,Airbnb開(kāi)源的一個(gè)牛逼的動(dòng)畫(huà)框架,絢麗的動(dòng)畫(huà)效果令人瞠目。

Example2.gif

沒(méi)錯(cuò)這在以往的意識(shí)來(lái)看是根本不可能實(shí)現(xiàn)的動(dòng)畫(huà)效果,那么究竟它是如何實(shí)現(xiàn)的呢?

初探

打開(kāi)LottieSample工程,并將它運(yùn)行起來(lái),首頁(yè)就可以看到上圖中間的這個(gè)動(dòng)畫(huà)效果,而代碼實(shí)現(xiàn)更是簡(jiǎn)單到?jīng)]朋友。

xml:

Paste_Image.png

java代碼:

Paste_Image.png

沒(méi)錯(cuò)就是初始化了一個(gè)LottieAnimationView并且調(diào)用playAnimation()方法,就出現(xiàn)了上圖的動(dòng)畫(huà)效果,這里注意到在xml初始化參數(shù)中有個(gè)lottie_fileName參數(shù),傳了一個(gè)貌似是json文件路徑,而在assets的Logo目錄下,確實(shí)有個(gè)LogoSmall.json文件,打開(kāi)一看懵逼了,完全看不懂。

原來(lái)這個(gè)json文件的內(nèi)容不是手寫(xiě)的,而是軟件生成的,設(shè)計(jì)師可以使用Adobe的 After Effects(簡(jiǎn)稱 AE)工具制作這個(gè)動(dòng)畫(huà),在AE中安裝一個(gè)叫做Bodymovin的插件,使用這個(gè)插件可以將動(dòng)畫(huà)效果生成一個(gè)json文件,而這個(gè)json文件通過(guò)LottieAnimationView解析并最終生成絢麗的動(dòng)畫(huà)效果展示在我們面前。

使用方法

Lottie supports API 14 and above,要求4.0以上

依賴

dependencies {  
  compile 'com.airbnb.android:lottie:1.5.1'
}
使用方法一:初始化一個(gè)LottieAnimationView
 <com.airbnb.lottie.LottieAnimationView
        android:id="@+id/animation_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:lottie_fileName="hello-world.json"
        app:lottie_loop="true"
        app:lottie_autoPlay="true" />

只接受這三個(gè)參數(shù),語(yǔ)意清楚就不多解釋了。

也可以通過(guò)java代碼設(shè)置

LottieAnimationView animationView = (LottieAnimationView) findViewById(R.id.animation_view);
animationView.setAnimation("hello-world.json");
animationView.loop(true);

setAnimation有三個(gè)方法

Paste_Image.png

其中String是fileName,是在assets目錄下的文件,CacheStrategy表示緩存策略,

Paste_Image.png

代表使用何種策略進(jìn)行存儲(chǔ),默認(rèn)為None即不存儲(chǔ),而使用時(shí)會(huì)優(yōu)先從內(nèi)存緩存中命中讀取,從而減小IO開(kāi)銷(xiāo)。

JSONObject直接傳入一段json數(shù)據(jù),可以通過(guò)網(wǎng)絡(luò)獲取一段json進(jìn)行解析處理。

使用方法二:使用LottieComposition
Paste_Image.png

在LottieComposition中提供了三種from方法,可以接受assets文件名、json對(duì)象、流對(duì)象三種參數(shù),Sync表示同步,但是卻是包可見(jiàn)方法,并不能被外部調(diào)用。

LottieComposition.fromJson(getResources(), jsonObject, new LottieComposition.OnCompositionLoadedListener() {
    @Override
    public void onCompositionLoaded(LottieComposition composition) {
        animationView.setComposition(composition);
        animationView.playAnimation();
    }
});

外部調(diào)用時(shí)只提供異步方法,使用AsyncTask進(jìn)行異步調(diào)用,將JsonObject的解析處理過(guò)程放在異步線程處理,并將解析生成的LottieComposition對(duì)象回調(diào)主線程,因?yàn)檫@個(gè)json對(duì)象可能有上百k之大,所以整個(gè)處理過(guò)程的復(fù)雜度和耗時(shí)還是很高的,所以不要在ui線程中解析處理。

一點(diǎn)想法

我們可以通過(guò)請(qǐng)求的方式獲取json對(duì)象,并將解析的過(guò)程放在網(wǎng)絡(luò)請(qǐng)求的異步線程中處理,使用反射調(diào)用同步方法,將調(diào)用放在異步線程中執(zhí)行,這樣就可以將整個(gè)過(guò)程請(qǐng)求和解析的過(guò)程封裝在一起。

注意點(diǎn):

LottieAnimationView內(nèi)部有個(gè)LottieDrawable對(duì)象,setComposition方法實(shí)質(zhì)上是將LottieComposition應(yīng)用到LottieDrawable上,官方readme上有這樣一段說(shuō)明

Paste_Image.png

但應(yīng)該是后面改過(guò),LottieDrawable是包可見(jiàn)的,外部無(wú)法調(diào)用到,并且在LottieDrawable類(lèi)注釋上有這樣一段描述。

Paste_Image.png

推薦使用LottieAnimationView而不是直接使用LottieDrawable,因?yàn)長(zhǎng)ottieDrawable的回收LottieAnimationView幫你做了,而自己操作LottieDrawable需要考慮的回收調(diào)用。

Paste_Image.png

所以僅推薦以上兩種用法,不推薦直接使用Drawable的方式除非一定需要。

源碼解析

好了,說(shuō)完用法,要來(lái)看看到底這個(gè)過(guò)程發(fā)生了什么。

有兩個(gè)重要的過(guò)程

一、json文件解析成LottieComposition的過(guò)程

所有的文件解析過(guò)程都會(huì)走到LottieComposition下的fromJsonSync方法,返回一個(gè)LottieComposition對(duì)象,中間都是對(duì)jsonObject的解析過(guò)程,將jsonObject中的信息解析到LottieComposition對(duì)象中。

static LottieComposition fromJsonSync(Resources res, JSONObject json) {
  LottieComposition composition = new LottieComposition(res);

  ···

  try {
    JSONArray jsonLayers = json.getJSONArray("layers");
    for (int i = 0; i < jsonLayers.length(); i++) {
      Layer layer = Layer.fromJson(jsonLayers.getJSONObject(i), composition);
      addLayer(composition, layer);
    }
  } catch (JSONException e) {
    throw new IllegalStateException("Unable to find layers.", e);
  }

  ····

  return composition;
}

這段代碼就是把jsonobject中的數(shù)據(jù)賦值給LottieComposition對(duì)象變量,看下圖LottieComposition的變量。

Paste_Image.png

bounds代表邊界,start和end代表開(kāi)始和結(jié)束時(shí)間,duration為時(shí)長(zhǎng),scale為為density。Layer就是圖層的概念,里面存放的是圖層的數(shù)據(jù),在循環(huán)遍歷jsonLayers生成Layer對(duì)象時(shí)調(diào)用了fromJson方法,同樣的也是解析和賦值過(guò)程。

static Layer fromJson(JSONObject json, LottieComposition composition) {
  Layer layer = new Layer(composition);

  ····

  return layer;
}
Paste_Image.png

以上為L(zhǎng)ayer類(lèi)中的變量,除了基礎(chǔ)變量外,會(huì)看到紅框中的變量,這些變量是跟動(dòng)畫(huà)相關(guān)的參數(shù),都是AnimatableValue的實(shí)現(xiàn)類(lèi)。

AnimatableValue的繼承關(guān)系如圖,看樣子是控制顏色、scale、path等基礎(chǔ)動(dòng)畫(huà)的。

Paste_Image.png

那么生成的LottieComposition對(duì)象可以理解成一個(gè)包含所有圖層動(dòng)畫(huà)信息的對(duì)象,等下看看這些變量是如何被使用的。

二、生成LayerView樹(shù)

生成的LottieComposition是通過(guò)LottieDrawable的setComposition方法將動(dòng)畫(huà)信息進(jìn)行設(shè)置的,核心調(diào)用方法為buildLayersForComposition。

private void buildLayersForComposition(LottieComposition composition) {
  ···
  LongSparseArray<LayerView> layerMap = new LongSparseArray<>(composition.getLayers().size());
  List<LayerView> layers = new ArrayList<>(composition.getLayers().size());
  LayerView maskedLayer = null;
  for (int i = composition.getLayers().size() - 1; i >= 0; i--) {
    Layer layer = composition.getLayers().get(i);
    LayerView layerView;
    if (maskedLayer == null) {
      layerView =
          new LayerView(layer, composition, getCallback(), mainBitmap, maskBitmap, matteBitmap);
    } else {
      ···
      layerView =
          new LayerView(layer, composition, getCallback(), mainBitmapForMatte, maskBitmapForMatte,
              null);
    }
    layerMap.put(layerView.getId(), layerView);
    if (maskedLayer != null) {
      maskedLayer.setMatteLayer(layerView);
      maskedLayer = null;
    } else {
      layers.add(layerView);
      if (layer.getMatteType() == Layer.MatteType.Add) {
        maskedLayer = layerView;
      }
    }
  }

  for (int i = 0; i < layers.size(); i++) {
    LayerView layerView = layers.get(i);
    addLayer(layerView);
  }

  for (int i = 0; i < layerMap.size(); i++) {
    long key = layerMap.keyAt(i);
    LayerView layerView = layerMap.get(key);
    LayerView parentLayer = layerMap.get(layerView.getLayerModel().getParentId());
    if (parentLayer != null) {
      layerView.setParentLayer(parentLayer);
    }
  }
}

將之前解析出來(lái)的Layers數(shù)據(jù)倒序遍歷并生成同等數(shù)量的LayerView,將LayerView通過(guò)addLayer方法添加到layers列表里面,這段代碼執(zhí)行完,就生成了一個(gè)LayerView的樹(shù)狀結(jié)構(gòu),以LottieDrawable為根節(jié)點(diǎn)(LottieDrawable也是繼承自AnimatableLayer,跟LayerView相同)。

void addLayer(AnimatableLayer layer) {
  layer.parentLayer = this;
  layers.add(layer);
  layer.setProgress(progress);
  invalidateSelf();
}

在LayerView的構(gòu)造器中有個(gè)方法:

private void setupForModel() {
  setBackgroundColor(layerModel.getSolidColor());
  setBounds(0, 0, layerModel.getSolidWidth(), layerModel.getSolidHeight());

  setPosition(layerModel.getPosition().createAnimation());
  setAnchorPoint(layerModel.getAnchor().createAnimation());
  setTransform(layerModel.getScale().createAnimation());
  setRotation(layerModel.getRotation().createAnimation());
  setAlpha(layerModel.getOpacity().createAnimation());

  setVisible(layerModel.hasInAnimation(), false);

  List<Object> reversedItems = new ArrayList<>(layerModel.getShapes());
  Collections.reverse(reversedItems);
  Transform currentTransform = null;
  ShapeTrimPath currentTrimPath = null;
  ShapeFill currentFill = null;
  ShapeStroke currentStroke = null;

  for (int i = 0; i < reversedItems.size(); i++) {
    Object item = reversedItems.get(i);
    if (item instanceof ShapeGroup) {
      GroupLayerView groupLayer = new GroupLayerView((ShapeGroup) item, currentFill,
          currentStroke, currentTrimPath, currentTransform, getCallback());
      addLayer(groupLayer);
    } else if (item instanceof ShapeTransform) {
      currentTransform = (ShapeTransform) item;
    } else if (item instanceof ShapeFill) {
      currentFill = (ShapeFill) item;
    } else if (item instanceof ShapeTrimPath) {
      currentTrimPath = (ShapeTrimPath) item;
    } else if (item instanceof ShapeStroke) {
      currentStroke = (ShapeStroke) item;
    } else if (item instanceof ShapePath) {
      ShapePath shapePath = (ShapePath) item;
      ShapeLayerView shapeLayer =
          new ShapeLayerView(shapePath, currentFill, currentStroke, currentTrimPath,
              new ShapeTransform(composition), getCallback());
      addLayer(shapeLayer);
    } else if (item instanceof RectangleShape) {
      RectangleShape shapeRect = (RectangleShape) item;
      RectLayer shapeLayer =
          new RectLayer(shapeRect, currentFill, currentStroke, new ShapeTransform(composition),
              getCallback());
      addLayer(shapeLayer);
    } else if (item instanceof CircleShape) {
      CircleShape shapeCircle = (CircleShape) item;
      EllipseShapeLayer shapeLayer =
          new EllipseShapeLayer(shapeCircle, currentFill, currentStroke, currentTrimPath,
              new ShapeTransform(composition), getCallback());
      addLayer(shapeLayer);
    }
  }

  if (maskBitmap != null && layerModel.getMasks() != null && !layerModel.getMasks().isEmpty()) {
    setMask(new MaskLayer(layerModel.getMasks(), getCallback()));
    maskCanvas = new Canvas(maskBitmap);
  }
  buildAnimations();
}

這里的layerModel就是剛才解析出來(lái)的Layer,這里用到了剛才紅框圈起來(lái)的那些變量,調(diào)用了AnimatableValue的createAnimation方法,生成了一個(gè)KeyframeAnimation對(duì)象,查看KeyframeAnimation,發(fā)現(xiàn)是抽象類(lèi),可以看到有幾個(gè)關(guān)鍵的變量。

首先有個(gè)AnimationListener的list,通過(guò)觀察者模式修改訂閱者的信息,等下看看誰(shuí)是訂閱者。還有個(gè)progress變量和setProgress方法,應(yīng)為進(jìn)度控制。

void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
  if (progress < getStartDelayProgress()) {
    progress = 0f;
  } else if (progress > getDurationEndProgress()) {
    progress = 1f;
  } else {
    progress = (progress - getStartDelayProgress()) / getDurationRangeProgress();
  }
  if (progress == this.progress) {
    return;
  }
  this.progress = progress;

  T value = getValue();
  for (int i = 0; i < listeners.size(); i++) {
    listeners.get(i).onValueChanged(value);
  }
}

調(diào)用setProgress方法,會(huì)將getValue的結(jié)果傳遞給所有的訂閱者。

拿ColorKeyframeAnimation的getValue的實(shí)現(xiàn)類(lèi)為例

float percentageIntoFrame = 0;
if (!isDiscrete) {
  percentageIntoFrame = (progress - startKeytime) / (endKeytime - startKeytime);
  if (interpolators != null) {
    percentageIntoFrame =
        interpolators.get(keyframeIndex).getInterpolation(percentageIntoFrame);
  }
}

int startColor = values.get(keyframeIndex);
int endColor = values.get(keyframeIndex + 1);

return (Integer) argbEvaluator.evaluate(percentageIntoFrame, startColor, endColor);

以上這段代碼是getValue的具體實(shí)現(xiàn),可以看到是將開(kāi)始顏色和結(jié)束顏色通過(guò)progress計(jì)算一個(gè)當(dāng)前進(jìn)度值,并計(jì)算介于兩個(gè)顏色的中間顏色。

其他類(lèi)似。

最后再看一下AnimatableLayer的變量

Paste_Image.png

每個(gè)圖層會(huì)有自己的parentLayer,會(huì)有平移動(dòng)畫(huà)、透明度動(dòng)畫(huà)、旋轉(zhuǎn)動(dòng)畫(huà)、位置及進(jìn)度信息,這些都放在animations列表里面,同時(shí)還有個(gè)layers列表,表示當(dāng)前層還會(huì)包含的一些圖層信息。

所以第二步可以理解為把第一步的信息生成AnimatableLayer樹(shù)的過(guò)程,包含所有的圖層實(shí)現(xiàn),進(jìn)度控制,動(dòng)畫(huà)信息,都已經(jīng)準(zhǔn)備好等待被調(diào)用了。

三、動(dòng)畫(huà)執(zhí)行

最后來(lái)說(shuō)動(dòng)畫(huà)執(zhí)行,調(diào)用了playAnimation方法,最終是調(diào)用到一個(gè)屬性動(dòng)畫(huà)執(zhí)行,

private final ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
      @Override public void onAnimationUpdate(ValueAnimator animation) {
        setProgress(animation.getAnimatedFraction());
      }
    });

屬性動(dòng)畫(huà)的執(zhí)行是通過(guò)調(diào)用setProgress。

public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
  this.progress = progress;
  for (int i = 0; i < animations.size(); i++) {
    animations.get(i).setProgress(progress);
  }

  for (int i = 0; i < layers.size(); i++) {
    layers.get(i).setProgress(progress);
  }
}

剛才提到這是個(gè)樹(shù)狀結(jié)構(gòu),所以通過(guò)修改progress,整個(gè)樹(shù)就運(yùn)作起來(lái),通過(guò)layers.setProgress設(shè)置所有子圖層的progress,子圖層又包含了animations和layers,每個(gè)圖層的animations存放了很多的AnimatableValue,通過(guò)setProgress,將修改的value值回調(diào)訂閱者,而訂閱者其實(shí)就是LottieDrawable,從根節(jié)點(diǎn)開(kāi)始invalidateSelf,調(diào)用到draw方法中進(jìn)行繪制。

@Override
public void draw(@NonNull Canvas canvas) {
  int saveCount = canvas.save();
  applyTransformForLayer(canvas, this);

  int backgroundAlpha = Color.alpha(backgroundColor);
  if (backgroundAlpha != 0) {
    int alpha = backgroundAlpha;
    if (this.alpha != null) {
      alpha = alpha * this.alpha.getValue() / 255;
    }
    solidBackgroundPaint.setAlpha(alpha);
    if (alpha > 0) {
      canvas.drawRect(getBounds(), solidBackgroundPaint);
    }
  }
  for (int i = 0; i < layers.size(); i++) {
    layers.get(i).draw(canvas);
  }
  canvas.restoreToCount(saveCount);
}

void applyTransformForLayer(@Nullable Canvas canvas, AnimatableLayer layer) {
    if (canvas == null) {
      return;
    }
    // TODO: Determine if these null checks are necessary.
    if (layer.position != null) {
      PointF position = layer.position.getValue();
      if (position.x != 0 || position.y != 0) {
        canvas.translate(position.x, position.y);
      }
    }

    if (layer.rotation != null) {
      float rotation = layer.rotation.getValue();
      if (rotation != 0f) {
        canvas.rotate(rotation);
      }
    }

    if (layer.transform != null) {
      ScaleXY scale = layer.transform.getValue();
      if (scale.getScaleX() != 1f || scale.getScaleY() != 1f) {
        canvas.scale(scale.getScaleX(), scale.getScaleY());
      }
    }

    if (layer.anchorPoint != null) {
      PointF anchorPoint = layer.anchorPoint.getValue();
      if (anchorPoint.x != 0 || anchorPoint.y != 0) {
        canvas.translate(-anchorPoint.x, -anchorPoint.y);
      }
    }
  }

看到這里,明白了,每次value值發(fā)生變化,drawable就會(huì)重繪,所有的圖層都會(huì)進(jìn)行繪制,重繪時(shí)使用新的值進(jìn)行繪制,從而完成了動(dòng)畫(huà)的變化。簡(jiǎn)單點(diǎn)說(shuō),就是每個(gè)progress的值,會(huì)對(duì)應(yīng)每個(gè)圖層中的一個(gè)狀態(tài),progress的改變,就是把這些狀態(tài)不斷繪制出來(lái),從而實(shí)現(xiàn)了動(dòng)畫(huà)的效果。

一開(kāi)始以為是屬性動(dòng)畫(huà)相關(guān),沒(méi)想到深入到view的繪制,實(shí)現(xiàn)相當(dāng)復(fù)雜,?膜拜大神。

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

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