Android UI-自定义Drawable(一)

概述

有了前面几篇博客的预备知识,现在就可以来学习下自定义Drawble了。这篇主要是介绍一个开源项目的自定义Drawble的实现,主要是没有看到效果无法讲清楚原理。下一篇再介绍原理。

开源项目

开元项目地址
<a>https://github.com/dinuscxj/LoadingDrawable</a>
项目效果


CircleRotateDrawable.gif

这篇会介绍右上角的代码,注释都会写在代码中。先来看下XML中的入口,自定义了一个View。

        <app.dinus.com.loadingdrawable.LoadingView
            android:id="@+id/material_view"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:background="#ff2a8cc8"
            app:loading_renderer="MaterialLoadingRenderer"/>

看下自定义的View,创建了Drawable和Render,这是是先动画效果最终要的类。其中Drawble是android自带的,BitmapDrawble就是其子类,主要是绘制的用处。Render是作者自定义的。用来实现效果。

public class LoadingView extends ImageView {
    private LoadingDrawable mLoadingDrawable;

    public LoadingView(Context context) {
        super(context);
    }

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

    private void initAttrs(Context context, AttributeSet attrs) {
        try {
            TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.LoadingView);
            int loadingRendererId = ta.getInt(R.styleable.LoadingView_loading_renderer, 0);
            // 创建一个Render,这个是动画的主要实现类
            LoadingRenderer loadingRenderer = LoadingRendererFactory.createLoadingRenderer(context, loadingRendererId);
            // 创建Drawable,并关联Drawable
            setLoadingRenderer(loadingRenderer);
            ta.recycle();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void setLoadingRenderer(LoadingRenderer loadingRenderer) {
        mLoadingDrawable = new LoadingDrawable(loadingRenderer);
        setImageDrawable(mLoadingDrawable);
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        startAnimation();
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        stopAnimation();
    }

    @Override
    protected void onVisibilityChanged(View changedView, int visibility) {
        super.onVisibilityChanged(changedView, visibility);
        if (visibility == View.VISIBLE) {
            startAnimation();
        } else {
            stopAnimation();
        }
    }

    // 让Drawable开始冻哈
    private void startAnimation() {
        if (mLoadingDrawable != null) {
            mLoadingDrawable.start();
        }
    }
    
    // 让Drawbale停止动画
    private void stopAnimation() {
        if (mLoadingDrawable != null) {
            mLoadingDrawable.stop();
        }
    }
}

看下Drawble,这个类代码比较少,继承了Drawable,实现了Animatable接口。然后就是重写了一些方法。mCallback是Render通知Drawble重新绘制用的。start()和stop()方法中可以看到,实现动画效果的是Render。

public class LoadingDrawable extends Drawable implements Animatable {
    private final LoadingRenderer mLoadingRender;

    // 传递给Render的回调
    private final Callback mCallback = new Callback() {
        @Override
        public void invalidateDrawable(Drawable d) {
            invalidateSelf();
        }

        @Override
        public void scheduleDrawable(Drawable d, Runnable what, long when) {
            scheduleSelf(what, when);
        }

        @Override
        public void unscheduleDrawable(Drawable d, Runnable what) {
            unscheduleSelf(what);
        }
    };

    public LoadingDrawable(LoadingRenderer loadingRender) {
        this.mLoadingRender = loadingRender;
        // 设置回调
        this.mLoadingRender.setCallback(mCallback);
    }

    @Override
    protected void onBoundsChange(Rect bounds) {
        super.onBoundsChange(bounds);
        // 改变绘制区域
        this.mLoadingRender.setBounds(bounds);
    }

    @Override
    public void draw(Canvas canvas) {
        if (!getBounds().isEmpty()) {
            // 调用Render的绘制方法
            this.mLoadingRender.draw(canvas);
        }
    }

    @Override
    public void setAlpha(int alpha) {
        // 设置透明度
        this.mLoadingRender.setAlpha(alpha);
    }

    @Override
    public void setColorFilter(ColorFilter cf) {
        // 设置过滤器
        this.mLoadingRender.setColorFilter(cf);
    }

    // 获取不透明策略
    @Override
    public int getOpacity() {
        return PixelFormat.TRANSLUCENT;
    }

    // 开始
    @Override
    public void start() {
        this.mLoadingRender.start();
    }

    // 结束
    @Override
    public void stop() {
        this.mLoadingRender.stop();
    }

    // 是否在运行
    @Override
    public boolean isRunning() {
        return this.mLoadingRender.isRunning();
    }

    // 获取默认的高度
    @Override
    public int getIntrinsicHeight() {
        return (int) this.mLoadingRender.mHeight;
    }

    // 获取默认的宽度
    @Override
    public int getIntrinsicWidth() {
        return (int) this.mLoadingRender.mWidth;
    }
}

看下Render,这是一个抽象类。实现了主要的逻辑,就是创建了属性动画ValueAnimator ,实现开始和结束属性动画的方法。mAnimatorUpdateListener实现了属性动画的动画进度的监听。但是方法computeRender是在子类中去实现的。

// 所有着色器的父类
public abstract class LoadingRenderer {
    // 动画持续时间
    private static final long ANIMATION_DURATION = 1333;
    // 默认图形的大小
    private static final float DEFAULT_SIZE = 56.0f;

    // 插值器的监听器
    private final ValueAnimator.AnimatorUpdateListener mAnimatorUpdateListener
            = new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            // 计算,子类实现
            computeRender((float) animation.getAnimatedValue());
            // 通知Drawble重新绘制
            invalidateSelf();
        }
    };

    /**
     * Whenever {@link LoadingDrawable} boundary changes mBounds will be updated.
     * More details you can see {@link LoadingDrawable#onBoundsChange(Rect)}
     */
    protected final Rect mBounds = new Rect();

    // 回调,通知Drawable
    private Drawable.Callback mCallback;
    // 属性动画,用来控制动画的进度
    private ValueAnimator mRenderAnimator;
    // 动画时长
    protected long mDuration;
    // 长宽
    protected float mWidth;
    protected float mHeight;

    public LoadingRenderer(Context context) {
        // 初始化参数
        initParams(context);
        // 设置动画
        setupAnimators();
    }

    @Deprecated
    protected void draw(Canvas canvas, Rect bounds) {
    }

    // 子类去实现
    protected void draw(Canvas canvas) {
        draw(canvas, mBounds);
    }
    // 子类去实现
    protected abstract void computeRender(float renderProgress);
    // 子类去实现
    protected abstract void setAlpha(int alpha);
    // 子类去实现
    protected abstract void setColorFilter(ColorFilter cf);

    protected abstract void reset();
    // 设置监听
    protected void addRenderListener(Animator.AnimatorListener animatorListener) {
        mRenderAnimator.addListener(animatorListener);
    }

    // 开始属性动画
    void start() {
        reset();
        mRenderAnimator.addUpdateListener(mAnimatorUpdateListener);

        mRenderAnimator.setRepeatCount(ValueAnimator.INFINITE);
        mRenderAnimator.setDuration(mDuration);
        mRenderAnimator.start();
    }

    // 结束属性动画
    void stop() {
        // if I just call mRenderAnimator.end(),
        // it will always call the method onAnimationUpdate(ValueAnimator animation)
        // why ? if you know why please send email to me (dinus_developer@163.com)
        mRenderAnimator.removeUpdateListener(mAnimatorUpdateListener);

        mRenderAnimator.setRepeatCount(0);
        mRenderAnimator.setDuration(0);
        mRenderAnimator.end();
    }

    boolean isRunning() {
        return mRenderAnimator.isRunning();
    }

    // 设置与Drawbale相关的回调
    void setCallback(Drawable.Callback callback) {
        this.mCallback = callback;
    }

    // 当bounds改变时,改变render的bounds
    void setBounds(Rect bounds) {
        mBounds.set(bounds);
    }

    private void initParams(Context context) {
        // 设置长宽
        mWidth = DensityUtil.dip2px(context, DEFAULT_SIZE);
        mHeight = DensityUtil.dip2px(context, DEFAULT_SIZE);
        // 1.333秒
        mDuration = ANIMATION_DURATION;
    }

    // 初始化属性动画
    private void setupAnimators() {
        mRenderAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
        mRenderAnimator.setRepeatCount(Animation.INFINITE);
        mRenderAnimator.setRepeatMode(Animation.RESTART);
        mRenderAnimator.setDuration(mDuration);
        //fuck you! the default interpolator is AccelerateDecelerateInterpolator
        mRenderAnimator.setInterpolator(new LinearInterpolator());
        mRenderAnimator.addUpdateListener(mAnimatorUpdateListener);
    }

    // 调用Drawbale中的invalidate
    private void invalidateSelf() {
        mCallback.invalidateDrawable(null);
    }
}

看下具体实现类MaterialLoadingRenderer。 draw方法中的canvas.drawArc(mTempBounds, mStartDegrees, mSwipeDegrees, false, mPaint);用绘制圆弧的方法实现了旋转的效果。逻辑是这样,首先根据属性动画的Update监听返回fraction值,然后根据这个值计算出圆弧的动画策略。如果进度在50%下,那么圆弧的头前进。如果大于50%,那么圆弧的尾前进。同时还自带一个缓慢旋转的效果。等到执行了一个周期(这里是5次旋转回到起点)就重新开始。还会在每一次动画效果中改变颜色。每一次计算后,将通知Drawble去重新绘制,随后会调用draw方法绘制。

public class MaterialLoadingRenderer extends LoadingRenderer {
    private static final Interpolator MATERIAL_INTERPOLATOR = new FastOutSlowInInterpolator();

    // 360
    private static final int DEGREE_360 = 360;
    // 循环周期里有5此swipe
    private static final int NUM_POINTS = 5;

    // 一次swipe的角度
    private static final float MAX_SWIPE_DEGREES = 0.8f * DEGREE_360;
    // 一个周期总的角度
    private static final float FULL_GROUP_ROTATION = 3.0f * DEGREE_360;
    // 进度的80%
    private static final float COLOR_START_DELAY_OFFSET = 0.8f;
    // 进度的100%
    private static final float END_TRIM_DURATION_OFFSET = 1.0f;
    // 进度的50%
    private static final float START_TRIM_DURATION_OFFSET = 0.5f;

    // 半径
    private static final float DEFAULT_CENTER_RADIUS = 12.5f;
    // 圈的宽度
    private static final float DEFAULT_STROKE_WIDTH = 2.5f;

    // 预设的颜色
    private static final int[] DEFAULT_COLORS = new int[]{
            Color.RED, Color.GREEN, Color.BLUE
    };

    // 画笔
    private final Paint mPaint = new Paint();
    // 绘制区域的矩形
    private final RectF mTempBounds = new RectF();

    // 设置动画的监听
    private final Animator.AnimatorListener mAnimatorListener = new AnimatorListenerAdapter() {
        @Override
        public void onAnimationRepeat(Animator animator) {
            super.onAnimationRepeat(animator);
            // 重新设置初始值(mOriginEndDegrees,mOriginStartDegrees)
            storeOriginals();
            // 获取颜色数组的下一个颜色
            goToNextColor();
            // 重置开始值
            mStartDegrees = mEndDegrees;
            // 重置开始次数
            mRotationCount = (mRotationCount + 1) % (NUM_POINTS);
        }

        @Override
        public void onAnimationStart(Animator animation) {
            super.onAnimationStart(animation);
            // 开始时设置次数为0
            mRotationCount = 0;
        }
    };

    // 颜色数组
    private int[] mColors;
    // 当前颜色的index
    private int mColorIndex;
    // 当前颜色
    private int mCurrentColor;
    // 需要拓展或者搜索的宽度
    private float mStrokeInset;

    // 一个周期内循环的次数,如再次回到起点需要5此次
    private float mRotationCount;
    // 在周期循环的基础上,还要加上不停的转动
    private float mGroupRotation;

    // 结束的角度
    private float mEndDegrees;
    // 开始的角度
    private float mStartDegrees;
    // 结束-开始
    private float mSwipeDegrees;
    // 初始的结束角度
    private float mOriginEndDegrees;
    // 初始的开始角度
    private float mOriginStartDegrees;
    // 圈的粗细
    private float mStrokeWidth;
    // 内径
    private float mCenterRadius;

    private MaterialLoadingRenderer(Context context) {
        super(context);
        // 初始化参数
        init(context);
        // 初始化画笔和绘制模式
        setupPaint();
        // 设置监听
        addRenderListener(mAnimatorListener);
    }

    private void init(Context context) {
        // 从dp变换成px
        mStrokeWidth = DensityUtil.dip2px(context, DEFAULT_STROKE_WIDTH);
        mCenterRadius = DensityUtil.dip2px(context, DEFAULT_CENTER_RADIUS);
        // 默认颜色数组
        mColors = DEFAULT_COLORS;
        // 设置数组中为0的颜色
        setColorIndex(0);
        // 计算需要扩大或者收缩的宽度
        initStrokeInset(mWidth, mHeight);
    }

    private void setupPaint() {
        mPaint.setAntiAlias(true);
        mPaint.setStrokeWidth(mStrokeWidth);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeCap(Paint.Cap.ROUND);
    }

    @Override
    protected void draw(Canvas canvas) {
        // 将Canvas当前状态保存在堆栈
        int saveCount = canvas.save();
        // 设置绘制区域
        mTempBounds.set(mBounds);
        // 设置需要扩大或者收缩绘制区域
        mTempBounds.inset(mStrokeInset, mStrokeInset);
        // 画笔旋转一定角度,这样看上去会像是在转动
        canvas.rotate(mGroupRotation, mTempBounds.centerX(), mTempBounds.centerY());
        // 如果swipe角度不为0,绘制
        if (mSwipeDegrees != 0) {
            mPaint.setColor(mCurrentColor);
            canvas.drawArc(mTempBounds, mStartDegrees, mSwipeDegrees, false, mPaint);
        }
        // 恢复为之前堆栈保存的Canvas状态,即旋转前的状态
        canvas.restoreToCount(saveCount);
    }

    // 根据属性动画的Update来绘制
    @Override
    protected void computeRender(float renderProgress) {
        // 刷新
        updateRingColor(renderProgress);
        // Moving the start trim only occurs in the first 50% of a single ring animation
        // 如果进度还小于50%,那么改变开始的角度
        if (renderProgress <= START_TRIM_DURATION_OFFSET) {
            // 计算百分比
            float startTrimProgress = renderProgress / START_TRIM_DURATION_OFFSET;
            // 开始角度+一次swipe的最大角度*快出慢进插值器计算出来的值
            mStartDegrees = mOriginStartDegrees + MAX_SWIPE_DEGREES
                    * MATERIAL_INTERPOLATOR.getInterpolation(startTrimProgress);
        }

        // Moving the end trim starts after 50% of a single ring animation completes
        // 如果进度还大于50%,那么改变结束的角度
        if (renderProgress > START_TRIM_DURATION_OFFSET) {
            float endTrimProgress = (renderProgress - START_TRIM_DURATION_OFFSET)
                    / (END_TRIM_DURATION_OFFSET - START_TRIM_DURATION_OFFSET);
            mEndDegrees = mOriginEndDegrees + MAX_SWIPE_DEGREES
                    * MATERIAL_INTERPOLATOR.getInterpolation(endTrimProgress);
        }
        // 计算swipe的值
        if (Math.abs(mEndDegrees - mStartDegrees) > 0) {
            mSwipeDegrees = mEndDegrees - mStartDegrees;
        }
        // 计算滚动角度
        mGroupRotation = ((FULL_GROUP_ROTATION / NUM_POINTS) * renderProgress)
                + (FULL_GROUP_ROTATION * (mRotationCount / NUM_POINTS));
    }

    @Override
    protected void setAlpha(int alpha) {
        // 设置透明度
        mPaint.setAlpha(alpha);
    }

    @Override
    protected void setColorFilter(ColorFilter cf) {
        // 设置颜色过滤器
        mPaint.setColorFilter(cf);
    }

    @Override
    protected void reset() {
        // 重置
        resetOriginals();
    }

    private void setColorIndex(int index) {
        // 设置当前颜色
        mColorIndex = index;
        mCurrentColor = mColors[mColorIndex];
    }

    private int getNextColor() {
        // 获取下一个颜色
        return mColors[getNextColorIndex()];
    }

    private int getNextColorIndex() {
        return (mColorIndex + 1) % (mColors.length);
    }

    private void goToNextColor() {
        // 跳到下一个颜色
        setColorIndex(getNextColorIndex());
    }

    private void initStrokeInset(float width, float height) {
        // 长宽中的最小值
        float minSize = Math.min(width, height);
        // 从长宽最小值中计算需要插入的宽度
        float strokeInset = minSize / 2.0f - mCenterRadius;
        // 从画笔粗细中计算
        float minStrokeInset = (float) Math.ceil(mStrokeWidth / 2.0f);
        // 取较大值
        mStrokeInset = strokeInset < minStrokeInset ? minStrokeInset : strokeInset;
    }

    private void storeOriginals() {
        mOriginEndDegrees = mEndDegrees;
        mOriginStartDegrees = mEndDegrees;
    }

    private void resetOriginals() {
        mOriginEndDegrees = 0;
        mOriginStartDegrees = 0;

        mEndDegrees = 0;
        mStartDegrees = 0;
    }

    private int getStartingColor() {
        return mColors[mColorIndex];
    }

    private void updateRingColor(float interpolatedTime) {
        // 如果进度已经超过0.8,那么计算新的颜色
        if (interpolatedTime > COLOR_START_DELAY_OFFSET) {
            // 在剩余的20%中重新计算百分比,然后从当前颜色渐变到下一个颜色
            mCurrentColor = evaluateColorChange((interpolatedTime - COLOR_START_DELAY_OFFSET)
                    / (1.0f - COLOR_START_DELAY_OFFSET), getStartingColor(), getNextColor());
        }
    }

    // 计算过度颜色
    private int evaluateColorChange(float fraction, int startValue, int endValue) {
        int startA = (startValue >> 24) & 0xff;
        int startR = (startValue >> 16) & 0xff;
        int startG = (startValue >> 8) & 0xff;
        int startB = startValue & 0xff;

        int endA = (endValue >> 24) & 0xff;
        int endR = (endValue >> 16) & 0xff;
        int endG = (endValue >> 8) & 0xff;
        int endB = endValue & 0xff;

        return ((startA + (int) (fraction * (endA - startA))) << 24)
                | ((startR + (int) (fraction * (endR - startR))) << 16)
                | ((startG + (int) (fraction * (endG - startG))) << 8)
                | ((startB + (int) (fraction * (endB - startB))));
    }

    private void apply(Builder builder) {
        this.mWidth = builder.mWidth > 0 ? builder.mWidth : this.mWidth;
        this.mHeight = builder.mHeight > 0 ? builder.mHeight : this.mHeight;
        this.mStrokeWidth = builder.mStrokeWidth > 0 ? builder.mStrokeWidth : this.mStrokeWidth;
        this.mCenterRadius = builder.mCenterRadius > 0 ? builder.mCenterRadius : this.mCenterRadius;

        this.mDuration = builder.mDuration > 0 ? builder.mDuration : this.mDuration;

        this.mColors = builder.mColors != null && builder.mColors.length > 0 ? builder.mColors : this.mColors;

        setColorIndex(0);
        setupPaint();
        initStrokeInset(this.mWidth, this.mHeight);
    }

    public static class Builder {
        private Context mContext;

        private int mWidth;
        private int mHeight;
        private int mStrokeWidth;
        private int mCenterRadius;

        private int mDuration;

        private int[] mColors;

        public Builder(Context mContext) {
            this.mContext = mContext;
        }

        public Builder setWidth(int width) {
            this.mWidth = width;
            return this;
        }

        public Builder setHeight(int height) {
            this.mHeight = height;
            return this;
        }

        public Builder setStrokeWidth(int strokeWidth) {
            this.mStrokeWidth = strokeWidth;
            return this;
        }

        public Builder setCenterRadius(int centerRadius) {
            this.mCenterRadius = centerRadius;
            return this;
        }

        public Builder setDuration(int duration) {
            this.mDuration = duration;
            return this;
        }

        public Builder setColors(int[] colors) {
            this.mColors = colors;
            return this;
        }

        public MaterialLoadingRenderer build() {
            MaterialLoadingRenderer loadingRenderer = new MaterialLoadingRenderer(mContext);
            loadingRenderer.apply(this);
            return loadingRenderer;
        }
    }
}

总结

这篇主要是一个开源框架的讲解,介绍了Drawable的实现。如果可能的话,那么下一篇将介绍自定义Drawable的实现步骤。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容