自定义地图下方可拖拽列表布局DragUpDownLinearLayout

这个控件是仿制高德地图下面的可拖拽列表栏做的。实现主要就是一个LinearLayout响应用户手势拖拽,有全屏,半屏,和隐藏三个模式。依据拖拽到松手的位置的y坐标占屏幕的百分比来确定对应的模式位置,再利用动画移动到对应的模式位置。
完整的代码我会贴在文末。


DragUpDownLinearlayout.gif

一、确定三个模式的位置

我这里使用的是铺满contentView,占contentView的1/3,和全部在隐藏在下面只留一个拖拽条三个模式。contentView的概念我这里大概讲一下,android的布局是在decorView这个根布局下的,分为titleView和ContentView。
titleView放的是ActionBar等位置,如果设置noActionBar就没有titleView的位置了。
而contentView就是我们平时Activity里面onCreate中setContent的那个ContentView,相当于我们的内容布局的父布局,在这个控件我们计算主要依靠它来完成。
三个模式的height也就是Y坐标值是:

 switch (customMode) {
                    case TOP_MODE:
                        top = topHeight;
                        bottom = getHeight() + topHeight;
                        break;
                    case MIDDLE_MODE:
                        top = middleHeight;
                        bottom = getHeight() + middleHeight;
                        break;
                    case BOTTOM_MODE:
                        int topUp = contentViewHeight - indicatorHeightPx;
                        top = topUp;
                        bottom = getHeight() + topUp;
                        break;
                }

主要看一个top的赋值 这个top就是我们要设给onLayout的参数,控件的顶部的y坐标。

topHeight contentView里面除了这个控件之外 顶部还有其他控件占位置,我们需要加上这个控件的高度,不然会覆盖掉它,例如上面有一个自定义的标题栏没有加入到Toolbar的位置而是放在contentView里面,那么这个情况就需要被考虑。这个值我是由外部初始化的时候计算传入的。

middleHeight 计算方法:

contentViewHeight = ((Activity) getContext()).getWindow().
                    findViewById(Window.ID_ANDROID_CONTENT).getMeasuredHeight();
middleHeight = (contentViewHeight / 3) * 2;

上面说的是1/3但这里写的是2/3是因为android屏幕的Y坐标是向下的,我们需要在1/3的位置就需要让控件向下移动2/3。

**topUp ** 相当于留一个indicatorHeightPx(那个灰色的长条)的位置 其他全部在屏幕下方。

OK,位置的计算就只有这些了。

二、拖拽控件

接下来就是主要的功能拖拽了。
这里需要用到手势类GestureDetector,不熟悉的同学可以去搜索一下看一看,它里面封装了各种手势的触发条件和触发回调,使用起来比自己重写onTouch再分类要有效率的多。它的使用就是在onTouch方法里将参数传递给它:

public boolean onTouch(View view, MotionEvent event) {
        mGestureDetector.onTouchEvent(event);
        return true;
    }

它的实现类:

    @Override
    public boolean onScroll(MotionEvent motionEvent, MotionEvent motionEvent1, float distanceX, float distanceY) {
        int y = (int) motionEvent1.getY();
        // 获取本次移动的距离
        int dy = y - y0;
        int top = getTop();
        int bottom = getBottom();
        if (top <= topHeight && dy < 0) {
            // 高出顶部 则不改变位置防止超出顶部
            return false;
        }
        layout(getLeft(), (top + dy),
                getRight(), (bottom + dy));
        isScrolling = true;
        return false;
    }

    @Override
    public boolean onFling(MotionEvent motionEvent, MotionEvent motionEvent1, float x, float speedY) {
        float v = motionEvent1.getRawY() - rawYDown;
        switch (customMode) {
            case TOP_MODE:
                animator = ObjectAnimator.ofFloat(DragUpDownLinearLayout.this, ANIMATOR_MODE,
                        getTranslationY(), getTranslationY() + (middleHeight - getY()));
                customMode = MIDDLE_MODE;
                break;
            case MIDDLE_MODE:
                if (v > 0) {
                    animator = ObjectAnimator.ofFloat(DragUpDownLinearLayout.this, ANIMATOR_MODE,
                            getTranslationY(), getTranslationY() + contentViewHeight - getY() - indicatorHeightPx);
                    customMode = BOTTOM_MODE;
                } else {
                    animator = ObjectAnimator.ofFloat(DragUpDownLinearLayout.this, ANIMATOR_MODE,
                            getTranslationY(), getTranslationY() - getY() + topHeight);
                    customMode = TOP_MODE;
                }
                break;
            case BOTTOM_MODE:
                animator = ObjectAnimator.ofFloat(DragUpDownLinearLayout.this, ANIMATOR_MODE,
                        getTranslationY(), getTranslationY() + (middleHeight - getY()));
                customMode = MIDDLE_MODE;
                break;
            default:
        }

        animator.setDuration(500);
        animator.start();
        // 动画结束时,将控件的translation偏移量转化为Top值,便于计算
        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                float translationY = getTranslationY();
                setTranslationY(0);
                layout(getLeft(), (int) (getTop() + translationY),
                        getRight(), (int) (getBottom() + translationY));
                animator = null;
            }
        });
        isScrolling = false;
        hasFiling = true;
        return true;
    }

就使用了这两个方法。思路就是在onScroll里面响应拖拽调用layout方法不断修改布局位置,然后结束的时候通常情况下回触发onFiling方法,在这个方法里计算位置开始动画等将控件移动到指定的位置。
还需要注意的是当你慢慢拖拽的时候会触发不了onFiling这个方法 所以我在这里添加了一个hasFiling的标志位去判断onFiling是否调用了,没调用的话在onTouch里面再处理一下:

 @Override
    public boolean onTouch(View view, MotionEvent event) {
        mGestureDetector.onTouchEvent(event);
        // 是否有执行filing
        if (event.getAction() == MotionEvent.ACTION_UP) {
            if (!hasFiling) {
                isScrolling = false;
                // 松手时固定位置 计算占屏幕的百分比
                float yUP = getTop();
                float i = yUP / screenHeight;
                if (i < 0.30) {
                    animator = ObjectAnimator.ofFloat(DragUpDownLinearLayout.this, ANIMATOR_MODE,
                            getTranslationY(), getTranslationY() - getY() + topHeight);
                    customMode = TOP_MODE;
                } else if (i < 0.75) {
                    animator = ObjectAnimator.ofFloat(DragUpDownLinearLayout.this, ANIMATOR_MODE,
                            getTranslationY(), getTranslationY() + (middleHeight - getY()));
                    customMode = MIDDLE_MODE;
                } else {
                    animator = ObjectAnimator.ofFloat(DragUpDownLinearLayout.this, ANIMATOR_MODE,
                            getTranslationY(), getTranslationY() + contentViewHeight - getY() - indicatorHeightPx);
                    customMode = BOTTOM_MODE;
                }
                animator.setDuration(500);
                animator.start();
                // 动画结束时,将控件的translation偏移量转化为Top值,便于计算
                animator.addListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        super.onAnimationEnd(animation);
                        float translationY = getTranslationY();
                        setTranslationY(0);
                        layout(getLeft(), (int) (getTop() + translationY),
                                getRight(), (int) (getBottom() + translationY));
                        animator = null;
                    }
                });
            }
        }
        return true;
    }

整体的逻辑还是看文末的代码吧 这里只是介绍一下功能的实现。

三、解决事件分发冲突

一般在这里内部都会有一个ListView控件来展示数据,它与我们的这个控件就会有滑动冲突。
解决方法是用外部拦截法来解决。
我在这里新建了一个接口来回调给调用类

    public void setInterceptCallBack(RequestInterceptCallBack interceptCallBack) {
        this.interceptCallBack = interceptCallBack;
    }

    public interface RequestInterceptCallBack {
        boolean canIntercept(boolean isDown);
    }
    
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercept = false;
        switch (ev.getAction()) {

            case MotionEvent.ACTION_DOWN:
                y0 = (int) ev.getY();
                rawYDown = ev.getRawY();
                intercept = false;
                hasFiling = false;
                break;

            case MotionEvent.ACTION_MOVE:
                float dy = ev.getY() - y0;
                Log.i(TAG, "dy" + dy);
                if (Math.abs(dy) < 7 || animator != null || (customMode == TOP_MODE && dy < 0)) {
                    // 移动过小视为点击事件。不拦截 或者 动画尚未结束 本次不拦截
                    intercept = false;
                } else if (dy > 0) {
                    intercept = interceptCallBack.canIntercept(true);
                } else {
                    intercept = interceptCallBack.canIntercept(false);
                }
                break;

            case MotionEvent.ACTION_UP:
                intercept = false;
                break;
        }
        return intercept;
    }

canIntercept(boolean isDown)这个参数我设置的是手势是否下滑,如果是recyclerView则在方法重写里面判断
recyclerView.canScrollVertically(-1);这个方法。
例如:

@Override
    public boolean canIntercept(boolean isDown) {
        if (isDown) {
            Log.i(TAG, "-1: " + recyclerView.canScrollVertically(-1));
            return !recyclerView.canScrollVertically(-1);
        } else {
            Log.i(TAG, "1: " + recyclerView.canScrollVertically(1));
            return !recyclerView.canScrollVertically(1);
        }
    }

其中的逻辑需要自己揣摩一下。

四、最后调用类的初始化工作

在Activity中:

  dragUpDownLinearLayout.setInterceptCallBack(this);
            dragUpDownLinearLayout.setTopHeight(relativeLayout.getMeasuredHeight());
            handler.post(new Runnable() {
                @Override
                public void run() {
                    dragUpDownLinearLayout.setLocation(DragUpDownLinearLayout.MIDDLE_MODE);
                    dragUpDownLinearLayout.setVisibility(View.VISIBLE);
                }
            });  

我这里是直接设置的Middle_Mode模式。布局里设置的空间隐藏,设置完再显示,不这样的话会出现闪一下的变化位置,比较不好,其实也可以进入的时候走一个动画。这些都看爱好和需求吧。

代码

/**
 * Created by Vito 
 */
public class DragUpDownLinearLayout extends LinearLayout implements View.OnTouchListener,
        GestureDetector.OnGestureListener {
    public final static String TAG = "DragUpDownLinearLayout";
    public final static int TOP_MODE = 1;
    public final static int MIDDLE_MODE = 2;
    public final static int BOTTOM_MODE = 3;
    public int customMode = 0;
    // 手势监听对象
    private GestureDetector mGestureDetector;
    // 拖拽条的高度
    private final static int indicatorHeight = 30;
    private int indicatorHeightPx;
    // 中间位置的高度
    private int middleHeight;
    // contentView(去掉状态栏、toolbar和导航栏部分)的高度
    private int contentViewHeight;
    // 顶部其他控件的高度
    private int topHeight;
    // 屏幕的高度
    private float screenHeight;
    // 滑动开始手指落点
    private int y0;
    private float rawYDown;
    // 第一次加载标志位
    private boolean isFirstLayout = true;
    // 是否拦截事件接口回调,用于判断子控件的是否可滑动
    private RequestInterceptCallBack interceptCallBack;
    // 动画对象
    private ObjectAnimator animator = null;
    private static final String ANIMATOR_MODE = "translationY";
    // 是否触发了Filing方法,未触发交由onTouch方法完成移动
    private boolean hasFiling;
    // 是否在滚动触发的layout的标志位
    private boolean isScrolling;

    public DragUpDownLinearLayout(Context context) {
        this(context, null);
    }

    public DragUpDownLinearLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public DragUpDownLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    @SuppressLint("ClickableViewAccessibility")
    private void init(Context context) {
        // 界面
        indicatorHeightPx = dp2px(indicatorHeight);
        setBackgroundColor(Color.WHITE);
        FrameLayout frameLayout = new FrameLayout(context);
        frameLayout.setLayoutParams(
                new LinearLayoutCompat.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, indicatorHeightPx));
        addView(frameLayout);
        View view = new View(context);
        FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(dp2px(75), dp2px(8));
        params.gravity = Gravity.CENTER;
        view.setLayoutParams(params);
        view.setBackgroundResource(R.drawable.shape_drag_up_down_indicator);
        frameLayout.addView(view);
        // 获取屏幕的高
        DisplayMetrics dm = context.getResources().getDisplayMetrics();
        screenHeight = dm.heightPixels;
        setOnTouchListener(this);
        mGestureDetector = new GestureDetector(getContext(), this);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercept = false;
        if (interceptCallBack != null) {
            switch (ev.getAction()) {

                case MotionEvent.ACTION_DOWN:
                    y0 = (int) ev.getY();
                    rawYDown = ev.getRawY();
                    intercept = false;
                    hasFiling = false;
                    break;

                case MotionEvent.ACTION_MOVE:
                    float dy = ev.getY() - y0;
                    Log.i(TAG, "dy" + dy);
                    if (Math.abs(dy) < 7 || animator != null || (customMode == TOP_MODE && dy < 0)) {
                        // 移动过小视为点击事件。不拦截 或者 动画尚未结束 本次不拦截
                        intercept = false;
                    } else if (dy > 0) {
                        intercept = interceptCallBack.canIntercept(true);
                    } else {
                        intercept = interceptCallBack.canIntercept(false);
                    }
                    break;

                case MotionEvent.ACTION_UP:
                    intercept = false;
                    break;
            }
        }
        return intercept;
    }

    @Override
    public boolean onTouch(View view, MotionEvent event) {
        mGestureDetector.onTouchEvent(event);
        // 是否有执行filing
        if (event.getAction() == MotionEvent.ACTION_UP) {
            if (!hasFiling) {
                isScrolling = false;
                // 松手时固定位置 计算占屏幕的百分比
                float yUP = getTop();
                float i = yUP / screenHeight;
                if (i < 0.30) {
                    animator = ObjectAnimator.ofFloat(DragUpDownLinearLayout.this, ANIMATOR_MODE,
                            getTranslationY(), getTranslationY() - getY() + topHeight);
                    customMode = TOP_MODE;
                } else if (i < 0.75) {
                    animator = ObjectAnimator.ofFloat(DragUpDownLinearLayout.this, ANIMATOR_MODE,
                            getTranslationY(), getTranslationY() + (middleHeight - getY()));
                    customMode = MIDDLE_MODE;
                } else {
                    animator = ObjectAnimator.ofFloat(DragUpDownLinearLayout.this, ANIMATOR_MODE,
                            getTranslationY(), getTranslationY() + contentViewHeight - getY() - indicatorHeightPx);
                    customMode = BOTTOM_MODE;
                }
                animator.setDuration(500);
                animator.start();
                // 动画结束时,将控件的translation偏移量转化为Top值,便于计算
                animator.addListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        super.onAnimationEnd(animation);
                        float translationY = getTranslationY();
                        setTranslationY(0);
                        layout(getLeft(), (int) (getTop() + translationY),
                                getRight(), (int) (getBottom() + translationY));
                        animator = null;
                    }
                });
            }
        }
        return true;
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        Log.e(TAG, "onLayout" + t);
        if (isFirstLayout) {
            contentViewHeight = ((Activity) getContext()).getWindow().
                    findViewById(Window.ID_ANDROID_CONTENT).getMeasuredHeight();
            middleHeight = (contentViewHeight / 3) * 2;
            isFirstLayout = false;
            Log.e(TAG, "contentViewHeight" + contentViewHeight);
        } else {
            Log.e(TAG, "isScrolling" + isScrolling);
            if (!isScrolling) {
                switch (customMode) {
                    case TOP_MODE:
                        t = topHeight;
                        b = getHeight() + topHeight;
                        break;
                    case MIDDLE_MODE:
                        t = middleHeight;
                        b = getHeight() + middleHeight;
                        break;
                    case BOTTOM_MODE:
                        int topUp = contentViewHeight - indicatorHeightPx;
                        t = topUp;
                        b = getHeight() + topUp;
                        break;
                }
                setTop(t);
                setBottom(b);
            }
        }
        super.onLayout(changed, l, t, r, b);
    }

    /**
     * 设置位置,同于指定初始化位置
     */
    public void setLocation(int mode) {
        switch (mode) {
            case TOP_MODE:
                layout(getLeft(),
                        topHeight,
                        getRight(),
                        getHeight() + topHeight);
                customMode = TOP_MODE;
                break;
            case MIDDLE_MODE:
                layout(getLeft(), middleHeight,
                        getRight(), middleHeight + getHeight());
                customMode = MIDDLE_MODE;
                break;
            case BOTTOM_MODE:
                int topUp = contentViewHeight - indicatorHeightPx;
                layout(getLeft(), topUp,
                        getRight(), topUp + getHeight());
                customMode = BOTTOM_MODE;
                break;
        }

    }

    @Override
    public boolean onDown(MotionEvent motionEvent) {

        return false;
    }


    @Override
    public boolean onScroll(MotionEvent motionEvent, MotionEvent motionEvent1, float distanceX, float distanceY) {
        int y = (int) motionEvent1.getY();
        // 获取本次移动的距离
        int dy = y - y0;
        int top = getTop();
        int bottom = getBottom();
        if (top <= topHeight && dy < 0) {
            // 高出顶部 则不改变位置防止超出顶部
            return false;
        }
        layout(getLeft(), (top + dy),
                getRight(), (bottom + dy));
        isScrolling = true;
        return false;
    }

    @Override
    public boolean onFling(MotionEvent motionEvent, MotionEvent motionEvent1, float x, float speedY) {
        float v = motionEvent1.getRawY() - rawYDown;
        switch (customMode) {
            case TOP_MODE:
                animator = ObjectAnimator.ofFloat(DragUpDownLinearLayout.this, ANIMATOR_MODE,
                        getTranslationY(), getTranslationY() + (middleHeight - getY()));
                customMode = MIDDLE_MODE;
                break;
            case MIDDLE_MODE:
                if (v > 0) {
                    animator = ObjectAnimator.ofFloat(DragUpDownLinearLayout.this, ANIMATOR_MODE,
                            getTranslationY(), getTranslationY() + contentViewHeight - getY() - indicatorHeightPx);
                    customMode = BOTTOM_MODE;
                } else {
                    animator = ObjectAnimator.ofFloat(DragUpDownLinearLayout.this, ANIMATOR_MODE,
                            getTranslationY(), getTranslationY() - getY() + topHeight);
                    customMode = TOP_MODE;
                }
                break;
            case BOTTOM_MODE:
                animator = ObjectAnimator.ofFloat(DragUpDownLinearLayout.this, ANIMATOR_MODE,
                        getTranslationY(), getTranslationY() + (middleHeight - getY()));
                customMode = MIDDLE_MODE;
                break;
            default:
        }

        animator.setDuration(500);
        animator.start();
        // 动画结束时,将控件的translation偏移量转化为Top值,便于计算
        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                float translationY = getTranslationY();
                setTranslationY(0);
                layout(getLeft(), (int) (getTop() + translationY),
                        getRight(), (int) (getBottom() + translationY));
                animator = null;
            }
        });
        isScrolling = false;
        hasFiling = true;
        return true;
    }

    @Override
    public void onLongPress(MotionEvent motionEvent) {
    }

    @Override
    public void onShowPress(MotionEvent motionEvent) {
    }

    @Override
    public boolean onSingleTapUp(MotionEvent motionEvent) {
        return false;
    }

    private int dp2px(float dipValue) {
        final float scale = getContext().getResources().getDisplayMetrics().density;
        return (int) (dipValue * scale + 0.5f);
    }

    public void setInterceptCallBack(RequestInterceptCallBack interceptCallBack) {
        this.interceptCallBack = interceptCallBack;
    }

    public interface RequestInterceptCallBack {
        boolean canIntercept(boolean isDown);
    }

    /**
     * 重新请求一次contentView 因为toolbar将它往下顶了一部分,也就是加一个偏移量
     */
    public void resetContentViewHeight(int off) {
        contentViewHeight = ((Activity) getContext()).getWindow().
                findViewById(Window.ID_ANDROID_CONTENT).getMeasuredHeight() - off;
        middleHeight = (contentViewHeight / 3) * 2;
        Log.e(TAG, "resetContentViewHeight" + contentViewHeight);
    }

    /**
     * 设置顶部高度
     */
    public void setTopHeight(int topHeight) {
        this.topHeight = topHeight;
    }
}

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