下拉刷新与自动加载

下拉刷新是Android App开发中非常常用的功能, 网上也有很多开源的下拉刷新控件. CoreLibs中原先使用的handmark pulltorefresh, 现在选用的则是 Ultra-Pull-To-Refresh. 新的PTR框架有如下优点:

  • 轻量

  • 理论上支持所有的View

  • 易扩展

  • 易自定义

  • 性能不错

具体的使用方法请参考上面的链接, 这里就不再赘述了. 但是呢, Ultra ptr也不是没有缺点, 比如库中提供了Lollipop风格的下拉头部, 如果想要在每一个下拉控件中使用还需要加入不少代码. 然后每次使用下拉组件的时候需要一些相似的配置代码. 最主要的是Ultra ptr只支持下拉, 而不支持加载更多. 因此我们需要扩展一下这个库, 目标有两个:

  1. 默认头部变为Lollipop风格, 去掉重复代码, 使用更简洁

  2. 加入自动加载更多 - auto load more.

ptr的扩展类均位于com.corelibs.views.ptr下. 以下是包结构:

| ptr
  | layout 扩展的布局
    -PtrAutoLoadMoreLayout //自动加载更多布局
    -PtrLollipopLayout //Lolipop头部风格布局
  | loadmore
    | adapter
      -GridViewAdapter //GridView系列适配器
      -ListViewAdapter //ListView系列适配器
      -LoadMoreAdapter //自动加载更多的适配类
      -RecyclerViewAdapter //RecyclerView系列适配器
    | widget
      -AutoLoadMoreGridView //自动加载更多的GridView
      -AutoLoadMoreListView //自动加载更多的ListView
      -AutoLoadMoreSwipeMenuListView //自动加载更多的带侧滑菜单的ListView
      -AutoLoadMoreRecyclerView //自动加载更多的RecyclerView
    -AutoLoadMoreHandler //自动加载更多的真正处理类
    -AutoLoadMoreHook //PtrAutoLoadMoreLayout的child需实现此类以供PtrAutoLoadMoreLayout获取AutoLoadMoreHandler
    -OnScrollListener //兼容AdapterView与RecyclerView的OnScrollListener

layout.PtrLollipopLayout

我们首要目标是去掉重复的配置代码, 使用起来更简洁. 首先看看一个简单的例子:

<com.corelibs.views.ptr.layout.PtrLollipopLayout
    android:id="@+id/ptrLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:textSize="30sp"
        android:text="拉我拉我"/>

</com.corelibs.views.ptr.layout.PtrLollipopLayout>

Activity代码:

@Bind(R.id.ptrLayout) PtrLollipopLayout<TextView> ptrLayout;

ptrLayout.setRefreshCallback(new PtrLollipopLayout.RefreshCallback() {
    @Override public void onRefreshing(PtrFrameLayout frame) {
        ptrLayout.getPtrView().setText("我被刷了");
        ptrLayout.complete();
    }
});

效果如下:

用法非常简单, 只需使用PtrLollipopLayout包裹任意你想要刷新的控件即可. 在代码中, 如果在声明PtrLollipopLayout时加上泛型, 如PtrLollipopLayout<TextView>, 就可以使用ptrLayout.getPtrView()将PtrLollipopLayout内的TextView取出. 如果不加泛型, 则需要单独为TextView设置id, 并使用ButterKnife bind出来. 两种方式均可.

PtrLollipopLayout内部默认使用了Lollipop风格的下拉头部, 并且做了一些配置工作. 我们同样可以在代码中为PtrLollipopLayout做一些个性化的配置, 如通过setHeaderView(View header)设置自己的头部, 请注意, 自定义的头部必须实现PtrUIHandler接口. 如果出现PtrLollipopLayout解决不了的滑动冲突, 可以调用setPtrHandler(PtrHandler ptrHandler)自行处理滑动.

以下是几个需要注意的点:

  1. 此控件只能包含一个子View.

  2. 此控件仅支持下拉刷新, 如果需要自动加载, 请使用PtrAutoLoadMoreLayout

  3. 如果出现横向滑动冲突, 请设置disableWhenHorizontalMove(boolean)为true.

  4. 如果不想为child设置id并使用findViewById取出, 可以在声明PtrLollipopLayout的时候带上child类型的泛型, 然后就可以使用getPtrView()取出child. 如PtrLollipopLayout<ScrollView>.

  5. 刷新完成或加载完成后请调用complete().

layout.PtrAutoLoadMoreLayout

接下来是第二个目标 - 自动加载更多. 先看栗子:

<com.corelibs.views.ptr.layout.PtrAutoLoadMoreLayout
    android:id="@+id/ptrLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.corelibs.views.ptr.loadmore.widget.AutoLoadMoreListView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:listSelector="#00000000"
        android:divider="#aaa"
        android:dividerHeight="1dp"/>

</com.corelibs.views.ptr.layout.PtrAutoLoadMoreLayout>

ListView item布局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="#333"
    android:gravity="center">

    <TextView
        android:id="@+id/text"
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:gravity="center"
        android:textColor="#fff"
        android:textSize="14sp"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"/>

</LinearLayout>

Activity代码:

@Bind(R.id.ptrLayout) PtrAutoLoadMoreLayout<AutoLoadMoreListView> ptrLayout;

private Handler handler = new Handler(); // 使用handler模拟网络请求
private int count = 0; // 模拟页数计数

protected void init(Bundle savedInstanceState) {
    // 简单的适配器
    final QuickAdapter<String> adapter = new QuickAdapter<String>(this, R.layout.item_test) {
        @Override protected void convert(BaseAdapterHelper helper, String item) {
            helper.setText(R.id.text, item);
        }
    };

    ptrLayout.setLoadingBackgroundColor(0xff333333); // 设置自动加载视图的背景颜色
    ptrLayout.getPtrView().setAdapter(adapter); // 为AutoLoadMoreListView设置Adapter

    adapter.addAll(getData()); // 为adapter添加数据

    // 设置刷新和加载回调
    ptrLayout.setRefreshLoadCallback(new PtrAutoLoadMoreLayout.RefreshLoadCallback() {
        @Override public void onRefreshing(PtrFrameLayout frame) {
            adapter.replaceAll(getData()); // 替换adapter中的数据

            ptrLayout.enableLoading(); // 重新启用自动加载
            ptrLayout.complete(); // 刷新完成
            count = 0; // 重置模拟计数
        }

        @Override public void onLoading(PtrFrameLayout frame) {
            count++; // 模拟页数++
            handler.postDelayed(new Runnable() { // 模拟网络加载延迟
                @Override public void run() {
                    adapter.addAll(getData()); // 将数据加入adapter中.
                    ptrLayout.complete(); // 加载完成

                    if (count > 2)
                        ptrLayout.disableLoading(); // 禁用自动加载
                }
           }, 1500);
        }
    });
}

// 模拟数据
private List<String> getData() {
    List<String> data = new ArrayList<>();
    for (int i = 0; i < 25; i++)
        data.add("呵呵呵呵" + (i + 1));

    return data;
}

效果:

使用带自动加载的下拉刷新就要比单纯的下拉刷新复杂的多. 这种时候就不能使用PtrLollipopLayout而需要使用PtrAutoLoadMoreLayout. 一般情况下, 自动加载更多只会出现在有ListView/GridView的情况下. 因此PtrAutoLoadMoreLayout的子视图基本都是ListView/GridView, 或他们的派生类.

但是如果直接使用PtrAutoLoadMoreLayout加上ListView/GridView, 也是无法实现自动加载的, 如:

<com.corelibs.views.ptr.layout.PtrAutoLoadMoreLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ListView
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</com.corelibs.views.ptr.layout.PtrAutoLoadMoreLayout>

不仅没有效果, 还会报如下错误:

java.lang.IllegalStateException: PtrAutoLoadMoreLayout child should implement AutoLoadMoreHook

这是因为PtrAutoLoadMoreLayout只是一个外壳, 本身只带有下拉刷新的功能, 不带有自动加载的功能. PtrAutoLoadMoreLayout所有有关自动加载的api全部是代理至另外一个类 - AutoLoadMoreHandler. AutoLoadMoreHandler才是真正处理自动加载功能的类. PtrAutoLoadMoreLayout需要借助AutoLoadMoreHook来获取AutoLoadMoreHandler, 因此PtrAutoLoadMoreLayout的子控件必须实现AutoLoadMoreHook. AutoLoadMoreHook的定义:

/**
 * {@link com.corelibs.views.ptr.layout.PtrAutoLoadMoreLayout}的child view需要实现此接口,
 * 供PtrAutoLoadMoreLayout获取{@link AutoLoadMoreHandler}.
 */
public interface AutoLoadMoreHook {
    /**
     * {@link com.corelibs.views.ptr.layout.PtrAutoLoadMoreLayout}需通过此方法获取
     * {@link AutoLoadMoreHandler}对象.
     */
    AutoLoadMoreHandler getLoadMoreHandler();
}

现在PtrAutoLoadMoreLayout就可以通过getLoadMoreHandler来获取AutoLoadMoreHandler实现自动加载更多的功能了. 那么, 例子中的AutoLoadMoreListView又是什么鬼? AutoLoadMoreListView是CoreLibs中预定义好的一个控件, 它实现了AutoLoadMoreHook. 如果我们需要一个带自动加载的ListView, 就可以使用AutoLoadMoreListView. AutoLoadMoreListView全部代码:

public class AutoLoadMoreListView extends ListView implements AutoLoadMoreHook {

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

    public AutoLoadMoreListView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public AutoLoadMoreListView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    @Override
    public AutoLoadMoreHandler getLoadMoreHandler() {
        return new AutoLoadMoreHandler<>(getContext(), new ListViewAdapter<ListView>(this));
    }
}

AutoLoadMoreListView的代码非常简单, 除了三个继承自ListView需要实现的构造函数外, 就只有一个实现了AutoLoadMoreHook的getLoadMoreHandler方法. 该方法中也只是new了一个AutoLoadMoreHandler对象并返回而已.

如果我们有一个自定义的ListView, 实现了侧滑菜单, 名字叫SwipeMenuListView, 我们想要为SwipeMenuListView加上下拉和自动加载怎么办? 很简单, 定义一个新的继承自SwipeMenuListView, 并且实现了AutoLoadMoreHook的控件, 然后我们在PtrAutoLoadMoreLayout中包含该控件即可. 使用方法除了SwipeMenuListView自己的API外, 其他与默认的ListView完全一样. 代码如下:

public class AutoLoadMoreSwipeMenuListView extends SwipeMenuListView implements AutoLoadMoreHook {

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

    public AutoLoadMoreSwipeMenuListView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public AutoLoadMoreSwipeMenuListView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    @Override
    public AutoLoadMoreHandler getLoadMoreHandler() {
        return new AutoLoadMoreHandler<>(getContext(), new ListViewAdapter<SwipeMenuListView>(this));
    }
}

接着看看AutoLoadMoreHandler的定义与构造函数:

public class AutoLoadMoreHandler<T extends LoadMoreAdapter> {
    public AutoLoadMoreHandler(Context context, T wrapper);
}

AutoLoadMoreHandler的构造函数需要两个参数, 第一个context不用多说, 第二个LoadMoreAdapter又是何方神圣? LoadMoreAdapter是一个接口, 类似于适配器, 因此取名叫做Adapter:

/**
 * 针对ListView/GridView等View的适配接口, 用于带自动加载更多的视图
 * <BR/>
 * Created by Ryan on 2016/1/21.
 */
public interface LoadMoreAdapter<T extends ViewGroup> {
    /**
     * 添加FooterView的适配
     */
    void addFooterView(View v, Object data, boolean isSelectable);
    /**
     * 删除FooterView的适配
     */
    boolean removeFooterView(View v);
    /**
     * 设置OnScrollListener的适配
     */
    void setOnScrollListener(OnScrollListener<T> l);
    /**
     * 获取总行数的适配
     */
    int getRowCount();
    /**
     * 获取被包装的View
     */
    T getView();
}

可以看到LoadMoreAdapter的方法基本都是对ListView/GridView的一些方法的包装. LoadMoreAdapter的存在就是为了适配各种ListView/GridView. AutoLoadMoreHandler内部会在需要添加底部视图的时候去调用LoadMoreAdapter的addFooterView, 在需要计算何时要显示底部视图的时候调用getLastVisiblePosition, getRowCount等. 至于具体实现, 则交由各种实现类去处理. CoreLibs中目前定义好了两个实现类 - ListViewAdapter和GridViewAdapter. 两个类的代码差不多, 这里只贴ListViewAdapter的:

/**
 * 针对ListView或继承自ListView的控件的适配类
 * <BR/>
 * Created by Ryan on 2016/1/21.
 */
public class ListViewAdapter<T extends ListView> implements LoadMoreAdapter<T> {

    private T listView;

    public ListViewAdapter(T listView) {
        this.listView = listView;
    }

    @Override
    public void addFooterView(View v, Object data, boolean isSelectable) {
        listView.addFooterView(v, data, isSelectable);
    }

    @Override
    public boolean removeFooterView(View v) {
        return listView.removeFooterView(v);
    }

    @Override
    public void setOnScrollListener(final OnScrollListener<T> l) {
        listView.setOnScrollListener(new AbsListView.OnScrollListener() {
            @Override public void onScrollStateChanged(AbsListView view, int scrollState) {}

            @Override public void onScroll(AbsListView view, int firstVisibleItem,
                                           int visibleItemCount, int totalItemCount) {
                if (l != null)
                    l.onScroll(listView, firstVisibleItem, visibleItemCount, totalItemCount);
            }
        });
    }

    @Override
    public int getRowCount() {
        return listView.getCount();
    }

    @Override
    public T getView() {
        return listView;
    }
}

代码逻辑比较简单, 基本都是直接调用listView的方法而没有很多逻辑判断. 可以看到, AutoLoadMoreSwipeMenuListView中的getLoadMoreHandler的返回语句new AutoLoadMoreHandler<>(getContext(), new ListViewAdapter<SwipeMenuListView>(this))也是使用的ListViewAdapter, 只不过泛型传递的是SwipeMenuListView. 这意味着, 只要是继承自ListView的控件都可以使用ListViewAdapter. GridView同理. 如果以后有了其他类型的控件可以创建一个新的LoadMoreAdapter的实现类.

接下来看看RecylerView的Adapter, 相较于ListView/GridView的会复杂一下:

/**
 * 针对RecyclerView或继承自RecyclerView的控件的适配类
 * <BR/>
 * Created by Ryan on 2016/9/21.
 */
public class RecyclerViewAdapter<T extends RecyclerView>
        implements LoadMoreAdapter<T> {

    private T recyclerView;

    public RecyclerViewAdapter(T recyclerView) {
        this.recyclerView = recyclerView;
    }

    @Override
    public void addFooterView(View v, Object data, boolean isSelectable) {
        AbstractHeaderAndFooterWrapper adapter =
                (AbstractHeaderAndFooterWrapper) recyclerView.getAdapter();
        adapter.addFootView(v);
    }

    @Override
    public boolean removeFooterView(View v) {
        AbstractHeaderAndFooterWrapper adapter =
                (AbstractHeaderAndFooterWrapper) recyclerView.getAdapter();
        adapter.removeFootView(v);
        return true;
    }

    @Override @SuppressWarnings("unchecked")
    public void setOnScrollListener(final OnScrollListener<T> l) {
        recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                int firstVisibleItem = 0, lastVisibleItem, visibleItemCount = 0;
                RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();

                if (layoutManager instanceof LinearLayoutManager) {
                    LinearLayoutManager manager = (LinearLayoutManager) layoutManager;
                    firstVisibleItem = manager.findFirstVisibleItemPosition();
                    lastVisibleItem = manager.findLastVisibleItemPosition();
                    visibleItemCount = lastVisibleItem - firstVisibleItem + 1;
                }

                if (layoutManager instanceof StaggeredGridLayoutManager) {
                    StaggeredGridLayoutManager manager = (StaggeredGridLayoutManager) layoutManager;
                    firstVisibleItem = manager.findFirstVisibleItemPositions(null)[0];
                    lastVisibleItem = manager.findLastVisibleItemPositions(null)[1];
                    visibleItemCount = lastVisibleItem - firstVisibleItem + 1;
                }

                if (l != null)
                    l.onScroll((T) recyclerView, firstVisibleItem, visibleItemCount, recyclerView.getAdapter().getItemCount());

            }
        });
    }

    @Override
    public int getRowCount() {
        return recyclerView.getAdapter().getItemCount();
    }

    @Override
    public T getView() {
        return recyclerView;
    }
}

由于RecyclerView与传统的AdapterView的OnScrollListener不一样, 所以在setOnScrollListener代码中做了一些额外的适配工作. 如果需要使用自定义的LayoutManager, 建议继承自系统默认的三个 - LinearLayoutManager, GridLayoutManager以及StaggeredGridLayoutManager, 因为RecyclerViewAdapter中只适配了这几个的LayoutManager. 或者可以新建一个Adapter专门对自定义的LayoutManager做处理.

接着, 我们来看看具体实现自动加载更多功能的AutoLoadMoreHandler的关键代码:

public class AutoLoadMoreHandler<T extends LoadMoreAdapter> {
    public static final int DEFAULT_WHEN_TO_LOADING = 1;

    private T adapter;
    private int whenToLoading = DEFAULT_WHEN_TO_LOADING; // 当滚动到倒数第几个条目时触发加载

    private PtrAutoLoadMoreLayout.RefreshLoadCallback callback;  //刷新与加载的回调
    private PtrFrameLayout ptrFrameLayout;

    public AutoLoadMoreHandler(Context context, T adapter) {
        this.context = context;
        this.adapter = adapter;
    }

    public void setup(PtrFrameLayout ptrFrameLayout) {
        this.ptrFrameLayout = ptrFrameLayout;
        init();
    }

    public void setRefreshLoadCallback(PtrAutoLoadMoreLayout.RefreshLoadCallback callback) {
        this.callback = callback;
    }

    private void init() {
        // 通过OnScrollListener来监听滑动, 判断何时触发加载操作
        adapter.setOnScrollListener(new AbsListView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(AbsListView view, int scrollState) {}

            @Override
            public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
                // 当前如果是DISABLED, REFRESHING以及FORCE_REFRESH状态, 或者处于下拉刷新的状态时,不再触发加载
                if (state != State.DISABLED && state != State.REFRESHING
                        && state != State.FORCE_REFRESH && !ptrFrameLayout.isRefreshing()) {
                    // 如果条目没有超过一个屏幕, 也不触发
                    if (visibleItemCount < totalItemCount) {
                        if (view.getCount() > 0)
                            if (aboutToLoad())
                                setLoadingStatus();
                    }
                }
            }
        });
    }

    private boolean aboutToLoad() {
        for (int i = 0; i < whenToLoading; i++) {
            // 如果当前最后可见的条目等于总条目-whenToLoading-i, 则触发加载
            if (adapter.getLastVisiblePosition() == (adapter.getRowCount() - whenToLoading - i))
                return true;
        }
        return false;
    }

    private void setLoadingStatus() {
        setLoadingState(State.REFRESHING);
    }

    private void setLoadingState(State state) {
        // 如果当前处于DISABLED, 则状态不能被设置成FINISHED或者REFRESHING, 也就是说无法触发加载
        if (this.state == State.DISABLED && (state == State.FINISHED || state == State.REFRESHING))
            return;
        this.state = state;
        load();
    }

    private void load() {
        switch (state) {
            case ENABLED:
                break;

            case REFRESHING:
            case FORCE_REFRESH:
                showLoadingView();
                break;

            case DISABLED:
            case FINISHED:
                hideLoadingView();
                break;
        }
    }

    private synchronized void showLoadingView() {
        if (!isLoading) {
            loadingContent.setVisibility(View.VISIBLE);
            progress.startAnimation();
            isLoading = true;
            if (callback != null)
                callback.onLoading(ptrFrameLayout);
        }
    }

    private synchronized void hideLoadingView() {
        if (isLoading) {
            loadingContent.setVisibility(View.GONE);
            progress.stopAnimation();
            isLoading = false;
        }
    }

    public enum State {
        /** 刷新结束 **/
        FINISHED,
        /** 刷新中 **/
        REFRESHING,
        /** 失效 **/
        DISABLED,
        /** 生效, 默认状态 **/
        ENABLED,
        /** 强制刷新 **/
        FORCE_REFRESH
    }
}

上述代码逻辑也不是很复杂, 相信配合注释不难理解. 需要注意的是, 在构造函数中, AutoLoadMoreHandler并没有做一些初始化操作, 仅仅是赋值. 初始化操作init是在setup方法中被调用的. 那么setup何时会被调用? 这需要看看PtrAutoLoadMoreLayout的部分代码:

public class PtrAutoLoadMoreLayout<T> extends PtrLollipopLayout<T> {
    private AutoLoadMoreHandler loadMoreHandler;

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();

        loadMoreHandler = setupHook().getLoadMoreHandler();

        if (loadMoreHandler == null)
            throw new IllegalStateException("AutoLoadMoreHandler should not be null");

        loadMoreHandler.setup(this);
    }

    private AutoLoadMoreHook setupHook() {
        if (mContent != null && mContent instanceof AutoLoadMoreHook) {
            return (AutoLoadMoreHook) mContent;
        } else {
            throw new IllegalStateException("PtrAutoLoadMoreLayout child should implement AutoLoadMoreHook");
        }
    }
}

可以看到, 在PtrAutoLoadMoreLayout被从xml解析完成之后, 就会去调用setupHook()获取AutoLoadMoreHook, 并通过AutoLoadMoreHook的getLoadMoreHandler来获取AutoLoadMoreHandler对象. 然后调用setup. setupHook方法中的mContent就是被PtrAutoLoadMoreLayout包裹的子控件. 到此, 整个自动加载更多就解释的差不多了.

以下是使用PtrAutoLoadMoreLayout需要注意的几个地方:

  1. 如果只需下拉刷新功能, 请使用PtrLollipopLayout

  2. 此控件中的child view必须实现AutoLoadMoreHook

  3. 此控件是对AutoLoadMoreHandler功能的转发. AutoLoadMoreHook的getLoadMoreHandler()需要的就是AutoLoadMoreHandler.

  4. 刷新完成或加载完成后请调用complete(), 而不是refreshComplete()或loadingFinished().

总结

下面总结一下PtrLollipopLayout与PtrAutoLoadMoreLayout在使用上的相同点与区别:

  1. 只有下拉刷新功能时应该使用PtrLollipopLayout, 带有自动加载更多时应该使用PtrAutoLoadMoreLayout.

  2. 两者都应该使用complete()方法来结束刷新或者加载状态.

  3. PtrLollipopLayout使用setRefreshCallback(RefreshCallback callback)来设置回调.

  4. PtrAutoLoadMoreLayout使用setRefreshLoadCallback(RefreshLoadCallback callback)来设置回调.

  5. RefreshLoadCallback继承自RefreshCallback, 比RefreshCallback多了onLoading方法.

  6. RefreshCallback定义在PtrLollipopLayout内, RefreshLoadCallback定义在PtrAutoLoadMoreLayout内.

  7. PtrLollipopLayout与PtrAutoLoadMoreLayout都只能包含一个子视图.

  8. PtrLollipopLayout子视图可以是任意View, 但是PtrAutoLoadMoreLayout的子视图必须实现AutoLoadMoreHook接口.

Last updated