BaseRecyclerViewAdapterHelper源码解读(四) 上拉加载更多

上拉加载

上拉加载无需监听滑动事件,可自定义加载布局,显示异常提示,自定义异常提示。

此篇文章为BaseRecyclerViewAdapterHelper源码解读第四篇,开源库地址,如果没有看过之前3篇文章的同学可以先去看看,大神可直接跳过.

BaseRecyclerViewAdapterHelper源码解读(一) 封装简单的adapter和万能的BaseViewHolder

BaseRecyclerViewAdapterHelper源码解读(二) 添加header和footer

BaseRecyclerViewAdapterHelper源码解读(三) 添加动画

今天给大家带来BaseRecyclerViewAdapterHelper是如何添加动画的.由于本人才学尚浅,如有有不对的地方,欢迎指正,谢谢.

开源库使用自动加载方法

上拉加载

// 滑动最后一个Item的时候回调onLoadMoreRequested方法
setOnLoadMoreListener(RequestLoadMoreListener);

默认第一次加载会回调onLoadMoreRequested()加载更多这个方法,如果不需要可以配置如下:

mQuickAdapter.disableLoadMoreIfNotFullPage();

回调处理代码

mQuickAdapter.setOnLoadMoreListener(new BaseQuickAdapter.RequestLoadMoreListener() {
            @Override public void onLoadMoreRequested() {
                mRecyclerView.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        if (mCurrentCounter >= TOTAL_COUNTER) {
                            //数据全部加载完毕
                            mQuickAdapter.loadMoreEnd();
                        } else {
                            if (isErr) {
                                //成功获取更多数据
                                mQuickAdapter.addData(DataServer.getSampleData(PAGE_SIZE));
                                mCurrentCounter = mQuickAdapter.getData().size();
                                mQuickAdapter.loadMoreComplete();
                            } else {
                                //获取更多数据失败
                                isErr = true;
                                Toast.makeText(PullToRefreshUseActivity.this, R.string.network_err, Toast.LENGTH_LONG).show();
                                mQuickAdapter.loadMoreFail();

                            }
                        }
                    }

                }, delayMillis);
            }
        }, mReyclerView);

这里可能看的不是很清楚,详情请看官方demo,https://github.com/CymChad/BaseRecyclerViewAdapterHelper/blob/d296d1fb4e7a64b9fa8a2601f3f896d3a9518be5/app/src/main/java/com/chad/baserecyclerviewadapterhelper/PullToRefreshUseActivity.java

加载完成(注意不是加载结束,而是本次数据加载结束并且还有下页数据)

mQuickAdapter.loadMoreComplete();

加载失败

mQuickAdapter.loadMoreFail();

加载结束

mQuickAdapter.loadMoreEnd();

设置监听器,开启监听上拉加载

    /**
     * 设置监听RecyclerView上拉加载更多  并设置监听器
     * @param requestLoadMoreListener
     * @param recyclerView
     */
    public void setOnLoadMoreListener(RequestLoadMoreListener requestLoadMoreListener,
                                      RecyclerView recyclerView) {
        openLoadMore(requestLoadMoreListener);
        if (getRecyclerView() == null) {
            setRecyclerView(recyclerView);
        }
    }
    /**
     *
     * 开启上拉加载更多
     * @param requestLoadMoreListener
     */
    private void openLoadMore(RequestLoadMoreListener requestLoadMoreListener) {
        this.mRequestLoadMoreListener = requestLoadMoreListener;
        mNextLoadEnable = true;
        mLoadMoreEnable = true;
        mLoading = false;
    }

设置什么时候回调?

    /**
     * 设置当列表滑动到倒数第N个Item的时候(默认是1)回调onLoadMoreRequested()方法
     * @param preLoadNumber
     */
    public void setPreLoadNumber(int preLoadNumber) {
        if (preLoadNumber > 1) {
            mPreLoadNumber = preLoadNumber;
        }
    }

先来说简单的,上面这个方法比较简单,属于配置型的方法.
就是设置当列表滑动到倒数第N个Item的时候(默认是1)回调onLoadMoreRequested()方法.待会儿下面会用到这个参数,先放着.
另外,这个方法可以在使用时不必调用,因为已经有默认值了.


    /**
     * To bind different types of holder and solve different the bind events
     *
     * @param holder
     * @param position
     * @see #getDefItemViewType(int)
     */
    @Override
    public void onBindViewHolder(K holder, int position) {
        //Add up fetch logic, almost like load more, but simpler.
        //这里是判断是否需要下拉加载更多
        autoUpFetch(position);
        //Do not move position, need to change before LoadMoreView binding
        //判断是否需要进行上拉加载更多
        autoLoadMore(position);
        int viewType = holder.getItemViewType();

        switch (viewType) {
            case 0:
                convert(holder, getItem(position - getHeaderLayoutCount()));
                break;
            case LOADING_VIEW:
                mLoadMoreView.convert(holder);
                break;
            case HEADER_VIEW:
                break;
            case EMPTY_VIEW:
                break;
            case FOOTER_VIEW:
                break;
            default:
                convert(holder, getItem(position - getHeaderLayoutCount()));
                break;
        }
    }

    /**
     * 根据position位置判断当前是否需要进行加载更多
     *
     * @param position 当前onBindViewHolder()的Position
     */
    private void autoLoadMore(int position) {
        // 判断是否可以进行加载更多的逻辑
        if (getLoadMoreViewCount() == 0) {
            return;
        }
        //只有在当前列表的倒数mPreLoadNumber个item开始绑定数据时才进行加载更多的逻辑
        if (position < getItemCount() - mPreLoadNumber) {
            return;
        }
        //判断当前加载状态,如果不是默认状态(可能正处于 正在加载中 的状态),则不进行加载
        if (mLoadMoreView.getLoadMoreStatus() != LoadMoreView.STATUS_DEFAULT) {
            return;
        }
        //设置当前状态:加载中
        mLoadMoreView.setLoadMoreStatus(LoadMoreView.STATUS_LOADING);
        if (!mLoading) {
            mLoading = true;
            if (getRecyclerView() != null) {
                getRecyclerView().post(new Runnable() {
                    @Override
                    public void run() {
                        //回调  让调用者去处理加载更多的逻辑
                        mRequestLoadMoreListener.onLoadMoreRequested();
                    }
                });
            } else {
                mRequestLoadMoreListener.onLoadMoreRequested();
            }
        }
    }

    /**
     * Load more view count
     * 判断是否可以进行加载更多的逻辑
     * @return 0 or 1
     */
    public int getLoadMoreViewCount() {
        //参数合法性    加载更多状态
        if (mRequestLoadMoreListener == null || !mLoadMoreEnable) {
            return 0;
        }
        //可加载下一页               有无更多数据
        if (!mNextLoadEnable && mLoadMoreView.isLoadEndMoreGone()) {
            return 0;
        }
        //当前数据项个数
        if (mData.size() == 0) {
            return 0;
        }
        return 1;
    }

重点来了,加载更多的主要逻辑就在这里:当在onBindViewHolder()的时候,根据当前item的position位置,然后去判断是否应该执行加载更多.
具体判断逻辑:当一个item第一次进入window界面时,会调用onBindViewHolder()去绑定数据,这个时候我们知道该position的位置,
于是我们就可以这样干:设置一个mPreLoadNumber标志位置( 当列表滑动到倒数第N个Item的时候(默认是1)回调onLoadMoreRequested()方法 ),
当onBindViewHolder()在绑定数据时的position是最后mPreLoadNumber个时,我们即进行加载更多的回调,然后让调用者去处理.

当然,在回调之前,我们需要进行一些判断,确定当前是否可以进行加载更多.
- mRequestLoadMoreListener监听器是否为null,当前是否处于可以加载更多的状态(mLoadMoreEnable标志位控制)
- 当前有无更多数据(这个由外界调用者决定)
- 当前的数据项个数是否为0,如果没有数据项,那就不必加载更多
- 是否进入倒数的那mPreLoadNumber区域
- 判断当前(mLoadMoreView 这是加载更多的View )加载状态,如果不是默认状态(可能正处于 正在加载中 的状态),则不进行加载

好吧,细心的观众可能已经发现了,上面的这种方式其实有一个缺点:当数据项个数小于1屏幕,那么最后倒数的mPreLoadNumber个肯定是可见的,既然可见那么肯定会执行该item的onBindViewHolder(),执行该方法即会判断是否需要执行加载更多,显然这时是符合条件的,于是就会出现数据未满一屏幕会自动回调onLoadMoreRequested()并且还在那里显示正在加载中.

明显,这时不符合我们的需求的.于是官方有一个解决方案.往下看.

/**
     * bind recyclerView {@link #bindToRecyclerView(RecyclerView)} before use!
     *
     * @see #disableLoadMoreIfNotFullPage(RecyclerView)
     */
    public void disableLoadMoreIfNotFullPage() {
        //检查当前RecyclerView是否为null
        checkNotNull();
        disableLoadMoreIfNotFullPage(getRecyclerView());
    }

    /**
     * check if full page after {@link #setNewData(List)}, if full, it will enable load more again.
     * 

* 不是配置项!! *

* 这个方法是用来检查是否满一屏的,所以只推荐在 {@link #setNewData(List)} 之后使用 * 原理:先关闭 load more,检查完了再决定是否开启 * 数据项个数未满一屏幕,则不开启load more * 数据项个数 > 一屏幕,则继续开启load more *

* 不是配置项!! * * @param recyclerView your recyclerView * @see #setNewData(List) */ public void disableLoadMoreIfNotFullPage(RecyclerView recyclerView) { // 设置加载状态为false setEnableLoadMore(false); if (recyclerView == null) return; RecyclerView.LayoutManager manager = recyclerView.getLayoutManager(); if (manager == null) return; if (manager instanceof LinearLayoutManager) { final LinearLayoutManager linearLayoutManager = (LinearLayoutManager) manager; recyclerView.postDelayed(new Runnable() { @Override public void run() { //数据项个数 > 一屏幕,则继续开启load more if ((linearLayoutManager.findLastCompletelyVisibleItemPosition() + 1) != getItemCount()) { setEnableLoadMore(true); } } }, 50); } else if (manager instanceof StaggeredGridLayoutManager) { final StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager) manager; recyclerView.postDelayed(new Runnable() { @Override public void run() { //返回StaggeredGridLayoutManager布局的跨度数 final int[] positions = new int[staggeredGridLayoutManager.getSpanCount()]; //返回每一个跨度(列)的最后一个可见的item的位置 赋值到该数组里面 staggeredGridLayoutManager.findLastCompletelyVisibleItemPositions(positions); //找出数组中最大的数(即StaggeredGridLayoutManager布局的当前可见的最下面那个item) int pos = getTheBiggestNumber(positions) + 1; // 数据项个数 > 一屏幕,则继续开启load more if (pos != getItemCount()) { setEnableLoadMore(true); } } }, 50); } } /** * 返回数组中的最大值 * @param numbers * @return */ private int getTheBiggestNumber(int[] numbers) { int tmp = -1; if (numbers == null || numbers.length == 0) { return tmp; } for (int num : numbers) { if (num > tmp) { tmp = num; } } return tmp; } /** * Set the enabled state of load more. * 设置上拉加载更多是否可用 * * @param enable True if load more is enabled, false otherwise. */ public void setEnableLoadMore(boolean enable) { //之前的状态需要和现在的状态做对比 int oldLoadMoreCount = getLoadMoreViewCount(); mLoadMoreEnable = enable; int newLoadMoreCount = getLoadMoreViewCount(); if (oldLoadMoreCount == 1) { if (newLoadMoreCount == 0) { //之前有 现在没有 需要移除 notifyItemRemoved(getLoadMoreViewPosition()); } } else { if (newLoadMoreCount == 1) { //将加载布局插入 mLoadMoreView.setLoadMoreStatus(LoadMoreView.STATUS_DEFAULT); notifyItemInserted(getLoadMoreViewPosition()); } } }

这段代码我看到在开源项目的讨论区异常热门,好像很多人都遇到了使用disableLoadMoreIfNotFullPage()无效的事件.
可能是他们用错了吧,可能.disableLoadMoreIfNotFullPage()是需要在setNewData()之后调用才有效.
disableLoadMoreIfNotFullPage()里面想做的事情就是:判断是否需要load more,他判断的依据是:
查看当前屏幕内的最底部的那个item的索引是否与总的数据项个数相等.
- 如果相等,那么说明未满一屏幕,不需要开启load more
- 如果不相等,那么说明满了一屏幕,需要开启laod more

创建加载布局item 并 设置加载布局的点击事件

 @Override
    public K onCreateViewHolder(ViewGroup parent, int viewType) {
        K baseViewHolder = null;
        this.mContext = parent.getContext();
        this.mLayoutInflater = LayoutInflater.from(mContext);
        switch (viewType) {
            case LOADING_VIEW:
                baseViewHolder = getLoadingView(parent);
                break;
            case HEADER_VIEW:
                baseViewHolder = createBaseViewHolder(mHeaderLayout);
                break;
            case EMPTY_VIEW:
                baseViewHolder = createBaseViewHolder(mEmptyLayout);
                break;
            case FOOTER_VIEW:
                baseViewHolder = createBaseViewHolder(mFooterLayout);
                break;
            default:
                baseViewHolder = onCreateDefViewHolder(parent, viewType);
                bindViewClickListener(baseViewHolder);
        }
        baseViewHolder.setAdapter(this);
        return baseViewHolder;

    }

    private K getLoadingView(ViewGroup parent) {
        //加载 加载布局
        View view = getItemView(mLoadMoreView.getLayoutId(), parent);
        //生成baseviewholder
        K holder = createBaseViewHolder(view);
        //设置加载布局的点击事件
        holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mLoadMoreView.getLoadMoreStatus() == LoadMoreView.STATUS_FAIL) {
                    //之前是加载失败状态时   前去刷新
                    notifyLoadMoreToLoading();
                }
                if (mEnableLoadMoreEndClick && mLoadMoreView.getLoadMoreStatus() == LoadMoreView
                        .STATUS_END) {
                    //加载更多布局可以被点击  并且  之前状态是结束状态
                    notifyLoadMoreToLoading();
                }
            }
        });
        return holder;
    }

    /**
     * The notification starts the callback and loads more
     * 通知启动回调并加载更多
     */
    public void notifyLoadMoreToLoading() {
        //如果当前正在加载中,则不用管
        if (mLoadMoreView.getLoadMoreStatus() == LoadMoreView.STATUS_LOADING) {
            return;
        }
        //将加载更多布局的状态设置为默认状态  这样当下面刷新adapter时会回调onBindViewHolder()从而触发
        //autoLoadMore()方法去判断是否需要加载更多,这时候刚好又是默认状态是可以更新的,于是就去回调onLoadMoreRequested()方法
        mLoadMoreView.setLoadMoreStatus(LoadMoreView.STATUS_DEFAULT);
        notifyItemChanged(getLoadMoreViewPosition());
    }

这里的目标是给加载更多布局设置点击事件,可以看到其实在代码里面把加载更多布局是直接设置了点击事件的,只是根据不同的状态决定是否需要执行加载更多的逻辑.只有下面2种情况需要去加载更多.
- 之前是加载失败状态时 加载布局被点击
- 之前是结束状态 并且 加载更多布局可以被点击

满足这两种情况时,就把加载布局view的状态设置成默认状态,并且刷新adapter的最后一项(即加载更多布局那一项),这样adapter会回调onBindViewHolder(),而在onBindViewHolder()又调用了autoLoadMore()方法去判断是否需要加载更多,
显然此时是符合条件的,需要刷新,于是回调onLoadMoreRequested(),并且把加载布局的状态改为STATUS_LOADING正在加载的状态,这样加载布局的样式也跟着改变了.

加载完成

注意不是加载结束,而是本次数据加载结束并且还有下页数据

    /**
     * Refresh complete
     * 刷新完成时调用
     *
     */
    public void loadMoreComplete() {
        if (getLoadMoreViewCount() == 0) {
            return;
        }
        //将当前加载状态改为false  表示未在加载
        mLoading = false;
        //可进行下一页加载
        mNextLoadEnable = true;
        // 恢复加载更多布局的状态
        mLoadMoreView.setLoadMoreStatus(LoadMoreView.STATUS_DEFAULT);
        // 告知加载更多布局被更新了,需要刷新一下
        notifyItemChanged(getLoadMoreViewPosition());
    }

    /**
     * Gets to load more locations
     * 获取加载更多的布局的索引
     * @return
     */
    public int getLoadMoreViewPosition() {
        return getHeaderLayoutCount() + mData.size() + getFooterLayoutCount();
    }

刷新完成之后,需要做一些善后操作,如上所示,代码注释已经很清楚了.

加载失败

    /**
     * Refresh failed
     * 加载失败
     */
    public void loadMoreFail() {
        if (getLoadMoreViewCount() == 0) {
            return;
        }
        //当前加载状态  切换为未在加载中
        mLoading = false;
        //加载布局设置为加载失败
        mLoadMoreView.setLoadMoreStatus(LoadMoreView.STATUS_FAIL);
        //通知加载布局更新了,需要刷新
        notifyItemChanged(getLoadMoreViewPosition());
    }

就是简单地做一下判断,是否可以继续加载,并且更新布局.

加载结束

    /**
     * Refresh end, no more data
     * 加载更多,并且没有更多数据了    调用此方法即表示无更多数据了
     * 这里设置加载更多布局依然可见
     */
    public void loadMoreEnd() {
        loadMoreEnd(false);
    }

    /**
     * Refresh end, no more data
     * 加载更多,并且没有更多数据了   调用此方法即表示无更多数据了
     * gone:设置加载更多布局是否可见   true:不可见   false:可见
     * @param gone if true gone the load more view
     */
    public void loadMoreEnd(boolean gone) {
        if (getLoadMoreViewCount() == 0) {
            return;
        }
        当前加载状态  切换为未在加载中
        mLoading = false;
        //不能再加载下一页了  因为已经没有更多数据了
        mNextLoadEnable = false;
        //设置加载更多布局是否可见
        mLoadMoreView.setLoadMoreEndGone(gone);
        if (gone) {
            //如果布局不可见,则更新
            notifyItemRemoved(getLoadMoreViewPosition());
        } else {
            //如果布局可见,则先更新布局(切换为STATUS_END状态那种布局)
            mLoadMoreView.setLoadMoreStatus(LoadMoreView.STATUS_END);
            //并更新adapter
            notifyItemChanged(getLoadMoreViewPosition());
        }
    }

设置加载结束,即表示没有更多的数据可以加载了,于是把mNextLoadEnable标志位设为false,表示无法再加载下一页.
然后根据是否需要显示加载布局,进行刷新adapter.

上拉加载布局

在源码里面有一个抽象类LoadMoreView.

public abstract class LoadMoreView {

    public static final int STATUS_DEFAULT = 1;
    /**
     * 加载中
     */
    public static final int STATUS_LOADING = 2;
    /**
     * 加载失败
     */
    public static final int STATUS_FAIL = 3;
    /**
     * 加载结束  没有更多数据
     */
    public static final int STATUS_END = 4;

    /**
     * 当前加载更多的状态
     */
    private int mLoadMoreStatus = STATUS_DEFAULT;
    private boolean mLoadMoreEndGone = false;

    public void setLoadMoreStatus(int loadMoreStatus) {
        this.mLoadMoreStatus = loadMoreStatus;
    }

    public int getLoadMoreStatus() {
        return mLoadMoreStatus;
    }

    public void convert(BaseViewHolder holder) {
        //根据不同的状态
        switch (mLoadMoreStatus) {
            case STATUS_LOADING:
                visibleLoading(holder, true);
                visibleLoadFail(holder, false);
                visibleLoadEnd(holder, false);
                break;
            case STATUS_FAIL:
                visibleLoading(holder, false);
                visibleLoadFail(holder, true);
                visibleLoadEnd(holder, false);
                break;
            case STATUS_END:
                visibleLoading(holder, false);
                visibleLoadFail(holder, false);
                visibleLoadEnd(holder, true);
                break;
            case STATUS_DEFAULT:
                visibleLoading(holder, false);
                visibleLoadFail(holder, false);
                visibleLoadEnd(holder, false);
                break;
        }
    }

    private void visibleLoading(BaseViewHolder holder, boolean visible) {
        holder.setVisible(getLoadingViewId(), visible);
    }

    private void visibleLoadFail(BaseViewHolder holder, boolean visible) {
        holder.setVisible(getLoadFailViewId(), visible);
    }

    private void visibleLoadEnd(BaseViewHolder holder, boolean visible) {
        final int loadEndViewId = getLoadEndViewId();
        if (loadEndViewId != 0) {
            holder.setVisible(loadEndViewId, visible);
        }
    }

    /**
     * 设置标志  有无更多数据
     * @param loadMoreEndGone true:无更多数据
     */
    public final void setLoadMoreEndGone(boolean loadMoreEndGone) {
        this.mLoadMoreEndGone = loadMoreEndGone;
    }

    public final boolean isLoadEndMoreGone() {
        if (getLoadEndViewId() == 0) {
            return true;
        }
        return mLoadMoreEndGone;
    }

    /**
     * No more data is hidden
     *
     * @return true for no more data hidden load more
     * @deprecated Use {@link BaseQuickAdapter#loadMoreEnd(boolean)} instead.
     */
    @Deprecated
    public boolean isLoadEndGone() {
        return mLoadMoreEndGone;
    }

    /**
     * load more layout
     *
     * @return
     */
    public abstract
    @LayoutRes
    int getLayoutId();

    /**
     * loading view
     *
     * @return
     */
    protected abstract
    @IdRes
    int getLoadingViewId();

    /**
     * load fail view
     *
     * @return
     */
    protected abstract
    @IdRes
    int getLoadFailViewId();

    /**
     * load end view, you can return 0
     *
     * @return
     */
    protected abstract
    @IdRes
    int getLoadEndViewId();
}

该类是用于管理加载布局的,不同的状态显示不同的布局.
源码里面已经给我们提供了一个默认的加载布局,可以直接使用,当然了,是支持自定义的,只需要继承LoadMoreView就行.

默认的加载布局如下:

public final class SimpleLoadMoreView extends LoadMoreView {

    @Override
    public int getLayoutId() {
        return R.layout.quick_view_load_more;
    }

    @Override
    protected int getLoadingViewId() {
        return R.id.load_more_loading_view;
    }

    @Override
    protected int getLoadFailViewId() {
        return R.id.load_more_load_fail_view;
    }

    @Override
    protected int getLoadEndViewId() {
        return R.id.load_more_load_end_view;
    }
}

下面是xml


<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="@dimen/dp_40">

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

        <ProgressBar
            android:id="@+id/loading_progress"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            style="?android:attr/progressBarStyleSmall"
            android:layout_marginRight="@dimen/dp_4"/>

        <TextView
            android:id="@+id/loading_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="@dimen/dp_4"
            android:text="@string/loading"
            android:textColor="@android:color/black"
            android:textSize="@dimen/sp_14"/>
    LinearLayout>

    <FrameLayout
        android:id="@+id/load_more_load_fail_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="gone">


        <TextView
            android:id="@+id/tv_prompt"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:text="@string/load_failed"/>

    FrameLayout>

    <FrameLayout
        android:id="@+id/load_more_load_end_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="gone">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:text="@string/load_end"
            android:textColor="@android:color/darker_gray"/>
    FrameLayout>
FrameLayout>

其实就是一个布局,根据根据状态,动态的显示和隐藏某一种容器就行.

总结

这一块,感觉稍微复杂一些,用了2天的琐碎时间才看完,可能是因为比较菜吧.
大体实现思路就是当检测到滑动到RecyclerView的最后倒数N项时,就开始去刷新,并显示加载布局和回调接口,让外部去实现刷新.
道理虽然很简单,但是实现起来的话,有很多很多细节在里面,很多很多的坑,再次感谢开源库的作者们.

你可能感兴趣的:(Android)