lottie

概念

Lottie 是 Airbnb開(kāi)源的一套跨平臺(tái)的完整的動(dòng)畫(huà)效果解決方案,它可以使用Bodymovin解析以json導(dǎo)出的Adobe After Effects動(dòng)畫(huà)襟士,并在移動(dòng)設(shè)備上進(jìn)行本地渲染盗飒。
可以直接運(yùn)用在 iOSAndroid陋桂、WebReact Native之上逆趣。開(kāi)發(fā)者無(wú)需關(guān)注動(dòng)畫(huà)中的實(shí)現(xiàn)細(xì)節(jié)。

特點(diǎn)

  • 傳統(tǒng)動(dòng)畫(huà)
    使用序列幀嗜历,開(kāi)發(fā)者需要設(shè)計(jì)時(shí)序宣渗、位移、透明度梨州、尺寸痕囱、插值器等各類(lèi)參數(shù)信息,再播放出動(dòng)畫(huà)暴匠。

  • lottie的設(shè)計(jì)方案
    將設(shè)計(jì)軟件中的時(shí)間軸完整地導(dǎo)出來(lái)鞍恢,包括里面的各種關(guān)鍵幀信息、矢量路徑、層級(jí)有序、樣式等等抹腿。
    即動(dòng)畫(huà)的描述文件導(dǎo)出,再將動(dòng)畫(huà)元素導(dǎo)出旭寿,然后在對(duì)應(yīng)的客戶端警绩,解析描述文件,還原出整個(gè)動(dòng)畫(huà)盅称。

  • 大小
    導(dǎo)出的json文件比gif文件小很多
  • 性能
    性能也更好(應(yīng)用到矢量圖肩祥,內(nèi)存占用小,縮放效果好)
  • 使用
    API簡(jiǎn)單缩膝,代碼實(shí)現(xiàn)簡(jiǎn)單混狠,開(kāi)發(fā)無(wú)需編寫(xiě)動(dòng)畫(huà),降低動(dòng)畫(huà)的開(kāi)發(fā)成本
  • 靈活度
    可動(dòng)態(tài)配置下發(fā)疾层,更換替換動(dòng)畫(huà)效果将饺,易于調(diào)試和維護(hù)。
  • 適配
    不同的手機(jī)分辨率無(wú)需適配
  • 通用
    跨平臺(tái)痛黎,設(shè)計(jì)稿導(dǎo)出一份動(dòng)畫(huà)描述文件予弧,android,ios湖饱,react native掖蛤,web多端通用
  • 效果
    幾乎與設(shè)計(jì)出的動(dòng)畫(huà)無(wú)差別

使用流程

image.png

使用方法

由于API文檔是1.0.3未及時(shí)更新,建議在源碼里看方法
API文檔

2.7.0版本

 動(dòng)畫(huà)來(lái)源可以從本地,網(wǎng)絡(luò)等
 public void setAnimation(@RawRes final int rawRes)  //src/main/res/raw
 public void setAnimation(final String assetName)        //src/main/assets
 public void setAnimationFromJson(String jsonString)
 public void setAnimationFromJson(String jsonString, @Nullable String cacheKey) {
 public void setAnimation(JsonReader reader, @Nullable String cacheKey)  //JSON文件或zip文件的InputStream
 public void setAnimationFromUrl(String url)  //json或zip文件的網(wǎng)址

public boolean addLottieOnCompositionLoadedListener(lottieOnCompositionLoadedListener)
public boolean removeLottieOnCompositionLoadedListener(lottieOnCompositionLoadedListener) 
public void playAnimation()
public void pauseAnimation()
public void setProgress(float progress)
...
public void setImageBitmap(Bitmap bm)
public void setImageResource(int resId)
public void setImageDrawable(Drawable drawable) 
...
AnimatorListener 、AnimatorUpdateListener 接口的支持

使用示例

  <....ui.lottie.RecyclableLottieAnimationView
    android:id="@+id/title_bar_toolbox_ani"
    android:layout_width="@dimen/titlebar_web_action_width"
    android:layout_height="@dimen/titlebar_web_action_width"
    android:layout_centerVertical="true"
    android:layout_alignParentRight="true">

mToolBoxAniView = mTopView.findViewById(R.id.title_bar_toolbox_ani);
mToolBoxAniView.setAnimation("sniffer_animation.json");
mToolBoxAniView.playAnimation();

public class RecyclableLottieAnimationView extends LottieAnimationView {
    public RecyclableLottieAnimationView(Context context) {
        super(context);
    }

    public RecyclableLottieAnimationView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public RecyclableLottieAnimationView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        /**
         * lottie依賴(lài) onDetachedFromWindow停止動(dòng)畫(huà),回收動(dòng)畫(huà)資源
         * 但動(dòng)畫(huà)的 play可能是異步的,
         * 如果有post出去的異步任務(wù),在detach后動(dòng)畫(huà)仍會(huì)執(zhí)行
         */
        cancelAnimation();
    }
}

兼容性

版本支持 sdkVersion: >=16

= 2.8以上需AndroidX
依賴(lài)庫(kù)的版本與動(dòng)畫(huà)導(dǎo)出版本有關(guān)
效果支持

引入后的影響

包大小新增: 71K

Json文件結(jié)構(gòu)

json格式3.png
  • 回憶: 幀動(dòng)畫(huà)的播放.

核心類(lèi)

LottieComposition 將json文件解析成數(shù)據(jù)對(duì)象.
LottieDrawable 承載所有的繪制工作, 將LottieComposition 解析的數(shù)據(jù)對(duì)象, 繪制成 drawable井厌。
LottieAnimationView 提供了異步加載, 反序列化,顯示, 封裝了一些動(dòng)畫(huà)的操作,并處理了圖片的回收onDetachWindow . 控制動(dòng)畫(huà)的實(shí)際操作委托給LottieDrawable
備注:
如直接使用LottieDrawable,需在合適的時(shí)機(jī) invoke recycleBitmaps,否則內(nèi)存泄漏.

解析流程.jpg
類(lèi)結(jié)構(gòu).png

動(dòng)畫(huà)播放原理

    #LottieAnimationView.java
    @MainThread
    public void playAnimation() {
      lottieDrawable.playAnimation();
      enableOrDisableHardwareLayer();
    }

    #LottieDrawable.java
    @MainThread
    public void playAnimation() {
      if (compositionLayer == null) {
        lazyCompositionTasks.add(new LazyCompositionTask() {
          @Override public void run(LottieComposition composition) {
            playAnimation();
          }
        });
        return;
      }
      animator.playAnimation();  // LottieValueAnimator
    }

 #LottieValueAnimator.java
  @MainThread
  public void playAnimation() {
    running = true;
    notifyStart(isReversed());
   // update frame and notifyUpdate
    setFrame((int) (isReversed() ? getMaxFrame() : getMinFrame())); 
    lastFrameTimeNs = System.nanoTime();
    repeatCount = 0;
    postFrameCallback();
  }

// 說(shuō)明
BaseLottieAnimator  extends ValueAnimator
LottieValueAnimator extends BaseLottieAnimator 
BaseLottieAnimator 提供了 notifyStart 蚓庭、notifyEnd、notifyCancel 仅仆、notifyUpdate器赞、 notifyRepeat 等notifyX方法.
通知 所有l(wèi)isteners : ValueAnimator.AnimatorUpdateListener 與ValueAnimator.AnimatorListener
對(duì)應(yīng)者各自的回調(diào)方法 eg: onAnimationStart  、onAnimationEnd蝇恶、onAnimationX方法

#LottieDrawable.java
public LottieDrawable() {
    this.animator.addUpdateListener(new AnimatorUpdateListener() {
        public void onAnimationUpdate(ValueAnimator animation) {
            if (LottieDrawable.this.compositionLayer != null) {
                LottieDrawable.this.compositionLayer.setProgress(LottieDrawable.this.animator.getAnimatedValueAbsolute());
            }
        }
    });
}

#CompositionLayer.java
@Override public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
  super.setProgress(progress);
    //... update the progress
  for (int i = layers.size() - 1; i >= 0; i--) {
    layers.get(i).setProgress(progress);
  }
}

#BaseLayer.java
void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
  //... layout.setProgress
  for (int i = 0; i < animations.size(); i++) {
  // List<BaseKeyframeAnimation<?, ?>> animations 
    animations.get(i).setProgress(progress);   
  }
}

#BaseKeyframeAnimation.java
public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
    //... update the progress 
   notifyListeners();
}

public void notifyListeners() {
  for (int i = 0; i < listeners.size(); i++) {
    listeners.get(i).onValueChanged();
  }
}

#BaseLayer
  @Override public void onValueChanged() {
    invalidateSelf();
  }

  private void invalidateSelf() {
    lottieDrawable.invalidateSelf();
  }

動(dòng)畫(huà)播放時(shí)序圖.jpg

動(dòng)畫(huà)適配原理

① Android開(kāi)發(fā)中, 不同屏幕分辨率適配.
② 為自定義View為其添加縮放屬性

LottieAnimationView.java
private void init(@Nullable AttributeSet attrs) {
   ...
    if (ta.hasValue(R.styleable.LottieAnimationView_lottie_scale)) {
      lottieDrawable.setScale(ta.getFloat(R.styleable.LottieAnimationView_lottie_scale, 1f)); 
    }
   ...
}

LottieAnimationView$setScale 委托給LottieDrawable
#LottieDrawable.java
public void setScale(float scale) {
  this.scale = scale;
  updateBounds();
}

#LottieDrawable.java
private void updateBounds() {
  if (composition == null) {
    return;
  }
  float scale = getScale();
  // Drawable#setBounds
  setBounds(0, 0, (int) (composition.getBounds().width() * scale),
      (int) (composition.getBounds().height() * scale));
}

#LottieDrawable.java
private float getMaxScale(@NonNull Canvas canvas) {
  float maxScaleX = canvas.getWidth() / (float) composition.getBounds().width();
  float maxScaleY = canvas.getHeight() / (float) composition.getBounds().height();
  return Math.min(maxScaleX, maxScaleY);
}

  @Override public void draw(@NonNull Canvas canvas) {
    float scale = this.scale;
    float extraScale = 1f;
    float maxScale = getMaxScale(canvas);
    if (scale > maxScale) {
      scale = maxScale;
      extraScale = this.scale / scale;
    }

    if (extraScale > 1) { 
    // ...  translate and scale
    }

    matrix.reset();
    matrix.preScale(scale, scale);
    compositionLayer.draw(canvas, matrix, alpha);
  }

 #LottieCompositionParser.java
  public static LottieComposition parse(JsonReader reader) throws IOException {
    float scale = Utils.dpScale();
    // ...parse
    int scaledWidth = (int) (width * scale);
    int scaledHeight = (int) (height * scale);
    Rect bounds = new Rect(0, 0, scaledWidth, scaledHeight);
    composition.init(bounds, startFrame, endFrame, frameRate, layers, layerMap, precomps,
        images, characters, fonts);
  }

小結(jié):
Lottie 適配原理:
解析json文件,獲得取寬高之后, 乘以手機(jī)的相對(duì)密度拳魁。得到初始的Rect邊界.
在使用的時(shí)候判斷適配后的寬高是否超過(guò)屏幕的寬高,如果超過(guò)則再進(jìn)行縮放撮弧。以此保障 Lottie 在 Android 平臺(tái)的顯示效果潘懊。

繪制原理

猜想: 參考動(dòng)畫(huà)播放思路,猜想下繪制流程

 BaseLayer.java
  @Override
  public void draw(Canvas canvas, Matrix parentMatrix, int parentAlpha) {
    if (!visible) {
      return;
    }
    buildParentLayerListIfNeeded();
    if (!hasMatteOnThisLayer() && !hasMasksOnThisLayer()) {
      matrix.preConcat(transform.getMatrix());
      drawLayer(canvas, matrix, alpha);
      recordRenderTime(L.endSection(drawTraceName));
      return;
    }

    if (hasMasksOnThisLayer()) {
        //...draw maskLayer 
      applyMasks(canvas, matrix); 

    }
    if (hasMatteOnThisLayer()) {
        //...draw matteLayer
        matteLayer.draw(canvas, parentMatrix, alpha);
    } 
  }

  CompositionLayer.java
  @Override void drawLayer(Canvas canvas, Matrix parentMatrix, int parentAlpha) {    
  //...    
    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);
      }
    }
    //...
  }
遇到的問(wèn)題與解決方案

①依賴(lài)庫(kù)的版本與導(dǎo)出動(dòng)畫(huà)版本

java.lang.IllegalStateException: Missing values for keyframe.
        at com.airbnb.lottie.animation.keyframe.FloatKeyframeAnimation.getValue(FloatKeyframeAnimation.java:16)
        at com.airbnb.lottie.animation.keyframe.FloatKeyframeAnimation.getValue(FloatKeyframeAnimation.java:8)
        at com.airbnb.lottie.animation.keyframe.BaseKeyframeAnimation.getValue(BaseKeyframeAnimation.java:125)
        at com.airbnb.lottie.animation.keyframe.TransformKeyframeAnimation.getMatrix(TransformKeyframeAnimation.java:113)

原因:
json格式與解析規(guī)則不匹配
Lottie 3.0和Bodymovin 5.5有一些重要的json優(yōu)化,可以節(jié)省json大小和解析速度的1/3贿衍。 但是授舟,必須在3.0以上生效,否則就在bodymovin設(shè)置中啟用“導(dǎo)出為舊格式”

②issues: zip 播放的問(wèn)題
https://github.com/airbnb/lottie-android/issues/1009

③. 內(nèi)存泄漏問(wèn)題

圖片在回收時(shí)機(jī)
  @Override protected void onDetachedFromWindow() {
    if (isAnimating()) {  // 動(dòng)畫(huà)的加載play是異步的 
      cancelAnimation();  
      wasAnimatingWhenDetached = true;
    }
    recycleBitmaps();
    super.onDetachedFromWindow();
  }

以此可能引發(fā)的內(nèi)存抖動(dòng)的場(chǎng)景
假設(shè)在RecyclerView中使用包涵mattes或者mask的動(dòng)畫(huà)

④ 內(nèi)存抖動(dòng)的風(fēng)險(xiǎn)
bitmap在動(dòng)畫(huà)加載到window時(shí)被創(chuàng)建贸辈,onDetachedFromWindow刪除時(shí)回收释树。所以不宜在RecyclerView中使用包涵mattes或者mask的動(dòng)畫(huà),否則會(huì)引起bitmap抖動(dòng)。
⑤ 版本變更比較多, API變化比較大 解決方案: 封裝,提供統(tǒng)一接口外觀

與SVGA對(duì)比

SVGA里面的每一幀都是關(guān)鍵幀,SVGA已經(jīng)在導(dǎo)出動(dòng)畫(huà)的時(shí)候奢啥,把每一幀的信息都計(jì)算好了秸仙,如此一來(lái),
播放時(shí)無(wú)需關(guān)心插值計(jì)算的過(guò)程桩盲。
通過(guò)幀率去刷每一幀的畫(huà)面寂纪,這個(gè)思路跟gif很像,SVGA可以同時(shí)支持Flash和After Effects的導(dǎo)出.
且通過(guò)配置使得動(dòng)畫(huà)過(guò)程中圖片都可以得到復(fù)用。

  1. 由于擁有所有幀, 不用解析高階插值(二次線性方程赌结,貝塞爾曲線方程),節(jié)省了CPU
  2. 2.x之后的svga捞蛋,使用Protocol Buffers 來(lái)做序列化,序列化的數(shù)據(jù)體更小,傳遞效率比xml,json 更高柬姚。
    Lottie關(guān)鍵幀Keyframes, 是通過(guò)傳參的方式,交由cpu去運(yùn)算. 所以復(fù)雜動(dòng)畫(huà)實(shí)現(xiàn)耗費(fèi)cpu

與SVGA對(duì)比

SVGA動(dòng)畫(huà)原理
逐幀渲染拟杉,每一幀均為關(guān)鍵幀,只需渲染每個(gè)元素?zé)o需插值計(jì)算
播放前一次性上傳紋理到 GPU量承,并在動(dòng)畫(huà)過(guò)程中復(fù)用紋理
2.x之后的svga搬设,使用Protocol Buffers 來(lái)做序列化,序列化的數(shù)據(jù)體更小,傳遞效率比xml,json 更高宴合。

Lottie動(dòng)畫(huà)原理
逐層渲染焕梅,完全按照設(shè)計(jì)工具的設(shè)計(jì)思路還原
播放解析多個(gè)圖層配置并添加相應(yīng)動(dòng)畫(huà),并在動(dòng)畫(huà)過(guò)程中復(fù)用圖層
當(dāng)需要解析高階插值卦洽,性能相對(duì)差一些 (關(guān)鍵幀Keyframes,是通過(guò)傳參的方式,交由cpu去運(yùn)算. 所以復(fù)雜動(dòng)畫(huà)實(shí)現(xiàn)耗費(fèi)cpu)

通過(guò)幀率去刷每一幀的畫(huà)面,與gif很像,所以SVGA可以同時(shí)支持Flash和After Effects的導(dǎo)出.

  1. 由于擁有所有幀, 不用解析高階插值(二次線性方程,貝塞爾曲線方程),節(jié)省了CPU
    Lottie關(guān)鍵幀Keyframes, 是通過(guò)傳參的方式,交由cpu去運(yùn)算. 所以復(fù)雜動(dòng)畫(huà)實(shí)現(xiàn)耗費(fèi)cpu

位圖與json
SVGA是將圖片與描述文件集成在.svga文件當(dāng)中的斜棚,而Lottie則是把二者分離開(kāi)阀蒂。
Lottie可以在導(dǎo)出后,再對(duì)圖片進(jìn)行文件大小優(yōu)化弟蚀;而SVGA最好是在事先就對(duì)圖片進(jìn)行大小優(yōu)化蚤霞。

SVGA應(yīng)用場(chǎng)景:
在直播應(yīng)用場(chǎng)景,禮物播放,游戲炫酷動(dòng)畫(huà).

Lottie應(yīng)用場(chǎng)景:
高德地圖,支付寶,全民K歌
阿里提供的犸良動(dòng)畫(huà)
它最底層采用的技術(shù)就是Lottie,阿里對(duì)其二次封裝了許多預(yù)設(shè)的動(dòng)畫(huà)效果义钉,
可以自定義其中的元素與參數(shù)昧绣,然后試著導(dǎo)出你的第一個(gè)json文件~

文獻(xiàn)參考

Lottie生態(tài)
https://github.com/airbnb/lottie-android 31.1K star
https://github.com/airbnb/lottie-ios
https://github.com/airbnb/lottie-web
Lottie doc

導(dǎo)出工具
https://github.com/bodymovin/bodymovin
動(dòng)畫(huà)預(yù)覽器
https://lottiefiles.com/preview
https://airbnb.design/lottie/

SVGA生態(tài)
開(kāi)源iOS/Android/Web三個(gè)平臺(tái)的源碼。
https://github.com/yyued/SVGAPlayer-Android 2.5K star
https://github.com/yyued/SVGAPlayer-iOS
https://github.com/yyued/SVGAPlayer-Web

設(shè)計(jì)師工具
http://svga.io/designer.html

動(dòng)畫(huà)預(yù)覽器
http://svga.io/svga-preview.html

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末捶闸,一起剝皮案震驚了整個(gè)濱河市夜畴,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌删壮,老刑警劉巖贪绘,帶你破解...
    沈念sama閱讀 218,122評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異央碟,居然都是意外死亡税灌,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)菱涤,“玉大人苞也,你說(shuō)我怎么就攤上這事≌掣眩” “怎么了如迟?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,491評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)翻擒。 經(jīng)常有香客問(wèn)我氓涣,道長(zhǎng),這世上最難降的妖魔是什么陋气? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,636評(píng)論 1 293
  • 正文 為了忘掉前任劳吠,我火速辦了婚禮,結(jié)果婚禮上巩趁,老公的妹妹穿的比我還像新娘痒玩。我一直安慰自己,他們只是感情好议慰,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布蠢古。 她就那樣靜靜地躺著,像睡著了一般别凹。 火紅的嫁衣襯著肌膚如雪草讶。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,541評(píng)論 1 305
  • 那天炉菲,我揣著相機(jī)與錄音堕战,去河邊找鬼。 笑死拍霜,一個(gè)胖子當(dāng)著我的面吹牛嘱丢,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播祠饺,決...
    沈念sama閱讀 40,292評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼越驻,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了道偷?” 一聲冷哼從身側(cè)響起缀旁,我...
    開(kāi)封第一講書(shū)人閱讀 39,211評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎试疙,沒(méi)想到半個(gè)月后诵棵,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,655評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡祝旷,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評(píng)論 3 336
  • 正文 我和宋清朗相戀三年履澳,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了嘶窄。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,965評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡距贷,死狀恐怖柄冲,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情忠蝗,我是刑警寧澤现横,帶...
    沈念sama閱讀 35,684評(píng)論 5 347
  • 正文 年R本政府宣布,位于F島的核電站阁最,受9級(jí)特大地震影響戒祠,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜速种,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評(píng)論 3 329
  • 文/蒙蒙 一姜盈、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧配阵,春花似錦馏颂、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,894評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至瘫拣,卻和暖如春亿絮,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背麸拄。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,012評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工壹无, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人感帅。 一個(gè)月前我還...
    沈念sama閱讀 48,126評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像地淀,于是被迫代替她去往敵國(guó)和親失球。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評(píng)論 2 355

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