Android自定義控件進(jìn)階篇设江,自定義LayoutManager

前言

「知足常樂」锦茁,很多人不滿足現(xiàn)狀,各種折騰叉存,往往舍本逐末码俩,常樂才能少一分浮躁,多一分寧靜歼捏。近期在小編身上發(fā)生了許多事情稿存,心態(tài)也發(fā)生了很大的改變,有感于現(xiàn)實(shí)的無奈瞳秽,在離家鄉(xiāng)遙遠(yuǎn)城市里的落寂挠铲,追逐名利的浮躁;可能生活就是這樣的寂诱,每個年齡段都有自己的煩惱拂苹。

說道折騰,很久以前就看到了各種自定義LayoutManager做出各種炫酷的動畫痰洒,就想自己也要實(shí)現(xiàn)瓢棒。但每次都因?yàn)橄到y(tǒng)自帶的LinearLayoutManager源碼搞得一臉懵逼。正好這段時間不忙丘喻,折騰了一天脯宿,寫了個簡單的Demo,效果如下:

效果預(yù)覽

在這里插入圖片描述
在這里插入圖片描述

RecyclerView的重要性不必多說泉粉,據(jù)過往開發(fā)經(jīng)驗(yàn)而談连霉,超過一屏可滑動的界面,基本都可以采用 「RecyclerView的多類型」 來做嗡靡,不僅維護(hù)還是擴(kuò)展都是非常有效率的跺撼。RecyclerView相關(guān)的面試題也是各大廠常問的問題之一(權(quán)重非常高)。

使用

    mRecyclerView.setLayoutManager(stackLayoutManager = new StackLayoutManager(this));

跟系統(tǒng)的LinearLayoutManager使用方式一致讨彼,文本只是簡單的Demo歉井,功能單一,主要講解流程與步驟哈误,請根據(jù)特定的需求修改哩至。

各屬性意義見圖:


在這里插入圖片描述

湊合看躏嚎,由于ps太爛。注意:因?yàn)閕tem隨著滑動會有不同的縮放菩貌,所以實(shí)際normalViewGap會被縮放計(jì)算卢佣。

自定義LayoutManager基礎(chǔ)知識

有關(guān)自定義LayoutManager基礎(chǔ)知識,請查閱以下文章箭阶,寫的非常棒:

1珠漂、陳小緣的自定義LayoutManager第十一式之飛龍?jiān)谔欤ㄐ【壌罄凶远x文章邏輯清晰明了,堪稱教科書尾膊,非常經(jīng)典)

https://blog.csdn.net/u011387817/article/details/81875021

2媳危、 張旭童的掌握自定義LayoutManager(一) 系列開篇 常見誤區(qū)、問題冈敛、注意事項(xiàng)待笑,常用API

https://blog.csdn.net/zxt0601/article/details/52948009

3、張旭童的掌握自定義LayoutManager(二) 實(shí)現(xiàn)流式布局

https://blog.csdn.net/zxt0601/article/details/52956504

4抓谴、勇朝陳的Android仿豆瓣書影音頻道推薦表單堆疊列表RecyclerView-LayoutManager

https://blog.csdn.net/ccy0122/article/details/90515386

這幾篇文章針對自定義LayoutManager的誤區(qū)暮蹂、注意事項(xiàng),分析的非常到位癌压,來來回回我看了好幾篇仰泻,希望對你有所幫助。

自定義LayoutManager基本流程

讓Items顯示出來

我們在自定義ViewGroup中滩届,想要顯示子View集侯,無非就三件事:

  1. 添加 通過addView方法把子View添加進(jìn)ViewGroup或直接在xml中直接添加;
  2. 測量 重寫onMeasure方法并在這里決定自身尺寸以及每一個子View大兄南棠枉;
  3. 布局 重寫onLayout方法,在里面調(diào)用子View的layout方法來確定它的位置和尺寸泡挺;

其實(shí)在自定義LayoutManager中辈讶,在流程上也是差不多的,我們需要重寫onLayoutChildren方法娄猫,這個方法會在初始化或者Adapter數(shù)據(jù)集更新時回調(diào)贱除,在這方法里面,需要做以下事情:

  1. 進(jìn)行布局之前媳溺,我們需要調(diào)用detachAndScrapAttachedViews方法把屏幕中的Items都分離出來月幌,內(nèi)部調(diào)整好位置和數(shù)據(jù)后,再把它添加回去(如果需要的話)褂删;
  2. 分離了之后飞醉,我們就要想辦法把它們再添加回去了冲茸,所以需要通過addView方法來添加屯阀,那這些View在哪里得到呢缅帘? 我們需要調(diào)用 Recycler的getViewForPosition(int position) 方法來獲取难衰;
  3. 獲取到Item并重新添加了之后钦无,我們還需要對它進(jìn)行測量,這時候可以調(diào)用measureChild或measureChildWithMargins方法盖袭,兩者的區(qū)別我們已經(jīng)了解過了失暂,相信同學(xué)們都能根據(jù)需求選擇更合適的方法;
  4. 在測量完還需要做什么呢鳄虱? 沒錯弟塞,就是布局了,我們也是根據(jù)需求來決定使用layoutDecorated還是layoutDecoratedWithMargins方法拙已;
  5. 在自定義ViewGroup中决记,layout完就可以運(yùn)行看效果了,但在LayoutManager還有一件非常重要的事情倍踪,就是回收了系宫,我們在layout之后,還要把一些不再需要的Items回收建车,以保證滑動的流暢度扩借;

以上內(nèi)容出自陳小緣的自定義LayoutManager第十一式之飛龍?jiān)谔?/a>。

布局實(shí)現(xiàn)

再看下相關(guān)參數(shù):


在這里插入圖片描述

如果去掉itemView的縮放缤至,透明度動畫潮罪,那么效果是這樣的:


在這里插入圖片描述

看到的效果與LinearLayoutManager一樣,但本篇并不使用LinearLayoutManager领斥,而是通過自定義LayoutManager來實(shí)現(xiàn)错洁。

索引值為0的view 一次完全滑出屏幕所需要的移動距離,定位為 firstChildCompleteScrollLength 戒突;非索引值為0的view滑出屏幕所需要移動的距離為:
firstChildCompleteScrollLength + onceCompleteScrollLength ; item 之間的間距為 normalViewGap

我們在 scrollHorizontallyBy 方法中記錄偏移量 dx屯碴,保存一個累計(jì)偏移量 mHorizontalOffset ,然后針對索引值為0與非0兩種情況膊存,在 mHorizontalOffset 小于 firstChildCompleteScrollLength 情況下导而,用該偏移量除以 firstChildCompleteScrollLength 獲取到已經(jīng)滾動了的百分比 fraction ;同理索引值非0的情況下隔崎,偏移量需要減去 firstChildCompleteScrollLength 來獲取到滾動的百分比今艺。根據(jù)百分比,怎么布局childview就很容易了爵卒。

接下來開始寫代碼虚缎,先取個比較接地氣的名字,就叫 StackLayoutManager 钓株,好普通的名字实牡,哈哈陌僵。

StackLayoutManager 繼承 RecyclerView.LayoutManager ,需要重寫 generateDefaultLayoutParams 方法:

    @Override
    public RecyclerView.LayoutParams generateDefaultLayoutParams() {
        return new RecyclerView.LayoutParams(RecyclerView.LayoutParams.WRAP_CONTENT, RecyclerView.LayoutParams.WRAP_CONTENT);
    }

先看看成員變量:

    /**
     * 一次完整的聚焦滑動所需要的移動距離
     */
    private float onceCompleteScrollLength = -1;

    /**
     * 第一個子view的偏移量
     */
    private float firstChildCompleteScrollLength = -1;

    /**
     * 屏幕可見第一個view的position
     */
    private int mFirstVisiPos;

    /**
     * 屏幕可見的最后一個view的position
     */
    private int mLastVisiPos;

    /**
     * 水平方向累計(jì)偏移量
     */
    private long mHorizontalOffset;

    /**
     * view之間的margin
     */
    private float normalViewGap = 30;

    private int childWidth = 0;

    /**
     * 是否自動選中
     */
    private boolean isAutoSelect = true;
    // 選中動畫
    private ValueAnimator selectAnimator;

接著看看 scrollHorizontallyBy 方法:

    @Override
    public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
        // 手指從右向左滑動创坞,dx > 0; 手指從左向右滑動碗短,dx < 0;
        // 位移0、沒有子View 當(dāng)然不移動
        if (dx == 0 || getChildCount() == 0) {
            return 0;
        }

        // 誤差處理
        float realDx = dx / 1.0f;
        if (Math.abs(realDx) < 0.00000001f) {
            return 0;
        }

        mHorizontalOffset += dx;

        dx = fill(recycler, state, dx);

        return dx;
    }

    private int fill(RecyclerView.Recycler recycler, RecyclerView.State state, int dx) {
        int resultDelta = dx;
        resultDelta = fillHorizontalLeft(recycler, state, dx);
        recycleChildren(recycler);
        return resultDelta;
    }

    private int fillHorizontalLeft(RecyclerView.Recycler recycler, RecyclerView.State state, int dx) {
        //----------------1题涨、邊界檢測-----------------
        if (dx < 0) {
            // 已到達(dá)左邊界
            if (mHorizontalOffset < 0) {
                mHorizontalOffset = dx = 0;
            }
        }

        if (dx > 0) {
            if (mHorizontalOffset >= getMaxOffset()) {
                // 根據(jù)最大偏移量來計(jì)算滑動到最右側(cè)邊緣
                mHorizontalOffset = (long) getMaxOffset();
                dx = 0;
            }
        }

        // 分離全部的view偎谁,加入到臨時緩存
        detachAndScrapAttachedViews(recycler);

        float startX = 0;
        float fraction = 0f;
        boolean isChildLayoutLeft = true;

        View tempView = null;
        int tempPosition = -1;

        if (onceCompleteScrollLength == -1) {
            // 因?yàn)閙FirstVisiPos在下面可能被改變,所以用tempPosition暫存一下
            tempPosition = mFirstVisiPos;
            tempView = recycler.getViewForPosition(tempPosition);
            measureChildWithMargins(tempView, 0, 0);
            childWidth = getDecoratedMeasurementHorizontal(tempView);
        }

        // 修正第一個可見view mFirstVisiPos 已經(jīng)滑動了多少個完整的onceCompleteScrollLength就代表滑動了多少個item
        firstChildCompleteScrollLength = getWidth() / 2 + childWidth / 2;
        if (mHorizontalOffset >= firstChildCompleteScrollLength) {
            startX = normalViewGap;
            onceCompleteScrollLength = childWidth + normalViewGap;
            mFirstVisiPos = (int) Math.floor(Math.abs(mHorizontalOffset - firstChildCompleteScrollLength) / onceCompleteScrollLength) + 1;
            fraction = (Math.abs(mHorizontalOffset - firstChildCompleteScrollLength) % onceCompleteScrollLength) / (onceCompleteScrollLength * 1.0f);
        } else {
            mFirstVisiPos = 0;
            startX = getMinOffset();
            onceCompleteScrollLength = firstChildCompleteScrollLength;
            fraction = (Math.abs(mHorizontalOffset) % onceCompleteScrollLength) / (onceCompleteScrollLength * 1.0f);
        }

        // 臨時將mLastVisiPos賦值為getItemCount() - 1纲堵,放心巡雨,下面遍歷時會判斷view是否已溢出屏幕,并及時修正該值并結(jié)束布局
        mLastVisiPos = getItemCount() - 1;

        float normalViewOffset = onceCompleteScrollLength * fraction;
        boolean isNormalViewOffsetSetted = false;

        //----------------3席函、開始布局-----------------
        for (int i = mFirstVisiPos; i <= mLastVisiPos; i++) {
            View item;
            if (i == tempPosition && tempView != null) {
                // 如果初始化數(shù)據(jù)時已經(jīng)取了一個臨時view
                item = tempView;
            } else {
                item = recycler.getViewForPosition(i);
            }

            addView(item);
            measureChildWithMargins(item, 0, 0);

            if (!isNormalViewOffsetSetted) {
                startX -= normalViewOffset;
                isNormalViewOffsetSetted = true;
            }

            int l, t, r, b;
            l = (int) startX;
            t = getPaddingTop();
            r = l + getDecoratedMeasurementHorizontal(item);
            b = t + getDecoratedMeasurementVertical(item);

            layoutDecoratedWithMargins(item, l, t, r, b);

            startX += (childWidth + normalViewGap);

            if (startX > getWidth() - getPaddingRight()) {
                mLastVisiPos = i;
                break;
            }
        }
        return dx;
    }

涉及的方法:

    /**
     * 最大偏移量
     *
     * @return
     */
    private float getMaxOffset() {
        if (childWidth == 0 || getItemCount() == 0) return 0;
        return (childWidth + normalViewGap) * (getItemCount() - 1);
    }
 
    /**
     * 獲取某個childView在水平方向所占的空間鸯隅,將margin考慮進(jìn)去
     *
     * @param view
     * @return
     */
    public int getDecoratedMeasurementHorizontal(View view) {
        final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
                view.getLayoutParams();
        return getDecoratedMeasuredWidth(view) + params.leftMargin
                + params.rightMargin;
    }

    /**
     * 獲取某個childView在豎直方向所占的空間,將margin考慮進(jìn)去
     *
     * @param view
     * @return
     */
    public int getDecoratedMeasurementVertical(View view) {
        final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
                view.getLayoutParams();
        return getDecoratedMeasuredHeight(view) + params.topMargin
                + params.bottomMargin;
    }

回收復(fù)用

這里使用Android仿豆瓣書影音頻道推薦表單堆疊列表RecyclerView-LayoutManager中使用的回收技巧:

 /**
     * @param recycler
     * @param state
     * @param delta
     */
    private int fill(RecyclerView.Recycler recycler, RecyclerView.State state, int delta) {
        int resultDelta = delta;
        //。向挖。蝌以。省略
        
        recycleChildren(recycler);
       log("childCount= [" + getChildCount() + "]" + ",[recycler.getScrapList().size():" + recycler.getScrapList().size());
        return resultDelta;
    }
    
    /**
     * 回收需回收的Item。
     */
    private void recycleChildren(RecyclerView.Recycler recycler) {
        List<RecyclerView.ViewHolder> scrapList = recycler.getScrapList();
        for (int i = 0; i < scrapList.size(); i++) {
            RecyclerView.ViewHolder holder = scrapList.get(i);
            removeAndRecycleView(holder.itemView, recycler);
        }
    }

回收復(fù)用這里就不驗(yàn)證了何之,感興趣的小伙伴可自行驗(yàn)證跟畅。

動畫效果

    private int fillHorizontalLeft(RecyclerView.Recycler recycler, RecyclerView.State state, int dx) {
        // 省略 ......
        //----------------3、開始布局-----------------
        for (int i = mFirstVisiPos; i <= mLastVisiPos; i++) {
            // 省略 ......
            
            // 縮放子view
            final float minScale = 0.6f;
            float currentScale = 0f;
            final int childCenterX = (r + l) / 2;
            final int parentCenterX = getWidth() / 2;
            isChildLayoutLeft = childCenterX <= parentCenterX;
            if (isChildLayoutLeft) {
                final float fractionScale = (parentCenterX - childCenterX) / (parentCenterX * 1.0f);
                currentScale = 1.0f - (1.0f - minScale) * fractionScale;
            } else {
                final float fractionScale = (childCenterX - parentCenterX) / (parentCenterX * 1.0f);
                currentScale = 1.0f - (1.0f - minScale) * fractionScale;
            }
            item.setScaleX(currentScale);
            item.setScaleY(currentScale);
            item.setAlpha(currentScale);
            
            layoutDecoratedWithMargins(item, l, t, r, b);
           // 省略 ......
        }
        return dx;
    }

childView 越向屏幕中間移動縮放比越大溶推,越向兩邊移動縮放比越小徊件。

自動選中

1、滾動停止后自動選中

監(jiān)聽 onScrollStateChanged蒜危,在滾動停止時計(jì)算出應(yīng)當(dāng)停留的 position虱痕,再計(jì)算出停留時的 mHorizontalOffset 值,播放屬性動畫將當(dāng)前 mHorizontalOffset 不斷更新至最終值即可辐赞。相關(guān)代碼如下:

    @Override
    public void onScrollStateChanged(int state) {
        super.onScrollStateChanged(state);
        switch (state) {
            case RecyclerView.SCROLL_STATE_DRAGGING:
                //當(dāng)手指按下時响委,停止當(dāng)前正在播放的動畫
                cancelAnimator();
                break;
            case RecyclerView.SCROLL_STATE_IDLE:
                //當(dāng)列表滾動停止后夹囚,判斷一下自動選中是否打開
                if (isAutoSelect) {
                    //找到離目標(biāo)落點(diǎn)最近的item索引
                    smoothScrollToPosition(findShouldSelectPosition());
                }
                break;
            default:
                break;
        }
    }
 
     /**
     * 平滑滾動到某個位置
     *
     * @param position 目標(biāo)Item索引
     */
    public void smoothScrollToPosition(int position) {
        if (position > -1 && position < getItemCount()) {
            startValueAnimator(position);
        }
    }

    private int findShouldSelectPosition() {
        if (onceCompleteScrollLength == -1 || mFirstVisiPos == -1) {
            return -1;
        }
        int position = (int) (Math.abs(mHorizontalOffset) / (childWidth + normalViewGap));
        int remainder = (int) (Math.abs(mHorizontalOffset) % (childWidth + normalViewGap));
        // 超過一半,應(yīng)當(dāng)選中下一項(xiàng)
        if (remainder >= (childWidth + normalViewGap) / 2.0f) {
            if (position + 1 <= getItemCount() - 1) {
                return position + 1;
            }
        }
        return position;
    }

    private void startValueAnimator(int position) {
        cancelAnimator();

        final float distance = getScrollToPositionOffset(position);

        long minDuration = 100;
        long maxDuration = 300;
        long duration;

        float distanceFraction = (Math.abs(distance) / (childWidth + normalViewGap));

        if (distance <= (childWidth + normalViewGap)) {
            duration = (long) (minDuration + (maxDuration - minDuration) * distanceFraction);
        } else {
            duration = (long) (maxDuration * distanceFraction);
        }
        selectAnimator = ValueAnimator.ofFloat(0.0f, distance);
        selectAnimator.setDuration(duration);
        selectAnimator.setInterpolator(new LinearInterpolator());
        final float startedOffset = mHorizontalOffset;
        selectAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float value = (float) animation.getAnimatedValue();
                mHorizontalOffset = (long) (startedOffset + value);
                requestLayout();
            }
        });
        selectAnimator.start();
    }
2舵抹、點(diǎn)擊非焦點(diǎn)view自動將其選中為焦點(diǎn)view

我們可以直接拿到 viewposition堰燎,直接調(diào)用 smoothScrollToPosition 方法秆剪,就可以實(shí)現(xiàn)自動選中為焦點(diǎn)。

中間view覆蓋在兩邊view之上

效果是這樣的:


在這里插入圖片描述

從效果中可以看出,索引為2的view覆蓋在1,3的上面汤锨,同時1又覆蓋在0的上面双抽,以此內(nèi)推柬泽。

RecyclerView 繼承于 ViewGroup ,那么在添加子view addView(View child, int index)index 的索引值越大有决,越顯示在上層揽趾。那么可以得出痒芝,為2的綠色卡片被添加是 index 最大牵素,分析可以得出以下結(jié)論:

index 的大小:

0 < 1 < 2 > 3 > 4

中間最大榕堰,兩邊逐漸減小的原則砍的。

獲取到中間 view 的索引值,如果小于等于該索引值則調(diào)用 addView(item) 沫勿,反之調(diào)用 addView(item, 0) 挨约;相關(guān)代碼如下:

    private int fillHorizontalLeft(RecyclerView.Recycler recycler, RecyclerView.State state, int dx) {
        //省略 ......
        //----------------3、開始布局-----------------
        for (int i = mFirstVisiPos; i <= mLastVisiPos; i++) {
             //省略 ......
            int focusPosition = (int) (Math.abs(mHorizontalOffset) / (childWidth + normalViewGap));
            if (i <= focusPosition) {
                addView(item);
            } else {
                addView(item, 0);
            }
             //省略 ...... 
        }
        return dx;
    }

文章到這里就差不多要結(jié)束了产雹。

源碼地址:

https://github.com/HpWens/MeiWidgetView

給個star唄~

結(jié)語

愛笑的人诫惭,運(yùn)氣一般都不會太差。同時也給自己一個鼓勵蔓挖,我們下期見夕土。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市瘟判,隨后出現(xiàn)的幾起案子怨绣,更是在濱河造成了極大的恐慌,老刑警劉巖拷获,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件篮撑,死亡現(xiàn)場離奇詭異,居然都是意外死亡匆瓜,警方通過查閱死者的電腦和手機(jī)赢笨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進(jìn)店門未蝌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人茧妒,你說我怎么就攤上這事萧吠。” “怎么了桐筏?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵纸型,是天一觀的道長。 經(jīng)常有香客問我梅忌,道長狰腌,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任铸鹰,我火速辦了婚禮癌别,結(jié)果婚禮上皂岔,老公的妹妹穿的比我還像新娘蹋笼。我一直安慰自己,他們只是感情好躁垛,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布剖毯。 她就那樣靜靜地躺著,像睡著了一般教馆。 火紅的嫁衣襯著肌膚如雪逊谋。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天土铺,我揣著相機(jī)與錄音胶滋,去河邊找鬼。 笑死悲敷,一個胖子當(dāng)著我的面吹牛究恤,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播后德,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼部宿,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了瓢湃?” 一聲冷哼從身側(cè)響起理张,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎绵患,沒想到半個月后雾叭,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡落蝙,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年织狐,在試婚紗的時候發(fā)現(xiàn)自己被綠了作煌。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡赚瘦,死狀恐怖粟誓,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情起意,我是刑警寧澤鹰服,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站揽咕,受9級特大地震影響悲酷,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜亲善,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一设易、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蛹头,春花似錦顿肺、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至耕拷,卻和暖如春讼昆,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背骚烧。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工浸赫, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人赃绊。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓既峡,卻偏偏與公主長得像,于是被迫代替她去往敵國和親凭戴。 傳聞我的和親對象是個殘疾皇子涧狮,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評論 2 345