Android 滚轮选择器的实现详解

简介

最近用一个日期选择控件,感觉官方的DatePicker操作有点复杂,而且不同的Android版本样式也都不一样。后来发现小米日历的日期选择控件蛮好看的,于是自己尝试仿写一个,感觉效果还不错。GitHub: https://github.com/ycuwq/DatePicker

效果图:


预览1

预览2

功能分析

  • 滚轮:首先绘制一列文本,然后添加一个偏移量,在onDraw中根据手指滑动,改变偏移量并重新绘制这一列文本,这样就实现了滑动的效果。
  • Fling:这个应该很常见了,用VelocityTrackerScroller来实现。
  • 循环滚动:当滚动超过数据集的大小后,从头继续获取数据即可。
  • 幕布效果:在中心区域绘制一个矩形。
  • 字体颜色渐变:
    • 从中心到两边,逐渐将Paint的透明度变小。
    • 从中心相邻项到中心,字体颜色渐变。
    • 中心选项文字变大: 从中心相邻项到中心,字体大小渐变。
  • 指示器文字,在中间的Item后边绘制一个文字。

到这里,所有的功能点的思路大概就清晰了。

实现方法

测量控件大小

这里主要是测量wrap_content模式的大小。
首先,要确定单个item的文字的宽高。代码如下:

public void computeTextSize() {
    mTextMaxWidth = mTextMaxHeight = 0;
    if (mDataList.size() == 0) {    
        return;
    }

    //这里使用最大的,防止文字大小超过布局大小。
    mPaint.setTextSize(mSelectedItemTextSize > mTextSize ? mSelectedItemTextSize : mTextSize);

    if (!TextUtils.isEmpty(mItemMaximumWidthText)) {
        mTextMaxWidth = (int) mPaint.measureText(mItemMaximumWidthText);
    } else {
        mTextMaxWidth = (int) mPaint.measureText(mDataList.get(0).toString());
    }
    Paint.FontMetrics metrics = mPaint.getFontMetrics();
    mTextMaxHeight = (int) (metrics.bottom - metrics.top);
}

然后确定布局的大小,布局的宽度就等于测量的mTextMaxWidth,高度为测量的mTextMaxHeight * itemCount。这里宽高中可以加入一个额外的Space,要不然文字就会挤到一起,比较难看。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int specWidthSize = MeasureSpec.getSize(widthMeasureSpec);
        int specWidthMode = MeasureSpec.getMode(widthMeasureSpec);
        int specHeightSize = MeasureSpec.getSize(heightMeasureSpec);
        int specHeightMode = MeasureSpec.getMode(heightMeasureSpec);
    
        int width = mTextMaxWidth + mItemWidthSpace;
        int height = (mTextMaxHeight + mItemHeightSpace) * getVisibleItemCount();
    
        width += getPaddingLeft() + getPaddingRight();
        height += getPaddingTop() + getPaddingBottom();
        setMeasuredDimension(measureSize(specWidthMode, specWidthSize, width),
                measureSize(specHeightMode, specHeightSize, height));
    }
    
    private int measureSize(int specMode, int specSize, int size) {
        if (specMode == MeasureSpec.EXACTLY) {
            return specSize;
        } else {
            return Math.min(specSize, size);
        }
    }

滚轮的绘制

一般滚轮被选中的位置都是在中间,所以显示的设置为奇数比较合适。为了方便,定义mHalfVisibleItemCount作为显示的个数的一半,总显示个数为mHalfVisibleItemCount * 2 + 1,这样就可以保证选中的item在正中间

首先最简单的,不考虑滚动的情况,直接绘制一列文字,这里为了方便计算,设置中间的为数据集的第0个,第一个item就为0-mHalfVisibleItemCount,最后一个则为mHalfVisibleItemCount,代码如下:

@Override
protected void onDraw(Canvas canvas) {
    for (int drawDataPos = -mHalfVisibleItemCount; drawDataPos <= mHalfVisibleItemCount; drawDataPos++) {
        if (pos < 0 || pos > mDataList.size() - 1) {
            continue;
        }
        int itemDrawY = mFirstItemDrawY + (drawDataPos + mHalfVisibleItemCount) * mItemHeight;
        T data = mDataList.get(pos);
        canvas.drawText(data.toString(), mFirstItemDrawX, itemDrawY, mPaint);
    }
}

接下来加入滚动,获取手指滑动的值, 然后绘制的时候添加偏移量就好了。

手指滑动,这个应该都懂,这里就不废话了,直接上代码:

@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mLastDownY = (int) event.getY();
            break;
        case MotionEvent.ACTION_MOVE:
            float move = event.getY() - mLastDownY;
            mScrollOffsetY += move;     //滑动的偏移量
            mLastDownY = (int) event.getY();
            invalidate();
            break;
        case MotionEvent.ACTION_UP:
            break;
    }

更改onDraw()的代码。这边有两个问题:

  1. 上下两边要多绘制一个出来,因为在滚动的时候,实际在容器内的item要比原定的item要多一个。
  2. 定位中间向位于数据集的位置,用偏移量 / item的高度即可。由于手指的坐标是以左上角为原点的,这里要注意坐标正负问题。
    代码如下:
@Override
protected void onDraw(Canvas canvas) {
    int drawnSelectedPos = - mScrollOffsetY / mItemHeight;
    for (int drawDataPos = drawnSelectedPos - mHalfVisibleItemCount - 1;drawDataPos <= drawnSelectedPos + mHalfVisibleItemCount + 1; drawDataPos++) {
        if (drawDataPos < 0 || drawDataPos > mDataList.size() - 1) {
            continue;
        }
        int itemDrawY = mFirstItemDrawY + (drawDataPos + mHalfVisibleItemCount) * mItemHeight + mScrollOffsetY;
        T data = mDataList.get(drawDataPos);
        canvas.drawText(data.toString(), mFirstItemDrawX, itemDrawY, mPaint);
    }
}

Fling的效果

先上主要代码:

@Override
public boolean onTouchEvent(MotionEvent event) {
    if (mTracker == null) {
        mTracker = VelocityTracker.obtain();
    }
    mTracker.addMovement(event);
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mTracker.clear();
            mTouchDownY = mLastDownY = (int) event.getY();
            break;
        case MotionEvent.ACTION_MOVE:
            mTouchSlopFlag = false;
            float move = event.getY() - mLastDownY;
            mScrollOffsetY += move;
            mLastDownY = (int) event.getY();
            invalidate();
            break;
        case MotionEvent.ACTION_UP:
            mTracker.computeCurrentVelocity(1000, mMaximumVelocity);
            int velocity = (int) mTracker.getYVelocity();
            mScroller.fling(0, mScrollOffsetY, 0, velocity,
                    0, 0, mMinFlingY, mMaxFlingY);
            mScroller.setFinalY(mScroller.getFinalY() +
                    computeDistanceToEndPoint(mScroller.getFinalY() % mItemHeight));
            mHandler.post(mScrollerRunnable);
            mTracker.recycle();
            mTracker = null;
            break;
    }
    return true;
}

private int computeDistanceToEndPoint(int remainder) {
    if (Math.abs(remainder) > mItemHeight / 2) {
        if (mScrollOffsetY < 0) {
            return -mItemHeight - remainder;
        } else {
            return mItemHeight - remainder;
        }
    } else {
        return -remainder;
    }
}

private Runnable mScrollerRunnable = new Runnable() {
    @Override
    public void run() {
        if (mScroller.computeScrollOffset()) {
            int scrollerCurrY = mScroller.getCurrY();
            mScrollOffsetY = scrollerCurrY;
            postInvalidate();
            mHandler.postDelayed(this, 16);
        }
    }
}

Fling效果的实现主要是用的VelocityTrackerScroller,网上已经有很多资料了,这里就不再说明了。这里主要就是获取Scroller当前滚动的值,然后加入到偏移量mScrollOffsetY后请求重新绘制。

这里有一个finalY的修正计算.当动画停止的时候,停止的位置如果随缘的话,就会经常在停在这种位置:


因为要保证当滑动停止的时候,要保证item正好在中间。也就是这样:


修正的方法就是:滚动的值只能为item高度的整数。这样就能保证,滚动结束中间的item只能在正中间。利用这个原理,上边的手指滑动,也能在手指离开屏幕后来修正位置。

这里还要考虑一个问题,滚动的时候可能会超过给定的数据集的大小,就是当滚动的值超过最后一个数据后,当手指松开后返回到最后一个数据的位置,即下图的效果:


161098a4ab2fdbe1.gif

方法还是在ACTION_UP的时候判断是否超过数据集的大小即可。代码如下:

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    ...
    mMinFlingY = - mItemHeight * (mDataList.size() - 1);
    mMaxFlingY = 0;
}


@Override
public boolean onTouchEvent(MotionEvent event) {
    ...
    
    case MotionEvent.ACTION_UP:
        mTracker.computeCurrentVelocity(1000, mMaximumVelocity);
        int velocity = (int) mTracker.getYVelocity();
        mScroller.fling(0, mScrollOffsetY, 0, velocity,
                0, 0, mMinFlingY, mMaxFlingY);
        mScroller.setFinalY(mScroller.getFinalY() +
                computeDistanceToEndPoint(mScroller.getFinalY() % mItemHeight));
                
        if (mScroller.getFinalY() > mMaxFlingY) {
            mScroller.setFinalY(mMaxFlingY);
        } else if (mScroller.getFinalY() < mMinFlingY) {
            mScroller.setFinalY(mMinFlingY);
        }
    
    ...
}

循环滚动

接下来要考虑循环滚动的问题。

首先 上边的mMinFlingYmMaxFlingY在循环滚动的时候就要设置成Integer的极限值,如下

mMinFlingY = mIsCyclic ? Integer.MIN_VALUE : - mItemHeight * (mDataList.size() - 1);
mMaxFlingY = mIsCyclic ? Integer.MAX_VALUE : 0;

然后考虑绘制的时候取值问题,在上边onDraw()方法的里面有一个安全值判断,如果超过数据集的大小就跳过此条item绘制,代码如下:

@Override
protected void onDraw(Canvas canvas) {
    int drawnSelectedPos = - mScrollOffsetY / mItemHeight;
    for (int drawDataPos = drawnSelectedPos - mHalfVisibleItemCount - 1;drawDataPos <= drawnSelectedPos + mHalfVisibleItemCount + 1; drawDataPos++) {
        if (drawDataPos < 0 || drawDataPos > mDataList.size() - 1) {
            continue;
        }
        ...
    }
}

我们要改动的就是这里。当循环滚动的时候上下滚动的极限都为无穷,所以- mScrollOffsetY / mItemHeight;得到的值应该也在正负无穷之间,我们要把值都映射到数据集中。
假设数据集有10条数据,当前滚动的位置为pos:

  • 当pos>10时,假设当pos = 10时,我们想让其回到第一个数据,对其取余即可:pos % 10
  • 当pos<0时,假设当pos = -1时,应该要展示最后一个数据,对其数据增加正数的修正为:10 + (pos % 10)
    所以上边的代码就可以改变成:
@Override
protected void onDraw(Canvas canvas) {
    int drawnSelectedPos = - mScrollOffsetY / mItemHeight;
    for (int drawDataPos = drawnSelectedPos - mHalfVisibleItemCount - 1;drawDataPos <= drawnSelectedPos + mHalfVisibleItemCount + 1; drawDataPos++) {
        int pos = drawDataPos;
        if  (mIsCyclic) {
            if (pos < 0) {
                pos = mDataList.size() + (pos % mDataList.size());
            }
            if (pos >= mDataList.size()){
                pos = pos % mDataList.size();
            }
        } else {
            if (drawDataPos < 0 || drawDataPos > mDataList.size() - 1) {
                continue;
            }
        }
        ...
    }
}

幕布

这个就比较简单了,在中间画一个矩形就好了,不过要在绘制滚轮之前绘制,否则会遮盖住文字,代码如下:

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    ...
    mDrawnRect.set(getPaddingLeft(), getPaddingTop(),
        getWidth() - getPaddingRight(), getHeight() - getPaddingBottom());
    mSelectedItemRect.set(getPaddingLeft(), mItemHeight * mHalfVisibleItemCount,getWidth() - getPaddingRight(), mItemHeight + mItemHeight * mHalfVisibleItemCount);
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    mPaint.setTextAlign(Paint.Align.CENTER);
    //是否绘制幕布
    if (mIsShowCurtain) {
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setColor(mCurtainColor);
        canvas.drawRect(mSelectedItemRect, mPaint);
    }
    //是否绘制幕布边框
    if (mIsShowCurtainBorder) {
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setColor(mCurtainBorderColor);
        canvas.drawRect(mSelectedItemRect, mPaint);
        canvas.drawRect(mDrawnRect, mPaint);
    }
    ...
}

字体颜色渐变

这个分三个部分:

1. 透明度渐变:

Paint在绘制文字的时候可以设置Alpha来设置透明度,Alpha的比例计算方法:

$$ \frac{绘制点到端点距离}{中心绘制点到端点距离} $$

如下图所示,计算“03”的比例:



主要代码如下:

@Override
protected void onDraw(Canvas canvas) {
    ...
    int drawnSelectedPos = - mScrollOffsetY / mItemHeight;
    for (int drawDataPos = drawnSelectedPos - mHalfVisibleItemCount - 1;drawDataPos <= drawnSelectedPos + mHalfVisibleItemCount + 1; drawDataPos++) {
        int pos = drawDataPos;
        if  (mIsCyclic) {
            if (pos < 0) {
                pos = mDataList.size() + (pos % 10);
            } else {
                pos = pos % mDataList.size();
            }
        } else {
            if (drawDataPos < 0 || drawDataPos > mDataList.size() - 1) {
                continue;
            }
        }
        
        T data = mDataList.get(pos);
        int itemDrawY = mFirstItemDrawY + (drawDataPos + mHalfVisibleItemCount) * mItemHeight + mScrollOffsetY;
        //距离中心的Y轴距离
        int distanceY = Math.abs(mCenterItemDrawnY - itemDrawY);
        
        float alphaRatio;
        if (itemDrawY > mCenterItemDrawnY) {
            alphaRatio = (mDrawnRect.height() - itemDrawY) /
                    (float) (mDrawnRect.height() - (mCenterItemDrawnY));
        } else {
            alphaRatio = itemDrawY / (float) mCenterItemDrawnY;
        }
        mPaint.setAlpha((int) (alphaRatio * 255));
        canvas.drawText(data.toString(), mFirstItemDrawX, itemDrawY, mPaint);
        ...
    }
}

2. 文字颜色渐变

当距离中心绘制点的距离小于一个ItemHeight时,进行文字颜色渐变。
首先需要一个线性颜色渐变的工具,思路就是指定一个开始颜色和结束颜色,传入比例获取颜色。比较简单,直接看代码:

public class LinearGradient {

    private int mStartColor;
    private int mEndColor;
    private int mRedStart;
    private int mBlueStart;
    private int mGreenStart;
    private int mRedEnd;
    private int mBlueEnd;
    private int mGreenEnd;
    
    public LinearGradient(@ColorInt int startColor, @ColorInt int endColor) {
        mStartColor = startColor;
        mEndColor = endColor;
        updateColor();
    }
    
    
    public void setStartColor(@ColorInt int startColor) {
        mStartColor = startColor;
        updateColor();
    }
    
    public void setEndColor(@ColorInt int endColor) {
        mEndColor = endColor;
        updateColor();
    }
    
    private void updateColor() {
        mRedStart = Color.red(mStartColor);
        mBlueStart = Color.blue(mStartColor);
        mGreenStart = Color.green(mStartColor);
        mRedEnd = Color.red(mEndColor);
        mBlueEnd = Color.blue(mEndColor);
        mGreenEnd = Color.green(mEndColor);
    }
    
    public int getColor(float ratio) {
        int red = (int) (mRedStart + ((mRedEnd - mRedStart) * ratio + 0.5));
        int greed = (int) (mGreenStart + ((mGreenEnd - mGreenStart) * ratio + 0.5));
        int blue = (int) (mBlueStart + ((mBlueEnd - mBlueStart) * ratio + 0.5));
        return Color.rgb(red, greed, blue);
    }
}

接下来就是计算文字颜色渐变了,和上边的计算透明度的类似,代码如下:

@Override
protected void onDraw(Canvas canvas) {
    ...
    int drawnSelectedPos = - mScrollOffsetY / mItemHeight;
    for (int drawDataPos = drawnSelectedPos - mHalfVisibleItemCount - 1;drawDataPos <= drawnSelectedPos + mHalfVisibleItemCount + 1; drawDataPos++) {
        ...
        
        T data = mDataList.get(pos);
        int itemDrawY = mFirstItemDrawY + (drawDataPos + mHalfVisibleItemCount) * mItemHeight + mScrollOffsetY;
        //距离中心的Y轴距离
        int distanceY = Math.abs(mCenterItemDrawnY - itemDrawY);
        
        //计算文字颜色渐变
        //文字颜色渐变要在设置透明度上边,否则透明度会被覆盖
        if (distanceY < mItemHeight) {
            float colorRatio = 1 - (distanceY / (float) mItemHeight);
            mPaint.setColor(mLinearGradient.getColor(colorRatio));
        } else {
            mPaint.setColor(mTextColor);
        }
        
        //计算透明度渐变
        float alphaRatio;
        if (itemDrawY > mCenterItemDrawnY) {
            alphaRatio = (mDrawnRect.height() - itemDrawY) /
                    (float) (mDrawnRect.height() - (mCenterItemDrawnY));
        } else {
            alphaRatio = itemDrawY / (float) mCenterItemDrawnY;
        }
        mPaint.setAlpha((int) (alphaRatio * 255));
        canvas.drawText(data.toString(), mFirstItemDrawX, itemDrawY, mPaint);
        ...
    }
}

3.文字大小渐变:

这个和文字颜色渐变实现思路一模一样,变化的项由文字的颜色变为大小。也是在中心位置一个ItemHeigh的距离进行计算,直接上代码:

@Override
protected void onDraw(Canvas canvas) {
    ...
    int drawnSelectedPos = - mScrollOffsetY / mItemHeight;
    for (int drawDataPos = drawnSelectedPos - mHalfVisibleItemCount - 1;drawDataPos <= drawnSelectedPos + mHalfVisibleItemCount + 1; drawDataPos++) {
        ...
        //计算透明度渐变
        float alphaRatio;
        if (itemDrawY > mCenterItemDrawnY) {
            alphaRatio = (mDrawnRect.height() - itemDrawY) /
                    (float) (mDrawnRect.height() - (mCenterItemDrawnY));
        } else {
            alphaRatio = itemDrawY / (float) mCenterItemDrawnY;
        }
        mPaint.setAlpha((int) (alphaRatio * 255));
        
        //靠近中心的Item字体放大
        if (distanceY < mItemHeight) {
            float addedSize = (mItemHeight - distanceY) / (float) mItemHeight * (mSelectedItemTextSize - mTextSize);
            mPaint.setTextSize(mTextSize + addedSize);
        } else {
            mPaint.setTextSize(mTextSize);
        }
        canvas.drawText(data.toString(), mFirstItemDrawX, itemDrawY, mPaint);
        ...
    }
}

指示器文字实现

指示器文字就是直接在中间的item后边绘制一个文字,只需要计算一下要绘制的坐标即可,直接在onDraw()方法的的最后加入就好了。代码如下:

@Override
protected void onDraw(Canvas canvas) {
    ...
    mPaint.setTextAlign(Paint.Align.CENTER);
    int drawnSelectedPos = - mScrollOffsetY / mItemHeight;
    for (int drawDataPos = drawnSelectedPos - mHalfVisibleItemCount - 1;drawDataPos <= drawnSelectedPos + mHalfVisibleItemCount + 1; drawDataPos++) {
        ...
        canvas.drawText(data.toString(), mFirstItemDrawX, itemDrawY, mPaint);
    }
    
    mPaint.setTextAlign(Paint.Align.LEFT);
    canvas.drawText(mIndicatorText, mFirstItemDrawX + mTextMaxWidth / 2, mCenterItemDrawnY, mPaint);
}

小结

到此, 一个滚轮选择器主要功能就实现了。接下来只要完善一些细节部分比如监听器、点击效果等部分,就完成了。接下来根据需要利用滚轮选择器就能很轻松的实现日期选择器,省市选择器等控件。

详细代码请移步我的GitHub: https://github.com/ycuwq/DatePicker
里边还封装了一个DatePicker和DatePicker从下方弹出的Dialog,使用方法请移步GitHub。

最后,非常欢迎到GitHub中提出您的问题或意见。

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

推荐阅读更多精彩内容