Compose動(dòng)畫原理-我的一點(diǎn)小思考

入門

https://developer.android.com/jetpack/compose/animation/quick-guide

思想

Compose的動(dòng)畫系統(tǒng)是基于值系統(tǒng)的動(dòng)畫,和傳統(tǒng)的基于回調(diào)的動(dòng)畫不同窝剖,Compose的動(dòng)畫api通常對(duì)外暴露一個(gè)可觀察的隨時(shí)間改變的狀態(tài)麻掸,進(jìn)而驅(qū)動(dòng)重組或者重繪,從而達(dá)成動(dòng)畫的效果

基本使用

可見性動(dòng)畫使用AnimatedVisibility,內(nèi)容尺寸動(dòng)畫用animateContentSize,根據(jù)不同的狀態(tài)展示不同的Composable之間的動(dòng)畫切換使用AnimatedContent,也可以使用Crossfade

簡(jiǎn)單的值動(dòng)畫使用使用animateXXAsState,多個(gè)動(dòng)畫同時(shí)啟用赐纱,可以用Transition進(jìn)行管理脊奋,Transition可以基于狀態(tài)啟動(dòng)多個(gè)動(dòng)畫

如果需要在啟動(dòng)的時(shí)候就進(jìn)行一個(gè)動(dòng)畫,推薦使用Transition或者Animtable動(dòng)畫接口疙描,通過在可組合函數(shù)中聲明一個(gè)基礎(chǔ)狀態(tài)诚隙,在compose中啟動(dòng)該動(dòng)畫是一個(gè)副作用效應(yīng),應(yīng)該在LaunchEffect中進(jìn)行起胰,Animatable提供的animateTo是一個(gè)中斷函數(shù)久又,直到動(dòng)畫狀態(tài)進(jìn)行到目標(biāo)狀態(tài)時(shí)才會(huì)恢復(fù),通過該特性我們可以執(zhí)行序列化的動(dòng)畫效五,同樣地地消,如果我們想要?jiǎng)赢嬐瑫r(shí)執(zhí)行的話,可以通過啟動(dòng)多個(gè)協(xié)程來(lái)達(dá)成這一點(diǎn)

Compose動(dòng)畫api的設(shè)計(jì)

底層動(dòng)畫為實(shí)現(xiàn)Animation接口的兩個(gè)對(duì)象畏妖,一個(gè)是TargetBasedAnimation脉执,提供了從起始值到目標(biāo)值變更的動(dòng)畫邏輯,DecayAnimation 一個(gè)無(wú)狀態(tài)的動(dòng)畫戒劫,提供了一個(gè)從初始速度漸漸慢下來(lái)的動(dòng)畫邏輯(滑動(dòng)動(dòng)畫)

Animation的目標(biāo)是提供無(wú)狀態(tài)的動(dòng)畫邏輯半夷,因此他是一個(gè)底層組件,頂層組件在Animation的基礎(chǔ)上構(gòu)建有狀態(tài)的動(dòng)畫邏輯,設(shè)計(jì)為無(wú)狀態(tài)的接口迅细,意味著不同于安卓View動(dòng)畫巫橄,是沒有pause,cancel等邏輯的,換句話說(shuō)疯攒,如果使用底層動(dòng)畫api創(chuàng)建的動(dòng)畫被取消了嗦随,并且需要重新開始,這種情況下應(yīng)該重新創(chuàng)建一個(gè)底層動(dòng)畫的實(shí)例敬尺,并且初始值和初始速度為之前取消時(shí)的當(dāng)前值和當(dāng)前速度

Animatable是在Animation的基礎(chǔ)上封裝的有狀態(tài)的動(dòng)畫api,提供停止和開始動(dòng)畫的能力

Animatable使用

  1. 創(chuàng)建動(dòng)畫實(shí)例枚尼,指定初始值,如果是在compose中使用砂吞,要用remember保存起來(lái)
val animatable = remember { Animatable(targetValue, typeConverter, visibilityThreshold, label) }

  1. 啟動(dòng)動(dòng)畫

根據(jù)動(dòng)畫是以值動(dòng)畫為基礎(chǔ)署恍,還是以速度為基礎(chǔ),分別調(diào)用不同的api,前者是animateTo,后者是animateDecay,這兩個(gè)api實(shí)際上也對(duì)應(yīng)了底層兩個(gè)不同的Animation類型蜻直,

可以看到在啟動(dòng)動(dòng)畫這里

animatable.animateTo(newTarget, animSpec)

  1. 停止動(dòng)畫

分為兩種兩種盯质,一種是我們需要手動(dòng)停止動(dòng)畫袁串,另外一種是隨著需要?jiǎng)赢嫷腸omposable退出組合自動(dòng)停止動(dòng)畫,其中手動(dòng)停止動(dòng)畫呼巷,直接調(diào)用Animatable的stop方法囱修,另外一種是自動(dòng)停止動(dòng)畫,只需要在使用該動(dòng)畫的composable處使用LaunchEffect啟動(dòng)動(dòng)畫就好

手動(dòng)停止動(dòng)畫會(huì)設(shè)置一個(gè)flag,在動(dòng)畫執(zhí)行的過程中會(huì)檢查該flag,從而達(dá)到停止動(dòng)畫的目的王悍,而自動(dòng)停止動(dòng)畫破镰,則是通過協(xié)程的取消機(jī)制來(lái)保證的,

suspend fun animateTo(
        targetValue: T,
        animationSpec: AnimationSpec<T> = defaultSpringSpec,
        initialVelocity: T = velocity,
        block: (Animatable<T, V>.() -> Unit)? = null
    ): AnimationResult<T, V> {
        val anim = TargetBasedAnimation(
            animationSpec = animationSpec,
            initialValue = value,
            targetValue = targetValue,
            typeConverter = typeConverter,
            initialVelocity = initialVelocity
        )
        return runAnimation(anim, initialVelocity, block)
    }

private suspend fun runAnimation(
        animation: Animation<T, V>,
        initialVelocity: T,
        block: (Animatable<T, V>.() -> Unit)?
    ): AnimationResult<T, V> {

        // Store the start time before it's reset during job cancellation.
        val startTime = internalState.lastFrameTimeNanos
        return mutatorMutex.mutate {
            try {
                internalState.velocityVector = typeConverter.convertToVector(initialVelocity)
                targetValue = animation.targetValue
                isRunning = true

                val endState = internalState.copy(
                    finishedTimeNanos = AnimationConstants.UnspecifiedTime
                )
                var clampingNeeded = false
                endState.animate(
                    animation,
                    startTime
                ) {
                    updateState(internalState)
                    val clamped = clampToBounds(value)
                    if (clamped != value) {
                        internalState.value = clamped
                        endState.value = clamped
                        block?.invoke(this@Animatable)
                        cancelAnimation()
                        clampingNeeded = true
                    } else {
                        block?.invoke(this@Animatable)
                    }
                }
                val endReason = if (clampingNeeded) BoundReached else Finished
                endAnimation()
                AnimationResult(endState, endReason)
            } catch (e: CancellationException) {
                // Clean up internal states first, then throw.
                endAnimation()
                throw e
            }
        }
    }

動(dòng)畫被放在mutatorMutex.mutate邏輯中压储,這個(gè)函數(shù)被設(shè)計(jì)成啟動(dòng)成獲取新鎖的時(shí)候會(huì)取消前一個(gè)協(xié)程鲜漩,等前一個(gè)協(xié)程取消時(shí)款熬,再重新獲取鎖炕矮,保證同一時(shí)間只有一段代碼在訪問臨界區(qū)的Animatable相關(guān)狀態(tài)拦键,可以看到多次調(diào)用Animatable的animateTo方法左腔,會(huì)將當(dāng)前動(dòng)畫的進(jìn)度和速度保存下來(lái),作為新的起點(diǎn)臀晃,因此如果動(dòng)畫進(jìn)行到一半驶沼,動(dòng)畫被取消了爆办,動(dòng)畫會(huì)從一半的進(jìn)度繼續(xù)播放到目標(biāo)進(jìn)度为朋,如果動(dòng)畫進(jìn)行已經(jīng)結(jié)束了臂拓,這個(gè)時(shí)候再調(diào)用animateTo,則會(huì)相當(dāng)于重新執(zhí)行動(dòng)畫

Animtable將一些動(dòng)畫的內(nèi)部狀態(tài)保留在AnimationState 這個(gè)數(shù)據(jù)結(jié)構(gòu)中,

實(shí)際的動(dòng)畫邏輯在此處

endState.animate(
                    animation,
                    startTime
                ) {
                    updateState(internalState)
                    val clamped = clampToBounds(value)
                    if (clamped != value) {
                        internalState.value = clamped
                        endState.value = clamped
                        block?.invoke(this@Animatable)
                        cancelAnimation()
                        clampingNeeded = true
                    } else {
                        block?.invoke(this@Animatable)
                    }
                }

首先我們看下animate函數(shù)本身做了什么

internal suspend fun <T, V : AnimationVector> AnimationState<T, V>.animate(
    animation: Animation<T, V>,
    startTimeNanos: Long = AnimationConstants.UnspecifiedTime,
    block: AnimationScope<T, V>.() -> Unit = {}
) {
    val initialValue = animation.getValueFromNanos(0) // 獲取動(dòng)畫的初始值
        // 獲取動(dòng)畫的初始速度
    val initialVelocityVector = animation.getVelocityVectorFromNanos(0)
      // 如果前一個(gè)動(dòng)畫是被取消的习寸,那么這兩個(gè)值都會(huì)繼承
    var lateInitScope: AnimationScope<T, V>? = null
    try {
        if (startTimeNanos == AnimationConstants.UnspecifiedTime) {
            val durationScale = coroutineContext.durationScale
            animation.callWithFrameNanos {
                lateInitScope = AnimationScope(
                    initialValue = initialValue,
                    typeConverter = animation.typeConverter,
                    initialVelocityVector = initialVelocityVector,
                    lastFrameTimeNanos = it, 
                    targetValue = animation.targetValue,
                    startTimeNanos = it,
                    isRunning = true,
                    onCancel = { isRunning = false }
                ).apply {
                    // First frame
                    doAnimationFrameWithScale(it, durationScale, animation, this@animate, block)
                }
            }
        } else {
            lateInitScope = AnimationScope(
                initialValue = initialValue,
                typeConverter = animation.typeConverter,
                initialVelocityVector = initialVelocityVector,
                lastFrameTimeNanos = startTimeNanos,
                targetValue = animation.targetValue,
                startTimeNanos = startTimeNanos,
                isRunning = true,
                onCancel = { isRunning = false }
            ).apply {
                // First frame
                doAnimationFrameWithScale(
                    startTimeNanos,
                    coroutineContext.durationScale,
                    animation,
                    this@animate,
                    block
                )
            }
        }
        // Subsequent frames
        while (lateInitScope!!.isRunning) {
            val durationScale = coroutineContext.durationScale
            animation.callWithFrameNanos {
                lateInitScope!!.doAnimationFrameWithScale(it, durationScale, animation, this, block)
            }
        }
        // End of animation
    } catch (e: CancellationException) {
        lateInitScope?.isRunning = false
        if (lateInitScope?.lastFrameTimeNanos == lastFrameTimeNanos) {
            // There hasn't been another animation.
            isRunning = false
        }
        throw e
    }
}

根據(jù)是第一次啟動(dòng)該動(dòng)畫(或者重新運(yùn)行)構(gòu)建一個(gè)AnimationScope,記錄下動(dòng)畫開始的正確時(shí)間戳,創(chuàng)建完該Scope對(duì)象傻工,調(diào)用該Scope對(duì)象上的doAnimationFrameWithScale方法霞溪,后續(xù)只要?jiǎng)赢嫑]有取消,就一直跟隨時(shí)鐘運(yùn)行動(dòng)畫

private fun <T, V : AnimationVector> AnimationScope<T, V>.doAnimationFrameWithScale(
    frameTimeNanos: Long,
    durationScale: Float,
    anim: Animation<T, V>,
    state: AnimationState<T, V>,  // endState
    block: AnimationScope<T, V>.() -> Unit
) {
    val playTimeNanos =
        if (durationScale == 0f) {
            anim.durationNanos
        } else {
            ((frameTimeNanos - startTimeNanos) / durationScale).toLong()
        }
    // 根據(jù)外部的尺度來(lái)決定動(dòng)畫的速度
    doAnimationFrame(frameTimeNanos, playTimeNanos, anim, state, block)
}

private fun <T, V : AnimationVector> AnimationScope<T, V>.doAnimationFrame(
    frameTimeNanos: Long,
    playTimeNanos: Long,
    anim: Animation<T, V>,
    state: AnimationState<T, V>,
    block: AnimationScope<T, V>.() -> Unit
) {
    lastFrameTimeNanos = frameTimeNanos
    value = anim.getValueFromNanos(playTimeNanos) // 根據(jù)時(shí)長(zhǎng)獲取當(dāng)前值
    velocityVector = anim.getVelocityVectorFromNanos(playTimeNanos)
    val isLastFrame = anim.isFinishedFromNanos(playTimeNanos)
    if (isLastFrame) {
        // TODO: This could probably be a little more granular
        // TODO: end time isn't necessarily last frame time
        finishedTimeNanos = lastFrameTimeNanos
        isRunning = false
    }
    updateState(state)
    block()
}

getValueFromNanos的調(diào)用鏈路中捆,以TargetBasedAnimation 為例鸯匹,

調(diào)用該方法,實(shí)際將方法轉(zhuǎn)發(fā)給插值器泄伪,給插值器當(dāng)前時(shí)間殴蓬,以及初始狀態(tài),目標(biāo)狀態(tài)蟋滴,初始速度等等值給出當(dāng)前的值染厅,如果當(dāng)前動(dòng)畫在這一幀結(jié)束,就不再運(yùn)行動(dòng)畫津函,將部分狀態(tài)同步到state中肖粮,然后執(zhí)行block, block代碼如下

updateState(internalState)
val clamped = clampToBounds(value)
if (clamped != value) {
   internalState.value = clamped
   endState.value = clamped
   block?.invoke(this@Animatable)
   cancelAnimation()
   clampingNeeded = true
                    } else {
   block?.invoke(this@Animatable)
                    }

首先是將scope的值同步到internalState,internalState對(duì)外暴露了一個(gè)state,這個(gè)state里面保存了最新的值,外部通過觀察這個(gè)值尔苦,就能夠得到當(dāng)前動(dòng)畫的最新值

Compose動(dòng)畫中的插值器

https://blog.csdn.net/EthanCo/article/details/129882487

首先這里存在兩個(gè)概念涩馆,第一個(gè)是轉(zhuǎn)換器行施,轉(zhuǎn)換器是將我們外部希望進(jìn)行動(dòng)畫變更的狀態(tài)轉(zhuǎn)換成動(dòng)畫庫(kù)內(nèi)部使用的狀態(tài),第二個(gè)是插值器魂那,插值器能夠?qū)?nèi)部狀態(tài)根據(jù)動(dòng)畫當(dāng)前進(jìn)行時(shí)長(zhǎng)給出一個(gè)合理的值蛾号,插值器的命名都是XXXSpec,根據(jù)動(dòng)畫是否是一直運(yùn)行的,分為FiniteAnimationSpec和InfiniteRepeatableSpec,插值器在安卓原生動(dòng)畫中的命名是以xxInterpolator來(lái)命名的涯雅,插值器的暗示意味更強(qiáng)

目前思考下來(lái)鲜结,動(dòng)畫的中間狀態(tài)都保存在動(dòng)畫本身中,插值器可以設(shè)計(jì)為無(wú)狀態(tài)的斩芭,這樣插值器在各個(gè)動(dòng)畫之間復(fù)用都不會(huì)出現(xiàn)bug轻腺,實(shí)際看下來(lái)也驗(yàn)證了這個(gè)結(jié)論

Compose動(dòng)畫插值器接口聲明如下

interface AnimationSpec<T> {
    /**
     * Creates a [VectorizedAnimationSpec] with the given [TwoWayConverter].
     *
     * The underlying animation system operates on [AnimationVector]s. [T] will be converted to
     * [AnimationVector] to animate. [VectorizedAnimationSpec] describes how the
     * converted [AnimationVector] should be animated. E.g. The animation could simply
     * interpolate between the start and end values (i.e.[TweenSpec]), or apply spring physics
     * to produce the motion (i.e. [SpringSpec]), etc)
     *
     * @param converter converts the type [T] from and to [AnimationVector] type
     */
    fun <V : AnimationVector> vectorize(
        converter: TwoWayConverter<T, V>
    ): VectorizedAnimationSpec<V>
}

可以看到可以對(duì)任意類型做動(dòng)畫插值,前提是能夠?qū)⑦@個(gè)類型轉(zhuǎn)換成動(dòng)畫庫(kù)內(nèi)部使用的類型划乖,也就是AnimationVector,AnimationVector本身也是一個(gè)接口贬养,目前支持了最多四個(gè)維度的變化,其中每一個(gè)維度的數(shù)據(jù)限定為Float

所以實(shí)際進(jìn)行動(dòng)畫的是VectorizedAnimationSpec琴庵,我們首先

VectorizedAnimationSpec家族類圖

vectorized動(dòng)畫家族.png

VectorizedFloatAnimationSpec 被spring動(dòng)畫和tween動(dòng)畫用來(lái)實(shí)現(xiàn)內(nèi)部邏輯误算,首先我們來(lái),

因此細(xì)化拆分迷殿,我們需要有一個(gè)對(duì)Float做動(dòng)畫的機(jī)制儿礼,能夠根據(jù)初始值,初始速度等等獲取目標(biāo)浮點(diǎn)值庆寺,VectorizedFloatAnimationSpec就是這個(gè)邏輯蚊夫,它將相關(guān)進(jìn)度值的獲取委托給了FloatAnimationSpec

因此Tween的動(dòng)畫核心在FloatTweenSpec中g(shù)etValueFromNanos

override fun getValueFromNanos(
        playTimeNanos: Long,
        initialValue: Float,
        targetValue: Float,
        initialVelocity: Float
    ): Float {
        // TODO: Properly support Nanos in the impl
        val playTimeMillis = playTimeNanos / MillisToNanos
        val clampedPlayTime = clampPlayTime(playTimeMillis)
        val rawFraction = if (duration == 0) 1f else clampedPlayTime / duration.toFloat()
        val fraction = easing.transform(rawFraction.coerceIn(0f, 1f))
        return lerp(initialValue, targetValue, fraction)
    }

可以看到進(jìn)度的確定是由easing來(lái)決定的,Easing有唯一一個(gè)實(shí)現(xiàn)CubicBezierEasing

class CubicBezierEasing(
    private val a: Float,
    private val b: Float,
    private val c: Float,
    private val d: Float
)

4個(gè)控制點(diǎn)懦尝,從我的理解來(lái)看知纷,x坐標(biāo)代表了時(shí)間,y坐標(biāo)了代表了這個(gè)時(shí)候的真實(shí)進(jìn)度,具體數(shù)學(xué)邏輯留待進(jìn)一步分析

Transition

公開給外部的有啟動(dòng)動(dòng)畫的接口animateTo,有結(jié)束Transition的接口onTransitionEnd,有添加各種動(dòng)畫的接口,以及創(chuàng)建子Transition的接口陵霉,

這部分邏輯應(yīng)該是最復(fù)雜的琅轧,所以留待最后一部分分析,Transition本質(zhì)上是根據(jù)狀態(tài)的變更去管理一群動(dòng)畫同時(shí)運(yùn)行,compose給我們提供的一個(gè)接口使用Transition的是updateTransition,這個(gè)函數(shù)可以驅(qū)動(dòng)Transition狀態(tài)變更踊挠,進(jìn)而驅(qū)動(dòng)動(dòng)畫的進(jìn)行

fun <T> updateTransition(
    targetState: T,
    label: String? = null
): Transition<T> {
    val transition = remember { Transition(targetState, label = label) }
    transition.animateTo(targetState)
    DisposableEffect(transition) {
        onDispose {
            // Clean up on the way out, to ensure the observers are not stuck in an in-between
            // state.
            transition.onTransitionEnd()
        }
    }
    return transition
}

Transition這里的各種各樣的增加動(dòng)畫接口是在Animation的基礎(chǔ)上擴(kuò)展出來(lái)的,通過TransitionAnimationState進(jìn)行管理該動(dòng)畫乍桂,后續(xù)所有的這些動(dòng)畫由Transition進(jìn)行驅(qū)動(dòng)管理,至于子Transition,首先它本身也是Transition效床,其次在原始Transition的概念上嵌套就可以了,什么場(chǎng)景適合Transition呢睹酌,當(dāng)我們需要根據(jù)一個(gè)狀態(tài)的變化同時(shí)做多種動(dòng)畫,且動(dòng)畫可能本身比較復(fù)雜的時(shí)候扁凛,就可以使用Transition來(lái)管理我們的東西忍疾,如果動(dòng)畫本身比較簡(jiǎn)單,根據(jù)自己的場(chǎng)景去挑選其他動(dòng)畫接口就可了谨朝,如果從某種意義上來(lái)說(shuō)卤妒,Transition相當(dāng)于AnimationManager,但它本身只能控制動(dòng)畫同時(shí)播放(除非設(shè)置延遲)甥绿,不能保證動(dòng)畫播放的先后順序,不過在Compose的場(chǎng)景下也是完全夠用了

通篇看下來(lái)则披,可以說(shuō)Compose的動(dòng)畫系統(tǒng)和原生的動(dòng)畫系統(tǒng)完全不是一回事共缕,再次感嘆State系統(tǒng)設(shè)計(jì)得十分巧妙!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末士复,一起剝皮案震驚了整個(gè)濱河市图谷,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌阱洪,老刑警劉巖便贵,帶你破解...
    沈念sama閱讀 219,110評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異冗荸,居然都是意外死亡承璃,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,443評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門蚌本,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)盔粹,“玉大人,你說(shuō)我怎么就攤上這事程癌∠衔耍” “怎么了?”我有些...
    開封第一講書人閱讀 165,474評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵嵌莉,是天一觀的道長(zhǎng)进萄。 經(jīng)常有香客問我,道長(zhǎng)锐峭,這世上最難降的妖魔是什么垮斯? 我笑而不...
    開封第一講書人閱讀 58,881評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮只祠,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘扰肌。我一直安慰自己抛寝,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,902評(píng)論 6 392
  • 文/花漫 我一把揭開白布曙旭。 她就那樣靜靜地躺著盗舰,像睡著了一般。 火紅的嫁衣襯著肌膚如雪桂躏。 梳的紋絲不亂的頭發(fā)上钻趋,一...
    開封第一講書人閱讀 51,698評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音剂习,去河邊找鬼蛮位。 笑死较沪,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的失仁。 我是一名探鬼主播尸曼,決...
    沈念sama閱讀 40,418評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼萄焦!你這毒婦竟也來(lái)了控轿?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,332評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤拂封,失蹤者是張志新(化名)和其女友劉穎茬射,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體冒签,經(jīng)...
    沈念sama閱讀 45,796評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡在抛,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,968評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了镣衡。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片霜定。...
    茶點(diǎn)故事閱讀 40,110評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖廊鸥,靈堂內(nèi)的尸體忽然破棺而出望浩,到底是詐尸還是另有隱情,我是刑警寧澤惰说,帶...
    沈念sama閱讀 35,792評(píng)論 5 346
  • 正文 年R本政府宣布磨德,位于F島的核電站,受9級(jí)特大地震影響吆视,放射性物質(zhì)發(fā)生泄漏典挑。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,455評(píng)論 3 331
  • 文/蒙蒙 一啦吧、第九天 我趴在偏房一處隱蔽的房頂上張望您觉。 院中可真熱鬧,春花似錦授滓、人聲如沸琳水。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,003評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)在孝。三九已至,卻和暖如春淮摔,著一層夾襖步出監(jiān)牢的瞬間私沮,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,130評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工和橙, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留仔燕,地道東北人造垛。 一個(gè)月前我還...
    沈念sama閱讀 48,348評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像涨享,于是被迫代替她去往敵國(guó)和親筋搏。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,047評(píng)論 2 355

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