Android屬性動(dòng)畫學(xué)習(xí)筆記

這段時(shí)間正好要做些動(dòng)畫皿哨,于是把屬性動(dòng)畫重新學(xué)習(xí)了一遍,做些總結(jié)

1. 前言

Android動(dòng)畫分為Frame AnimationTweened Animation 笙蒙,Property Animation
既然已經(jīng)有了前兩種動(dòng)畫罕扎,為什么還要Property Animation聚唐,核心點(diǎn)就是Property Animation是改變對(duì)象的屬性,不僅僅是對(duì)view本身做操作

2. 關(guān)鍵類的使用

  1. ObjectAnimator 動(dòng)畫的執(zhí)行類
  2. ValueAnimator 動(dòng)畫的執(zhí)行類
  3. AnimatorSet 控制一組動(dòng)畫的執(zhí)行
  4. AnimatorInflater 加載屬性動(dòng)畫的xml文件
  5. TypeEvaluator 類型估值腔召,主要用于設(shè)置動(dòng)畫操作屬性的值杆查。
  6. TimeInterpolator 時(shí)間插值,定義動(dòng)畫變化率
  7. LayoutTransition 布局動(dòng)畫臀蛛,為布局的容器設(shè)置動(dòng)畫
  8. ViewPropertyAnimator 為View的動(dòng)畫操作提供一種更加便捷的用法
2.1 ObjectAnimator的使用
ObjectAnimator.ofFloat(view, "translationY", 0f, 500f)
    .setDuration(500)
    .start();

view在0.5秒向下滑動(dòng)500px的效果

2.2 ValueAnimator的使用
ValueAnimator.ofFloat(0f, 1f)
    .setDuration(500)
    .start();

屬性0.5秒的從0變成1
執(zhí)行了好像什么都沒發(fā)生啊亲桦,那我們添加個(gè)監(jiān)聽器看看

ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f);
anim.setDuration(500);
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator valueAnimator) {
        float currentValue = (float) valueAnimator.getAnimatedValue();
        Log.d(TAG, "current value is " + currentValue);
    }
});
anim.start();

日志如圖


確實(shí)在0.5秒內(nèi)打印了(這邊先提一下崖蜜,打印的輸出不是線性的,參見TimeInterpolator 時(shí)間插值)
于是實(shí)現(xiàn)view在0.5秒向下滑動(dòng)500px的效果

ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f);
anim.setTarget(view);
anim.setDuration(500);
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator valueAnimator) {
        float currentValue = (float) valueAnimator.getAnimatedValue();
        view.setTranslationY(currentValue * 500);
    }
});
anim.start();
2.3 AnimatorSet的使用
ObjectAnimator moveIn = ObjectAnimator.ofFloat(view, "translationX", -500f, 0f);
ObjectAnimator rotate = ObjectAnimator.ofFloat(view, "rotation", 0f, 360f);
ObjectAnimator fadeInOut = ObjectAnimator.ofFloat(view, "alpha", 1f, 0f, 1f);
AnimatorSet animSet = new AnimatorSet();
animSet.play(rotate).with(fadeInOut).after(moveIn);animSet.setDuration(5000);
animSet.start();

view先從屏幕外移動(dòng)進(jìn)屏幕客峭,然后開始旋轉(zhuǎn)360度豫领,旋轉(zhuǎn)的同時(shí)進(jìn)行淡入淡出操作

其實(shí)還有更簡單的方式,實(shí)現(xiàn)一個(gè)動(dòng)畫更改多個(gè)效果:使用propertyValuesHolder舔琅,幾個(gè)動(dòng)畫同時(shí)執(zhí)行

PropertyValuesHolder pvhX = PropertyValuesHolder.ofFloat("alpha",1f,0f, 1f);
PropertyValuesHolder pvhY = PropertyValuesHolder.ofFloat("scaleX", 1f, 0, 1f);
PropertyValuesHolder pvhZ = PropertyValuesHolder.ofFloat("scaleY", 1f, 0, 1f);
ObjectAnimator.ofPropertyValuesHolder(view, pvhX, pvhY, pvhZ).setDuration(1000).start();
2.4 AnimatorInflater的使用

加載xml中的屬性動(dòng)畫
在res下建立animator文件夾等恐,然后建立res/animator/alpha.xml

<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="500"
    android:propertyName="alpha"
    android:valueFrom="1.0"
    android:valueTo="0.0"
    android:valueType="floatType" >
</objectAnimator>
Animator anim = AnimatorInflater.loadAnimator(this, R.animator.alpha);
anim.setTarget(view);
anim.start();

view的一個(gè)0.5秒淡出效果

2.5 TypeEvaluator的使用

ValueAnimator.ofFloat()方法就是實(shí)現(xiàn)了初始值與結(jié)束值之間的平滑過度,那么這個(gè)平滑過度是怎么做到的呢备蚓?其實(shí)就是系統(tǒng)內(nèi)置了一個(gè)FloatEvaluator鼠锈,它通過計(jì)算告知?jiǎng)赢嬒到y(tǒng)如何從初始值過度到結(jié)束值

public class FloatEvaluator implements TypeEvaluator {  
    public Object evaluate(float fraction, Object startValue, Object endValue) {  
        float startFloat = ((Number) startValue).floatValue();  
        return startFloat + fraction * (((Number) endValue).floatValue() - startFloat);  
    }  
}  

ValueAnimator中還有一個(gè)ofObject()方法,是用于對(duì)任意對(duì)象進(jìn)行動(dòng)畫操作的

public class PointEvaluator implements TypeEvaluator{  
    @Override  
    public Object evaluate(float fraction, Object startValue, Object endValue) {  
        Point startPoint = (Point) startValue;  
        Point endPoint = (Point) endValue;  
        float x = startPoint.getX() + fraction * (endPoint.getX() - startPoint.getX());  
        float y = startPoint.getY() + fraction * (endPoint.getY() - startPoint.getY());  
        Point point = new Point(x, y);  
        return point;  
    }  
}  

重寫了evaluate()方法

Point point1 = new Point(0, 0);  
Point point2 = new Point(300, 300);  
ValueAnimator anim = ValueAnimator.ofObject(new PointEvaluator(), point1, point2);  
anim.setDuration(5000);  
anim.start();  

通過對(duì)Point對(duì)象進(jìn)行動(dòng)畫操作星著,從而實(shí)現(xiàn)整個(gè)自定義View的動(dòng)畫效果购笆。

2.6 TimeInterpolator的使用
ValueAnimator anim = ObjectAnimator.ofFloat(view, "translationY", 0f, 500f);
anim.setDuration(1000);
anim.setInterpolator(new LinearInterpolator());
anim.start();

設(shè)置了一個(gè)勻速運(yùn)動(dòng)

2.7 LayoutTransition的使用
LayoutTransition transition = new LayoutTransition();  
    transition.setAnimator(LayoutTransition.CHANGE_APPEARING,  
            transition.getAnimator(LayoutTransition.CHANGE_APPEARING));  
    transition.setAnimator(LayoutTransition.APPEARING,  
            null);  
    transition.setAnimator(LayoutTransition.DISAPPEARING,  
            null);  
    transition.setAnimator(LayoutTransition.CHANGE_DISAPPEARING,  
            null);  
    mGridLayout.setLayoutTransition(transition);  

過渡的類型一共有四種:
LayoutTransition.APPEARING 當(dāng)一個(gè)View在ViewGroup中出現(xiàn)時(shí),對(duì)此View設(shè)置的動(dòng)畫
LayoutTransition.CHANGE_APPEARING 當(dāng)一個(gè)View在ViewGroup中出現(xiàn)時(shí)虚循,對(duì)此View對(duì)其他View位置造成影響同欠,對(duì)其他View設(shè)置的動(dòng)畫
LayoutTransition.DISAPPEARING 當(dāng)一個(gè)View在ViewGroup中消失時(shí),對(duì)此View設(shè)置的動(dòng)畫
LayoutTransition.CHANGE_DISAPPEARING 當(dāng)一個(gè)View在ViewGroup中消失時(shí)横缔,對(duì)此View對(duì)其他View位置造成影響铺遂,對(duì)其他View設(shè)置的動(dòng)畫
LayoutTransition.CHANGE 不是由于View出現(xiàn)或消失造成對(duì)其他View位置造成影響,然后對(duì)其他View設(shè)置的動(dòng)畫茎刚。
注意動(dòng)畫到底設(shè)置在誰身上襟锐,此View還是其他View。

2.8 ViewPropertyAnimator
view.animate().x(500).y(500).setDuration(5000)  
        .setInterpolator(new BounceInterpolator());  
2.9 Animator的監(jiān)聽器
anim.addListener(new AnimatorListener() {  
    @Override  
    public void onAnimationStart(Animator animation) {  
    }  
  
    @Override  
    public void onAnimationRepeat(Animator animation) {  
    }  
  
    @Override  
    public void onAnimationEnd(Animator animation) {  
    }  
  
    @Override  
    public void onAnimationCancel(Animator animation) {  
    }  
}); 

可以監(jiān)聽到動(dòng)畫的各種事件膛锭,如果覺得不想用到這么多粮坞,可以用AnimatorListenerAdapter,這個(gè)抽象類有對(duì)AnimatorListener的空實(shí)現(xiàn)初狰,這樣就可以單獨(dú)重寫某個(gè)事件了

anim.addListener(new AnimatorListenerAdapter() {  
});  

3. 關(guān)鍵類的詳解

3.1 ObjectAnimator

上面用了ofFloat
還有ofInt莫杈、ofFloatofObject奢入,這幾個(gè)方法都是設(shè)置動(dòng)畫作用的元素筝闹、作用的屬性,動(dòng)畫開始腥光、結(jié)束关顷、以及中間的任意個(gè)屬性值。
當(dāng)設(shè)置1個(gè)值武福,則為從當(dāng)前屬性開始改變
當(dāng)設(shè)置2個(gè)值议双,則一個(gè)為開始、一個(gè)為結(jié)束
當(dāng)設(shè)置多個(gè)值艘儒,則依次改變
來看看ofFloat的具體實(shí)現(xiàn)聋伦,主要看propertyName參數(shù)

public static ObjectAnimator ofFloat(Object target, String propertyName, float... values) {
    ObjectAnimator anim = new ObjectAnimator(target, propertyName);
    anim.setFloatValues(values);
    return anim;
}
  1. 先看構(gòu)造方法
private ObjectAnimator(Object target, String propertyName) {
    setTarget(target);
    setPropertyName(propertyName);}
public void setPropertyName(@NonNull String propertyName) {
    // mValues could be null if this is being constructed piecemeal. Just record the
    // propertyName to be used later when setValues() is called if so.
    if (mValues != null) {
        PropertyValuesHolder valuesHolder = mValues[0];
        String oldName = valuesHolder.getPropertyName();
        valuesHolder.setPropertyName(propertyName);
        mValuesMap.remove(oldName);
        mValuesMap.put(propertyName, valuesHolder);
    }
    mPropertyName = propertyName;
    // New property/values/target should cause re-initialization prior to starting
    mInitialized = false;
}

mValuesMap.put(propertyName, valuesHolder);于是存入了一個(gè)map,key是propertyName界睁,value是存有propertyName的PropertyValuesHolder

  1. 再看anim.setFloatValues(values);
@Overridepublic void setFloatValues(float... values) {
    if (mValues == null || mValues.length == 0) {
        // No values yet - this animator is being constructed piecemeal. Init the values with
        // whatever the current propertyName is
        if (mProperty != null) {
            setValues(PropertyValuesHolder.ofFloat(mProperty, values));
        } else {
            setValues(PropertyValuesHolder.ofFloat(mPropertyName, values));
        }
    } else {
        super.setFloatValues(values);
    }
}
public static PropertyValuesHolder ofFloat(String propertyName, float... values) {
    return new FloatPropertyValuesHolder(propertyName, values);}
public FloatPropertyValuesHolder(String propertyName, float... values) {
    super(propertyName);
    setFloatValues(values);
}
@Overridepublic void setFloatValues(float... values) {
    super.setFloatValues(values);
    mFloatKeyframes = (Keyframes.FloatKeyframes) mKeyframes;
}
public void setFloatValues(float... values) {
    mValueType = float.class;
    mKeyframes = KeyframeSet.ofFloat(values);
}

這里有個(gè)KeyframeSet觉增,是Keyframe的集合,而Keyframe叫做關(guān)鍵幀翻斟,為一個(gè)動(dòng)畫保存time/value(時(shí)間與值)對(duì)逾礁。再看KeyframeSet.ofFloat(values)

public static KeyframeSet ofFloat(float... values) {
    boolean badValue = false;
    int numKeyframes = values.length;
    FloatKeyframe keyframes[] = new FloatKeyframe[Math.max(numKeyframes,2)];
    if (numKeyframes == 1) {
        keyframes[0] = (FloatKeyframe) Keyframe.ofFloat(0f);
        keyframes[1] = (FloatKeyframe) Keyframe.ofFloat(1f, values[0]);
        if (Float.isNaN(values[0])) {
            badValue = true;
        }
    } else {
        keyframes[0] = (FloatKeyframe) Keyframe.ofFloat(0f, values[0]);
        for (int i = 1; i < numKeyframes; ++i) {
            keyframes[i] =
                    (FloatKeyframe) Keyframe.ofFloat((float) i / (numKeyframes - 1), values[i]);
            if (Float.isNaN(values[i])) {
                badValue = true;
            }
        }
    }
    if (badValue) {
        Log.w("Animator", "Bad value (NaN) in float animator");
    }
    return new FloatKeyframeSet(keyframes);
}
public static Keyframe ofFloat(float fraction) {
    return new FloatKeyframe(fraction);
}
public static Keyframe ofFloat(float fraction, float value) {
    return new FloatKeyframe(fraction, value);
}

value被拆分成了許多時(shí)間片fraction
上面都是存儲(chǔ)設(shè)置,這邊還有個(gè)疑問访惜,那就是我設(shè)置的propertyName是如何利用的呢嘹履,我們往下看

因?yàn)?code>ObjectAnimator extends ValueAnimator,我們來看ValueAnimatorstart()函數(shù)

@Overridepublic void start() {
    start(false);
}
private void start(boolean playBackwards) {
    if (Looper.myLooper() == null) {
        throw new AndroidRuntimeException("Animators may only be run on Looper threads");
    }
    mReversing = playBackwards;
    mPlayingBackwards = playBackwards;
    if (playBackwards && mSeekFraction != -1) {
        if (mSeekFraction == 0 && mCurrentIteration == 0) {
            // special case: reversing from seek-to-0 should act as if not seeked at all            mSeekFraction = 0;
        } else if (mRepeatCount == INFINITE) {
            mSeekFraction = 1 - (mSeekFraction % 1);
        } else {
            mSeekFraction = 1 + mRepeatCount - (mCurrentIteration + mSeekFraction);
        }
        mCurrentIteration = (int) mSeekFraction;
        mSeekFraction = mSeekFraction % 1;
    }
    if (mCurrentIteration > 0 && mRepeatMode == REVERSE &&
            (mCurrentIteration < (mRepeatCount + 1) || mRepeatCount == INFINITE)) {
        // if we were seeked to some other iteration in a reversing animator,
        // figure out the correct direction to start playing based on the iteration
        if (playBackwards) {
            mPlayingBackwards = (mCurrentIteration % 2) == 0;
        } else {
            mPlayingBackwards = (mCurrentIteration % 2) != 0;
        }
    }
    int prevPlayingState = mPlayingState;
    mPlayingState = STOPPED;
    mStarted = true;
    mStartedDelay = false;
    mPaused = false;
    updateScaledDuration();
 // in case the scale factor has changed since creation time
    AnimationHandler animationHandler = getOrCreateAnimationHandler();
    animationHandler.mPendingAnimations.add(this);
    if (mStartDelay == 0) {
        // This sets the initial value of the animation, prior to actually starting it running
        if (prevPlayingState != SEEKED) {
            setCurrentPlayTime(0);
        }
        mPlayingState = STOPPED;
        mRunning = true;
        notifyStartListeners();
    }
    animationHandler.start();}

setCurrentPlayTime(0)

public void setCurrentPlayTime(long playTime) { 
   float fraction = mUnscaledDuration > 0 ? (float) playTime / mUnscaledDuration : 1; 
   setCurrentFraction(fraction);
}
public void setCurrentFraction(float fraction) {
    ...
    animateValue(fraction);
}

我們?cè)诳椿?code>ObjectAnimator中

@Overridevoid animateValue(float fraction) {
    final Object target = getTarget();
    if (mTarget != null && target == null) {
        // We lost the target reference, cancel and clean up.
        cancel();
        return;
    }
    super.animateValue(fraction);
    int numValues = mValues.length;
    for (int i = 0; i < numValues; ++i) {
        mValues[i].setAnimatedValue(target);
    }
}

PropertyValuesHolder[] mValues我們?cè)偃?code>PropertyValuesHolder中看

Method mSetter = null;
void setAnimatedValue(Object target) {
    if (mProperty != null) {
        mProperty.set(target, getAnimatedValue());
    }
    if (mSetter != null) {
        try {
            mTmpValueArray[0] = getAnimatedValue();
            mSetter.invoke(target, mTmpValueArray);
        } catch (InvocationTargetException e) {
            Log.e("PropertyValuesHolder", e.toString());
        } catch (IllegalAccessException e) {
            Log.e("PropertyValuesHolder", e.toString());
        }
    }
}

因此ObjectAnimator內(nèi)部的工作機(jī)制并不是直接對(duì)我們傳入的屬性名進(jìn)行操作的债热,而是會(huì)去尋找這個(gè)屬性名對(duì)應(yīng)的get和set方法

3.2 TimeInterpolator
public interface TimeInterpolator {
    /**
     * Maps a value representing the elapsed fraction of an animation to a value that represents
     * the interpolated fraction. This interpolated value is then multiplied by the change in
     * value of an animation to derive the animated value at the current elapsed animation time.
     *
     * @param input A value between 0 and 1.0 indicating our current point
     *        in the animation where 0 represents the start and 1.0 represents
     *        the end
     * @return The interpolation value. This value can be more than 1.0 for
     *         interpolators which overshoot their targets, or less than 0 for
     *         interpolators that undershoot their targets.
     */
    float getInterpolation(float input);
}

getInterpolation()方法中接收一個(gè)input參數(shù)砾嫉,這個(gè)參數(shù)的值會(huì)隨著動(dòng)畫的運(yùn)行而不斷變化,不過它的變化是非常有規(guī)律的窒篱,就是根據(jù)設(shè)定的動(dòng)畫時(shí)長勻速增加焕刮,變化范圍是0到1。也就是說當(dāng)動(dòng)畫一開始的時(shí)候input的值是0墙杯,到動(dòng)畫結(jié)束的時(shí)候input的值是1配并,而中間的值則是隨著動(dòng)畫運(yùn)行的時(shí)長在0到1之間變化的。
而input的值決定了fraction的值高镐。input的值是由系統(tǒng)經(jīng)過計(jì)算后傳入到getInterpolation()方法中的溉旋,然后我們可以自己實(shí)現(xiàn)getInterpolation()方法中的算法,根據(jù)input的值來計(jì)算出一個(gè)返回值嫉髓,而這個(gè)返回值就是fraction了观腊。

參考:
Android 屬性動(dòng)畫(Property Animation) 完全解析 (上)
Android 屬性動(dòng)畫(Property Animation) 完全解析 (下)
Android 屬性動(dòng)畫 源碼解析 深入了解其內(nèi)部實(shí)現(xiàn)
Android屬性動(dòng)畫完全解析(上)
Android屬性動(dòng)畫完全解析(中)
Android屬性動(dòng)畫完全解析(下)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市算行,隨后出現(xiàn)的幾起案子恕沫,更是在濱河造成了極大的恐慌,老刑警劉巖纱意,帶你破解...
    沈念sama閱讀 219,539評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件婶溯,死亡現(xiàn)場離奇詭異,居然都是意外死亡偷霉,警方通過查閱死者的電腦和手機(jī)迄委,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來类少,“玉大人叙身,你說我怎么就攤上這事×蚰” “怎么了信轿?”我有些...
    開封第一講書人閱讀 165,871評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵晃痴,是天一觀的道長。 經(jīng)常有香客問我财忽,道長倘核,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,963評(píng)論 1 295
  • 正文 為了忘掉前任即彪,我火速辦了婚禮紧唱,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘隶校。我一直安慰自己漏益,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評(píng)論 6 393
  • 文/花漫 我一把揭開白布深胳。 她就那樣靜靜地躺著绰疤,像睡著了一般。 火紅的嫁衣襯著肌膚如雪舞终。 梳的紋絲不亂的頭發(fā)上峦睡,一...
    開封第一講書人閱讀 51,763評(píng)論 1 307
  • 那天,我揣著相機(jī)與錄音权埠,去河邊找鬼榨了。 笑死,一個(gè)胖子當(dāng)著我的面吹牛攘蔽,可吹牛的內(nèi)容都是我干的龙屉。 我是一名探鬼主播,決...
    沈念sama閱讀 40,468評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼满俗,長吁一口氣:“原來是場噩夢啊……” “哼转捕!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起唆垃,我...
    開封第一講書人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤五芝,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后辕万,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體枢步,經(jīng)...
    沈念sama閱讀 45,850評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評(píng)論 3 338
  • 正文 我和宋清朗相戀三年渐尿,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了醉途。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,144評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡砖茸,死狀恐怖隘擎,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情凉夯,我是刑警寧澤货葬,帶...
    沈念sama閱讀 35,823評(píng)論 5 346
  • 正文 年R本政府宣布采幌,位于F島的核電站,受9級(jí)特大地震影響震桶,放射性物質(zhì)發(fā)生泄漏休傍。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評(píng)論 3 331
  • 文/蒙蒙 一尼夺、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧炒瘸,春花似錦淤堵、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至隘截,卻和暖如春扎阶,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背婶芭。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評(píng)論 1 272
  • 我被黑心中介騙來泰國打工东臀, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人犀农。 一個(gè)月前我還...
    沈念sama閱讀 48,415評(píng)論 3 373
  • 正文 我出身青樓惰赋,卻偏偏與公主長得像,于是被迫代替她去往敵國和親呵哨。 傳聞我的和親對(duì)象是個(gè)殘疾皇子赁濒,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評(píng)論 2 355

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