LottieAndroid使用詳解及源碼解析—輕而易舉實(shí)現(xiàn)各種復(fù)雜動(dòng)畫

LottieAndroid使用詳解及源碼解析,讓你的應(yīng)用加載動(dòng)畫變得輕而易舉。
看源碼的時(shí)候跟著我最下面的兩張時(shí)序圖慢慢走,一遍看不懂就再看一遍,別著急,多看幾遍就能懂了。雖然這里不是用的最新框架源碼,但是基本原理都是一致的,不影響你使用最新版本時(shí)候遇到問題,然后定位解決。

看懂源碼有什么用?當(dāng)你使用這個(gè)框架遇到一些奇怪的Bug的時(shí)候,有時(shí)候不一定是你的問題,可能是源碼中的問題,追蹤源碼有利于你快速定位問題。我在使用的時(shí)候遇到過源碼的問題,后來在GitHub的issue中看到有人提了,再后來的版本就修復(fù)了。

為了寫好這篇文章,我花了一周多的時(shí)間,因?yàn)槭墙o公司做分享,所以很多細(xì)節(jié)我是講出來的,并沒有寫出來,因?yàn)橐獙懙脑挄?huì)很多,但這篇文章基本上完全可以滿足你對(duì)Lottie的使用和對(duì)源碼的理解,接下來耐心看吧

我們主要從以下四個(gè)方面來講解:

一、Lottie簡(jiǎn)介

二、LottieAndroid的使用

三、LottieAndroid源碼解析

四、可能遇到的問題會(huì)有哪些


一、Lottie簡(jiǎn)介

  • Lottie是什么?

    • Lottie是Airbnb開源的一個(gè)動(dòng)畫渲染庫(kù),同時(shí)支持Android、IOS、React Native和Web平臺(tái),Lottie目前只支持渲染播放After Effects動(dòng)畫。Lottie使用bobymovin(After Effects插件)到處的json數(shù)據(jù)作為動(dòng)畫數(shù)據(jù)源。使用Lottie可以讓動(dòng)畫顯示變得簡(jiǎn)單方便。

    • 從動(dòng)畫制作到動(dòng)畫顯示流程如下:


    • 工作流程:

      1. 設(shè)計(jì)師使用Affer Effects制作動(dòng)畫并導(dǎo)出json文件,參考:Lottie動(dòng)畫社區(qū)
      2. 各端開發(fā)使用相應(yīng)的LottieSDK實(shí)現(xiàn)動(dòng)畫效果,GitHub下載地址:Lottie AndroidLottie IOSReact NativeWeb
    • 注意事項(xiàng):

      設(shè)計(jì)師同學(xué)制作各個(gè)平臺(tái)(Android、IOS、React Netive)動(dòng)畫時(shí)需要查看Lottie在不同平臺(tái)支持的特性,否則制作出來的動(dòng)畫顯示可能會(huì)有問題,設(shè)計(jì)同學(xué)制作動(dòng)畫參考:不同平臺(tái)Lottie支持特性

  • 為什么要使用Lottie?

    • 先看看在沒有Lottie之前我們是怎么實(shí)現(xiàn)相對(duì)復(fù)雜動(dòng)畫的:

      1. 使用GIF,占用空間大,有些動(dòng)畫顯示效果不佳,需要適配分辨率,Android原生不支持GIF動(dòng)畫的顯示。
      2. 使用幀動(dòng)畫,占用空間大,依然會(huì)遇到不同分辨率適配的問題。
      3. 組合式動(dòng)畫,通過大量代碼實(shí)現(xiàn)復(fù)雜的動(dòng)畫效果。
    • 使用Lottie可以解決的問題:

      1. 降低動(dòng)畫設(shè)計(jì)和開發(fā)成本
      2. 解決設(shè)計(jì)提供動(dòng)畫效果與實(shí)現(xiàn)不一致問題
      3. 占用空間更小
      4. 不同的手機(jī)分辨率不需要適配
  • Lottie適用于哪些場(chǎng)景?

    • 啟動(dòng)(splash)動(dòng)畫:典型場(chǎng)景是APP logo動(dòng)畫的播放
    • 上下拉刷新(refresh)動(dòng)畫:所有APP都必備的功能,利用 Lottie 可以做的更加簡(jiǎn)單酷炫了
    • 加載(loading)動(dòng)畫:典型場(chǎng)景是網(wǎng)絡(luò)請(qǐng)求的loading動(dòng)畫
    • 提示(tips)動(dòng)畫:典型場(chǎng)景是空白頁的提示
    • 按鈕(button)動(dòng)畫:典型場(chǎng)景如switch按鈕、編輯按鈕、播放按鈕等按鈕的
    • 禮物(gift)動(dòng)畫:典型場(chǎng)景是直播類APP的高級(jí)動(dòng)畫播放
  • 我們想要使用Lottie替代哪些動(dòng)畫?

    • 首先并不是在APP中所有的動(dòng)畫都要用Lottie來替換
    • 一些可以通過屬性動(dòng)畫來實(shí)現(xiàn)的簡(jiǎn)單動(dòng)畫就不需要用Lottie來實(shí)現(xiàn)了
    • 替代一些通過代碼不好實(shí)現(xiàn)的動(dòng)畫效果
    • 替代GIF動(dòng)畫和幀動(dòng)畫

二、LottieAndroid的使用

  • 集成到項(xiàng)目中(以2.2.0版本為例)

    • 添加依賴:compile 'com.airbnb.android:lottie:2.2.0'

      Lottie版本號(hào)參考:Maven庫(kù)查看Lottie各版本號(hào)

    • Gradle依賴修改:

      最低版本:MIN_SDK_VERSION = 16
      編譯版本:COMPILE_SDK_VERSION = 25
      所有的兼容包需要升級(jí)到版本號(hào)為25.3.1

        compile 'com.android.support:appcompat-v7:25.3.1'
        compile 'com.android.support:cardview-v7:25.3.1'
        compile 'com.android.support:design:25.3.1'
        compile 'com.android.support:recyclerview-v7:25.3.1'
        compile 'com.android.support:palette-v7:25.3.1'
        compile 'com.android.support:support-v4:25.3.1'
      
    • 如何查看Lottie各版本需要對(duì)應(yīng)的AndroidSDK編譯版本和兼容包版本號(hào)?

      以Lottie2.2.0版本為例:mvnrepository lottie2.2.0

  • 使用方法

    • Lottie基本用法查看官方文檔

      1. 首先將json文件放到assets文件夾下:

        可以直接放到assets目錄下,或者在assets目錄下創(chuàng)建一個(gè)二級(jí)目錄放在二級(jí)目錄下。
      2. 在布局中添加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" />
              
         如上圖,如果json文件在assets子文件夾中,lottie_fileName="lottieani/stars.json"
        

        LottieAnimationView可以設(shè)置的屬性如下:
      3. 得到LottieAnimationView對(duì)象進(jìn)行動(dòng)畫操作:

         LottieAnimationView animationView = (LottieAnimationView)findViewById(R.id.animation_view);
         // 布局中不指定文件可以在此設(shè)置,路徑設(shè)置同布局文件
         animationView.setAnimation("hello-world.json");
         // 是否循環(huán)播放
         animationView.loop(true);
         // 設(shè)置播放速率,例如:2代表播放速率是不設(shè)置時(shí)的二倍
         animationView.setSpeed(2f);
         // 開始播放
         animationView.playAnimation();
         
         // 暫停播放
         animationView.pauseAnimation();
         // 取消播放
         animationVIew.cancelAnimation();
         
         // 設(shè)置播放進(jìn)度
         animationView.setProgress(0.5f);
         // 判斷是否正在播放
         animationView.isAnimating();
        

        setAnimation()有三種方法,可以直接設(shè)置動(dòng)畫的Json對(duì)象,或者設(shè)置Json文件相對(duì)路徑名,且支持設(shè)置緩存類型:

        playAnimation()有三種方法:

        支持三種緩存類型,不緩存、弱類型和強(qiáng)類型:
      4. 添加動(dòng)畫監(jiān)聽

         mAnimationView.addAnimatorListener(new Animator.AnimatorListener() {
             @Override
             public void onAnimationStart(Animator animation) {
                 Log.d(TAG, "onAnimationStart : " + animation.getDuration());
             }
        
             @Override
             public void onAnimationEnd(Animator animation) {
                 Log.d(TAG, "onAnimationEnd");
             }
        
             @Override
             public void onAnimationCancel(Animator animation) {
                 Log.d(TAG, "onAnimationCancel");
             }
        
             @Override
             public void onAnimationRepeat(Animator animation) {
                 Log.d(TAG, "onAnimationRepeat");
             }
         });
        
         mAnimationView.addAnimatorUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
             @Override
             public void onAnimationUpdate(ValueAnimator animation) {
                 //Log.d(TAG, "onAnimationUpdate : " + animation.getCurrentPlayTime());
             }
         });
        
    • 其他用法

      1. 自定義動(dòng)畫的速率和時(shí)長(zhǎng)

         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();
        
      2. 給整個(gè)動(dòng)畫添加一個(gè)特定圖層,或者一個(gè)圖層的特定內(nèi)容添加一個(gè)顏色過濾器

         // 任何符合顏色過濾界面的類
         final PorterDuffColorFilter colorFilter = new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.LIGHTEN);
         
         // 在整個(gè)視圖中添加一個(gè)顏色過濾器
         animationView.addColorFilter(colorFilter);
         
         //在特定的圖層中添加一個(gè)顏色濾鏡
         animationView.addColorFilterToLayer("hello_layer", colorFilter);
         
         // 添加一個(gè)彩色過濾器特效“hello_layer”上的內(nèi)容
         animationView.addColorFilterToContent("hello_layer", "hello", colorFilter);
         
         // 清除所有的顏色濾鏡
         animationView.clearColorFilters();
        
      3. 在列表中添加動(dòng)畫,每個(gè)item循環(huán)播放該動(dòng)畫

        如果按照下面寫法會(huì)有什么問題?如果按照下面寫法,即便是已經(jīng)設(shè)置了lottieViewLive.loop(true)循環(huán)播放,item被劃走再劃回來動(dòng)畫依舊會(huì)停止播放。

         <com.airbnb.lottie.LottieAnimationView
             android:id="@+id/lottieViewLive"
             android:layout_width="30dp"
             android:layout_height="30dp"/>
        
         // 在viewHolder中initView的時(shí)候開始動(dòng)畫
         LottieAnimationView lottieViewLive = (LottieAnimationView) layout.findViewById(R.id.lottieViewLive);
         lottieViewLive.setAnimation("lottiejson/living.json");
         // 循環(huán)播放動(dòng)畫
         lottieViewLive.loop(true);
         lottieViewLive.playAnimation();         
        

        為了解決上訴問題,只需要在布局中添加,屬性app:lottie_autoPlay="true";可以直接按照寫法在xml布局中添加動(dòng)畫即可。為什么會(huì)這樣?待會(huì)在源碼解析部分講解。

         <com.airbnb.lottie.LottieAnimationView
             android:id="@+id/lottieViewLive"
             android:layout_width="30dp"
             android:layout_height="30dp"
             app:lottie_autoPlay="true"
             app:lottie_fileName="lottiejson/living.json"
             app:lottie_loop="true"/>
        
      4. 從網(wǎng)絡(luò)直接獲取json數(shù)據(jù)顯示動(dòng)畫(Lottie本身不提供網(wǎng)絡(luò)請(qǐng)求)

        為什么要使用下面方式,而不是從網(wǎng)絡(luò)獲取json數(shù)據(jù)后直接animationView.setAnimation(json)。主要是為了使用OnCompositionLoadedListener,防止從網(wǎng)絡(luò)獲取的json數(shù)據(jù)錯(cuò)誤解析出來的composition為null。

         LottieComposition.Factory.fromJson(getResources(), jsonObject, new OnCompositionLoadedListener() {
             @Override
             public void onCompositionLoaded(@Nullable LottieComposition composition) {
                 if (composition != null) {
                     mAnimationView.setComposition(composition);
                     mAnimationView.playAnimation();
                 } else {
                     // showErrorView();
                 }
             }
         });
        
      5. 加載含有圖片的復(fù)雜文件

        有些動(dòng)畫比較復(fù)雜不是簡(jiǎn)單的線條、圖塊所能實(shí)現(xiàn)的,這樣的動(dòng)畫通過AE插件bobymovin生成的文件除了有一個(gè)json文件外,還會(huì)有一些png圖片。如何加載這樣的動(dòng)畫呢?

        ① 加載本地含有圖片的動(dòng)畫資源,json文件放在assets文件夾下,生成的png圖片同樣放在assets文件夾下(或者自己在assets文件夾下創(chuàng)建一個(gè)子目錄,假如叫l(wèi)ottieimage)。
         // 代碼這樣寫,需要設(shè)置圖片的文件夾地址
         mAnimationView.setImageAssetsFolder("lottieweaccept/images");
         mAnimationView.setAnimation("lottieweaccept/WeAccept.json");
         mAnimationView.playAnimation();
         
         // 或者在布局文件中添加這句代碼,指定圖片路徑
         app:lottie_imageAssetsFolder="lottieweaccept/images"
        

        ② 加載從網(wǎng)絡(luò)獲取的含有圖片的動(dòng)畫資源,首先需要將資源下載到手機(jī)SD卡中
         final String absolutePath = imagesDir.getAbsolutePath();
         mAnimationView.setImageAssetDelegate(new ImageAssetDelegate() {
             @Override
             public Bitmap fetchBitmap(LottieImageAsset asset) {
                 return BitmapFactory.decodeFile(absolutePath + File.separator + asset.getFileName());
             }
         });
         LottieComposition.Factory.fromInputStream(LottieViewActivity.this, inputStream, new OnCompositionLoadedListener() {
             @Override
             public void onCompositionLoaded(@Nullable LottieComposition composition) {
                 if (composition != null) {
                     mAnimationView.setComposition(composition);
                     mAnimationView.playAnimation();
                 } else {
                     // doOtherThing();
                 }
             }
         });     
        

三、源碼解析

  • 從Json到動(dòng)畫顯示的實(shí)現(xiàn)思路

    • 將復(fù)雜的圖片使用圖層表示,每一層表示不同的內(nèi)容

    • 根據(jù)動(dòng)畫需求可以只針對(duì)某一層做相應(yīng)的平移、旋轉(zhuǎn)、縮放等動(dòng)畫

    • Json文件中數(shù)據(jù)轉(zhuǎn)成LottieComposition數(shù)據(jù)對(duì)象,LottieDrawable負(fù)責(zé)將數(shù)據(jù)繪制成drawable,LottieAnimationView負(fù)責(zé)將LottieDrawable顯示出來。LottieAnimationView繼承自AppCompatImageView,LottieDrawable繼承自Drawable。
  • 先看看生成的Json數(shù)據(jù),參考這里:Lottie:讓動(dòng)畫如此簡(jiǎn)單

  • 如何加載json數(shù)據(jù)并顯示圖像的?

    • animationView.setAnimation("hello-world.json");

      通過setAnimation()來看看,上面json數(shù)據(jù)到顯示圖像的過程。源碼時(shí)序圖如下(以2.2.0版本為例):
      1. LottieAnimationView初始化的時(shí)候會(huì)首先創(chuàng)建LottieDrawable對(duì)象,private final LottieDrawable lottieDrawable = new LottieDrawable()。init(),進(jìn)行初始化的時(shí)候,解析xml設(shè)置的屬性。

      2. setAnimation(String fileName),加載Json文件。

      3. 通過異步加載,最終會(huì)調(diào)用到fromJsonSync()對(duì)Json文件進(jìn)行解析。

      4. 解析結(jié)果通過onCompositionLoaded()回調(diào)到主線程,然后會(huì)將Json數(shù)據(jù)轉(zhuǎn)換成LottieComposition對(duì)象。

      5. lottieDrawable.setComposition(),將LottieComposition對(duì)象設(shè)置給LottieDrawable。

      6. 生成CompositionLayer對(duì)象,這個(gè)對(duì)象就是每一個(gè)圖層的對(duì)象。

      7. 通過setImageDrawable(lottieDrawable)將圖像顯示出來,顯示第一幀動(dòng)畫。

  • 動(dòng)畫如何運(yùn)行起來的?

    • animationView.playAnimation();

      調(diào)用playAnimation()動(dòng)畫是如何動(dòng)起來的,源碼時(shí)序圖如下(以2.2.0版本為例):
      1. ValueAnimator實(shí)現(xiàn)的控制動(dòng)畫進(jìn)度。

      2. setProgress實(shí)現(xiàn)的顯示具體進(jìn)度動(dòng)畫。

      3. LottieDrawable中的draw繪制圖像,Canvas進(jìn)行繪制。

  • 為什么在列表中加載動(dòng)畫時(shí)設(shè)置了循環(huán)播放,item劃走再劃回來動(dòng)畫還是會(huì)停止播放?為什么添加app:lottie_autoPlay="true"才行?
  • 我們來看看這兩句代碼,有沒有什么問題?

      // 異步加載json文件
      animationView.setAnimation("hello-world.json");
      animationView.playAnimation();
      animationView.setSpeed(2f);
    

    setAnimation是異步加載json文件的,調(diào)用setAnimation之后直接調(diào)用playAnimation()源碼是如何保證加載完數(shù)據(jù)之后再開始動(dòng)畫呢?最終會(huì)走到LottieDrawable類中,如下:

性能問題

  • 性能和內(nèi)存

    • 如果沒有mask和mattes,那么性能和內(nèi)存非常好,沒有bitmap創(chuàng)建,大部分操作都是簡(jiǎn)單的cavas繪制。
    • 如果存在mattes,將會(huì)創(chuàng)建2~3個(gè)bitmap。bitmap在動(dòng)畫加載到window時(shí)被創(chuàng)建,被window刪除時(shí)回收。所以不宜在RecyclerView中使用包涵mattes或者mask的動(dòng)畫,否則會(huì)引起bitmap抖動(dòng)。除了內(nèi)存抖動(dòng),mattes和mask中必要的bitmap.eraseColor()和canvas.drawBitmap()也會(huì)降低動(dòng)畫性能。對(duì)于簡(jiǎn)單的動(dòng)畫,在實(shí)際使用時(shí)性能不太明顯。
    • 如果在列表中使用動(dòng)畫,推薦使用緩存LottieAnimationView.setAnimation(String, CacheStrategy) 。

全屏適配參考


更新:添加2.5.1版本時(shí)遇到的問題

1、 編譯版本需要 27
2、所有的suport包版本需要27.1.0

  compile 'com.android.support:appcompat-v7:27.1.0'

3、如果有引入lifecycle,版本需要1.1.0

  android.arch.lifecycle:extensions:1.1.0
  android.arch.lifecycle:compiler:1.1.0

如果項(xiàng)目中引入了livedata并且lifecycle版本號(hào)不對(duì),會(huì)報(bào)如下錯(cuò)誤:
Error:Program type already present: android.arch.lifecycle.LiveData
參考:https://stackoverflow.com/questions/49056723/errorprogram-type-already-present-android-arch-lifecycle-livedata
com.firebaseui:firebase-ui-firestore:3.1.0 depends on android.arch.lifecycle:extensions:1.0.0-beta1. Switching to version 3.2.2 fixes the issue by using the Lifecycle 1.1 libraries that Support Library 27.1.0 are built upon.

最后編輯于
?著作權(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閱讀 228,412評(píng)論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,514評(píng)論 3 416
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,373評(píng)論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,975評(píng)論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,743評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,199評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,262評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,414評(píng)論 0 288
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,951評(píng)論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,780評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,983評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,527評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,218評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,649評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,889評(píng)論 1 286
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,673評(píng)論 3 391
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,967評(píng)論 2 374

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