美文网首页android基础各种长见识涨姿势android控件
ListView增加下拉刷新,上拉加载更多

ListView增加下拉刷新,上拉加载更多

作者: 炮八平五 | 来源:发表于2016-10-12 23:31 被阅读1512次

    项目简介

    为ListView增加header和footer来实现下拉刷新和上拉加载更多,已经是Android开发老生常谈的问题,网络上有数不清的demo来方便你的项目开发,但是网络上这么多的资源导致你在选择第三方ListView时往往会陷入选择困难,与其每次都要纠结哪一家的第三方ListView比较完美,倒不如参照这些第三方ListView打造出自己想要的ListView。

    项目结构

    项目结构比较清晰,主要分为三部分:header,footer,以及自定义的ListView,在这个项目中,笔者将其定义为:RefreshHeader,RefreshFooter以及RefreshListView

    RefreshHeader实现

    首先来看RefreshHeader的截图:

    0.png 1.png 2.png

    从图中我们可以清晰的看出图中有三种状态:

    • 下拉刷新
    • 松开刷新
    • 正在加载

    创建RefreshHeader.java文件让其extendsLinearLayout,可以为其增加三种状态并初始化状态:

    public static final int PULL_TO_REFRESH = 0;    //下拉刷新
    public static final int RELEASE_TO_REFRESH = 1; //松开刷新
    public static final int REFRESHING = 2;         //正在刷新
    int mState = PULL_TO_REFRESH;
    

    编写RefreshHeader我们可以分为四步来执行:

    1. 编写布局文件
    2. 根据状态改变RefreshHeader界面显示
    3. 根据手指拖动位置来改变RefreshHeader高度和状态
    4. 根据手指松开位置再次改变状态,滚动至相应位置

    首先编写布局文件:

    refresh_header.xml

    <?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="bottom"
        android:orientation="vertical">
    
        <RelativeLayout
            android:id="@+id/header_content"
            android:layout_width="match_parent"
            android:layout_height="@dimen/refresh_header_height">
    
    
            <LinearLayout
                android:id="@+id/ll_pull_state"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerInParent="true"
                android:gravity="center"
                android:orientation="vertical">
    
                <TextView
                    android:id="@+id/tv_pull_state"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="下拉刷新" />
            </LinearLayout>
    
            <ProgressBar
                android:id="@+id/progressBar"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerVertical="true"
                android:layout_gravity="center"
                android:layout_marginRight="10dp"
                android:layout_toLeftOf="@id/ll_pull_state"
                android:visibility="invisible" />
    
            <ImageView
                android:id="@+id/iv_arrow"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerVertical="true"
                android:layout_marginRight="15dp"
                android:layout_toLeftOf="@id/ll_pull_state"
                android:src="@drawable/arrow_down" />
        </RelativeLayout>
    
    </LinearLayout>
    

    布局文件比较简单,就是一些简单的LinearLayout于RelativeLayout的嵌套,这里不再细说,主要来讲解下RefreshHeader.java文件:

    private void init(Context context) {
        LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, 0);
        mContainer = LayoutInflater.from(context).inflate(R.layout.refresh_header, null);
        addView(mContainer, lp);
    
        rl_my_content = (RelativeLayout) findViewById(R.id.header_content);
        tv_pull_state = (TextView) findViewById(R.id.tv_pull_state);
        iv_arrow = (ImageView) findViewById(R.id.iv_arrow);
        progressBar = (ProgressBar) findViewById(R.id.progressBar);
    
        rotateUpAnimation = new RotateAnimation(0, -180, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
        rotateUpAnimation.setDuration(180);
        rotateUpAnimation.setFillAfter(true);
    
        rotateDownAnimation = new RotateAnimation(-180, 0, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
        rotateDownAnimation.setDuration(180);
        rotateDownAnimation.setFillAfter(true);
    }
    
    /**
     * 获取头部的高度
     * @return
     */
    public int getHeaderHeight() {
        return mContainer.getHeight();
    }
    
    /**
     * 修改header的高度
     * @param height
     */
    public void setHeaderHeight(int height) {
        if (height < 0) height = 0;
        LayoutParams layoutParams = (LayoutParams) mContainer.getLayoutParams();
        layoutParams.height = height;
        mContainer.setLayoutParams(layoutParams);
    }
    

    由代码可以看出,在我们根据布局文件创建mContainer 时候,由于xml文件中android:layout_widthandroid:layout_height失效,所以我们需要为其指定了一个LayoutParams,一是在为之后ListView的addHeader()方法调用之后能隐藏RefreshHeader,二是宽度让其匹配父窗体(如果不这么做,控件不居中)。由于执行过程中涉及到ImageView的旋转动画效果,我们为其指定两个旋转动画,最后再增加获取和修改RefreshHeader的高度的方法,为后续操作作准备。

    根据状态改变RefreshHeader界面显示

    初始化操作之后,我们需要根据状态为其设置相应的布局:

    /**
     * 根据状态设置相应的布局
     * @param state
     */
    public void setHeaderState(int state) {
        if (state == mState && !isFirst) {
            return;
        }
        isFirst = false;
        if (state == REFRESHING) {
            iv_arrow.clearAnimation();
            progressBar.setVisibility(View.VISIBLE);
            iv_arrow.setVisibility(View.INVISIBLE);
            tv_pull_state.setText("正在刷新");
        } else {
            progressBar.setVisibility(View.INVISIBLE);
            iv_arrow.setVisibility(View.VISIBLE);
        }
    
        switch (state) {
            case PULL_TO_REFRESH:
                if (mState == RELEASE_TO_REFRESH) {
                    iv_arrow.clearAnimation();
                    iv_arrow.startAnimation(rotateDownAnimation);
                }
                tv_pull_state.setText("下拉刷新");
                break;
            case RELEASE_TO_REFRESH:
                if (mState == PULL_TO_REFRESH) {
                    iv_arrow.clearAnimation();
                    iv_arrow.startAnimation(rotateUpAnimation);
                }
                tv_pull_state.setText("松开刷新");
                break;
    
        }
        mState = state;
    }
    

    方法逻辑比较简单,根据传递进来的state不同来改变不同的布局UI,这里需要注意的是,因为布局有用到动画来控制iv_arrow这个控件,所以每次改变状态时需要执行clearAnimation()方法,来消除之前动画效果的影响。

    根据手指拖动位置来改变RefreshHeader高度和状态

    首先我们来看一下项目操作:

    执行下拉刷新 不执行下拉刷新

    1.当拖动位置大于指定高度()时,状态改变执行setHeaderState()并传入RELEASE_TO_REFRESH参数。松开手指后,状态改变,执行setHeaderState()并传入REFRESHING参数,位置自动滚动到相应位置。

    2.当拖动位置小于指定高度()时,状态改变执行setHeaderState()并传入PULL_TO_REFRESH参数。松开手指后,状态不改变,位置自动滚动到隐藏位置。

    • 获取指定高度

    初始化高度在RefreshListView初始化方法init()中调用:

    private void init(Context context) {
        mContext = context;
    
        //初始化mScroller,设置插值器为减速插值器,隐藏header或者footer时,数值变化先快后慢
        mScroller = new Scroller(mContext, new DecelerateInterpolator());
        setOnScrollListener(this);
    
        //初始化header
        mHeader = new RefreshHeader(mContext);
        mHeaderContent = (RelativeLayout) mHeader.findViewById(R.id.header_content);
        this.addHeaderView(mHeader);
    
        //初始化footer
        mFooter = new RefreshFooter(mContext);
        this.addFooterView(mFooter);
    
        // 获取header头部的固定高度
        ViewTreeObserver observer = mHeader.getViewTreeObserver();
        if (null != observer) {
            observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
                @SuppressWarnings("deprecation")
                @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
                @Override
                public void onGlobalLayout() {
                    mHeaderHeight = mHeaderContent.getHeight();
    
                    ViewTreeObserver observer = getViewTreeObserver();
                    if (null != observer) {
                        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
                            observer.removeGlobalOnLayoutListener(this);
                        } else {
                            observer.removeOnGlobalLayoutListener(this);
                        }
                    }
                }
            });
        }
    }
    
    • 改变RefreshHeader高度和RefreshHeader状态

    我们知道,RefreshHeader高度是随着手指的变化而变化,所以重写RefreshListView的onTouchEvent()方法:

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if (mLastY == -1) {
            mLastY = ev.getRawY();
        }
    
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastY = ev.getRawY();
                break;
            case MotionEvent.ACTION_MOVE:
                float dy = ev.getRawY() - mLastY;
                mLastY = ev.getRawY();
                if (getFirstVisiblePosition() == 0 && (mHeader.getHeaderHeight() > 0 ||
                        dy > 0)) {
                    //OFFSET_RADIO的设置,让下拉header时感觉到阻力,增强体验
                    updateHeaderHeight(dy / OFFSET_RADIO);
                }
    
                if (getLastVisiblePosition() == mTotalItemCount - 1 &&
                        (mFooter.getFooterMargin() > 0 || dy < 0)) {
                    //原理同上
                    updateFooterHeight(-dy / OFFSET_RADIO);
                }
    
                break;
            case MotionEvent.ACTION_UP:
                mLastY = ev.getRawY();
                if (getFirstVisiblePosition() == 0) {
                    if (!mPullRefreshing && mEnablePullRefresh && (mHeader.getHeaderHeight() > mHeaderHeight)) {
                        refresh();
                    }
                    resetHeaderHeight();
                }
    
                if (getLastVisiblePosition() == mTotalItemCount - 1) {
                    if (!mPullLoading && mEnableLoadMore && (mFooter.getFooterMargin() > PULL_LOAD_MORE_DELTA)) {
                        loadMore();
                    }
                    resetFooterHeight();
                }
    
                break;
    
        }
        return super.onTouchEvent(ev);
    }
    

    注意看:

     if (getFirstVisiblePosition() == 0 && (mHeader.getHeaderHeight() > 0 ||dy > 0)) {
          //OFFSET_RADIO的设置,让下拉header时感觉到阻力,增强体验
          updateHeaderHeight(dy / OFFSET_RADIO);
     }
    

    OFFSET_RADIO是一个大于1.0的数,通常设置在1.5到2.0之间,这里取1.8,目的是让用户下拉时RefreshHeader高度的变化比手指拖动距离小,给用户一种下拉阻力的感觉。接下来我们看updateHeaderHeight(dy / OFFSET_RADIO);方法:

    private void updateHeaderHeight(float delta) {    
        //改变RefreshHeader高度
        mHeader.setHeaderHeight((int) delta + mHeader.getHeaderHeight());
        //改变RefreshHeader状态
        if (mEnablePullRefresh && !mPullRefreshing) {
            if (mHeader.getHeaderHeight() > mHeaderHeight) {
                mHeader.setHeaderState(RefreshHeader.RELEASE_TO_REFRESH);
            } else {
                mHeader.setHeaderState(RefreshHeader.PULL_TO_REFRESH);
            }
        }
        //保证改变RefreshListView高度的同时,不滑动RefreshListView
        setSelection(0);
    }
    

    代码根据传进来的delta值来改变RefreshHeader的界面高度,并将与之前获取到的mHeaderHeight高度进行对比,来改变RefreshHeader的状态,进而改变RefreshHeader的界面内容。需要注意的是这里需要加上setSelection(0);这个方法。我们在父类ListView中找到该方法:

    @Override
    public void setSelection(int position) {
        setSelectionFromTop(position, 0);
    }
    

    跟进到setSelectionFromTop(int position, int y)方法中,我们可以在注释中找到:

    Sets the selected item and positions the selection y pixels from the top edgeof the ListView. (If in touch mode, the item will not be selected but it willstill be positioned appropriately.)
    

    该方法的作用就是让该position上的Item顶到距离ListView上方边缘y像素的地方,说白了这个方法的使用避免了你在改变RefreshHeader高度的同时又滑动RefreshListView,避免造成你手指下拉1个单位同时,RefreshHeader的高度改变1+1/OFFSET_RADIO(非精确)的高度(由于设置RefreshHeader的高度时会把delta强制转换int类型,实际也不会这么精确)。setSelection(0);的设置确保你的RefreshHeader的改变高度是你所期望的1/OFFSET_RADIO手指下拉高度。

    根据手指松开位置再次改变状态,滚动至相应位置

    看到这里,我们再把之前写的两句话复制过来:

    1.当拖动位置大于指定高度()时,状态改变执行setHeaderState()并传入RELEASE_TO_REFRESH参数。松开手指后,状态改变,执行setHeaderState()并传入REFRESHING参数,位置自动滚动到相应位置。

    2.当拖动位置小于指定高度()时,状态改变执行setHeaderState()并传入PULL_TO_REFRESH参数。松开手指后,状态不改变,位置自动滚动到隐藏位置。

    case MotionEvent.ACTION_UP:
        mLastY = ev.getRawY();
        if (getFirstVisiblePosition() == 0) {
            if (!mPullRefreshing && mEnablePullRefresh && (mHeader.getHeaderHeight() > mHeaderHeight)) {
                refresh();
            }
            //RefreshHeader位置自动滚动到相应位置
            resetHeaderHeight();
        }
    
    private void refresh() {
        mPullRefreshing = true;
        if (mEnablePullRefresh && refreshListViewListener != null) {
            mHeader.setHeaderState(RefreshHeader.REFRESHING);
            refreshListViewListener.onRefresh();
        }
    }
    

    当拖动位置大于指定高度()时,松开手指,执行refresh()方法,方法中干了两件事:

    • 改变RefreshHeader的状态
    • 将正在刷新状态传递到外部,让其去处理

    不论最后拖动位置如何,最后都要调用resetHeaderHeight()方法:

    private void resetHeaderHeight() {
        int height = mHeader.getHeaderHeight();
        if (height == 0)
            return;
    
        if (mPullRefreshing && height < mHeaderHeight) {  //正在刷新并且将header向上拖动隐藏时
            return;
        }
    
        int finalHeight = 0;
    
        if (mPullRefreshing && mHeader.getHeaderHeight() >= mHeaderHeight) {
            finalHeight = mHeaderHeight;
        }
        mScrollBack = SCROLL_BACK_HEADER;
        mScroller.startScroll(0, height, 0, finalHeight - height, SCROLL_DURATION);
    
        //激活computeScroll方法
        invalidate();
    }
    

    在这个方法中,我们最终会调用mScroller的startScroll方法,通过激活computeScroll()的方法来让我们的RefreshHeader自动滚动到我们想要的位置:

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            if (mScrollBack == SCROLL_BACK_HEADER) {
                mHeader.setHeaderHeight(mScroller.getCurrY());
            } else {
                mFooter.setFooterMargin(mScroller.getCurrY());
            }
        }
        super.computeScroll();
    }
    

    自此,ListView下拉刷新部分就告一段落。

    RefreshFooter实现

    同样地,编写RefreshFooter我们可以分为四步来执行:

    1. 编写布局文件
    2. 根据状态改变RefreshFooter界面显示
    3. 根据手指拖动位置来改变RefreshFooter高度和状态
    4. 根据手指松开位置再次改变状态,滚动至相应位置

    由于RefreshFooter代码逻辑与RefreshHeader相似,这里需要注意的是RefreshFooter的初始高度默认不是0,目的是使用户松开手指惯性滑动到最底部时,显示的最后一个Item时我们设置的RefreshFooter:

    mContainer.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
    

    最后

    整体来说,RefreshListView的逻辑整体看起来并不复杂,只要将其逻辑按照前文说的4个步骤来实现,化繁为简,就不会觉得无从下手。
    RefreshListView是我在学习android阶段一直想写的一个项目,借由国庆期间的几天时间,研读了Github上的XListView以及PullToRefreshListView,趁着最近刚刚学习,将思路贴上来与大家分享,也算是做一份总结。如写得有错误,欢迎指正~

    项目代码:

    https://github.com/MakeDeath/RefreshListView

    相关文章

      网友评论

      • 我一定会学会:为什么你监听得到的
        // 获取header头部的固定高度
        ViewTreeObserver observer = mHeader.getViewTreeObserver();
        if (null != observer) {
        observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
        @SuppressWarnings("deprecation")
        @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
        @Override
        public void onGlobalLayout() {
        mHeaderHeight = mHeaderContent.getHeight();

        ViewTreeObserver observer = getViewTreeObserver();
        if (null != observer) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
        observer.removeGlobalOnLayoutListener(this);
        } else {
        observer.removeOnGlobalLayoutListener(this);
        }
        }
        }
        });
        }
        得到的高度是xml定义的高度,而不是你代码中 LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, 0);
        把它重置为0,的高度
      • 我一定会学会:private void updateHeaderHeight(float delta) {
        //改变RefreshHeader高度
        mHeader.setHeaderHeight((int) delta + mHeader.getHeaderHeight());
        //改变RefreshHeader状态
        if (mEnablePullRefresh && !mPullRefreshing) {
        if (mHeader.getHeaderHeight() > mHeaderHeight) {
        mHeader.setHeaderState(RefreshHeader.RELEASE_TO_REFRESH);
        } else {
        mHeader.setHeaderState(RefreshHeader.PULL_TO_REFRESH);
        }
        }
        //保证改变RefreshListView高度的同时,不滑动RefreshListView
        setSelection(0);
        }
        这个判断(mHeader.getHeaderHeight() > mHeaderHeight)这个判断为什么获取到的不会是永远相等?mHeaderHeight的值不是在高度变化的时候就变化的,而mHeader.getHeaderHeight() 如果高度变化也会变化,为什么他们会不相等,可否告知一下
        炮八平五:mHeaderHeight这个数值在init()方法里面就已经被测出来了,是一个固定数值,也是xml文件里面已经定义好的60dp对应数值;mHeader.getHeaderHeight() 这个方法得到的是RefreshHeader的高度,这个是实时变化的,为什么实时变化,因为在onTouchEvent监听手指的移动,调用了updateHeaderHeight()方法,这个方法的第一行,改变了mHeader的高度。

      本文标题:ListView增加下拉刷新,上拉加载更多

      本文链接:https://www.haomeiwen.com/subject/otjmyttx.html