Android如何实现一个上拉刷新下拉加载的ListView

Stella981
• 阅读 583

2019-12-20

关键字:自定义上下拉ListView


在 APK 开发中,一个具备在列表顶部下拉刷新、在列表尾部上拉加载功能的 ListView 的需求还是比较多的。

具备这种功能的优秀开源代码同样也有很多。

但今天,笔者就非要自己实现一个这样的控件不可。

以下是成品效果图:

Android如何实现一个上拉刷新下拉加载的ListView

这个控件的结构很简单:

1、一个LinearLayout容器打底;

2、一个ListView置于中间;

3、一个用于标识头部“下拉刷新”标语的控件;

4、一个用于标识尾部“上拉加载”标语的控件;

仅此而已。

所以,笔者这个上下拉列表控件其实是需要自定义一个LinearLayout容器控件。然后在这个容器控件里根据规则来处理触摸事件、点击事件并通知上下拉事件等。

public class PullingListView extends LinearLayout

这里有几个难点:

1、如何监听列表滚动到头部还是尾部亦或正处于中间?

2、上在列表上的上、下滑事件应如何响应成滑出对应的提示标语?

3、首尾提示标语应如何随手势滑出来?

关于第 1 点,直接通过监听 ListView 的 onScrollListener 即可勉强达到目的。

listview.setOnScrollListener(this);

为什么说是勉强呢?因为这个监听会在ListView滚动时回调,虽然它会告诉我们当前ListView中第 1 个可见Item的标号与最后一个可见Item的标号以及总Item数量。但它会在Item刚一加载时就通知,而不是在Item真正展示出来或者真正展示完全以后才通知。这就会存在一个“超前通知”的问题。就是实际上我们还没有看到第 1 个Item,但你却在回调方法中告诉我它已经展示出来了。这会让我们误判。关于这个问题,笔者目前还没有找到解决办法。

而关于第 2 点,则是通过监听ListView的触摸事件,并根据前面 onScrollListener 中得到的当前列表位置,再根据手势方向来决定是该滑出提示语还是让其滚动ListView。

listview.setOnTouchListener(this);

第 3 点其实也不难,只需要在 onTouch 中判断出当前是要滑出头提示还是尾提示,然后再根据手势滑动的垂直距离来实时改变头尾控件的高度,再调用容器中的更新子布局方法即可。

head.setLayoutHeight((int) distanceVertical);
requestLayout();

整个控件的核心就这些东西。整体代码量不多,能实现上面效果图中的功能,但同样也存在一些问题。具体问题就是在列表中数量超过一屏幕容量时,上、下滑动未及端点即开始响应滑出提示语的现象。这个现象的原因笔者在上面已经分析过了。

以下贴出完整源码:

Android如何实现一个上拉刷新下拉加载的ListView Android如何实现一个上拉刷新下拉加载的ListView

/**
 * 一个具备上拉刷新与下拉加载功能的ListView
 * */
public class PullingListView extends LinearLayout implements View.OnTouchListener, AdapterView.OnItemClickListener, AbsListView.OnScrollListener {

    private static final String TAG = "PullingListView";

    private static final int LISTVIEW_SCROLL_STATUS_IN_HEAD = 0;
    private static final int LISTVIEW_SCROLL_STATUS_IN_MIDDLE = 1;
    private static final int LISTVIEW_SCROLL_STATUS_IN_TAIL = 2;

    private float y0;
    private float lastDisHeight; //上次垂直移动的高度。

    private int listViewPos;

    private ListView listview;

    private Header head;
    private Header foot;

    private OnPullingListViewListener listener;
    private ListAdapter adapter;

    public PullingListView(Context context){
        super(context);
        init();
    }

    private void init(){
        Logger.v(TAG, "init()");
        setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
        setOrientation(VERTICAL);

        listview = new ListView(getContext());
        LinearLayout.LayoutParams llp = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
        llp.weight = 1;
        listview.setLayoutParams(llp);

        head = new Header(true);
        foot = new Header(false);

        listview.setOnTouchListener(this);
        listview.setOnItemClickListener(this);
        listview.setOnScrollListener(this);

        addView(head.getView());
        addView(listview);
        addView(foot.getView());
    }

    @Override
    public boolean onTouch(View v, MotionEvent event){
//        Logger.v(TAG, "onTouch,action:" + event.getAction() + ",listViewPos:" + listViewPos);
        switch(event.getAction()){
            case MotionEvent.ACTION_DOWN:{
                y0 = event.getY();
                lastDisHeight = 0;
                listview.scrollTo(0, 0);
                head.setLayoutHeight(0);
                foot.setLayoutHeight(0);
                requestLayout();
            }break;
            case MotionEvent.ACTION_MOVE:{
                float distanceVertical = (event.getY() - y0) / 2.0f; //为了避免响应过于灵敏,垂直滑动距离应延缓5倍。
                switch(listViewPos){
                    case LISTVIEW_SCROLL_STATUS_IN_MIDDLE:{
                        Logger.d(TAG, "MIDDLE");
                        return false;
                    }
                    case LISTVIEW_SCROLL_STATUS_IN_HEAD:{
                        Logger.d(TAG, "HEAD");
                        if(distanceVertical > 0){
                            //往下滑动。
                            head.setLayoutHeight((int) distanceVertical);
                            requestLayout();
                        }else{
                            //往上滑动,要看有没有填满。
                            if(adapter.getCount() > 0){
                                int shownHeight = listview.getChildCount() * (listview.getChildAt(0).getHeight() + listview.getDividerHeight());
                                if(shownHeight <= listview.getHeight()){
//                                    Logger.d(TAG, "None fill out.");
                                    //没填满.
                                    foot.setLayoutHeight((int) distanceVertical);
                                    requestLayout();

                                    if(foot.getView().getLayoutParams().height >= foot.HEAD_LAYOUT_HEIGHT_MAX){
                                        lastDisHeight = distanceVertical;
                                        listview.scrollTo(0, foot.getView().getLayoutParams().height);
                                    }else{
                                        listview.scrollBy(0,  (int) (distanceVertical - lastDisHeight) * -1);
                                    }

                                    lastDisHeight = distanceVertical;
                                }else{
//                                    Logger.d(TAG, "filled out.");
                                    //填满了,要滑动item。
                                    return false;
                                }
                            }else{
//                                Logger.d(TAG, "No records");
                                //没有数据,则忽略掉滑动事件。
                                return true;
                            }
                        }
                    }break;
                    case LISTVIEW_SCROLL_STATUS_IN_TAIL:{
                        Logger.d(TAG, "TAIL");
                        if(distanceVertical < 0){
                            //往上滑动,加载。
                            foot.setLayoutHeight((int) distanceVertical);
                            requestLayout();
                            listview.scrollTo(0, 0);
                        }else{
                            //往下滑动
                            return false;
                        }
                    }break;
                }
            }break;
            case MotionEvent.ACTION_UP:{
                if(head.canLoad()){
                    head.load();
                    if(listener != null) {
                        listener.onRefresh();
                    }
                }else if(foot.canLoad()){
                    foot.load();
                    if(listener != null) {
                        listener.onLoad();
                    }
                }else{
                    foot.setLayoutHeight(0);
                    head.setLayoutHeight(0);
                    requestLayout();
                    listview.scrollTo(0, 0);
                }

            }break;
        }//switch(event.getAction()) -- end

        return false;
    }//onTouch()  -- end

    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        if(listener != null) {
            listener.onItemClick(parent, view, position, id);
        }
    }

    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {
        // do nothing.
        Logger.d(TAG, "onScrollStateChanged,scrollState:" + scrollState);
    }

    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        Logger.d(TAG, "onScroll,firstVisibleItem:" + firstVisibleItem + ",visibleItemCount:" + visibleItemCount + ",totalItemCount:" + totalItemCount);
        if (firstVisibleItem == 0) {
            listViewPos = LISTVIEW_SCROLL_STATUS_IN_HEAD;
        }else if (visibleItemCount + firstVisibleItem == totalItemCount) {
            listViewPos = LISTVIEW_SCROLL_STATUS_IN_TAIL;
        }else{
            listViewPos = LISTVIEW_SCROLL_STATUS_IN_MIDDLE;
        }
    }

    public void setDivider(Drawable divider){
        listview.setDivider(divider);
    }

    public void setDividerHeight(int height){
        listview.setDividerHeight(height);
    }

    public void setAdapter(ListAdapter adapter){
        this.adapter = adapter;
        listview.setAdapter(adapter);
    }

    public void setOnPullingListViewListener(OnPullingListViewListener listener){
        this.listener = listener;
    }

    public void refreshed(){
        Logger.v(TAG, "refreshed");
        listview.post(new Runnable() {
            @Override
            public void run() {
                foot.loadFinished();
                head.loadFinished();
                requestLayout();
                listview.scrollTo(0, 0);
            }
        });
    }

    public void setSelction(int selection){
        Logger.v(TAG, "setSelction:" + selection);
        listview.setSelection(selection);
    }

    /**
     * 上下两个页眉的布局管理。
     * */
    private class Header extends BaseLayoutManager {

        private final int HEAD_LAYOUT_HEIGHT_MAX = UnitManager.px2dp(60);

        private final int STATUS_NORMAL = 0;
        private final int STATUS_TIP = 1;
        private final int STATUS_LOADING = 2;

        private int status;

        private boolean isTop;

        private TextView tv;

        private Header(boolean isTop){
            super(null);
            this.isTop = isTop;
            LinearLayout linearLayout = new LinearLayout(PullingListView.this.getContext());
            LinearLayout.LayoutParams llp = new LinearLayout.LayoutParams(-1, 0);
            linearLayout.setLayoutParams(llp);
            linearLayout.setBackgroundColor(ResourcesManager.getColor(R.color.activity_base_bg));
            linearLayout.setOrientation(LinearLayout.HORIZONTAL);
            linearLayout.setGravity(Gravity.CENTER);

            view = linearLayout;

            ProgressBar pb = new ProgressBar(getContext());
            llp = new LinearLayout.LayoutParams(UnitManager.px2dp(35), UnitManager.px2dp(35));
            pb.setLayoutParams(llp);


            tv = new TextView(getContext());
            if(isTop) {
                tv.setText("刷新列表");
            }else {
                tv.setText("加载更多");
            }
            tv.setGravity(Gravity.CENTER_VERTICAL);
            llp = new LinearLayout.LayoutParams(-2, -1);
            llp.leftMargin = UnitManager.px2dp(15);
            tv.setLayoutParams(llp);

            linearLayout.addView(pb);
            linearLayout.addView(tv);
        }

        private void setLayoutHeight(int height){
            height = Math.abs(height);
            if(height < HEAD_LAYOUT_HEIGHT_MAX){
                view.getLayoutParams().height = height;
                if(height > (HEAD_LAYOUT_HEIGHT_MAX * 0.7)){
                    if(status != STATUS_TIP){
                        status = STATUS_TIP;
                        if(isTop) {
                            tv.setText("松开以刷新");
                        }else{
                            tv.setText("松开以加载");
                        }
                    }
                }else {
                    if(status != STATUS_NORMAL){
                        status = STATUS_NORMAL;
                        if(isTop) {
                            tv.setText("刷新列表");
                        }else{
                            tv.setText("加载更多");
                        }
                    }
                }
            }else{
                view.getLayoutParams().height = HEAD_LAYOUT_HEIGHT_MAX;
            }
        }

        private boolean canLoad(){
            return status == STATUS_TIP;
        }

        private void load(){
            status = STATUS_LOADING;
            if(isTop) {
                tv.setText("刷新中,请稍候");
            }else{
                tv.setText("加载中,请稍候");
            }
        }

        private void loadFinished(){
            status = STATUS_NORMAL;
            view.getLayoutParams().height = 0;
            if(isTop) {
                tv.setText("刷新列表");
            }else {
                tv.setText("加载更多");
            }
        }

    }// class Header -- end

    public interface OnPullingListViewListener {
        void onRefresh();
        void onLoad();
        void onItemClick(AdapterView<?> parent, View view, int position, long id);
    }

}

一种具备上下拉刷新功能的ListView


点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Stella981 Stella981
2年前
Android So动态加载 优雅实现与原理分析
背景:漫品Android客户端集成适配转换功能(基于目标识别(So库35M)和人脸识别库(5M)),导致apk体积50M左右,为优化客户端体验,决定实现So文件动态加载.!(https://oscimg.oschina.net/oscnet/00d1ff90e4b34869664fef59e3ec3fdd20b.png)点击上方“蓝字”关注我
Wesley13 Wesley13
2年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Easter79 Easter79
2年前
SwipeRefreshLayout下拉刷新冲突解决
使用SwipeRefreshLayout,网上资料copy了一个OnScrollListener给ListView,结果当第一个item长度超过一屏,明明还没有到达列表顶部,Scroll事件就被拦截,列表无法滚动,同时启动了刷新。修正代码后,自定义的OnScrollListener如下:/ 由于Listview与下拉刷新的Scroll
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
2年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
3个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这