Android 支持刷新、加载更多、带反弹效果的RecyclerView

开篇

  当前市面上很多支持刷新、加载更多RecyclerView开源库,为何我这里还要自己再写一个?因为市面上的这些支持刷新加载更多的RecyclerView开源库实现方式基本上都是:在Adapter的外层在包裹一层Adapter,这种实现方式主要有以下两个不方便

  • 1、在用户添加ItemDecoration的时候,会影响到刷新头部和加载更多底部的样式。
  • 2、在用户更新列表某条记录时,不方便找到该记录对应的position。例如notifyItemInserted(int position)等。

效果截屏

SimpleAdapter.png
PullToRefreshRecyclerView

立即体验

扫描以下二维码下载体验App(体验App内嵌版本更新检测功能):


扫描下载体验App

传送门:https://github.com/JustinRoom/SimpleAdapterDemo

gradle引用

    implementation 'jsc.kit.adapter:adapter-component:_latestVersion'

属性

PullToRefreshRecyclerView

名称 类型 描述
prvHeaderLayout reference 下拉刷新头部layout
prvFooterLayout reference 上拉加载更多底部layout
prvPullDownToRefreshText string 下拉刷新提示
prvReleaseToRefreshText string 释放刷新提示
prvRefreshingText string 正在刷新提示
prvRefreshCompletedText string 刷新完成提示
prvPullUpToLoadMoreText string 上拉加载更多提示
prvReleaseToLoadMoreText string 释放加载更多提示
prvLoadingMoreText string 正在加载更多提示
prvLoadMoreCompletedText string 加载更多完成提示

简析源码

public class PullToRefreshRecyclerView extends ViewGroup {}

1、初始化布局

    private void initView(Context context) {
        inflate(context, R.layout.recycler_pull_to_refresh_recycler_view, this);
        recyclerView = findViewById(R.id.recycler_view);

        final ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
        mMinimumVelocity = viewConfiguration.getScaledMinimumFlingVelocity();
        mMaximumVelocity = viewConfiguration.getScaledMaximumFlingVelocity();
        scaledTouchSlop = viewConfiguration.getScaledTouchSlop();
    }

    private void initAttrs(Context context, AttributeSet attrs, int defStyleAttr) {
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PullToRefreshRecyclerView, defStyleAttr, 0);
        int headerLayoutId = a.getResourceId(R.styleable.PullToRefreshRecyclerView_prvHeaderLayout, -1);
        int footerLayoutId = a.getResourceId(R.styleable.PullToRefreshRecyclerView_prvFooterLayout, -1);

        //refresh text
        pullDownToRefreshText = a.hasValue(R.styleable.PullToRefreshRecyclerView_prvPullDownToRefreshText) ?
                a.getString(R.styleable.PullToRefreshRecyclerView_prvPullDownToRefreshText) :
                getResources().getString(R.string.recycler_default_pull_down_to_refresh);
        releaseToRefreshText = a.hasValue(R.styleable.PullToRefreshRecyclerView_prvReleaseToRefreshText) ?
                a.getString(R.styleable.PullToRefreshRecyclerView_prvReleaseToRefreshText) :
                getResources().getString(R.string.recycler_default_release_to_refresh);
        refreshingText = a.hasValue(R.styleable.PullToRefreshRecyclerView_prvRefreshingText) ?
                a.getString(R.styleable.PullToRefreshRecyclerView_prvRefreshingText) :
                getResources().getString(R.string.recycler_default_refreshing);
        refreshCompletedText = a.hasValue(R.styleable.PullToRefreshRecyclerView_prvRefreshCompletedText) ?
                a.getString(R.styleable.PullToRefreshRecyclerView_prvRefreshCompletedText) :
                getResources().getString(R.string.recycler_default_refresh_completed);

        //load more text
        pullUpToLoadMoreText = a.hasValue(R.styleable.PullToRefreshRecyclerView_prvPullUpToLoadMoreText) ?
                a.getString(R.styleable.PullToRefreshRecyclerView_prvPullUpToLoadMoreText) :
                getResources().getString(R.string.recycler_default_pull_up_to_load_more);
        releaseToLoadMoreText = a.hasValue(R.styleable.PullToRefreshRecyclerView_prvReleaseToLoadMoreText) ?
                a.getString(R.styleable.PullToRefreshRecyclerView_prvReleaseToLoadMoreText) :
                getResources().getString(R.string.recycler_default_release_to_load_more);
        loadingMoreText = a.hasValue(R.styleable.PullToRefreshRecyclerView_prvLoadingMoreText) ?
                a.getString(R.styleable.PullToRefreshRecyclerView_prvLoadingMoreText) :
                getResources().getString(R.string.recycler_default_loading_more);
        loadMoreCompletedText = a.hasValue(R.styleable.PullToRefreshRecyclerView_prvLoadMoreCompletedText) ?
                a.getString(R.styleable.PullToRefreshRecyclerView_prvLoadMoreCompletedText) :
                getResources().getString(R.string.recycler_default_load_more_completed);
        a.recycle();

        if (headerLayoutId == -1) {
            headerView = LayoutInflater.from(context).inflate(R.layout.recycler_default_header_view, this, false);
            setHeader(createDefaultHeader());
        } else {
            headerView = LayoutInflater.from(context).inflate(headerLayoutId, this, false);
        }
        if (footerLayoutId == -1) {
            footerView = LayoutInflater.from(context).inflate(R.layout.recycler_default_footer_view, this, false);
            setFooter(createDefaultFooter());
        } else {
            footerView = LayoutInflater.from(context).inflate(footerLayoutId, this, false);
        }
        addView(headerView, 0);
        addView(footerView);


        setHaveMore(false);
    }

2、测量

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        headerHeight = headerView.getMeasuredHeight();
        footerHeight = footerView.getMeasuredHeight();
    }

3、排版页面元素

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        headerView.layout(0, 0 - headerView.getMeasuredHeight(), getMeasuredWidth(), 0);
        recyclerView.layout(0, 0, getMeasuredWidth(), getMeasuredHeight());
        footerView.layout(0, getMeasuredHeight(), getMeasuredWidth(), getMeasuredHeight() + footerView.getMeasuredHeight());
    }

4、touch事件分发拦截。这里我们只拦截滑动事件,其他事件交由RecyclerView自己去处理。

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (getState() == REFRESH_COMPLETED
                || getState() == LOAD_MORE_COMPLETED)
            return super.onInterceptTouchEvent(ev);

        int action = ev.getActionMasked();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                stopReboundAnim();
                recyclerView.stopScroll();
                lastTouchY = ev.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                float curTouchY = ev.getY();
                float dy = curTouchY - lastTouchY;
                dy = dy > 0 ? dy + 0.5f : dy - 0.5f;
                lastTouchY = curTouchY;
                //如果滑动距离小于scaledTouchSlop,则把事件交给子View消耗;
                //否则此事件交由自己的onTouchEvent(MotionEvent event)方法消耗。
                if (Math.abs((int) dy) >= scaledTouchSlop / 2)
                    return true;
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }

5、处理拦截到的滑动事件。VelocityTracker跟踪滑动速度。

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        enSureVelocityTrackerNonNull();
        int action = ev.getActionMasked();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                trackMotionEvent(ev);
                lastTouchY = ev.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                trackMotionEvent(ev);
                float curTouchY = ev.getY();
                float dy = curTouchY - lastTouchY;
                if (dy != 0) {
                    dy = dy > 0 ? dy + 0.5f : dy - 0.5f;
                    lastTouchY = curTouchY;
                    executeMove((int) -dy);
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                final VelocityTracker tracker = velocityTracker;
                tracker.computeCurrentVelocity(1000, mMaximumVelocity);
                int velocity = (int) tracker.getYVelocity();
                recycleVelocityTracker();
                executeUpOrCancelMotionEvent(velocity);
                break;
        }
        return true;
    }

6、执行滑动。

    private void executeMove(int distance) {
        if (distance == 0)
            return;

        int scrollY = getScrollY();
        int scrolledY = 0;
        if (distance < 0) {//向下滑动
            //如果正在加载更多,我们避免加载更多底部视图被滑动至不可见。
            if (!isLoadingMore() && scrollY > 0) {
                scrolledY = Math.max(0 - scrollY, distance);
                scrollBy(0, scrolledY);
                distance = distance - scrolledY;
            }

            //滑动列表。
            scrolledY = Math.max(0 - getRecyclerViewMaxCanPullDownDistance(), distance);
            if (scrolledY != 0)
                recyclerView.scrollBy(0, scrolledY);

            //如果正在加载更多且已滑动至列表顶部,不可再向下滑动。
            if (!isLoadingMore()) {
                distance = distance - scrolledY;
                distance = toScaledValue(distance);
                if (distance != 0)
                    scrollBy(0, distance);
            }
        } else {//向上滑动
            //如果正在刷新,我们避免刷新头部视图别滑动至不可见。
            if (!isRefreshing() && scrollY < 0) {
                scrolledY = Math.min(Math.abs(scrollY), distance);
                scrollBy(0, scrolledY);
                distance = distance - scrolledY;
            }

            //滑动列表
            scrolledY = Math.min(getRecyclerViewMaxCanPullUpDistance(), distance);
            if (scrolledY != 0)
                recyclerView.scrollBy(0, scrolledY);

            //如果正在刷新且已滑动至列表底部,不可再向上滑动。
            if (!isRefreshing()) {
                distance = distance - scrolledY;
                distance = toScaledValue(distance);
                if (distance != 0)
                    scrollBy(0, distance);
            }
        }

        if (getScrollY() < 0) {
            if (!isRefreshEnable() || isRefreshing()) {
                header.onScroll(getState(), isRefreshEnable(), isRefreshing(), getScrollY(), headerHeight, getRefreshThresholdValue());
                return;
            }

            // getRefreshThresholdValue()释放执行刷新阈值
            if (getScrollY() < getRefreshThresholdValue()) {
                //release to refresh
                setState(RELEASE_TO_REFRESH);
            } else {
                //pull down to refresh
                setState(PULL_DOWN_TO_REFRESH);
            }
            header.onScroll(getState(), isRefreshEnable(), isRefreshing(), getScrollY(), headerHeight, getRefreshThresholdValue());
        } else if (getScrollY() > 0) {
            if (!isLoadMoreEnable() || isLoadingMore()) {
                footer.onScroll(getState(), isLoadMoreEnable(), isLoadingMore(), getScrollY(), footerHeight);
                return;
            }

            // getLoadMoreThresholdValue()释放执行加载更多阈值
            if (getScrollY() > getLoadMoreThresholdValue()) {
                //release to load more
                setState(RELEASE_TO_LOAD_MORE);
            } else {
                //pull up to load more
                setState(PULL_UP_TO_LOAD_MORE);
            }
            footer.onScroll(getState(), isLoadMoreEnable(), isLoadingMore(), getScrollY(), footerHeight);
        } else {
            header.onScroll(getState(), isRefreshEnable(), isRefreshing(), getScrollY(), headerHeight, getRefreshThresholdValue());
            footer.onScroll(getState(), isLoadMoreEnable(), isLoadingMore(), getScrollY(), footerHeight);
        }
    }

7、执行touch结束事件。

    private void executeUpOrCancelMotionEvent(int velocity) {
        switch (getState()) {
            case REFRESHING:
                executeRebound(0 - headerHeight);
                recyclerView.fling(0, 0 - velocity);
                break;
            case LOADING_MORE:
                executeRebound(footerHeight);
                recyclerView.fling(0, 0 - velocity);
                break;
            case RELEASE_TO_REFRESH:
                executeRebound(0 - headerHeight);
                break;
            case RELEASE_TO_LOAD_MORE:
                executeRebound(isHaveMore() ? footerHeight : 0);
                break;
            default:
                executeRebound(0);
                recyclerView.fling(0, 0 - velocity);
                break;
        }
    }

    private void executeRebound(int destinationScrollY) {
        int scrollYDistance = destinationScrollY - getScrollY();
        int duration = Math.abs(scrollYDistance);
        duration = Math.max(200, duration);
        duration = Math.min(500, duration);
        if (animator == null) {
            animator = ObjectAnimator.ofPropertyValuesHolder(this, PropertyValuesHolder.ofInt(SCROLL_Y, getScrollY(), destinationScrollY));
            animator.setInterpolator(new AccelerateDecelerateInterpolator());
            animator.addListener(new SimpleAnimatorListener() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    switch (getState()) {
                        case RELEASE_TO_REFRESH:
                            if (!isRefreshing() && onRefreshListener != null) {
                                setState(REFRESHING);
                                currentPage = startPage;
                                onRefreshListener.onRefresh(getContext(), currentPage, pageSize);
                            }
                            break;
                        case RELEASE_TO_LOAD_MORE:
                            if (isHaveMore() && !isLoadingMore() && onRefreshListener != null) {
                                setState(LOADING_MORE);
                                currentPage++;
                                onRefreshListener.onLoadMore(getContext(), currentPage, pageSize);
                            }
                            break;
                        case REFRESH_COMPLETED:
                            setState(INIT);
                            lastRefreshTimeStamp = System.currentTimeMillis();
                            header.updateLastRefreshTime(lastRefreshTimeStamp);
                            break;
                        case LOAD_MORE_COMPLETED:
                            setState(INIT);
                            break;
                    }
                }
            });
        } else {
            animator.setIntValues(getScrollY(), destinationScrollY);
        }
        animator.setDuration(duration);
        animator.start();
    }

使用示例

  • 1、简单使用示例:
        PullToRefreshRecyclerView pullToRefreshRecyclerView;
    
        //设置分页加载的起始页序号以及每页数据数量
        pullToRefreshRecyclerView.initializeParameters(1, 10);
        //关闭下拉刷新
//        pullToRefreshRecyclerView.setRefreshEnable(false);
        //关闭加载更多
//        pullToRefreshRecyclerView.setLoadMoreEnable(false);
        //设置下拉刷新和上拉加载更多监听
        pullToRefreshRecyclerView.setOnRefreshListener(new PullToRefreshRecyclerView.OnRefreshListener() {
            @Override
            public void onRefresh(@NonNull Context context, int currentPage, int pageSize) {
                index = -1;
                loadNetData();
            }

            @Override
            public void onLoadMore(@NonNull Context context, int currentPage, int pageSize) {
                loadNetData();
            }
        });
        RecyclerView recyclerView = pullToRefreshRecyclerView.getRecyclerView();
        recyclerView.setLayoutManager(new LinearLayoutManager(inflater.getContext()));
        recyclerView.addItemDecoration(new SpaceItemDecoration(
                CompatResourceUtils.getDimensionPixelSize(this, R.dimen.space_16),
                CompatResourceUtils.getDimensionPixelSize(this, R.dimen.space_2)
        ));
        
        
//模拟加载网络数据
    private int index = -1;
    private Random random = new Random();
    private void loadNetData(){
        pullToRefreshRecyclerView.postDelayed(new Runnable() {
            @Override
            public void run() {
                //刷新(或加载更多)完成
                pullToRefreshRecyclerView.completed();
                List<ClassItem> items = new ArrayList<>();
                int count = 7 + random.nextInt(12);
                for (int i = 0; i < count; i++) {
                    index ++;
                    ClassItem item = new ClassItem();
                    item.setLabel("this is " + index);
                    items.add(item);
                }

                //判定是否是第一页数据
                if (pullToRefreshRecyclerView.isFirstPage()) {
                    adapter3.setData(items);
                } else {
                    adapter3.addData(items);
                }
                //设置是否还有下一页数据
                pullToRefreshRecyclerView.setHaveMore(items.size() >= pullToRefreshRecyclerView.getPageSize());
            }
        }, 50 + random.nextInt(2000));
    }
  • 2、自定义下拉刷新:

2.1、设置刷新(头部)

<jsc.kit.adapter.refresh.PullToRefreshRecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    app:prvHeaderLayout="@layout/xxx"
    android:id="@+id/pull_to_refresh_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

2.2、设置刷新逻辑监听

public <H extends IHeader> void setHeader(@NonNull H header)

2.3、实现刷新逻辑

        IHeader header =  new IHeader() {

            @Override
            public void initChildren(@NonNull View headerView) {
                //这里初始化下拉刷新view
                //也就是app:prvHeaderLayout="@layout/xxx"属性对应的布局文件
            }

            @Override
            public void updateLastRefreshTime(long lastRefreshTimeStamp) {
                //这里是上次刷新时间更新监听
            }

            @Override
            public void onUpdateState(int state, CharSequence txt) {
                //这里是监听下拉刷新的各种状态
                //监听到的状态有:PULL_DOWN_TO_REFRESH、RELEASE_TO_REFRESH、REFRESHING、REFRESH_COMPLETED
                switch (state) {
                    case PullToRefreshRecyclerView.REFRESHING:
                        //正在刷新,我们可以正在这里启动正在刷新的动画
                        
                        break;
                    case PullToRefreshRecyclerView.REFRESH_COMPLETED:
                        //刷新完成,我们可以在这里关闭正在刷新的动画以及头部复位
                        
                        break;
                    default:
                        break;
                }
            }

            @Override
            public void onScroll(int state, boolean refreshEnable, boolean isRefreshing, int scrollY, int headerHeight, int refreshThresholdValue) {
                //这里是监听下拉刷新动作
                //监听到的状态有:INIT、PULL_DOWN_TO_REFRESH、RELEASE_TO_REFRESH、REFRESHING、REFRESH_COMPLETED
            }
        };
  • 3、自定义上拉加载更多:

3.1、设置加载更多(底部)

<jsc.kit.adapter.refresh.PullToRefreshRecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    app:prvFooterLayout="@layout/xxx"
    android:id="@+id/pull_to_refresh_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

3.2、设置加载更多逻辑监听

public <H extends IHeader> void setHeader(@NonNull H header)

3.3、实现加载更多逻辑

        IFooter footer = new IFooter() {

            @Override
            public void initChildren(@NonNull View footerView) {
                //这里初始化上拉加载更多view
                //也就是app:prvFooterLayout="@layout/xxx"属性对应的布局文件
            }

            @Override
            public void onUpdateState(@State int state, CharSequence txt) {
                //这里是监听上拉加载更多的各种状态
                //监听到的状态有:PULL_UP_TO_LOAD_MORE、RELEASE_TO_LOAD_MORE、LOADING_MORE、LOAD_MORE_COMPLETED
                switch (state) {
                    case PullToRefreshRecyclerView.LOADING_MORE:
                        //正在加载更多,我们可以正在这里启动正在加载更多的动画
                        
                        break;
                    case PullToRefreshRecyclerView.LOAD_MORE_COMPLETED:
                        //加载更多完成,我们可以在这里关闭正在加载更多的动画以及底部复位
                        
                        break;
                    default:
                        break;
                }
            }

            @Override
            public void onScroll(int state, boolean loadMoreEnable, boolean isLoadingMore, int scrollY, int footerHeight) {
                //这里是监听上拉加载更多动作
                //监听到的状态有:INIT、PULL_UP_TO_LOAD_MORE、RELEASE_TO_LOAD_MORE、LOADING_MORE、LOAD_MORE_COMPLETED
            }
        };

使用介绍就到这里。
从0撸出这个开源库不容易,希望童鞋们在GitHub上给一颗星星✨支持下。谢谢!如果在使用过程中不懂(或需要改进的地方),可以在评论里给我留言,也可以联系我。
微信:eoy9527QQ:1006368252

篇尾

在人生的道路上,当你的希望一个个落空的时候,你也要坚定,要沉着。 —— 朗费罗

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

推荐阅读更多精彩内容