Android常用布局之LinearLayout简析

LinearLayout是我们日常开发中一个非常常用的基本布局父控件。对于它的用法,我们肯定已经熟悉的不能再熟悉了。但是对于这种基础控件我们不光要停留再会用的基础上,知道它的原理是更加的重要,我们只有将这些基础控件的原理研究的很清楚了,我们才能举一反三,自定义出更多更好的控件满足我们日常开发的各种五花八门的需求。

如何开始LinearLayout的源码学习

当我们打开一个自定义控件的时候,我们首先肯定是去了解它的测量、布局和绘制过程,所以肯定是从这个三个过程回调onMeasure onLayout onDraw开始。只要搞清楚了这些过程,整个控件我们也就明白了。

构造方法

打开 LinearLayout.java 类,看看它的构造方法,其实也没有什么特别的地方,就是解析一些控件自定义熟悉什么的,这一部分也没什么好看的了。

measure 过程

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 根据 LinearLayout 布局方向进行measure
        if (mOrientation == VERTICAL) {
            measureVertical(widthMeasureSpec, heightMeasureSpec);
        } else {
            measureHorizontal(widthMeasureSpec, heightMeasureSpec);
        }
    }

LinearLayout 会根据自己的方向去进去 measure,但是内部的原理什么基本相同,这里就看看它的纵向measure过程measureVertical()吧。这部分的代码比较长,我们可以分几个部分来看:

  • 定义的变量
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {

        // mTotalLength作为LinearLayout成员变量,其主要目的是在测量的时候通过累加得到所有子控件的高度和(Vertical)或者宽度和(Horizontal)
        mTotalLength = 0;
        // maxWidth用来记录所有子控件中控件宽度最大的值。
        int maxWidth = 0;
        // 子控件的测量状态,会在遍历子控件测量的时候通过combineMeasuredStates来合并上一个子控件测量状态与当前遍历到的子控件的测量状态,采取的是按位相或
        int childState = 0;

        /**
         * 以下两个最大宽度跟上面的maxWidth最大的区别在于matchWidthLocally这个参数
         * 当matchWidthLocally为真,那么以下两个变量只会跟当前子控件的左右margin和相比较取大值
         * 否则,则跟maxWidth的计算方法一样
         */
        // 子控件中layout_weight<=0的View的最大宽度
        int alternativeMaxWidth = 0;
        // 子控件中layout_weight>0的View的最大宽度
        int weightedMaxWidth = 0;
        // 是否子控件全是match_parent的标志位,用于判断是否需要重新测量
        boolean allFillParent = true;
        // 所有子控件的weight之和
        float totalWeight = 0;

        // 如您所见,得到所有子控件的数量,准确的说,它得到的是所有同级子控件的数量
        // 在官方的注释中也有着对应的例子
        // 比如TableRow,假如TableRow里面有N个控件,而LinearLayout(TableLayout也是继承LinearLayout哦)下有M个TableRow,那么这里返回的是M,而非M*N
        // 但实际上,官方似乎也只是直接返回getChildCount(),起这个方法名的原因估计是为了让人更加的明白,毕竟如果是getChildCount()可能会让人误认为为什么没有返回所有(包括不同级)的子控件数量
        final int count = getVirtualChildCount();

        // 得到测量模式
        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        // 当子控件为match_parent的时候,该值为ture,同时判定的还有上面所说的matchWidthLocally,这个变量决定了子控件的测量是父控件干预还是填充父控件(剩余的空白位置)。
        boolean matchWidth = false;

        boolean skippedMeasure = false;

        final int baselineChildIndex = mBaselineAlignedChildIndex;
        final boolean useLargestChild = mUseLargestChild;

        int largestChildHeight = Integer.MIN_VALUE;
    }

注意:这一块儿定义了很多变量,我们需要重点关注(mTotalLength、三个跟width相关的变量、weight相关的变量)

  • measureVertical 中的测量部分
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
        // ...省略上面的一大堆变量
        for (int i = 0; i < count; ++i) {
            // 遍历所有子view
            final View child = getVirtualChildAt(i);
            // 如果子view为空,mTotalLength += measureNullChild(i)之后,进行下一个
            if (child == null) {
                // 目前而言,measureNullChild()方法返回的永远是0,估计是设计者留下来以后或许有补充的。
                mTotalLength += measureNullChild(i);
                continue;
            }
           
            if (child.getVisibility() == GONE) {
               // 同上,返回的都是0。
               // 事实上这里的意思应该是当前遍历到的View为Gone的时候,就跳过这个View,下一句的continue关键字也正是这个意思。
               // 忽略当前的View,这也就是为什么Gone的控件不占用布局资源的原因。(毕竟根本没有分配空间)
                i += getChildrenSkipCount(child, i);
                continue;
            }

            // 根据showDivider的值(before/middle/end)来决定遍历到当前子控件时,高度是否需要加上divider的高度
            // 比如showDivider为before,那么只会在第0个子控件测量时加上divider高度,其余情况下都不加
            if (hasDividerBeforeChildAt(i)) {
                mTotalLength += mDividerWidth;
            }

            final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
                    child.getLayoutParams();
            // 得到每个子控件的LayoutParams后,累加权重和,后面用于跟weightSum相比较
            totalWeight += lp.weight;
            
            // 我们都知道,测量模式有三种:
            // * UNSPECIFIED:父控件对子控件无约束
            // * Exactly:父控件对子控件强约束,子控件永远在父控件边界内,越界则裁剪。如果要记忆的话,可以记忆为有对应的具体数值或者是Match_parent
            // * AT_Most:子控件为wrap_content的时候,测量值为AT_MOST。
            
            // 下面的if/else分支都是跟weight相关
            if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
                // 这个if里面需要满足三个条件:
                // * LinearLayout的高度为match_parent(或者有具体值)
                // * 子控件的高度为0
                // * 子控件的weight>0
                // 这其实就是我们通常情况下用weight时的写法
                // 测量到这里的时候,会给个标志位,稍后再处理。此时会计算总高度
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
                skippedMeasure = true;
            } else {
                // 到这个分支,则需要对不同的情况进行测量
                int oldHeight = Integer.MIN_VALUE;

                if (lp.height == 0 && lp.weight > 0) {
                    // 满足这两个条件,意味着父类即LinearLayout是wrap_content,或者mode为UNSPECIFIED
                    // 那么此时将当前子控件的高度置为wrap_content
                    // 为何需要这么做,主要是因为当父类为wrap_content时,其大小实际上由子控件控制
                    // 我们都知道,自定义控件的时候,通常我们会指定测量模式为wrap_content时的默认大小
                    // 这里强制给定为wrap_content为的就是防止子控件高度为0.
                    oldHeight = 0;
                    lp.height = LayoutParams.WRAP_CONTENT;
                }
                
                /**【1】*/
                // 下面这句虽然最终调用的是ViewGroup通用的同名方法,但传入的height值是跟平时不一样的
                // 这里可以看到,传入的height是跟weight有关,关于这里,稍后的文字描述会着重阐述
                measureChildBeforeLayout(
                       child, i, widthMeasureSpec, 0, heightMeasureSpec,
                       totalWeight == 0 ? mTotalLength : 0);

                // 重置子控件高度,然后进行精确赋值
                if (oldHeight != Integer.MIN_VALUE) {
                   lp.height = oldHeight;
                }

                final int childHeight = child.getMeasuredHeight();
                final int totalLength = mTotalLength;
                // getNextLocationOffset返回的永远是0,因此这里实际上是比较child测量前后的总高度,取大值。
                mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                       lp.bottomMargin + getNextLocationOffset(child));

                if (useLargestChild) {
                    largestChildHeight = Math.max(childHeight, largestChildHeight);
                }
            }

            if ((baselineChildIndex >= 0) && (baselineChildIndex == i + 1)) {
               mBaselineChildTop = mTotalLength;
            }

            if (i < baselineChildIndex && lp.weight > 0) {
                throw new RuntimeException("A child of LinearLayout with index "
                        + "less than mBaselineAlignedChildIndex has weight > 0, which "
                        + "won't work.  Either remove the weight, or don't set "
                        + "mBaselineAlignedChildIndex.");
            }

            boolean matchWidthLocally = false;
            
            // 还记得我们变量里又说到过matchWidthLocally这个东东吗
            // 当父类(LinearLayout)不是match_parent或者精确值的时候,但子控件却是一个match_parent
            // 那么matchWidthLocally和matchWidth置为true
            // 意味着这个控件将会占据父类(水平方向)的所有空间
            if (widthMode != MeasureSpec.EXACTLY && lp.width == LayoutParams.MATCH_PARENT) {
                matchWidth = true;
                matchWidthLocally = true;
            }

            final int margin = lp.leftMargin + lp.rightMargin;
            final int measuredWidth = child.getMeasuredWidth() + margin;
            maxWidth = Math.max(maxWidth, measuredWidth);
            childState = combineMeasuredStates(childState, child.getMeasuredState());

            allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
            
            if (lp.weight > 0) {
                weightedMaxWidth = Math.max(weightedMaxWidth,
                        matchWidthLocally ? margin : measuredWidth);
            } else {
                alternativeMaxWidth = Math.max(alternativeMaxWidth,
                        matchWidthLocally ? margin : measuredWidth);
            }

            i += getChildrenSkipCount(child, i);
        }
    }

这一块儿中,我们要注意一下measureChildBeforeLayout() 方法。这个方法将会决定子控件可用的剩余分配空间。

measureChildBeforeLayout()最终调用的实际上是ViewGroup的measureChildWithMargins(),不同的是,在传入高度值的时候(垂直测量情况下),会对weight进行一下判定

假如当前子控件的weight加起来还是为0,则说明在当前子控件之前还没有遇到有weight的子控件,那么LinearLayout将会进行正常的测量,若之前遇到过有weight的子控件,那么LinearLayout传入0。

那么measureChildWithMargins()的最后一个参数,也就是LinearLayout在这里传入的这个高度值是用来干嘛的呢?

如果我们追溯下去,就会发现,这个函数最终其实是为了结合父类的MeasureSpec以及child自身的LayoutParams来对子控件测量。而最后传入的值,在子控件测量的时候被添加进去。

protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

官方给出了一句这个的注释

@param heightUsed Extra space that has been used up by the parent vertically (possibly by other children of the parent)

事实上,我们在代码中也可以很清晰的看到,在getChildMeasureSpec()中,子控件需要把父控件的padding,自身的margin以及一个可调节的量三者一起测量出自身的大小。

那么假如在测量某个子控件之前,weight一直都是0,那么该控件在测量时,需要考虑在本控件之前的总高度,来根据剩余控件分配自身大小。而如果有weight,那么就不考虑已经被占用的控件,因为有了weight,子控件的高度将会在后面重新赋值。

  • weight的再次测量
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
        //...接上面
        // 下面的这一段代码主要是为useLargestChild属性服务的,不在本文主要分析范围,略过
        if (mTotalLength > 0 && hasDividerBeforeChildAt(count)) {
            mTotalLength += mDividerHeight;
        }

        if (useLargestChild &&
                (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED)) {
            mTotalLength = 0;

            for (int i = 0; i < count; ++i) {
                final View child = getVirtualChildAt(i);

                if (child == null) {
                    mTotalLength += measureNullChild(i);
                    continue;
                }

                if (child.getVisibility() == GONE) {
                    i += getChildrenSkipCount(child, i);
                    continue;
                }

                final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
                        child.getLayoutParams();
                // Account for negative margins
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + largestChildHeight +
                        lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
            }
        }
        
        // Add in our padding
        mTotalLength += mPaddingTop + mPaddingBottom;

        int heightSize = mTotalLength;

        // Check against our minimum height
        heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
        
        // Reconcile our calculated size with the heightMeasureSpec
        int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
        heightSize = heightSizeAndState & MEASURED_SIZE_MASK;

}

上面这里是为weight情况做的预处理。

我们略过useLargestChild 的情况,主要看看if处理外的代码。在这里,我没有去掉官方的注释,而是保留了下来。

从中我们不难看出heightSize做了两次赋值,为何需要做两次赋值。

因为我们的布局除了子控件,还有自己本身的background,因此这里需要比较当前的子控件的总高度和背景的高度取大值。

接下来就是判定大小,我们都知道测量的MeasureSpec实际上是一个32位的int,高两位是测量模式,剩下的就是大小,因此heightSize = heightSizeAndState & MEASURED_SIZE_MASK;作用就是用来得到大小的精确值(不含测量模式)

接下来我们看这个方法里面第二占比最大的代码:

void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
        //...接上面

        //算出剩余空间,假如之前是skipp的话,那么几乎可以肯定是有剩余空间(同时有weight)的
        int delta = heightSize - mTotalLength;
        if (skippedMeasure || delta != 0 && totalWeight > 0.0f) {
            // 限定weight总和范围,假如我们给过weighSum范围,那么子控件的weight总和受此影响
            float weightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;

            mTotalLength = 0;

            for (int i = 0; i < count; ++i) {
                final View child = getVirtualChildAt(i);
                
                if (child.getVisibility() == View.GONE) {
                    continue;
                }
                
                LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
                
                float childExtra = lp.weight;
                if (childExtra > 0) {
                    // 全篇最精华的一个地方。。。。拥有weight的时候计算方式,ps:执行到这里时,child依然还没进行自身的measure
                    
                    // 公式 = 剩余高度*(子控件的weight/weightSum),也就是子控件的weight占比*剩余高度
                    int share = (int) (childExtra * delta / weightSum);
                    // weightSum计余
                    weightSum -= childExtra;
                    // 剩余高度
                    delta -= share;
                    
                    final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                            mPaddingLeft + mPaddingRight +
                                    lp.leftMargin + lp.rightMargin, lp.width);
                   
                    if ((lp.height != 0) || (heightMode != MeasureSpec.EXACTLY)) {
                        int childHeight = child.getMeasuredHeight() + share;
                        if (childHeight < 0) {
                            childHeight = 0;
                        }
                        
                        child.measure(childWidthMeasureSpec,
                                MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY));
                    } else {
   
                        child.measure(childWidthMeasureSpec,
                                MeasureSpec.makeMeasureSpec(share > 0 ? share : 0,
                                        MeasureSpec.EXACTLY));
                    }


                    childState = combineMeasuredStates(childState, child.getMeasuredState()
                            & (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));
                }

                final int margin =  lp.leftMargin + lp.rightMargin;
                final int measuredWidth = child.getMeasuredWidth() + margin;
                maxWidth = Math.max(maxWidth, measuredWidth);

                boolean matchWidthLocally = widthMode != MeasureSpec.EXACTLY &&
                        lp.width == LayoutParams.MATCH_PARENT;

                alternativeMaxWidth = Math.max(alternativeMaxWidth,
                        matchWidthLocally ? margin : measuredWidth);

                allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;

                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + child.getMeasuredHeight() +
                        lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
            }


            mTotalLength += mPaddingTop + mPaddingBottom;

        } 
        
        // 没有weight的情况下,只看useLargestChild参数,如果都无相关,那就走layout流程了,因此这里忽略
        else {
            alternativeMaxWidth = Math.max(alternativeMaxWidth,
                                           weightedMaxWidth);

            if (useLargestChild && heightMode != MeasureSpec.EXACTLY) {
                for (int i = 0; i < count; i++) {
                    final View child = getVirtualChildAt(i);

                    if (child == null || child.getVisibility() == View.GONE) {
                        continue;
                    }

                    final LinearLayout.LayoutParams lp =
                            (LinearLayout.LayoutParams) child.getLayoutParams();

                    float childExtra = lp.weight;
                    if (childExtra > 0) {
                        child.measure(
                                MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(),
                                        MeasureSpec.EXACTLY),
                                MeasureSpec.makeMeasureSpec(largestChildHeight,
                                        MeasureSpec.EXACTLY));
                    }
                }
            }
        }
}
  • weight的两种情况

在weight计算方面,我们可以清晰的看到,weight为何是针对剩余空间进行分配的原理了。 我们打个比方,假如现在我们的LinearLayout的weightSum=10,总高度100,有两个子控件(他们的height=0dp),他们的weight分别为2:8。

那么在测量第一个子控件的时候,可用的剩余高度为100,第一个子控件的高度则是100*(2/10)=20,接下来可用的剩余高度为80

我们继续第二个控件的测量,此时它的高度实质上是80*(8/8)=80

到目前为止,看起来似乎都是正确的,但关于weight我们一直有一个疑问:** 就是我们为子控件给定height=0dp和height=match_parent时我们就会发现我们的子控件的高度比是不同的,前者是2:8而后者是调转过来变成8:2 **

对于这个问题,我们不妨继续看看代码。

接下来我们会看到这么一个分支:

if ((lp.height != 0) || (heightMode != MeasureSpec.EXACTLY)) { } else {}

首先我们不管heightMode,也就是父类的测量模式,剩下一个判定条件就是lp.height,也就是子类的高度。

既然有针对这个进行判定,那就是意味着肯定在此之前对child进行过measure,事实上,在这里我们一早就对这个地方进行过描述,这个方法正是measureChildBeforeLayout()。

还记得我们的measureChildBeforeLayout()执行的先行条件吗

正是不满足(LinearLayout的测量模式非EXACTLY/child.height==0/child.weight/child.weight>0)之中的child.height==0

因为除非我们指定height=0,否则match_parent是等于-1,wrap_content是等于-2.

在执行measureChildBeforeLayout(),由于我们的child的height=match_parent,因此此时可用空间实质上是整个LinearLayout,执行了measureChildBeforeLayout()后,此时的mTotalLength是整个LinearLayout的大小

回到我们的例子,假设我们的LinearLayout高度为100,两个child的高度都是match_parent,那么执行了measureChildBeforeLayout()后,我们两个子控件的高度都将会是这样:

child_1.height=100
child_2.height=100
mTotalLength=100+100=200

在一系列的for之后,执行到我们剩余空间:

int delta = heightSize - mTotalLength;
(delta=100[linearlayout的实际高度]-200=-100)

没错,你看到的的确是一个负数

接下来就是套用weight的计算公式:

share=(int) (childExtra * delta / weightSum)
即:share=-100(2/10)=-20

然后走到我们所说的if/else里面

if ((lp.height != 0) || (heightMode != MeasureSpec.EXACTLY)) {
                        // child was measured once already above...
                        // base new measurement on stored values
                        int childHeight = child.getMeasuredHeight() + share;
                        if (childHeight < 0) {
                            childHeight = 0;
                        }
                        
                        child.measure(childWidthMeasureSpec,
                                MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY));
                    }

我们知道child.getMeasuredHeight()=100

接着这里有一条int childHeight = child.getMeasuredHeight() + share;

这意味着我们的childHeight=100+(-20)=80;

接下来就是走child.measure,并把childHeight传进去,因此最终反馈到界面上,我们就会发现,在两个match_parent的子控件中,weight的比是反转的。

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