android流式布局实现与源码分析

版权声明:本文为博主原创文章,转载请注明出处。

项目地址: https://github.com/hongyangAndroid/FlowLayout

废话不多少,我们先来看一张效果图:

image.png

要实现图片中的布局效果,我们该如何操作呢?我们直接看代码:

final String[] tags = new String[]{"item1", "item2", "item3",
        "item4", "item5", "item6", "item7", "item8",
        "item9", "item10", "item11", "item12", "item13",
        "item14", "item15", "item16", "item17", "item18",
        "item19", "item20", "item21", "item22", "item23",
        "item24", "item25", "item26", "item27", "item28",
        "item29", "item30", "item31", "item32", "item33"
};
private TagFlowLayout mFlowLayout;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    final LayoutInflater mInflater = LayoutInflater.from(this);
    mFlowLayout = (TagFlowLayout) findViewById(R.id.id_flowlayout);
    mFlowLayout.setAdapter(new TagAdapter<String>(tags) {
        @Override
        public View getView(com.zhy.view.flowlayout.FlowLayout parent, int position, String s) {
            TextView tv =(TextView)mInflater.inflate(R.layout.tag_item,
                    mFlowLayout, false);
            tv.setText(s);  
            return tv;
        }

        // 为标签设置预点击内容(就是一开始就处于点击状态的标签)
        @Override
        public boolean setSelected(int position, String s) {
            return s.equals("item10");
        }
    });

   //  为点击标签设置点击事件.
    mFlowLayout.setOnTagClickListener(new TagFlowLayout.OnTagClickListener() {
        @Override
        public boolean onTagClick(View view, int position, com.zhy.view.flowlayout.FlowLayout parent) {
            Toast.makeText(MainActivity.this, tags[position], Toast.LENGTH_SHORT).show();
            return true;
        }
    });

    // 点击标签时,回传所有已选中标签
    mFlowLayout.setOnSelectListener(new TagFlowLayout.OnSelectListener() {
        @Override
        public void onSelected(Set<Integer> selectPosSet) {
            Log.e("jacky", "choose:" + selectPosSet.toString());
        }
    });
}

有没有熟悉的感觉,对,其实跟我们平时使用的布局设置适配器达到的显示效果差不多,只不过把以前的列表效果换成了流式布局,将对itemClick的点击效果替换成了tagClick而已,使用起来很简单。至于选中后的效果,当然我们可以自己在XML文件中定义选中状态,相信这个不会难倒大家的。

想必到这里大家基本上已经知道怎么使用了,接下来我们来看看源码里面是怎么实现的。首先从我们布局开始看起,而TagFlowLayout又是继承自FlowLayout,那我们先来看看FlowLayout中是如何实现的:

public class FlowLayout extends ViewGroup {
private static final String TAG = "FlowLayout";
private static final int LEFT = -1;
private static final int CENTER = 0;
private static final int RIGHT = 1;

protected List<List<View>> mAllViews = new ArrayList<List<View>>();
protected List<Integer> mLineHeight = new ArrayList<Integer>();
protected List<Integer> mLineWidth = new ArrayList<Integer>();
private int mGravity;
private List<View> lineViews = new ArrayList<>();

public FlowLayout(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TagFlowLayout);
    mGravity = ta.getInt(R.styleable.TagFlowLayout_gravity,LEFT);
    ta.recycle();
}

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

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

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
    int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
    int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
    int modeHeight = MeasureSpec.getMode(heightMeasureSpec);

    // wrap_content
    int width = 0;
    int height = 0;

    int lineWidth = 0;
    int lineHeight = 0;

    int cCount = getChildCount();

    for (int i = 0; i < cCount; i++)
    {
        View child = getChildAt(i);
        if (child.getVisibility() == View.GONE)
        {
            if (i == cCount - 1)
            {
                width = Math.max(lineWidth, width);
                height += lineHeight;
            }
            continue;
        }
        measureChild(child, widthMeasureSpec, heightMeasureSpec);
        MarginLayoutParams lp = (MarginLayoutParams) child
                .getLayoutParams();

        int childWidth = child.getMeasuredWidth() + lp.leftMargin
                + lp.rightMargin;
        int childHeight = child.getMeasuredHeight() + lp.topMargin
                + lp.bottomMargin;

        if (lineWidth + childWidth > sizeWidth - getPaddingLeft() - getPaddingRight())
        {
            width = Math.max(width, lineWidth);
            lineWidth = childWidth;
            height += lineHeight;
            lineHeight = childHeight;
        } else
        {
            lineWidth += childWidth;
            lineHeight = Math.max(lineHeight, childHeight);
        }
        if (i == cCount - 1)
        {
            width = Math.max(lineWidth, width);
            height += lineHeight;
        }
    }
    setMeasuredDimension(
            //
            modeWidth == MeasureSpec.EXACTLY ? sizeWidth : width + getPaddingLeft() + getPaddingRight(),
            modeHeight == MeasureSpec.EXACTLY ? sizeHeight : height + getPaddingTop() + getPaddingBottom()//
    );

}


@Override
protected void onLayout(boolean changed, int l, int t, int r, int b)
{
    mAllViews.clear();
    mLineHeight.clear();
    mLineWidth.clear();
    lineViews.clear();

    int width = getWidth();

    int lineWidth = 0;
    int lineHeight = 0;

    int cCount = getChildCount();

    for (int i = 0; i < cCount; i++)
    {
        View child = getChildAt(i);
        if (child.getVisibility() == View.GONE) continue;
        MarginLayoutParams lp = (MarginLayoutParams) child
                .getLayoutParams();

        int childWidth = child.getMeasuredWidth();
        int childHeight = child.getMeasuredHeight();

        if (childWidth + lineWidth + lp.leftMargin + lp.rightMargin > width - getPaddingLeft() - getPaddingRight())
        {
            mLineHeight.add(lineHeight);
            mAllViews.add(lineViews);
            mLineWidth.add(lineWidth);

            lineWidth = 0;
            lineHeight = childHeight + lp.topMargin + lp.bottomMargin;
            lineViews = new ArrayList<View>();
        }
        lineWidth += childWidth + lp.leftMargin + lp.rightMargin;
        lineHeight = Math.max(lineHeight, childHeight + lp.topMargin
                + lp.bottomMargin);
        lineViews.add(child);

    }
    mLineHeight.add(lineHeight);
    mLineWidth.add(lineWidth);
    mAllViews.add(lineViews);



    int left = getPaddingLeft();
    int top = getPaddingTop();

    int lineNum = mAllViews.size();

    for (int i = 0; i < lineNum; i++)
    {
        lineViews = mAllViews.get(i);
        lineHeight = mLineHeight.get(i);

        // set gravity
        int currentLineWidth = this.mLineWidth.get(i);
        switch (this.mGravity){
            case LEFT:
                left = getPaddingLeft();
                break;
            case CENTER:
                left = (width - currentLineWidth)/2+getPaddingLeft();
                break;
            case RIGHT:
                left = width - currentLineWidth + getPaddingLeft();
                break;
        }

        for (int j = 0; j < lineViews.size(); j++)
        {
            View child = lineViews.get(j);
            if (child.getVisibility() == View.GONE)
            {
                continue;
            }

            MarginLayoutParams lp = (MarginLayoutParams) child
                    .getLayoutParams();

            int lc = left + lp.leftMargin;
            int tc = top + lp.topMargin;
            int rc = lc + child.getMeasuredWidth();
            int bc = tc + child.getMeasuredHeight();

            child.layout(lc, tc, rc, bc);

            left += child.getMeasuredWidth() + lp.leftMargin
                    + lp.rightMargin;
        }
        top += lineHeight;
    }

}

@Override
public LayoutParams generateLayoutParams(AttributeSet attrs)
{
    return new MarginLayoutParams(getContext(), attrs);
}

@Override
protected LayoutParams generateDefaultLayoutParams() {
    return new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}

@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
  return new MarginLayoutParams(p);}}

从代码中我们可以看到,该控件是一个继承自ViewGroup的自定义view,而自定义View的流程这里不做深究,这里主要实现了onMeasure()与onLayout()函数,而在onMeasure中,首先通过
循环对width = Math.max(lineWidth, width)不断求最大值:
lineWidth为lineWidth += childWidth;当需要换行是,即lineWidth + childWidth > sizeWidth - getPaddingLeft() - getPaddingRight()时,lineWidth = childWidth(即该行第一个view的宽度),而height 也类似,那么到这里我们知道,lineWidth保存了当前行所有child view占父view(当前view)的最大宽度,所以可以得出width始终为最大宽度,而height为子view的占父的最大高度,从最后一行代码可以看到,setMeasuredDimension(
modeWidth == MeasureSpec.EXACTLY ? sizeWidth : width + getPaddingLeft() + getPaddingRight(),
modeHeight == MeasureSpec.EXACTLY ? sizeHeight : height + getPaddingTop() + getPaddingBottom()
);到这里是不是恍然大悟,是的,其实作者目的很明确,就是为了根据mode模式设置当前view的宽和高的。
接下来我们看一下onLayout中的实现:
首先看到在循环中有这么一条判断语句:
if (childWidth + lineWidth + lp.leftMargin + lp.rightMargin > width - getPaddingLeft() - getPaddingRight()) {
...
}
从代码中我们很容易知道,childWidth表示child view的宽度,lineWidth又表示什么呢?
我们不妨向下继续看:
lineWidth += childWidth + lp.leftMargin + lp.rightMargin;
到这里我们大概知道了,lineWidth表示每个子view以及它的margin累加,所以这是一个不断计算并累加view宽度的值(暂且认为是计算行宽度的值),然后回过头来看,就很容易知道if里面判断的是累加之后的lineWidth再加上childWidth + lp.leftMargin + lp.rightMargin与
width - getPaddingLeft() - getPaddingRight()的判断,我们很容易知道这是计算当前view(父view)的宽度的值(也可理解为所能容纳子view的最大宽度);当子view宽度大于这个值时,当然就要换行啦,到这里我们就差不多可以知道,mLineHeight保存了每行的高度,而mLineWidth保存了每行的宽度,lineViews存储了每行的所有view,而mAllViews存储的是每个lineViews对象,那么这几个关键变量我们就搞清楚了,我们继续向下看:
首先是一个for (int i = 0; i < lineNum; i++)很明显是按行遍历:
int currentLineWidth = this.mLineWidth.get(i);
switch (this.mGravity){
case LEFT:
left = getPaddingLeft();
break;
case CENTER:
left = (width - currentLineWidth)/2+getPaddingLeft();
break;
case RIGHT:
left = width - currentLineWidth + getPaddingLeft();
break;
}
结合mLineWidth和mGravity参数可以得出结论,这里主要是为了得到view的left值。
而for (int j = 0; j < lineViews.size(); j++)是遍历每行中的view,并通过
int lc = left + lp.leftMargin;
int tc = top + lp.topMargin;
int rc = lc + child.getMeasuredWidth();
int bc = tc + child.getMeasuredHeight();
child.layout(lc, tc, rc, bc);
left += child.getMeasuredWidth() + lp.leftMargin
+ lp.rightMargin;

并计算出每行中view的left,top,right与bottom信息,然后调用了child.layout(lc, tc, rc, bc);到这里我们基本上理清了,这个onlayout主要是为了计算每个child view的layout的坐标,并将child view放置在正确的位置上。
到这里我们基本上搞清楚了FlowLayout主要是完成流式布局相关的测量与child view位置计算的。
然后我们继续来看下FlowLayout子类TabFlowLayout中的实现:

public class TagFlowLayout extends FlowLayout implements TagAdapter.OnDataChangedListener {

private TagAdapter mTagAdapter;
private boolean mAutoSelectEffect = true;
private int mSelectedMax = -1;//-1为不限制数量
private static final String TAG = "TagFlowLayout";
private MotionEvent mMotionEvent;
private Set<Integer> mSelectedView = new HashSet<Integer>();
public TagFlowLayout(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TagFlowLayout);
    mAutoSelectEffect = ta.getBoolean(R.styleable.TagFlowLayout_auto_select_effect, true);
    mSelectedMax = ta.getInt(R.styleable.TagFlowLayout_max_select, -1);
    ta.recycle();
    if (mAutoSelectEffect) {
        setClickable(true);
    }
}

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

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


@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
    int cCount = getChildCount();

    for (int i = 0; i < cCount; i++)
    {
        TagView tagView = (TagView) getChildAt(i);
        if (tagView.getVisibility() == View.GONE) continue;
        if (tagView.getTagView().getVisibility() == View.GONE)
        {
            tagView.setVisibility(View.GONE);
        }
    }
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

public interface OnSelectListener
{
    void onSelected(Set<Integer> selectPosSet);
}

private OnSelectListener mOnSelectListener;

public void setOnSelectListener(OnSelectListener onSelectListener)
{
    mOnSelectListener = onSelectListener;
    if (mOnSelectListener != null) setClickable(true);
}

public interface OnTagClickListener
{
    boolean onTagClick(View view, int position, FlowLayout parent);
}

private OnTagClickListener mOnTagClickListener;


public void setOnTagClickListener(OnTagClickListener onTagClickListener)
{
    mOnTagClickListener = onTagClickListener;
    if (onTagClickListener != null) setClickable(true);
}


public void setAdapter(TagAdapter adapter)
{
    //if (mTagAdapter == adapter)
    //  return;
    mTagAdapter = adapter;
    mTagAdapter.setOnDataChangedListener(this);
    mSelectedView.clear();
    changeAdapter();

}

private void changeAdapter() {
    removeAllViews();
    TagAdapter adapter = mTagAdapter;
    TagView tagViewContainer = null;
    HashSet preCheckedList = mTagAdapter.getPreCheckedList();
    for (int i = 0; i < adapter.getCount(); i++)  {
        View tagView = adapter.getView(this, i, adapter.getItem(i));
        tagViewContainer = new TagView(getContext());
        tagView.setDuplicateParentStateEnabled(true);
        if (tagView.getLayoutParams() != null)
        {
            tagViewContainer.setLayoutParams(tagView.getLayoutParams());
        } else
        {
            MarginLayoutParams lp = new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
            lp.setMargins(dip2px(getContext(), 5),
                    dip2px(getContext(), 5),
                    dip2px(getContext(), 5),
                    dip2px(getContext(), 5));
            tagViewContainer.setLayoutParams(lp);
        }
        tagViewContainer.addView(tagView);
        addView(tagViewContainer);


        if (preCheckedList.contains(i))
        {
            tagViewContainer.setChecked(true);
        }

        if (mTagAdapter.setSelected(i, adapter.getItem(i)))
        {
            mSelectedView.add(i);
            tagViewContainer.setChecked(true);
        }
    }
    mSelectedView.addAll(preCheckedList);

}


@Override
public boolean onTouchEvent(MotionEvent event)
{
    if (event.getAction() == MotionEvent.ACTION_UP)
    {
        mMotionEvent = MotionEvent.obtain(event);
    }
    return super.onTouchEvent(event);
}

@Override
public boolean performClick()
{
    if (mMotionEvent == null) return super.performClick();

    int x = (int) mMotionEvent.getX();
    int y = (int) mMotionEvent.getY();
    mMotionEvent = null;

    TagView child = findChild(x, y);
    int pos = findPosByView(child);
    if (child != null)
    {
        doSelect(child, pos);
        if (mOnTagClickListener != null)
        {
            return mOnTagClickListener.onTagClick(child.getTagView(), pos, this);
        }
    }
    return true;
}


public void setMaxSelectCount(int count)
{
    if (mSelectedView.size() > count)
    {
        Log.w(TAG, "you has already select more than " + count + " views , so it will be clear .");
        mSelectedView.clear();
    }
    mSelectedMax = count;
}

public Set<Integer> getSelectedList()
{
    return new HashSet<Integer>(mSelectedView);
}

private void doSelect(TagView child, int position)
{
    if (mAutoSelectEffect)
    {
        if (!child.isChecked())
        {
            //处理max_select=1的情况
            if (mSelectedMax == 1 && mSelectedView.size() == 1)
            {
                Iterator<Integer> iterator = mSelectedView.iterator();
                Integer preIndex = iterator.next();
                TagView pre = (TagView) getChildAt(preIndex);
                pre.setChecked(false);
                child.setChecked(true);
                mSelectedView.remove(preIndex);
                mSelectedView.add(position);
            } else
            {
                if (mSelectedMax > 0 && mSelectedView.size() >= mSelectedMax)
                    return;
                child.setChecked(true);
                mSelectedView.add(position);
            }
        } else
        {
            child.setChecked(false);
            mSelectedView.remove(position);
        }
        if (mOnSelectListener != null)
        {
            mOnSelectListener.onSelected(new HashSet<Integer>(mSelectedView));
        }
    }
}

public TagAdapter getAdapter()
{
    return mTagAdapter;
}


private static final String KEY_CHOOSE_POS = "key_choose_pos";
private static final String KEY_DEFAULT = "key_default";


@Override
protected Parcelable onSaveInstanceState()
{
    Bundle bundle = new Bundle();
    bundle.putParcelable(KEY_DEFAULT, super.onSaveInstanceState());

    String selectPos = "";
    if (mSelectedView.size() > 0)
    {
        for (int key : mSelectedView)
        {
            selectPos += key + "|";
        }
        selectPos = selectPos.substring(0, selectPos.length() - 1);
    }
    bundle.putString(KEY_CHOOSE_POS, selectPos);
    return bundle;
}

@Override
protected void onRestoreInstanceState(Parcelable state)
{
    if (state instanceof Bundle)
    {
        Bundle bundle = (Bundle) state;
        String mSelectPos = bundle.getString(KEY_CHOOSE_POS);
        if (!TextUtils.isEmpty(mSelectPos))
        {
            String[] split = mSelectPos.split("\\|");
            for (String pos : split)
            {
                int index = Integer.parseInt(pos);
                mSelectedView.add(index);

                TagView tagView = (TagView) getChildAt(index);
                if (tagView != null)
                    tagView.setChecked(true);
            }

        }
        super.onRestoreInstanceState(bundle.getParcelable(KEY_DEFAULT));
        return;
    }
    super.onRestoreInstanceState(state);
}

private int findPosByView(View child)
{
    final int cCount = getChildCount();
    for (int i = 0; i < cCount; i++)
    {
        View v = getChildAt(i);
        if (v == child) return i;
    }
    return -1;
}

private TagView findChild(int x, int y)
{
    final int cCount = getChildCount();
    for (int i = 0; i < cCount; i++)
    {
        TagView v = (TagView) getChildAt(i);
        if (v.getVisibility() == View.GONE) continue;
        Rect outRect = new Rect();
        v.getHitRect(outRect);
        if (outRect.contains(x, y))
        {
            return v;
        }
    }
    return null;
}

@Override
public void onChanged()
{
    mSelectedView.clear();
    changeAdapter();
}

public static int dip2px(Context context, float dpValue) {
    final float scale = context.getResources().getDisplayMetrics().density;
    return (int) (dpValue * scale + 0.5f);
} }

当我们使用为TabFlowLayout设置adapter时,会调用到changeAdapter(),其实核心功能就这两句:
tagViewContainer.addView(tagView);
addView(tagViewContainer);
将adapter的getview中获取的view对象不断添加到TabFlowLayout中,并设置其选中状态,到这里基本上已经可以完成布局的流式显示了。最后让我们来分析下事件监听是如何处理的:
先来看下OnSelectListener事件,主要在doSelect()中处理的回调,而doSelect()是在performClick()调用的,那么就意味着只要有item的点击事件,就会回调OnSelectListener监听并将mSelectedView传回供开发者进行数据处理,同理performClick()也对mOnTagClickListener监听进行了处理,主要用来处理单个点击事件,到这里基本上完成了对FlowLayout的分析。

至于TagView我们需要知道的是TagView其实代表的是我们在布局中定义的item,作者在代码中通过TagAdapter的getview将item转化成了TagView,并通过checkable接口设置tagView的选中状态。

tagView与TagView的源码比较简单,读者可自行阅读TagView与TagAdapter源码。

到这里基本上完成了从FlowLayout的使用到源码解读的过程。

文章到此结束,感谢大家的阅读。

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

推荐阅读更多精彩内容