MagicIndicator系列之三 —— MagicIndicator原理淺析及擴展MagicIndicator的4種方式

這是 MagicIndicator 系列的第三篇文章,如果你沒有看過前兩篇,建議出門先看一下。當然你不看也沒關系,我用一句話來介紹它: MagicIndicator 是一個可定制、易擴展的頁面指示器框架,使用它可極大的簡化頁面指示器的開發。

本文將給大家簡單闡述 MagicIndicator 的原理,并介紹 4 種擴展 MagicIndicator 的方式,分別是:

  1. 繼承 IPagerNavigator 打造任意的指示效果
  2. 繼承 IPagerTitleView 打造任意效果的指示器標題
  3. 繼承 IPagerIndicator 打造任意效果的指示器
  4. 使用 CommonPagerTitleView 加載自定義布局

使用這四種方法,基本可以搞定所有的指示器效果,沒有做不到,只有想不到!

原理淺析


MagicIndicator 其實非常簡單。和其它所有指示器框架一樣,也是通過監聽 ViewPager.OnPageChangeListener 來實現切換效果的。但 MagicIndicator 有兩點明顯不同:

  • MagicIndicator 不提供 setViewPager 方法來和 ViewPager 強綁定,因此在不使用 ViewPager 的情況下(比如手動切換 Fragment,輕量級的廣告輪播控件,ViewFlipper 等),也是可以使用 MagicIndicator 的,只需要你手動調用 onPageXXX 系列方法。

  • MagicIndicator 將指示器進行了抽象,意在通過擴展來實現不同的切換效果,而不是像其他所有指示器框架那樣,提供了一大堆的 setter 方法,卻只能實現很有限的切換效果。

在布局文件中使用的 <MagicIndicator/> 標簽,本質上就是一個 FrameLayout:

public class MagicIndicator extends FrameLayout {
    private IPagerNavigator mNavigator;

    public MagicIndicator(Context context) {...}

    public MagicIndicator(Context context, AttributeSet attrs) {...}

    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        if (mNavigator != null) {
            mNavigator.onPageScrolled(position, positionOffset, positionOffsetPixels);
        }
    }

    public void onPageSelected(int position) {
        if (mNavigator != null) {
            mNavigator.onPageSelected(position);
        }
    }

    public void onPageScrollStateChanged(int state) {
        if (mNavigator != null) {
            mNavigator.onPageScrollStateChanged(state);
        }
    }

    public void setNavigator(IPagerNavigator navigator) {...}
}

MagicIndicator 中,指示器(也許叫導航器更為恰當)被抽象成了 IPagerNavigator,設置到 MagicIndicator 類中的 IPagerNavigator 被作為唯一的子元素添加到其中。onPageXXX 系列回調原封不動的傳遞給了 IPagerNavigator。因此,要想實現不同的指示器效果,只需繼承任意的 View 并實現 IPagerNavigator 接口即可。

考慮大多數情況下的指示器(導航器)都類似下面的效果:

magicindicator.gif

MagicIndicator 中內置了一個 CommonNavigator 來簡化這樣的指示器(導航器)的開發,CommonNavigator 繼承了 FrameLayout 并實現了 IPagerNavigator 接口,并根據指示器標題是否可變(數目是否可變,比如新聞應用的頻道數就可變)來加載不同的子元素(布局文件),如下:

指示器標題可變,可滾動

<?xml version="1.0" encoding="utf-8"?>
<HorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/scroll_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fadingEdge="none"
    android:scrollbars="none">

    <FrameLayout
        android:layout_width="wrap_content"
        android:layout_height="match_parent">

        <LinearLayout
            android:id="@+id/indicator_container"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="horizontal" />

        <LinearLayout
            android:id="@+id/title_container"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:orientation="horizontal" />

    </FrameLayout>

</HorizontalScrollView>

指示器標題不可變

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        android:id="@+id/indicator_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal" />

    <LinearLayout
        android:id="@+id/title_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal" />

</FrameLayout>

這兩個布局中都有兩個 LinearLayout,分別是 indicator_container 和 title_container,它倆被放在一個 FrameLayout 中,我想你已經明白了:指示器標題在指示器的上方,分別位于兩層,互不影響。indicator_container 的寬高和 title_container 相等

title_container 的子元素被抽象成了 IPagerTitleView,如下:

public interface IPagerTitleView {
    /**
     * 被選中
     */
    void onSelected(int index, int totalCount);

    /**
     * 未被選中
     */
    void onDeselected(int index, int totalCount);

    /**
     * 離開
     *
     * @param leavePercent 離開的百分比, 0.0f - 1.0f
     * @param leftToRight  從左至右離開
     */
    void onLeave(int index, int totalCount, float leavePercent, boolean leftToRight);

    /**
     * 進入
     *
     * @param enterPercent 進入的百分比, 0.0f - 1.0f
     * @param leftToRight  從左至右進入
     */
    void onEnter(int index, int totalCount, float enterPercent, boolean leftToRight);
}

onPageXXX 系列回調被 NavigatorHelper 轉換成了 onEnter、onLeave、onSelected、onDeselected 4 個回調傳遞給 IPagerTitleView。通過這 4 個回調,可實現各種各樣炫酷的效果。關于 onEnter 和 onLeave 回調,我打個比方:從 ViewPager 的第 2 頁切換到第 3 頁過程中,第 2 個 IPagerTitleView 會不斷收到 onLeave 回調,leavePerent 從 0.0f 漸變為 1.0f,leftToRight 始終為 true,第 3 個 IPagerTitleView 會不斷收到 onEnter 回調, enterPercent 從 0.0f 漸變成 1.0f,leftToRight 始終為 true。

indicator_container 僅有一個子元素且它被抽象成了 IPagerIndicator:

public interface IPagerIndicator {
    void onPageScrolled(int position, float positionOffset, int positionOffsetPixels);

    void onPageSelected(int position);

    void onPageScrollStateChanged(int state);

    void onPositionDataProvide(List<PositionData> dataList);
}

onPageXXX 系列回調原封不動的傳遞給了它,此外,還有個最重要的 onPositionDataProvide 回調,這個是干嘛的呢?試想一下,如果要使得擴展 IPagerIndicator 可以實現任意的切換效果,那最起碼應該把每一個 IPagerTitleView 的位置信息傳遞給 IPagerIndicator 吧,有了這些位置信息,繼承 View 并實現 IPagerIndicator 后,不論是畫圓還是畫直線,或是畫上圖中的貝塞爾吸附式效果,才有坐標可循啊。

我們看一下 PositionData 類:

public class PositionData {
    public int mLeft;
    public int mTop;
    public int mRight;
    public int mBottom;
    public int mContentLeft;
    public int mContentTop;
    public int mContentRight;
    public int mContentBottom;

    public int width() {
        return mRight - mLeft;
    }

    public int height() {
        return mBottom - mTop;
    }

    public int contentWidth() {
        return mContentRight - mContentLeft;
    }

    public int contentHeight() {
        return mContentBottom - mContentTop;
    }

    public int horizontalCenter() {
        return mLeft + width() / 2;
    }

    public int verticalCenter() {
        return mTop + height() / 2;
    }
}

PositionData 中不僅封裝了 IPagerTitleView 上下左右的位置,還封裝了其內容區域的位置,有內容區域的位置,我們才可能實現上圖中第三個指示器效果:不論 IPagerTitleView 的寬度如何變化,直線寬度始終和內容寬度相等

由于 IPagerTitleView 是抽象的,CommonNavigator 不可能知道其內容區域的邊界到底在哪里,因此還得我們告訴它,要提供內容邊界給 CommonNavigator,實現 IMeasurablePagerTitleView 即可:

public interface IMeasurablePagerTitleView extends IPagerTitleView {
    int getContentLeft();

    int getContentTop();

    int getContentRight();

    int getContentBottom();
}

如果不實現 IMeasuablePagerTitleView,則默認內容區域邊界就是 IPagerTitleView 的邊界(mLeft,mTop,mRight,mBottom)。

繼承 IPagerNavigator


一般情況下,使用 CommonNavigator 就能滿足需求。但是當遇到一些明顯 CommonNavigator 搞不定的情況,比如 Smartisan OS 桌面的指示器效果:

smartisan.gif

就需要繼承 View,實現 IPagerNavigator 接口,拿起手里的 Canvas 開畫吧!

額,今天就不去實現這個效果了,因為需要處理的細節比較多,后面我處理好后會把這個效果上傳到 demo 中,我們來個簡單的,效果如下:

custom_indicator.gif

這個效果沒有跟隨手指的過渡,看起來比較呆板,我就叫它 DummyCircleNavigator 吧:

public class DummyCircleNavigator extends View implements IPagerNavigator {

    public DummyCircleNavigator(Context context) {
        super(context);
    }

    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
    }

    @Override
    public void onPageSelected(int position) {
    }

    @Override
    public void onPageScrollStateChanged(int state) {
    }

    // 被添加到 magicindicator 時調用
    @Override
    public void onAttachToMagicIndicator() {
    }

    // 從 magicindicator 上移除時調用
    @Override
    public void onDetachFromMagicIndicator() {
    }

    // 當指示數目改變時調用
    @Override
    public void notifyDataSetChanged() {
    }
}

除了實現 onPageXXX 系列回調,還需要實現 onAttachToMagicIndicator、onDetachFromMagicIndicator、notifyDataSetChanged 三個方法。

我們需要讓外部來配置圓的半徑、顏色、數量,圓之間的間距以及圓的描邊寬度。同時,我們需要一個變量來表示當前選中了哪一個圓,當然,畫筆也必不可少:

private int mRadius;
private int mCircleColor;
private int mStrokeWidth;
private int mCircleSpacing;
private int mCurrentIndex;
private int mCircleCount;
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

根據用戶設置的 mCircleSpacing,mRadius,mCircleCount,結合當前的寬度,我們可以計算出每一個圓的圓心位置:

private List<PointF> mCirclePoints = new ArrayList<PointF>();

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    prepareCirclePoints();
}

private void prepareCirclePoints() {
    mCirclePoints.clear();
    if (mCircleCount > 0) {
        int y = getHeight() / 2;
        int measureWidth = mCircleCount * mRadius * 2 + (mCircleCount - 1) * mCircleSpacing;
        int centerSpacing = mRadius * 2 + mCircleSpacing;
        int startX = (getWidth() - measureWidth) / 2 + mRadius;
        for (int i = 0; i < mCircleCount; i++) {
            PointF pointF = new PointF(startX, y);
            mCirclePoints.add(pointF);
            startX += centerSpacing;
        }
    }
}

圓心位置已準備就緒,那就開畫吧:

@Override
protected void onDraw(Canvas canvas) {
    drawDeselectedCircles(canvas);
    drawSelectedCircle(canvas);
}

private void drawDeselectedCircles(Canvas canvas) {
    mPaint.setStyle(Paint.Style.STROKE);
    mPaint.setStrokeWidth(mStrokeWidth);
    mPaint.setColor(mCircleColor);
    for (int i = 0, j = mCirclePoints.size(); i < j; i++) {
        PointF pointF = mCirclePoints.get(i);
        canvas.drawCircle(pointF.x, pointF.y, mRadius, mPaint);
    }
}

private void drawSelectedCircle(Canvas canvas) {
    mPaint.setStyle(Paint.Style.FILL);
    if (mCirclePoints.size() > 0) {
        float selectedCircleX = mCirclePoints.get(mCurrentIndex).x;
        canvas.drawCircle(selectedCircleX, getHeight() / 2, mRadius, mPaint);
    }
}

最后,不要忘了給 mCurrentIndex 賦值,同時,mCircleCount 變化時需要重新計算圓心位置:

@Override
public void onPageSelected(int position) {
    mCurrentIndex = position;
    invalidate();
}

public void setCircleCount(int circleCount) {
    mCircleCount = circleCount;
}

@Override
public void notifyDataSetChanged() {
    prepareCirclePoints();
    invalidate();
}

注意,setCircleCount 方法中,并沒有重新計算圓心位置,而是希望外部調用 notifyDataSetChanged 來計算并刷新。希望自定義的 IPagerNavigator 都應該遵守此約定。

好了,大功告成了,是不是很容易!

繼承 IPagerTitleView


如果你使用了 CommonNavigator,但是內置的 IPagerTitleView 無法滿足需求,那就自定義 IPagerTitleView 吧。比如,簡書的這種效果靠內置的 IPagerTitleView 是 hold 不住的:

jianshu.gif

因為它既不是跟隨手指漸變,也不是抬起手指(onPageSelected)才去改變顏色。而是在滑動一段距離后且手指未抬起時去改變顏色。

我們來實現這種效果:直接繼承 TextView 并實現 IPagerTitleView,在 onEnter 回調中做判斷,如果 enterPercent 大于設定的閾值,就將文字顏色設為選中顏色,否則,設為未選中顏色,代碼如下:

public class ColorFlipPagerTitleView extends TextView implements IPagerTitleView {
    private int mNormalColor;
    private int mSelectedColor;
    private float mChangePercent = 0.45f;

    public ColorFlipPagerTitleView(Context context) {
        super(context);
        setGravity(Gravity.CENTER);
        int padding = UIUtil.dip2px(context, 10);
        setPadding(padding, 0, padding, 0);
        setSingleLine();
        setEllipsize(TextUtils.TruncateAt.END);
    }

    @Override
    public void onLeave(int index, int totalCount, float leavePercent, boolean leftToRight) {
        if (leavePercent >= mChangePercent) {
            setTextColor(mNormalColor);
        } else {
            setTextColor(mSelectedColor);
        }
    }

    @Override
    public void onEnter(int index, int totalCount, float enterPercent, boolean leftToRight) {
        if (enterPercent >= mChangePercent) {
            setTextColor(mSelectedColor);
        } else {
            setTextColor(mNormalColor);
        }
    }

    // 部分 setter、getter 略
}

如果你還想提供內容的邊界,那就繼承 IMeasuablePagerTitleView 吧,并實現以下方法:

@Override
public int getContentLeft() {
    Rect bound = new Rect();
    getPaint().getTextBounds(getText().toString(), 0, getText().length(), bound);
    int contentWidth = bound.width();
    return getLeft() + getWidth() / 2 - contentWidth / 2;
}

@Override
public int getContentTop() {
    Paint.FontMetrics metrics = getPaint().getFontMetrics();
    float contentHeight = metrics.bottom - metrics.top;
    return (int) (getHeight() / 2 - contentHeight / 2);
}

@Override
public int getContentRight() {
    Rect bound = new Rect();
    getPaint().getTextBounds(getText().toString(), 0, getText().length(), bound);
    int contentWidth = bound.width();
    return getLeft() + getWidth() / 2 + contentWidth / 2;
}

@Override
public int getContentBottom() {
    Paint.FontMetrics metrics = getPaint().getFontMetrics();
    float contentHeight = metrics.bottom - metrics.top;
    return (int) (getHeight() / 2 + contentHeight / 2);
}

效果如下:

jianshu.gif

繼承 IPagerIndicator


如果你使用了 CommonNavigator,但是內置的 IPagerIndicator hold不住你的需求,那就自定義吧。

目前內置的 IPagerIndicator 全是跟隨手指滑動的,我們來打造一個簡單的、不跟隨的指示器。這個指示器會在被選中的 IPagerTitleView 下方顯示一個小點。

我們繼承 View 并實現 IPagerIndicator,代碼很短,我就全貼代碼了:

public class DotPagerIndicator extends View implements IPagerIndicator {
    private List<PositionData> mDataList;
    private float mRadius;
    private float mYOffset;
    private float mCircleCenterX;
    private int mDotColor;
    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

    public DotPagerIndicator(Context context) {
        super(context);
    }

    @Override
    public void onPageSelected(int position) {
        if (mDataList == null || mDataList.isEmpty()) {
            return;
        }
        PositionData data = mDataList.get(position);
        mCircleCenterX = data.mLeft + data.width() / 2;
        invalidate();
    }

    @Override
    public void onPositionDataProvide(List<PositionData> dataList) {
        mDataList = dataList;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        mPaint.setColor(mDotColor);
        canvas.drawCircle(mCircleCenterX, getHeight() - mYOffset - mRadius, mRadius, mPaint);
    }

    // 一些 getter、setter 略
}

效果如下:

dot.gif

是不是很簡單!

使用 CommonPagerTitleView 加載自定義布局


每當內置的 IPagerTitleView 不滿足需求時,你可以選擇擴展它,但更好的方式是使用 CommonPagerTitleView。CommonPagerTitleView 繼承 FrameLayout 并實現了 IMeasurablePagerTitleView,它支持將自定義的布局文件設置進來,并且把 onEnter、onLeave . . . getContentLeft、getContentTop 等方法都回調出去,交給外面去實現,代碼如下:

public class CommonPagerTitleView extends FrameLayout implements IMeasurablePagerTitleView {
    private OnPagerTitleChangeListener mOnPagerTitleChangeListener;
    private ContentPositionDataProvider mContentPositionDataProvider;

    public CommonPagerTitleView(Context context) {
        super(context);
    }

    public void setContentView(int layoutId) {
        View child = LayoutInflater.from(getContext()).inflate(layoutId, null);
        setContentView(child, null);
    }

    @Override
    public void onSelected(int index, int totalCount) {
        if (mOnPagerTitleChangeListener != null) {
            mOnPagerTitleChangeListener.onSelected(index, totalCount);
        }
    }

    // 省略一部分方法

    public interface OnPagerTitleChangeListener {
        void onSelected(int index, int totalCount);

        void onDeselected(int index, int totalCount);

        void onLeave(int index, int totalCount, float leavePercent, boolean leftToRight);

        void onEnter(int index, int totalCount, float enterPercent, boolean leftToRight);
    }

    public interface ContentPositionDataProvider {
        int getContentLeft();

        int getContentTop();

        int getContentRight();

        int getContentBottom();
    }
}

上面的大圖中的最后一個效果就是這么做的。我們先定義一個布局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical"
    android:paddingLeft="10dp"
    android:paddingRight="10dp">

    <ImageView
        android:id="@+id/title_img"
        android:layout_width="20dp"
        android:layout_height="20dp" />

    <TextView
        android:id="@+id/title_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="5dp" />

</LinearLayout>

再將布局文件設置到 CommonPagerTitleView 并進行初始化:

@Override
public IPagerTitleView getTitleView(Context context, final int index) {
    CommonPagerTitleView commonPagerTitleView = new CommonPagerTitleView(MainActivity.this);
    commonPagerTitleView.setContentView(R.layout.simple_pager_title_layout);

    // 初始化
    final ImageView titleImg = (ImageView) commonPagerTitleView.findViewById(R.id.title_img);
    titleImg.setImageResource(R.mipmap.ic_launcher);
    final TextView titleText = (TextView) commonPagerTitleView.findViewById(R.id.title_text);
    titleText.setText(mDataList.get(index));

    commonPagerTitleView.setOnPagerTitleChangeListener(new CommonPagerTitleView.OnPagerTitleChangeListener() {

        @Override
        public void onSelected(int index, int totalCount) {
            titleText.setTextColor(Color.RED);
        }

        @Override
        public void onDeselected(int index, int totalCount) {
            titleText.setTextColor(Color.BLACK);
        }

        @Override
        public void onLeave(int index, int totalCount, float leavePercent, boolean leftToRight) {
            titleImg.setScaleX(1.3f + (0.8f - 1.3f) * leavePercent);
            titleImg.setScaleY(1.3f + (0.8f - 1.3f) * leavePercent);
        }

        @Override
        public void onEnter(int index, int totalCount, float enterPercent, boolean leftToRight) {
            titleImg.setScaleX(0.8f + (1.3f - 0.8f) * enterPercent);
            titleImg.setScaleY(0.8f + (1.3f - 0.8f) * enterPercent);
        }
    });

    return commonPagerTitleView;
}

通過設置一個 OnPagerTitleChangeListener 來實現切換效果。我們再回顧一下效果圖:

custom_layout.gif

結合代碼,我相信你已經完全掌握 CommonPagerTitleView 啦。

結語


今天就是這些。寫長文好累,給個 star 唄,地址:
https://github.com/hackware1993/MagicIndicator。對 MagicIndicator 還有疑問,歡迎加QQ群:373360748

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,619評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,155評論 3 425
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,635評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,539評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,255評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,646評論 1 326
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,655評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,838評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,399評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,146評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,338評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,893評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,565評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,983評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,257評論 1 292
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,059評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,296評論 2 376

推薦閱讀更多精彩內容