下拉刷新、上拉加载更多控件实现原理及解析(一)

之前写在CSDN上的文章,搬过来!

效果预览

接受hi大头鬼hi的建议,来一个动态图,方便大家知道这是个什么东西。

demo
demo

动机

项目中,需要一个支持任意View的下拉刷新+上拉加载控件,GitHub上有很多现成的实现,如Android-PullToRefresh, android-Ultra-Pull-To-Refresh等,这些Library都非常优秀,但是Android-PullToRefresh
已经不在维护了,android-Ultra-Pull-To-Refresh本身并不支持上拉加载更多,经过一番纠结后决定自己写一个。

原理

无论是下拉刷新还是上拉加载更多,原理都是在内容View(ListView、RecyclerView...)不能下拉或者上划时响应用户的触摸事件,在顶部或者底部显示一个刷新视图,在程序刷新操作完成后再隐藏掉。

实现

既然要在头部和顶部添加刷新视图,我们的控件应该是个ViewGroup,我是直接继承FrameLayout,这个控件的名字叫[NsRefreshLayout](https://github.com/xiaolifan/NsRefreshLayout)。然后我们需要定义一些属性,如是否自动触发上拉加载更多、刷新视图中的文字颜色等。

属性定义

<declare-styleable name="NsRefreshLayout">
    <!--Loading视图背景颜色-->
    <attr name="load_view_bg_color" format="color|reference"/>
    <!--进度条颜色-->
    <attr name="progress_bar_color" format="color|reference"/>
    <!--进度条背景色-->
    <attr name="progress_bg_color" format="color|reference"/>
    <!--Loading视图中文字颜色-->
    <attr name="load_text_color" format="color|reference"/>
    <!--下拉刷新问题描述-->
    <attr name="pull_refresh_text" format="string|reference"/>
    <!--上拉加载文字描述-->
    <attr name="pull_load_text" format="string|reference"/>
    <!--是否自动触发加载更多-->
    <attr name="auto_load_more" format="boolean"/>
    <!--下拉刷新是否可用-->
    <attr name="pull_refresh_enable" format="boolean"/>
    <!--上拉加载是否可用-->
    <attr name="pull_load_enable" format="boolean"/>
</declare-styleable>

属性读取

/**
 * 初始化控件属性
 */
private void initAttrs(Context context, AttributeSet attrs) {
    if (getChildCount() > 1) {
        throw new RuntimeException("can only have one child");
    }
    loadingViewFinalHeight = NrlUtils.dipToPx(context, LOADING_VIEW_FINAL_HEIGHT_DP);
    loadingViewOverHeight = loadingViewFinalHeight * 2;

    if (isInEditMode() && attrs == null) {
        return;
    }

    int resId;
    TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.NsRefreshLayout);
    Resources resources = context.getResources();

    //LoadView背景颜色
    resId = ta.getResourceId(R.styleable.NsRefreshLayout_load_view_bg_color, -1);
    if (resId == -1) {
        mLoadViewBgColor = ta.getColor(R.styleable.NsRefreshLayout_load_view_bg_color,
                Color.WHITE);
    } else {
        mLoadViewBgColor = resources.getColor(resId);
    }

    //加载文字颜色
    resId = ta.getResourceId(R.styleable.NsRefreshLayout_load_text_color, -1);
    if (resId == -1) {
        mLoadViewTextColor = ta.getColor(R.styleable.NsRefreshLayout_load_text_color,
                Color.BLACK);
    } else {
        mLoadViewTextColor = resources.getColor(resId);
    }

    //进度条背景颜色
    resId = ta.getResourceId(R.styleable.NsRefreshLayout_progress_bg_color, -1);
    if (resId == -1) {
        mProgressBgColor = ta.getColor(R.styleable.NsRefreshLayout_progress_bg_color,
                Color.WHITE);
    } else {
        mProgressBgColor = resources.getColor(resId);
    }

    //进度条颜色
    resId = ta.getResourceId(R.styleable.NsRefreshLayout_progress_bar_color, -1);
    if (resId == -1) {
        mProgressColor = ta.getColor(R.styleable.NsRefreshLayout_progress_bar_color,
                Color.RED);
    } else {
        mProgressColor = resources.getColor(resId);
    }

    //下拉刷新文字描述
    resId = ta.getResourceId(R.styleable.NsRefreshLayout_pull_refresh_text, -1);
    if (resId == -1) {
        mPullRefreshText = ta.getString(R.styleable.NsRefreshLayout_pull_refresh_text);
    } else {
        mPullRefreshText = resources.getString(resId);
    }

    //上拉加载文字描述
    resId = ta.getResourceId(R.styleable.NsRefreshLayout_pull_load_text, -1);
    if (resId == -1) {
        mPullLoadText = ta.getString(R.styleable.NsRefreshLayout_pull_load_text);
    } else {
        mPullLoadText = resources.getString(resId);
    }

    mAutoLoadMore = ta.getBoolean(R.styleable.NsRefreshLayout_auto_load_more, false);
    mPullRefreshEnable = ta.getBoolean(R.styleable.NsRefreshLayout_pull_refresh_enable, true);
    mPullLoadEnable = ta.getBoolean(R.styleable.NsRefreshLayout_pull_load_enable, true);

    ta.recycle();
}

属性使用

在内容View布局完成后(onFinishInflate),根据设置的属性,来确定是否需要添加下拉刷新视图、上拉加载更多视图,以及视图中的文字颜色、进度条颜色等。
@Override
protected void onFinishInflate() {
    super.onFinishInflate();
    mContentView = getChildAt(0);
    setupViews();
}

private void setupViews() {
    //下拉刷新视图
    LayoutParams lp;
    if (mPullRefreshEnable) {
        lp = new LayoutParams(LayoutParams.MATCH_PARENT, 0);
        headerView = new LoadView(getContext());
        headerView.setLoadText(TextUtils.isEmpty(mPullRefreshText) ?
                getContext().getString(R.string.default_pull_refresh_text) : mPullRefreshText);
        headerView.setStartEndTrim(0, 0.75f);
        headerView.setBackgroundColor(mLoadViewBgColor);
        headerView.setLoadTextColor(mLoadViewTextColor);
        headerView.setProgressBgColor(mProgressBgColor);
        headerView.setProgressColor(mProgressColor);
        addView(headerView, lp);
    }

    if (mPullLoadEnable) {
        //上拉加载更多视图
        lp = new LayoutParams(LayoutParams.MATCH_PARENT, 0);
        lp.gravity = Gravity.BOTTOM;
        footerView = new LoadView(getContext());
        footerView.setLoadText(TextUtils.isEmpty(mPullLoadText) ?
                getContext().getString(R.string.default_pull_load_text) : mPullLoadText);
        footerView.setStartEndTrim(0.5f, 1.25f);
        footerView.setBackgroundColor(mLoadViewBgColor);
        footerView.setLoadTextColor(mLoadViewTextColor);
        footerView.setProgressBgColor(mProgressBgColor);
        footerView.setProgressColor(mProgressColor);
        addView(footerView, lp);
    }
}

动态响应用户配置变化

有这样一种需求,一个列表分页加载,每一页10条,如果在上拉加载更多后只返回8条,说明已经没有更多数据了,所以在列表达到底部,用户再次上划时就不需要触发上拉加载更多了。基于这种需求,我设计了一个接口NsRefreshLayoutController。
public interface NsRefreshLayoutController {
    /**
     * 当前下拉刷新是否可用
     */
    boolean isPullRefreshEnable();

    /**
     * 当前上拉加载是否可用,比如列表已无更多数据,可禁用上拉加载功能
     */
    boolean isPullLoadEnable();
}

使用时,实现这个接口,根据当前数据的情况返回True或者False启用或者禁用两个功能了。控件内部,我们在用户每次触发触摸事件的时候获取接口返回值。

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (refreshLayoutController != null) {
        mPullRefreshEnable = refreshLayoutController.isPullRefreshEnable();
        mPullLoadEnable = refreshLayoutController.isPullLoadEnable();
    }
    return super.onInterceptTouchEvent(ev);
}

处理Touch事件

我们需要做到对Touch事件的处理不影响内容视图的功能,所以我们只处理Touch事件,不消耗Touch事件,一个合适的回调很重要,找来找去我选择了dispatchTouchEvent,官方文档对这个函数的描述如下:



处理Touch事件的流程如下,ACTION\_DOWN、ACTION\_MOVE时记录Touch的位置,ACTION\_MOVE时用当前Touch的位置减去上次DOWN或者MOVE的位置,得到手指滑动的距离,用这个距离来控制内容视图、刷新视图的显示位置,当达到触发刷新的位置后,提示用户松手触发刷新,用户松手后开始刷新动画并通知程序开始刷新。代码如下:
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
    if (!mPullRefreshEnable && !mPullLoadEnable) {
        return super.dispatchTouchEvent(event);
    }

    if (isRefreshing) {
        return super.dispatchTouchEvent(event);
    }

    switch (event.getActionMasked()) {
        case MotionEvent.ACTION_DOWN: {
            preY = event.getY();
            preX = event.getX();
            break;
        }

        case MotionEvent.ACTION_MOVE: {
            float currentY = event.getY();
            float currentX = event.getX();
            float dy = currentY - preY;
            float dx = currentX - preX;
            preY = currentY;
            preX = currentX;
            if (!actionDetermined) {
                //判断是下拉刷新还是上拉加载更多
                if (dy > 0 && !canChildScrollUp() && mPullRefreshEnable) {
                    mCurrentAction = ACTION_PULL_DOWN_REFRESH;
                    actionDetermined = true;
                } else if (dy < 0 && !canChildScrollDown() && mPullLoadEnable) {
                    mCurrentAction = ACTION_PULL_UP_LOAD_MORE;
                    actionDetermined = true;
                }
            }
            handleScroll(dy);
            observerArriveBottom();
            break;
        }

        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL: {
            //用户松手后需要判断当前的滑动距离是否满足触发刷新的条件
            if (releaseTouch()) {
                MotionEvent cancelEvent = MotionEvent.obtain(event);
                cancelEvent.setAction(MotionEvent.ACTION_CANCEL);
                return super.dispatchTouchEvent(cancelEvent);
            }
            break;
        }
    }

    return super.dispatchTouchEvent(event);
}

/**
 * 处理滚动
 */
private boolean handleScroll(float distanceY) {
    if (!canChildScrollUp() && mCurrentAction == ACTION_PULL_DOWN_REFRESH &&
            mPullRefreshEnable) {
        //下拉刷新
        LayoutParams lp = (LayoutParams) headerView.getLayoutParams();
        lp.height += distanceY;
        if (lp.height < 0) {
            lp.height = 0;
        } else if (lp.height > loadingViewOverHeight) {
            lp.height = (int) loadingViewOverHeight;
        }
        headerView.setLayoutParams(lp);
        if (lp.height < loadingViewOverHeight) {
            headerView.setLoadText(TextUtils.isEmpty(mPullRefreshText) ?
                    getContext().getString(R.string.default_pull_refresh_text) : mPullRefreshText);
        } else {
            headerView.setLoadText(getContext().getString(R.string.release_to_refresh));
        }
        headerView.setProgressRotation(lp.height / loadingViewOverHeight);
        adjustContentViewHeight(lp.height);
        return true;

    } else if (!canChildScrollDown() && mCurrentAction == ACTION_PULL_UP_LOAD_MORE && mPullLoadEnable) {
        //上拉加载更多
        LayoutParams lp = (LayoutParams) footerView.getLayoutParams();
        lp.height -= distanceY;
        if (lp.height < 0) {
            lp.height = 0;
        } else if (lp.height > loadingViewOverHeight) {
            lp.height = (int) loadingViewOverHeight;
        }
        footerView.setLayoutParams(lp);
        if (lp.height < loadingViewOverHeight) {
            footerView.setLoadText(TextUtils.isEmpty(mPullLoadText) ?
                    getContext().getString(R.string.default_pull_load_text) : mPullLoadText);
        } else {
            footerView.setLoadText(getContext().getString(R.string.release_to_load));
        }
        footerView.setProgressRotation(lp.height / loadingViewOverHeight);
        adjustContentViewHeight(-lp.height);
        return true;
    }
    return false;
}

private void adjustContentViewHeight(float h) {
    mContentView.setTranslationY(h);
    //下面的方式可以看到完整内容,但是有掉帧现象
    /*if (mCurrentAction == ACTION_PULL_DOWN_REFRESH) {
        mContentView.setTranslationY(h);
    }
    LayoutParams lp = (LayoutParams) mContentView.getLayoutParams();
    lp.height = (int) (getMeasuredHeight() - Math.abs(h));
    mContentView.setLayoutParams(lp);*/

}

private boolean releaseTouch() {
    boolean result = false;
    LayoutParams lp;
    if (mPullRefreshEnable && mCurrentAction == ACTION_PULL_DOWN_REFRESH) {
        lp = (LayoutParams) headerView.getLayoutParams();
        if (lp.height >= loadingViewOverHeight) {
            //触发下拉刷新
            startPullDownRefresh(lp.height);
            result = true;
        } else if (lp.height > 0) {
            //未满足下拉刷新触发条件,重置状态
            resetPullDownRefresh(lp.height);
            result = lp.height >= CLICK_TOUCH_DEVIATION;
        } else {
            resetPullRefreshState();
        }
    }

    if (mPullLoadEnable && mCurrentAction == ACTION_PULL_UP_LOAD_MORE) {
        lp = (LayoutParams) footerView.getLayoutParams();
        if (lp.height >= loadingViewOverHeight) {
            //触发上拉加载更多
            startPullUpLoadMore(lp.height);
            result = true;
        } else if (lp.height > 0) {
            //未满足上拉加载更多触发条件,重置状态
            resetPullUpLoadMore(lp.height);
            result = lp.height >= CLICK_TOUCH_DEVIATION;
        } else {
            resetPullLoadState();
        }
    }
    return result;
}

private void startPullDownRefresh(int headerViewHeight) {
    isRefreshing = true;
    ValueAnimator animator = ValueAnimator.ofFloat(headerViewHeight, loadingViewFinalHeight);
    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            LayoutParams lp = (LayoutParams) headerView.getLayoutParams();
            lp.height = (int) ((Float) animation.getAnimatedValue()).floatValue();
            headerView.setLayoutParams(lp);
            adjustContentViewHeight(lp.height);
        }
    });
    animator.addListener(new SimpleAnimatorListener() {
        @Override
        public void onAnimationEnd(Animator animation) {
            headerView.start();
            headerView.setLoadText(getContext().getString(R.string.refresh_text));

            if (refreshLayoutListener != null) {
                refreshLayoutListener.onRefresh();
            }
        }
    });
    animator.setDuration(300);
    animator.start();
}

/**
 * 重置下拉刷新状态
 *
 * @param headerViewHeight 当前下拉刷新视图的高度
 */
private void resetPullDownRefresh(int headerViewHeight) {
    headerView.stop();
    //headerView.setStartEndTrim(0, 0.75f);
    ValueAnimator animator = ValueAnimator.ofFloat(headerViewHeight, 0);
    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            LayoutParams lp = (LayoutParams) headerView.getLayoutParams();
            lp.height = (int) ((Float) animation.getAnimatedValue()).floatValue();
            headerView.setLayoutParams(lp);
            adjustContentViewHeight(lp.height);
        }
    });
    animator.addListener(new SimpleAnimatorListener() {
        @Override
        public void onAnimationEnd(Animator animation) {
            resetPullRefreshState();

        }
    });
    animator.setDuration(300);
    animator.start();
}

private void resetPullRefreshState() {
    //重置动画结束才算完全完成刷新动作
    isRefreshing = false;
    actionDetermined = false;
    mCurrentAction = -1;
    headerView.setLoadText(TextUtils.isEmpty(mPullRefreshText) ?
            getContext().getString(R.string.default_pull_refresh_text) : mPullRefreshText);
}

private void startPullUpLoadMore(int headerViewHeight) {
    isRefreshing = true;
    ValueAnimator animator = ValueAnimator.ofFloat(headerViewHeight, loadingViewFinalHeight);
    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            LayoutParams lp = (LayoutParams) footerView.getLayoutParams();
            lp.height = (int) ((Float) animation.getAnimatedValue()).floatValue();
            footerView.setLayoutParams(lp);
            adjustContentViewHeight(-lp.height);
        }
    });
    animator.addListener(new SimpleAnimatorListener() {
        @Override
        public void onAnimationEnd(Animator animation) {
            footerView.start();
            footerView.setLoadText(getContext().getString(R.string.load_text));

            if (refreshLayoutListener != null) {
                refreshLayoutListener.onLoadMore();
            }
        }
    });
    animator.setDuration(300);
    animator.start();
}

/**
 * 重置下拉刷新状态
 *
 * @param headerViewHeight 当前下拉刷新视图的高度
 */
private void resetPullUpLoadMore(int headerViewHeight) {
    footerView.stop();
    //footerView.setStartEndTrim(0.5f, 1.25f);
    ValueAnimator animator = ValueAnimator.ofFloat(headerViewHeight, 0);
    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            LayoutParams lp = (LayoutParams) footerView.getLayoutParams();
            lp.height = (int) ((Float) animation.getAnimatedValue()).floatValue();
            footerView.setLayoutParams(lp);
            adjustContentViewHeight(-lp.height);
        }
    });
    animator.addListener(new SimpleAnimatorListener() {
        @Override
        public void onAnimationEnd(Animator animation) {
            resetPullLoadState();

        }
    });
    animator.setDuration(300);
    animator.start();
}

private void resetPullLoadState() {
    //重置动画结束才算完全完成刷新动作
    isRefreshing = false;
    actionDetermined = false;
    mCurrentAction = -1;
    footerView.setLoadText(TextUtils.isEmpty(mPullLoadText) ?
            getContext().getString(R.string.default_pull_load_text) : mPullLoadText);
}

/**
 * @return 子视图是否可以下拉
 */
public boolean canChildScrollUp() {
    if (mContentView == null) {
        return false;
    }
    if (Build.VERSION.SDK_INT < 14) {
        if (mContentView instanceof AbsListView) {
            final AbsListView absListView = (AbsListView) mContentView;
            return absListView.getChildCount() > 0
                    && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0)
                    .getTop() < absListView.getPaddingTop());
        } else {
            return ViewCompat.canScrollVertically(mContentView, -1) || mContentView.getScrollY() > 0;
        }
    } else {
        return ViewCompat.canScrollVertically(mContentView, -1);
    }
}

/**
 * @return 子视图是否可以上划
 */
public boolean canChildScrollDown() {
    if (mContentView == null) {
        return false;
    }
    if (Build.VERSION.SDK_INT < 14) {
        if (mContentView instanceof AbsListView) {
            final AbsListView absListView = (AbsListView) mContentView;
            if (absListView.getChildCount() > 0) {
                int lastChildBottom = absListView.getChildAt(absListView.getChildCount() - 1)
                        .getBottom();
                return absListView.getLastVisiblePosition() == absListView.getAdapter().getCount() - 1
                        && lastChildBottom <= absListView.getMeasuredHeight();
            } else {
                return false;
            }

        } else {
            return ViewCompat.canScrollVertically(mContentView, 1) || mContentView.getScrollY() > 0;
        }
    } else {
        return ViewCompat.canScrollVertically(mContentView, 1);
    }
}

public void setRefreshLayoutListener(NsRefreshLayoutListener refreshLayoutListener) {
    this.refreshLayoutListener = refreshLayoutListener;
}
上面代码中有一个变量CLICK\_TOUCH\_DEVIATION,这个变量表示对用户点击事件的容错值,用户进行点击动作时,会产生很小的滑动距离,如果不做容错处理会出现刷新视图抖动出现的问题。

另外还有一个observerArriveBottom(); 这个函数就是处理自动加载更多的关键。该函数在Touch事件产生滑动距离后,采取类似轮询的机制,判断滑动是否已经停止,滑动事件停止后,根据内容控件当前状态、用户配置来确定是否触发加载更多事件。代码如下:
private void observerArriveBottom() {
    if (isRefreshing || !mAutoLoadMore || !mPullLoadEnable) {
        return;
    }
    mContentView.getViewTreeObserver().addOnScrollChangedListener(
            new ViewTreeObserver.OnScrollChangedListener() {

                @Override
                public void onScrollChanged() {
                    mContentView.removeCallbacks(flingRunnable);
                    mContentView.postDelayed(flingRunnable, 6);
                }
            });
}

private Runnable flingRunnable = new Runnable() {
    @Override
    public void run() {
        if (isRefreshing || !mAutoLoadMore || !mPullLoadEnable) {
            return;
        }

        if (!canChildScrollDown()) {
            mCurrentAction = ACTION_PULL_UP_LOAD_MORE;
            isRefreshing = true;
            startPullUpLoadMore(0);
        }
    }
};

对外接口

public interface NsRefreshLayoutListener {
    void onRefresh();

    void onLoadMore();
}

搞定

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,047评论 25 707
  • 移动端开发,不联网的App少之又少,所以下拉刷新数据、上拉加载更多数据作为一个基础功能,相关的库层出不穷,这里就记...
    沉思的Panda阅读 5,772评论 18 65
  • 什么是喜欢,我不知道,或许就是一瞬间的感觉吧,我喜欢上了他。 第一次见他是陪室友去看电脑,开学这么久,第一次知道我...
    生来彷徨yun阅读 532评论 2 2
  • 每次看着自己的大肚子 紧张的肩膀 僵硬的脊柱 没有力量的腿... 下定决心!这次一定要开始瑜伽练起来 瑜伽走起! ...
    塔帕斯阅读 331评论 0 0
  • 阳光洒进灰喑的房间,房间一角有一条毯子,淡蓝色的,似乎表示着这个房子的女生内心就像淡蓝大海,深不可测。 我的名字叫...
    傻夏阅读 148评论 0 0