自定义ListView实现下拉刷新上拉加载功能

本文介绍了一种基于 ListView 的 PullRefreshListView 控件实现方法,包括头部和尾部布局设计、触摸事件监听及滚动状态监测等关键步骤。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1,概述

本案例主要继承自ListView,通过添加"头布局"以及"尾布局",然后再监听onTouchEvent事件,实现 AbsListView. OnScrollListener接口,来监听滑动到顶部以及底部等状态来隐藏与显示view来达到效果的,Github地址:PullRefreshListView
 
 

2,实现步骤

1,创建“头布局”与“尾布局”

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

    <FrameLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="10dp"
        android:layout_marginRight="5dp"
        android:layout_marginTop="10dp">

        <ImageView
            android:id="@+id/refresh__header_arrow"
            android:layout_width="28dp"
            android:layout_height="28dp"
            android:layout_gravity="center"
            android:src="@mipmap/refresh_arrow" />

        <ImageView
            android:id="@+id/refresh_header_load"
            android:layout_width="34dp"
            android:layout_height="34dp"
            android:src="@drawable/refresh_loding"
            android:visibility="invisible" />
    </FrameLayout>

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="10dp"
        android:layout_marginTop="10dp"
        android:gravity="center_horizontal"
        android:orientation="vertical">

        <TextView
            android:id="@+id/refresh_header_status"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="下拉刷新"
            android:textColor="#777"
            android:textSize="12sp" />
    </LinearLayout>
</LinearLayout>
显示效果如下:


FooterView:

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

    <FrameLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="10dp"
        android:layout_marginRight="5dp">

        <ImageView
            android:id="@+id/refresh__header_arrow"
            android:layout_width="28dp"
            android:layout_height="28dp"
            android:layout_gravity="center"
            android:visibility="invisible"
            android:src="@mipmap/refresh_arrow" />

        <ImageView
            android:id="@+id/refresh_footer_load"
            android:layout_width="34dp"
            android:layout_height="34dp"
            android:src="@drawable/refresh_loding"
            android:visibility="visible" />
    </FrameLayout>

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="10dp"
        android:gravity="center_horizontal"
        android:orientation="vertical">

        <TextView
            android:id="@+id/refresh_footer_status"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="加载中..."
            android:textColor="#777"
            android:textSize="12sp" />
    </LinearLayout>
</LinearLayout>

显示效果如下:


2:创建PullRefreshListView

public class PullRefreshListView extends ListView implements AbsListView.OnScrollListener {
/**
     * 顶部刷控件
     */
    private View mHeader;//顶部刷新控件
    private ImageView refreshArrow, refreshHeaderLoad;
    private TextView tvRefreshHeaderStatus;
    private int mHeaderHeight;//顶部刷新控件的高度
    private int mMinTopPadding = 30;//最小下拉padding
    private int mMaxTopPadding = 0;//最大下拉padding

    /**
     * 底部加载控件
     */
    private View mFooter;//顶部刷新控件
    private ImageView refreshFooterLoad;
    private int mFooterHeight;//底部刷新控件的高度

    private int mMaxBottomPadding = 0;//最大上拉padding

    private int mMinBottomPadding = 30;//最小上拉padding
}

在构造方法中初始化这些控件:
 /**
     * 初始化顶部刷新控件
     *
     * @param context 上下文对象
     */
    private void initHeaderView(Context context) {
        mHeader = LayoutInflater.from(context).inflate(R.layout.view_refresh_header_normal, null);
        refreshArrow = (ImageView) mHeader.findViewById(R.id.refresh__header_arrow);
        refreshHeaderLoad = (ImageView) mHeader.findViewById(R.id.refresh_header_load);
        tvRefreshHeaderStatus = (TextView) mHeader.findViewById(R.id.refresh_header_status);
        measureView(mHeader);
        mHeaderHeight = mHeader.getMeasuredHeight();
        mMinTopPadding = -mHeaderHeight;
        setTopPadding(mMinTopPadding);
        addHeaderView(mHeader);
    }

    /**
     * 设置顶部刷新控件的padding,来控制它的显示与隐藏
     *
     * @param topPadding 距离顶部的内边距
     */
    private void setTopPadding(int topPadding) {
        if (mHeader != null && topPadding <= mMaxTopPadding && topPadding >= mMinTopPadding) {
            mHeader.setPadding(mHeader.getPaddingLeft(), topPadding, mHeader.getPaddingRight(), mHeader.getPaddingBottom());
            mHeader.invalidate();
        }
    }

其中比较重要的方法 measureView(mHeader);只有测量之后,才能得到控件的高,具体方法如下:
/**
     * 测量子控件,告诉父控件它占多大的高度和宽度
     *
     * @param child 要测量的view
     */
    private void measureView(View child) {
        ViewGroup.LayoutParams lp = child.getLayoutParams();
        if (lp == null) {
            lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        }
        int widthSpec = ViewGroup.getChildMeasureSpec(0, 0, MeasureSpec.UNSPECIFIED);
        int heightSpec;
        int tempHeight = lp.height;
        if (tempHeight > 0) {
            heightSpec = MeasureSpec.makeMeasureSpec(tempHeight, MeasureSpec.EXACTLY);
        } else {
            heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
        }
        child.measure(widthSpec, heightSpec);
    }

得到控件的高度之后,通过 setTopPadding方法来实现显示与隐藏控件的效果,初始传入的数值为控件高度的负数,FooterView的初始化方法一样

3,监听onTouchEvent事件

在监听触摸事件之前,我们先定义下拉拖动过程中的集中状态,使用枚举类型
public enum RefreshStatus {
        IDLE,  //无状态
        PULL_DOWN,  //开始下拉状态
        RELEASE_REFRESH,  //释放更新状态
        REFRESHING   //刷新中状态
    }

初始化时为 IDLE状态,接下来重点部分来了
/**
     * 如果listview消费了这个事件,就不能滑动了
     *
     * @param ev 事件
     * @return true:消费这个事件
     */
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (RefreshUtils.isAbsListViewToTop(this)) {
                    mDownY = (int) ev.getY();
                    isRemark = true;
                }
                break;
            case MotionEvent.ACTION_MOVE:
                if (handleAtMove(ev)) {
                    return true;
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                if (STATE == RefreshStatus.RELEASE_REFRESH) {
                    STATE = RefreshStatus.REFRESHING;
                    refreshHeaderStatus();
                } else if (STATE == RefreshStatus.PULL_DOWN) {
                    STATE = RefreshStatus.IDLE;
                    refreshHeaderStatus();
                }
                isRemark = false;
                mDownY = 0;
                break;
        }
        return super.onTouchEvent(ev);
    }

在按下时,我们先不记录按下的位置,因为这时候ListView可能并没有到达最顶端,
判断是否到达最顶端:
/**
     * absListView的子类是否已经下拉到最顶部
     *
     * @param absListView absListView
     * @return false
     */
    public static boolean isAbsListViewToTop(AbsListView absListView) {
        if (absListView != null) {
            int firstChildTop = 0;
            if (absListView.getChildCount() > 0) {
                // 如果AdapterView的子控件数量不为0,获取第一个子控件的top
                firstChildTop = absListView.getChildAt(0).getTop() - absListView.getPaddingTop();
            }
            //第一个child显示的下标以及距离顶部的高度都为0
            if (absListView.getFirstVisiblePosition() == 0 && firstChildTop == 0) {
                return true;
            }
        }
        return false;
    }


在滑动过程中,当达到最顶端时我们再开始记录按下的Y坐标,使用isRemark来标记,主要在 handleAtMove(ev)方法:
/**
     * 处理移动
     *
     * @param ev 事件
     */
    private boolean handleAtMove(MotionEvent ev) {
        if (!isRemark) {//如果不是在最顶端滑动的,当滑动到最顶端是,再来计算
            if (RefreshUtils.isAbsListViewToTop(this)) {
                mDownY = (int) ev.getY();
                isRemark = true;
            }
            return false;
        }
        int tempY = (int) ev.getY();
        int fy = (int) ((tempY - mDownY) / RATIO);//设置一个下拉系数,造成下拉比较困难的感觉
        int padding = fy - mHeaderHeight;
        //箭头滑动中的状态变化
        switch (STATE) {
            case IDLE:
                if (fy > 0) {
                    STATE = RefreshStatus.PULL_DOWN;
                    refreshHeaderStatus();
                }
                break;
            case PULL_DOWN:
                setTopPadding(padding);
                if (padding > mMaxTopPadding) {//当下拉到一定高度,状态变成可释放更新状态
                    STATE = RefreshStatus.RELEASE_REFRESH;
                    refreshHeaderStatus();
                } else if (fy <= 0) {
                    STATE = RefreshStatus.IDLE;
                    isRemark = false;
                    refreshHeaderStatus();
                }
                break;
            case RELEASE_REFRESH:
                setTopPadding(padding);
                if (padding <= mMaxTopPadding) {//当下拉到一定高度,状态变成可释放更新状态
                    STATE = RefreshStatus.PULL_DOWN;
                    refreshHeaderStatus();
                }
                break;
        }
        if (fy > 0 && RefreshUtils.isAbsListViewToTop(this)) {
            //ACTION_DOWN 时没有消费此事件,那么子空间会处于按下状态,这里设置ACTION_CANCEL,

            // 使子控件取消按下状态,否则子控件会执行长按事件

            ev.setAction(MotionEvent.ACTION_CANCEL);
            super.onTouchEvent(ev);
            return true;// 当前事件被我们处理并消费--这个特别重要--消费这个事件,listView将无法拖动,这个时候顶部刷新控件的topPadding不断在变大,所以看起来像在下拉的感觉
        } else {
            return false;
        }

当达到顶端且开始下拉的时候,消费这个事件,这个时候ListView处于不可拖动状态,fy大于0,接着我们计算下拉的距离,给HeaderView设置变化的toppadding,这个时候HeaderView慢慢显示出来,同时把ListView向下挤,所以显示出整体往下移的效果。
刚开始下拉的是,进入 PULL_DOWN状态,同时刷新UI,下拉的距离达到最大可下拉距离的时候,状态变成 RELEASE_REFRESH,接下来往回拉的时候又变成PULL_DOWN状态,继续上拉变成初始状态

各种状态刷新UI的方法:
/**
     * 刷新顶部刷新控件的状态
     */
    private void refreshHeaderStatus() {
        switch (STATE) {
            case IDLE:
                hiddenRefreshHeaderView();
                break;
            case PULL_DOWN:
                showTopArrowDown();
                tvRefreshHeaderStatus.setText(mPullDownRefreshText);
                break;
            case RELEASE_REFRESH:
                showTopArrowUp();
                tvRefreshHeaderStatus.setText(mReleaseRefreshText);
                break;
            case REFRESHING:
                setTopPadding(mMaxTopPadding);
                showTopLoadView();
                tvRefreshHeaderStatus.setText(mRefreshingText);
                if (onRefreshCallBack != null) {
                    onRefreshCallBack.refreshing();
                }
                break;
        }
    }

箭头转换动画:
    /**
     * 初始化箭头动画
     */
    private void initAnimation() {
        mUpAnim = new RotateAnimation(0, -180, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
        mUpAnim.setDuration(200);
        mUpAnim.setFillAfter(true);
        mDownAnim = new RotateAnimation(-180, 0, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
        mDownAnim.setDuration(200);
        mDownAnim.setFillAfter(true);
    }

    /**
     * 显示顶部箭头向上动画
     */
    private void showTopArrowUp() {
        refreshArrow.clearAnimation();
        refreshArrow.setVisibility(VISIBLE);
        refreshHeaderLoad.clearAnimation();
        refreshHeaderLoad.setVisibility(GONE);
        refreshArrow.startAnimation(mUpAnim);
    }

执行动画:
 /**
     * 显示顶部箭头向下动画
     */
    private void showTopArrowDown() {
        refreshArrow.clearAnimation();
        refreshArrow.setVisibility(VISIBLE);
        refreshHeaderLoad.clearAnimation();
        refreshHeaderLoad.setVisibility(GONE);
        refreshArrow.startAnimation(mDownAnim);
    }

    /**
     * 显示顶部刷新时的动画
     */
    private void showTopLoadView() {
        refreshArrow.clearAnimation();
        refreshArrow.setVisibility(GONE);
        refreshHeaderLoad.clearAnimation();
        refreshHeaderLoad.setVisibility(VISIBLE);
        AnimationDrawable animationDrawable = (AnimationDrawable) refreshHeaderLoad.getDrawable();
        animationDrawable.start();
    }

    /**
     * 显示顶部刷新时的动画
     */
    private void showBottomLoadView(boolean flag) {
        if (flag) {
            refreshFooterLoad.clearAnimation();
            refreshFooterLoad.setVisibility(VISIBLE);
            AnimationDrawable animationDrawable = (AnimationDrawable) refreshFooterLoad.getDrawable();
            animationDrawable.start();
        } else {
            refreshFooterLoad.clearAnimation();
        }
    }

以上属于下拉刷新部分,上拉加载部分如下:

4,监听滚动接口的变化:

 @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        if (isLoadingMore) {//如果正在进行加载更多操作,直接返回
            return;
        }
        boolean isMachScreen;//listView中的条目是否铺满屏幕,不足一屏不允许上拉加载更多

        isMachScreen = totalItemCount > visibleItemCount;
        Log.e(TAG, "------滑动状态-------" + scrollState);
        if (scrollState == SCROLL_STATE_FLING || scrollState == SCROLL_STATE_TOUCH_SCROLL) {
            isLoadingMore = isLoadMoreEnable && isMachScreen && RefreshUtils.isAbsListViewToBottom(this);
            Log.e(TAG, "------是否可以上拉加载-------" + isLoadingMore);
            if (isLoadingMore && onRefreshCallBack != null) {
                setBottomPadding(mMaxBottomPadding);
                onRefreshCallBack.loading();
            }
        }
    }

是否滚动到最底部:
/**
     * absListView的子类是否已经上拉到最底部
     *
     * @param absListView absListView
     * @return false
     */
    public static boolean isAbsListViewToBottom(AbsListView absListView) {
        //第一步,已经滚动到最后一个子控件
        if (absListView == null || absListView.getAdapter() == null || absListView.getAdapter().getCount() <= 0
                || absListView.getLastVisiblePosition() != absListView.getAdapter().getCount() - 1) {
            return false;
        }

//        View child = absListView.getChildAt(absListView.getLastVisiblePosition() - absListView.getFirstVisiblePosition());
//        if(absListView.getHeight() == child.getBottom()){
//            return true;
//        }
        return true;
    }

最后的效果是,在滚动的时候,只要一滚动到最底端,就显示加载更多布局,同时调用加载更多接口


以上是下拉刷新上拉加载的所有方法,全部代码在GitHub里面,有任何bug欢迎给我留言

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值