交互式動(dòng)畫(上):iOS 10 以下的實(shí)現(xiàn)

本文上下兩篇已授權(quán)在 InfoQ 的移動(dòng)開發(fā)前線公眾號(hào)上首發(fā),微信閱讀地址InfoQ 文章鏈接

不久前結(jié)束的 WWDC 2016 Session 216: Advances in UIKit Animations and Transitions 介紹了 iOS 10 的新動(dòng)畫 API,讓動(dòng)畫與交互無縫連接,這是「開發(fā)者的大事、大快所有人心的大好事」。兩年前 objc.io 在「交互式動(dòng)畫」一文在探討了這個(gè)話題,本文先來探討 iOS 10 以下的系統(tǒng)對(duì)交互動(dòng)畫的支持,在下篇中深度解讀 iOS 10 新 API。

交互動(dòng)畫類型

其實(shí)交互式動(dòng)畫在 iOS 系統(tǒng)里可以說是司空見慣的。在可交互動(dòng)畫的執(zhí)行過程中交互手段(一切控制當(dāng)前動(dòng)畫的手段,主要是手勢(shì))會(huì)隨時(shí)切入動(dòng)畫過程,根據(jù)交互結(jié)束后是否更改了動(dòng)畫流程可以將交互動(dòng)畫分為兩種:一種會(huì)更改動(dòng)畫流程,比如 UIScrollView 的滑動(dòng)動(dòng)畫,如今看來很普通,在 iPhone 問世之初這個(gè)效果可是征服人們的一大利器,「喬布斯在第一次展示 iPhone 時(shí),他特別指出當(dāng)他給別人看了這個(gè)滑動(dòng)例子,別人說的一句話: 當(dāng)這個(gè)界面滑動(dòng)的時(shí)候我就已經(jīng)被征服了。」(出自「交互式動(dòng)畫」一文),在這個(gè)滑動(dòng)動(dòng)畫里每次手指在界面上滑動(dòng)時(shí),前一個(gè)滑動(dòng)動(dòng)畫被中止,當(dāng)手指離開屏幕后,添加一個(gè)新的滑動(dòng)動(dòng)畫;另一種僅僅控制動(dòng)畫進(jìn)度而不修改動(dòng)畫,典型代表是交互轉(zhuǎn)場(chǎng)動(dòng)畫,除了帶來便利的操作,驚艷的轉(zhuǎn)場(chǎng)動(dòng)畫也是個(gè)有力的視覺征服利器。

這兩種交互動(dòng)畫的實(shí)現(xiàn)手法是不一樣的。后一種涉及暫停、恢復(fù)和逆轉(zhuǎn)動(dòng)畫,在系統(tǒng)支持的交互轉(zhuǎn)場(chǎng)里,只需要提供一個(gè)UIPercentDrivenInteractiveTransition實(shí)例并在交互過程中使用updateInteractiveTransition:來更新進(jìn)度即可,完全不用我們操心其他事情,實(shí)現(xiàn)非常簡(jiǎn)單。如何在普通的動(dòng)畫上實(shí)現(xiàn)這種控制呢?可以參考三個(gè)月前我在「iOS 開發(fā)」公眾號(hào)上發(fā)表的「iOS 視圖控制器轉(zhuǎn)場(chǎng)詳解」中的「自定義容器控制器轉(zhuǎn)場(chǎng)」章節(jié):暫停和恢復(fù)動(dòng)畫采用官方提供的方法:How to pause the animation of a layer tree?;手動(dòng)控制動(dòng)畫進(jìn)度則需要在暫停動(dòng)畫的基礎(chǔ)上更新 CAMediaTiming 協(xié)議(CALayer 遵守該協(xié)議)中的timeOffset屬性;而在交互結(jié)束后逆轉(zhuǎn)動(dòng)畫則需要CADisplayLink的幫助。iOS 10 引入的新 API 對(duì)這些操作進(jìn)行了封裝,實(shí)現(xiàn)會(huì)簡(jiǎn)單得多,同時(shí)兼容了前一種交互動(dòng)畫的實(shí)現(xiàn)方法,打破了兩種交互動(dòng)畫的界限。

objc.io 在「交互式動(dòng)畫」一文中探索了前一種交互式動(dòng)畫,實(shí)現(xiàn)了下面這種類似控制中心的效果:

Pane Control Interactive Animation.gif

這個(gè)簡(jiǎn)單的位移動(dòng)畫里包含了兩套交互:滑動(dòng)控制(pan 手勢(shì))和點(diǎn)擊控制(tap 手勢(shì)),要解決三個(gè)轉(zhuǎn)換問題,也是所有交互動(dòng)畫需要解決的問題:

  1. Animation to Gesture:動(dòng)畫過程中切入滑動(dòng)控制,需要中止當(dāng)前的動(dòng)畫并由手指來控制控制板的移動(dòng);
  2. Gesture to Animation:滑動(dòng)結(jié)束后添加新的動(dòng)畫,并與當(dāng)前的狀態(tài)平滑銜接;
  3. Animation to Animation:動(dòng)畫過程中每次點(diǎn)擊視圖后使動(dòng)畫逆轉(zhuǎn)。

objc.io 的兩位作者使用了三種方法來實(shí)現(xiàn)這個(gè)交互動(dòng)畫,手法都是實(shí)現(xiàn)彈簧動(dòng)畫(Spring Animation)去驅(qū)動(dòng)控制板視圖的移動(dòng):

  1. 基于 UIKit Dynamics 框架,這是 iOS 7 引入的模擬真實(shí)物理行為的動(dòng)畫框架,對(duì)控制板視圖賦予了彈簧的行為,每次移動(dòng)都如同有一個(gè)彈簧將視圖拉向目標(biāo)位置;
  2. 自己動(dòng)手實(shí)現(xiàn)彈簧動(dòng)畫,所謂動(dòng)畫就是數(shù)值的連續(xù)變化,作者根據(jù)彈簧的胡克定律實(shí)現(xiàn)一個(gè)算法來計(jì)算物體在運(yùn)動(dòng)過程中的位置,前面提到的CADisplayLink是個(gè)能夠與屏幕刷新頻率同步的定時(shí)器,通過調(diào)用指定的方法,每次屏幕刷新時(shí)更新視圖位置,效果與普通的動(dòng)畫無異。
  3. 將在2中實(shí)現(xiàn)的彈簧動(dòng)畫使用 Facebook 的 POP 框架驅(qū)動(dòng)。

這三種方法都沒有使用 UIView Animation 和 Core Animation(前者是后者的封裝),這樣實(shí)現(xiàn)普通動(dòng)畫的交互就比較困難,接下來討論如何使用這兩種動(dòng)畫 API 來實(shí)現(xiàn)上面的交互效果。

Animation to Gesture

添加到 CALayer 上的動(dòng)畫在結(jié)束前如果被取消會(huì)造成視覺突變,比如在一個(gè)右移的動(dòng)畫結(jié)束前取消該動(dòng)畫就會(huì)造成如下所示的跳躍,從中途直接跳到了終點(diǎn):

AnimationJump.mov.gif

因此交互動(dòng)畫首要解決的就是一個(gè)很知乎的問題:「如何優(yōu)雅地中止運(yùn)行中的動(dòng)畫而不造成畫面突變?」答案是:取消動(dòng)畫時(shí)讓 modelLayer 的狀態(tài)與當(dāng)前 presentationLayer 的狀態(tài)同步。在手勢(shì)切入控制板的動(dòng)畫過程后這樣做:

let currentPosition = (panelView.layer.presentationLayer() as! CALayer).position
panelView.layer.removeAllAnimations()//或者使用 removeAnimationForKey: 取消指定的動(dòng)畫
panelView.layer.position = currentPosition

這里有個(gè)需要注意的地方,如果你使用 UIView Animation,一定要使用帶options的 API,且必須將.AllowUserInteraction作為選項(xiàng)之一,不然在動(dòng)畫運(yùn)行過程中視圖不會(huì)響應(yīng)觸摸事件,使用 Core Animation 則不受此影響。

Gesture to Animation: Spring Animation

上面的目標(biāo)是:滑動(dòng)結(jié)束后添加新的動(dòng)畫,并與當(dāng)前的狀態(tài)平滑銜接。這需要手指離開屏幕后添加的新動(dòng)畫應(yīng)該以手指離開屏幕時(shí)沿 Y 軸的速度開始,否則速度曲線不連續(xù),看著很不自然。離開速度可以從手勢(shì)獲取,但是指定動(dòng)畫的初始速度,在 iOS 7 公開彈簧動(dòng)畫(Spring Animation)接口之前,現(xiàn)有的動(dòng)畫 API 里沒有能夠直接做到這點(diǎn)的,iOS 7 中引入的 UIKit Dynamics 動(dòng)畫框架也可以實(shí)現(xiàn)這個(gè)目標(biāo),除此之外,要么像 objc.io 的兩位作者那樣自己動(dòng)手打造 Spring 效果要么借助第三方的動(dòng)畫庫。

彈簧動(dòng)畫的 API:

animateWithDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion:

這個(gè) API 在時(shí)間曲線上模擬彈簧的簡(jiǎn)諧運(yùn)動(dòng)(簡(jiǎn)單來講就是來回振蕩),實(shí)現(xiàn)位移動(dòng)畫時(shí)模擬真實(shí)彈簧的行為。

其中的速率參數(shù)initialSpringVelocity是個(gè)CGFloat,這顯得很奇怪,為什么不是一個(gè)向量呢?「交互式動(dòng)畫」文中對(duì)此提出了質(zhì)疑:「當(dāng)我們給一個(gè)移動(dòng) view 的動(dòng)畫在其運(yùn)動(dòng)的方向上加一個(gè)初始的速率時(shí),你沒法告知?jiǎng)赢嬤@個(gè) view 現(xiàn)在的運(yùn)動(dòng)狀態(tài),比如我們不知道要添加的動(dòng)畫的方向是不是和原來的 view 的速度方向垂直。為了使其成為可能,這個(gè)速度需要用向量來表示」。實(shí)際上盡管速率參數(shù)是個(gè)數(shù)值而非向量,但彈簧動(dòng)畫的初始速度是有方向的:不管視圖從(100, 100)移動(dòng)到(200, 0),還是從(100, 100)移動(dòng)到(200, 200),初始速度始終是沿著起點(diǎn)到終點(diǎn)的直線方向的。我覺得在這里這兩位作者陷入了一個(gè)誤區(qū),且不說在這個(gè)場(chǎng)景里動(dòng)畫的方向是明確的(Y 軸,起點(diǎn)和終點(diǎn)我們也知道),他們似乎想用彈簧動(dòng)畫來實(shí)現(xiàn)添加反向的動(dòng)畫(即視圖在動(dòng)畫中途返回原點(diǎn),這是第三個(gè)轉(zhuǎn)換問題),這個(gè)質(zhì)疑的本質(zhì)是指彈簧動(dòng)畫無法合成速度,這類似一枚火箭在飛行中啟動(dòng)引擎在相反方向上添加推動(dòng)力來減速直至反向運(yùn)動(dòng)。但彈簧動(dòng)畫和其他的動(dòng)畫 API 都并非由力學(xué)引擎驅(qū)動(dòng),在兩位作者發(fā)布這篇文章的 iOS 7 時(shí)期,彈簧動(dòng)畫是無法做到這點(diǎn)的,從 iOS 8 開始就可以了,但是原因和這個(gè) API 本身沒有關(guān)系,下一節(jié)來解釋。兩位作者最終放棄了使用這個(gè) API,從而使得整個(gè)探索走向了完全不一樣的方向。

另外速率參數(shù)如何設(shè)置也很令人費(fèi)解,文檔里的解釋是這樣的:

A value of 1 corresponds to the total animation distance traversed in one second. For example, if the total animation distance is 200 points and you want the start of the animation to match a view velocity of 100 pt/s, use a value of 0.5.

initialSpringVelocity并非直接指定初始速率,動(dòng)畫初始(變化)速率 = (toValue - fromValue) * initialSpringVelocity,這種相對(duì)值的設(shè)計(jì)避開了動(dòng)畫的具體變化值,方便使用者估算和設(shè)置動(dòng)畫時(shí)間。那么從(100, 100)移動(dòng)到(300, 300),如果你希望視圖沿著目標(biāo)方向的初始速度為(150, 150),即合成速度約為150 X 1.4(2的開方值) = 210,直線距離約為 200 X 1.4 = 280,那么initialSpringVelocity約為 210/280 = 0.75。

回到這個(gè)階段的問題本身,怎么解決?

switch panGesture.state {
case .Began:
    cancelMoveAnimation()//封裝上一節(jié)中止動(dòng)畫運(yùn)行的代碼
case .Changed:
    //隨手指移動(dòng)視圖
    let point = panGesture.translationInView(view)
    panelView.center.y += point.y
    panGesture.setTranslation(CGPointZero, inView: view)
case .Ended, .Cancelled:
    //新動(dòng)畫初始速度與手指的速度同步,保證動(dòng)畫流暢自然。
    let gestureVelocity = panGesture.velocityInView(view)
    let velocity = abs(gestureVelocity.y) / abs(paneView.center.y - targetY) 
    UIView.animateWithDuration(0.5, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: velocity, options: .AllowUserInteraction, animations: {
        self.panelView.center.y = targetY //根據(jù)手勢(shì)的方向計(jì)算目標(biāo)位置
        }, completion: {/*更新相關(guān)狀態(tài)*/
    })
default:break
}

Animation to Animation: Additive Animation

在動(dòng)畫中途點(diǎn)擊控制板視圖后讓視圖返回到原來的位置,做法是再次添加一個(gè)同樣動(dòng)畫屬性的動(dòng)畫(使用 Core Animation 時(shí)注意使用不同的 key),但在效果上完全抵消,效果有如下幾種:

AnimationDiff.mov.gif

使用 UIView Animation 或者 Core Animation 不做特殊設(shè)置的話,效果是第一種;使用 UIView Animation 時(shí)指定 BeginFromCurrentState 選項(xiàng)的效果是第二種,位置不會(huì)突變但速度有突變;我們需要的是第三種效果,使用 Additive 類型的動(dòng)畫時(shí),在控制板打開或者關(guān)閉過程的任何時(shí)刻點(diǎn)擊視圖,視圖將會(huì)向反方向移動(dòng),動(dòng)畫不會(huì)有位置和速度突變,但 UIView Animation 沒有這個(gè)選項(xiàng)。

在 objc.io 的這篇文章發(fā)布后的半個(gè)多月正是 WWDC 2014 大會(huì),在 Session 236: Building Interruptible and Responsive Interactions 里介紹了解決上述三個(gè)轉(zhuǎn)換問題的方法,上面的動(dòng)圖都截取自該 session,前兩個(gè)問題的解決辦法就是上面說的那些,也提到了 objc.io 這篇文章里中使用的 UIKit Dynamics 這個(gè)技巧,而最為棘手的第三個(gè)問題需要實(shí)現(xiàn) Additive 類型的動(dòng)畫,該效果來自 CAAnimation 子類 CAPropertyAnimation 的additive屬性。

常用的動(dòng)畫類

additive屬性自 iOS 2 起就存在,文檔解釋:

If YES, the value specified by the animation will be added to the current render tree value of the property to produce the new render tree value. The addition function is type-dependent, e.g. for affine transforms the two matrices are concatenated. The default is NO.

使用 CAKeyframeAnimation 時(shí)必須將該屬性指定為true,否則不會(huì)出現(xiàn)期待的結(jié)果。不過,在 CABasicAnimation 里使用這個(gè)屬性很需要一番技巧,我在嘗試使用這個(gè)屬性時(shí)總是得不到想要的效果,直到觀看了這個(gè) session 才恍然大悟,原來是這么設(shè)計(jì)的,文檔的解釋是正確的廢話。

如何使用 CABasicAnimation 實(shí)現(xiàn)上面的效果呢?非 Additive 的動(dòng)畫的變化范圍是絕對(duì)值設(shè)計(jì),添加到 presentationLayer 的動(dòng)畫的變化范圍是:fromValue -> toValue,Additive 的動(dòng)畫采用的是相對(duì)值設(shè)計(jì),添加到 presentationLayer 的動(dòng)畫的變化范圍是:modelLayerValue + fromValue -> modelLayerValue + toValue。假設(shè)控制板開關(guān)后的 Y 軸差距為 500,這樣實(shí)現(xiàn) Additive 效果:

switch tapGeture.state {
case .Ended, .Cancelled:
    let openXcloseAni = CABasicAnimation(keyPath: "position.y")
    openXcloseAni.duration = 1
    openXcloseAni.additive = true //注意開啟 additive 屬性
    openXcloseAni.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionDefault)
    //盡管這里修改 modelLayer 數(shù)據(jù)的代碼在添加動(dòng)畫之后,但無論前后,這兩者是一起提交給渲染進(jìn)程的,所以 toValue 總是 0
    if panelOpened{
        //向上移動(dòng) 500 單位
        openXcloseAni.fromValue = 500
        openXcloseAni.toValue = 0
        panelView.layer.addAnimation(openXcloseAni, forKey: "open")
        panelView.center.y -= 500 
    }else{
        //向下移動(dòng) 500 單位
        openXcloseAni.fromValue = -500
        openXcloseAni.toValue = 0
        panelView.layer.addAnimation(openXcloseAni, forKey: "close")
        panelView.center.y += 500
    }
    panelOpened = !panelOpened
default: break
}

注意指定timingFunction,該值默認(rèn)為 nil,效果是線性曲線(Linear),兩個(gè)動(dòng)畫疊加后的效果與 BeginFromCurrentState 等同。但Core Animation 也沒有提供 Spring Timing Function,雖然從 iOS 6 起就有人發(fā)現(xiàn)了上面的 CASpringAnimation,但是這個(gè) API 才到 iOS 9 才公開,而且沒有文檔。而 UIView Animation 沒有提供實(shí)現(xiàn) Additive 效果的選項(xiàng),只能退而求其次實(shí)現(xiàn) BeginFromCurrentState 的效果。所以點(diǎn)擊后逆轉(zhuǎn)動(dòng)畫在 iOS 7 上的效果無法完全滿足設(shè)計(jì)的要求,可以依靠一些第三方彈簧動(dòng)畫來彌補(bǔ),比如 RBBAnimation,基于 CAKeyframeAnimation,支持 iOS 6。

iOS 8 中 UIView Animation 默認(rèn)實(shí)現(xiàn)了 Additive 效果,所以從 iOS 8 開始,解決第三個(gè)轉(zhuǎn)換問題就太容易了,直接添加反向的動(dòng)畫即可:

switch tapGeture.state {
case .Ended, .Cancelled:
   let targetY = panelOpened ? topY : bottomY //根據(jù)開關(guān)狀態(tài)計(jì)算目標(biāo)位置
   UIView.animateWithDuration(duration, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 2, options: .AllowUserInteraction, animations: {
       self.panelView.center.y = targetY
       }, completion: nil)
   panelOpened = !panelOpened
default: break
}

小結(jié)

從代碼上看,無比簡(jiǎn)單。不過別忘了沒有 Additive 類型的動(dòng)畫,objc.io 在「交互式動(dòng)畫」中做出的艱辛探索,實(shí)現(xiàn)成本要高出許多。在 iOS 7 中利用 UIView Animation/Core Animation 實(shí)現(xiàn)交互動(dòng)畫還有不完美的地方,而 UIKit Dynamics 框架是個(gè)非常好的替代選項(xiàng)。從 iOS 8 開始沒有了限制,而 iOS 7 以下的系統(tǒng)則需要自己打造 Spring 動(dòng)畫或者依靠第三方動(dòng)畫庫。

這個(gè)動(dòng)畫的完整代碼可在 ControlPanelAnimation 查看。

參考:

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

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