带你一步一步实现listview上拉加载下拉刷新

Posted by Lazy on July 3, 2016

#带你一步一步实现listview上拉加载下拉刷新

#思路:

  • 要是实现这个效果我们需要,有一个view能在头部和底部存在(通过查找的只listview有addheard,和addfoot的方法可以将一个view添加到头部的底部)
  • 如何进来的时候不显示头部的view呢?(我门可以设置View的初始高度为0,或者设置setPadding设置他距离上面的距离为-的高度就可以隐藏了,我们使用setPadding方法)
  • 如何隐藏底部的view?(同上)
  • 如何能让view跟着我们的手势移动呢?(第一种我们可以使用OverScroller这个类,第二种我们可以通过动态的改变头部view和底部view的高度来实现移动的的效果,我们使用后者)
  • 如何实现view的自动回弹呢?(我们可以使用OverScroller这个类的startScroll方法,然后实现computeScroll方法在这个里面去动态的改变view的高度)

#第一步我们创建HeaderView这个类当顶部的刷新的view


public class HeaderView extends LinearLayout {
	/** 刷新状态 */
	private LoadState mState = LoadState.NORMAL;

	private View mHeader = null;
	private ImageView mArrow = null;
	private ProgressBar mProgressBar = null;
	private TextView mRefreshTips = null;
	private TextView mRefreshLastTime = null;
	private RotateAnimation mRotateUp = null;
	private RotateAnimation mRotateDown = null;
	private final static int ROTATE_DURATION = 250;

	/** 一分钟的毫秒值,用于判断上次的更新时间. */
	private final long ONE_MINUTE = 60 * 1000;
	/** 一小时的毫秒值,用于判断上次的更新时间. */
	private final long ONE_HOUR = 60 * ONE_MINUTE;
	/** 一天的毫秒值,用于判断上次的更新时间. */
	private final long ONE_DAY = 24 * ONE_HOUR;
	/** 一月的毫秒值,用于判断上次的更新时间. */
	private final long ONE_MONTH = 30 * ONE_DAY;
	/** 一年的毫秒值,用于判断上次的更新时间. */
	private final long ONE_YEAR = 12 * ONE_MONTH;

	public HeaderView(Context context) {
		this(context, null);
	}

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

	private void initHeaderView(Context context) {
		LinearLayout.LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, 0);
		mHeader = LayoutInflater.from(context).inflate(R.layout.g_refresh_header, null);
		addView(mHeader, lp);
		setGravity(Gravity.BOTTOM);
		mArrow = (ImageView) mHeader.findViewById(R.id.ivArrow);
		mProgressBar = (ProgressBar) mHeader.findViewById(R.id.pbWaiting);
		mRefreshTips = (TextView) mHeader.findViewById(R.id.refresh_tips);
		mRefreshLastTime = (TextView) mHeader.findViewById(R.id.refresh_last_time);
		//初始化旋转的动画需要反转箭头
		mRotateUp = new RotateAnimation(0.0f, -180.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
		mRotateUp.setDuration(ROTATE_DURATION);
		mRotateUp.setFillAfter(true);

		mRotateDown = new RotateAnimation(-180.0f, 0.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
		mRotateDown.setDuration(ROTATE_DURATION);
		mRotateDown.setFillAfter(true);
		Log.e("HeaderView","HeaderView初始化了");
	}

	public void setHeaderState(LoadState state) {
		if (mState == state) {
			return;
		}
		mArrow.clearAnimation();
		if (state == LoadState.LOADING) {
			mArrow.setVisibility(View.GONE);
			mProgressBar.setVisibility(View.VISIBLE);
		} else {
			mProgressBar.setVisibility(View.GONE);
			mArrow.setVisibility(View.VISIBLE);
		}

		switch (state) {
		case NORMAL:
			mArrow.startAnimation(mRotateDown);
			mRefreshTips.setText(R.string.g_pull_down_for_refresh);
			break;

		case WILL_RELEASE:
			//旋转当前的箭头的状态
			mArrow.startAnimation(mRotateUp);
			mRefreshTips.setText(R.string.g_release_for_refresh);
			break;

		case LOADING:
			mRefreshTips.setText(R.string.g_refreshing);
			break;

		default:
			break;
		}

		mState = state;
	}

	public LoadState getCurrentState() {
		return mState;
	}

	public void setHeaderHeight(int height) {
		if (height <= 0) {
			height = 0;
		}
		LayoutParams lp = (LayoutParams) mHeader.getLayoutParams();
		lp.height = height;
		mHeader.setLayoutParams(lp);
	}

	public int getHeaderHeight() {
		return mHeader.getHeight();
	}



我们可以看到HeaderView这个类很简单,就是初始化一个view,然后提供了设置高度,和不同状态的判断显示的不同字体的逻辑

##我们创建FooterView这个类其实和heardview一样

		
/**
 * 底部
 */
public class FooterView extends LinearLayout {
	/** 加载更多. */
	private LoadState mState = LoadState.NORMAL;

	private View mFooter = null;
	private ImageView mArrow = null;
	private ProgressBar mProgressBar = null;
	private TextView mLoaderTips = null;

	private RotateAnimation mRotateUp = null;
	private RotateAnimation mRotateDown = null;
	private final static int ROTATE_DURATION = 250;

	public FooterView(Context context) {
		this(context, null);
	}

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

	private void initFooterView(Context context) {
		LinearLayout.LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, 0);
		mFooter = LayoutInflater.from(context).inflate(R.layout.g_loader_footer, null);
		addView(mFooter, lp);

		mArrow = (ImageView) mFooter.findViewById(R.id.ivLoaderArrow);
		mProgressBar = (ProgressBar) mFooter.findViewById(R.id.pbLoaderWaiting);
		mLoaderTips = (TextView) mFooter.findViewById(R.id.loader_tips);

		mRotateDown = new RotateAnimation(0.0f, 180.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
		mRotateDown.setDuration(ROTATE_DURATION);
		mRotateDown.setFillAfter(true);

		mRotateUp = new RotateAnimation(180.0f, 0.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
		mRotateUp.setDuration(ROTATE_DURATION);
		mRotateUp.setFillAfter(true);

		show();
	}

	public void setFooterState(LoadState state) {
		if (mState == state) {
			return;
		}

		mArrow.clearAnimation();
		if (state == LoadState.LOADING) {
			mProgressBar.setVisibility(View.VISIBLE);
			mArrow.setVisibility(View.GONE);
		} else if (state == LoadState.NODATA) {
			mProgressBar.setVisibility(View.GONE);
			mArrow.setVisibility(View.GONE);
		} else {
			mProgressBar.setVisibility(View.GONE);
			mArrow.setVisibility(View.VISIBLE);
		}

		switch (state) {
		case NORMAL:
			mArrow.startAnimation(mRotateUp);
			mLoaderTips.setText(R.string.g_pull_up_for_more);
			break;

		case WILL_RELEASE:
			mArrow.startAnimation(mRotateDown);
			mLoaderTips.setText(R.string.g_release_for_more);
			break;

		case LOADING:
			mLoaderTips.setText(R.string.g_loading);
			break;

		case NODATA:
			mLoaderTips.setText(R.string.g_nodata);
			break;

		default:
			break;
		}
		mState = state;
	}

	public LoadState getCurrentState() {
		return mState;
	}

	public void setFooterHeight(int height) {
		if (height <= 0) {
			height = 0;
		}

		LayoutParams lp = (LayoutParams) mFooter.getLayoutParams();
		lp.height = height;
		mFooter.setLayoutParams(lp);
	}

	public int getFooterHeight() {
		return mFooter.getHeight();
	}

}


逻辑也是一样,设置高度,提供不同状态的判断,显示不同的内容

##接下来上我们的主要类XrefershListview



/**
 * 作者:liujingyuan on 2015/12/21 13:14
 * 邮箱:906514731@qq.com
 * 自定义的下拉刷新上拉加载的Listview
 */
public class XrefershListview extends ListView implements AbsListView.OnScrollListener {
    private final static String TAG = "[XrefershListview]";
    private final static float OFFSET_Y = 0.7f;
    private OverScroller mScroller;
    private final static int SCROLL_HEADER = 0;
    private final static int SCROLL_FOOTER = 1;
    //刷新的监听
    public XrefershListviewListener mListViewListener;
    /** 滑动项. */
    private int iScrollWhich = SCROLL_HEADER;
    //是否显示footview
    public boolean isShowLoadeFooterView=false;
    HeaderView mHeaderView;
    FooterView mFooterView;
    int   iHeaderHeight;
    int iFooterHeight;
    float EndRawy;
    float mStartY;
    float dy;
    private float mLastY;

    public XrefershListview(Context context) {
        super(context);
        initHeardView(context);
    }

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

    public XrefershListview(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initHeardView(context);
    }
    //初始化头部的view
    private void initHeardView(Context context) {
        //初始化scmScroller
        mScroller = new OverScroller(context,new DecelerateInterpolator());
        mHeaderView = new HeaderView(context);
        mFooterView = new FooterView(context);
        //初始化hearview的高度
        mHeaderView.setHeaderHeight(200);
        //初始化footview
        initFooterView(context);
        final RelativeLayout header_content = (RelativeLayout) mHeaderView.findViewById(R.id.header_content);
        addHeaderView(mHeaderView);
        this.setOnScrollListener(this);
        //监听到view加载完毕获取view的高度
        mHeaderView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                iHeaderHeight =mHeaderView.getMeasuredHeight();
                Log.d(TAG, "iHeaderHeight = " + iHeaderHeight);
                //刚进来的时候我们隐藏heardview
                mHeaderView.setPadding(0,-iHeaderHeight,0,0);
                getViewTreeObserver().removeGlobalOnLayoutListener(this);
            }
        });
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //按下的时候我们需要记住起始点的Y轴的坐标
                mLastY = ev.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                //移动的时候获取当前的y轴的坐标
                mStartY = ev.getY();
                //计算滑动的距离
                dy=mStartY-mLastY;
                //再次获取一次,因为我们是增量增加宽度的,每次获取的是相对于上一点的位移
                mLastY = ev.getY();
                if (getFirstVisiblePosition()==0&& (mHeaderView.getMeasuredHeight() > 0 || dy > 0)){
                    //通过改变heardview的高度
                    upDataHeardView(dy*0.7f);
                }else if (getLastVisiblePosition() == getCount() - 1 && (mFooterView.getFooterHeight() > 0 || dy < 0)){
                    //通过改变footview的高度
                    updateFooterState(-dy);
                }
                Log.e(TAG,"dy============"+dy);
               break;
            case MotionEvent.ACTION_UP:
                EndRawy = ev.getRawY();
                //如果当前的可见的第一个条目是0,并且当前的hreadview的高度不为0,滑动的距离大于0就证明是在下拉刷新
                if (getFirstVisiblePosition()==0&& (mHeaderView.getMeasuredHeight() > 0 || dy > 0)){
                    //判断当前的状态不是正在刷新的状态就设置为刷新的状态
                    if (mHeaderView.getCurrentState() != LoadState.LOADING) {
                        if (mHeaderView.getHeaderHeight() > iHeaderHeight) {
                            mHeaderView.setHeaderState(LoadState.LOADING);
                            //调用是界面的刷新,延时1秒
                            mListViewListener.onRefresh();
                            new Handler().postDelayed(new Runnable() {
                                @Override
                                public void run() {
                                    resetHeaderHeight();
                                }
                            },1000);
                        }
                    }
                 //如果当前已经显示到最后一个条目了就是上拉加载,并且当前footview的高度大于0,移动的距离也大于0
                }else if(getLastVisiblePosition() == getCount() - 1 && (mFooterView.getFooterHeight() > 0 || dy < 0)){
                    //判断当前的状态不是正在刷新的状态就设置为刷新的状态
                    if (mFooterView.getCurrentState() != LoadState.LOADING) {
                        if (mFooterView.getFooterHeight() > iFooterHeight) {
                            mFooterView.setFooterState(LoadState.LOADING);
                            new Handler().postDelayed(new Runnable() {
                                @Override
                                public void run() {
                                    //调用是界面的刷新,延时1秒
                                    mListViewListener.onLoadMore();
                                    resetFooter();
                                }
                            },3000);
                        }
                    }
                }
                break;
            default:
                break;
        }
        return super.onTouchEvent(ev);
    }
    /**
     * 更新底部footview的状态
     *
     * @param delta
     */
    private void updateFooterState(float delta) {
        if (null == mFooterView) {
            return;
        }
        //设置footview的高度移动的时候,这个高度是增加每次移动的点相当于上一个点的位置的距离
        mFooterView.setFooterHeight((int) (delta + mFooterView.getFooterHeight()));
        //当前的状态不是在刷新的状态!
        if (mFooterView.getCurrentState() != LoadState.LOADING) {
            if (mFooterView.getFooterHeight() > iFooterHeight) {
                //设置当前的显示的内容为松开加载更多
                mFooterView.setFooterState(LoadState.WILL_RELEASE);
            } else {
                mFooterView.setFooterState(LoadState.NORMAL);
            }
        }
    }
    /**
     * 初始化底部footview
     */
    private void initFooterView(Context context) {
        mFooterView = new FooterView(context);
        mFooterView.setFooterHeight(200);
        //隐藏footview
        mFooterView.setPadding(0,0,0,-200);
        addFooterView(mFooterView);
        mFooterView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                //获取测量的footview的高度
                iFooterHeight = mFooterView.getMeasuredHeight();
                getViewTreeObserver().removeGlobalOnLayoutListener(this);
            }
        });
    }
    /**
     * 重置footview
     */
    private void resetFooter() {
        //重置刷新状态
        mFooterView.setFooterState(LoadState.NORMAL);
        if (null == mFooterView) {
            return;
        }
        int height = mFooterView.getMeasuredHeight();
        if (height == 0) {
            return;
        }
        int finalHeight = 0;
        if (height > iFooterHeight && mFooterView.getCurrentState() != LoadState.NORMAL) {
            finalHeight = iFooterHeight;
        } else if (mFooterView.getCurrentState() == LoadState.LOADING) {
            return;
        }
        iScrollWhich = SCROLL_FOOTER;
        mScroller.startScroll(0, height, 0, finalHeight - height, 300);
        invalidate();
    }
    /**
     * 重置heardview的状态
     */
    private void resetHeaderHeight() {
        //重置刷新的状态
        mHeaderView.setHeaderState(LoadState.NORMAL);
        //获取测量得到的heardview的高度
        int height = mHeaderView.getMeasuredHeight();
        //如果获取当前的View的高度等于0代表是没有移动就不做处理
        if (height == 0) // not visible.
            return;
        int finalHeight = 0;
        //如果超过HeaderView高度,则回滚到HeaderView高度即可
        if (height > iHeaderHeight && mHeaderView.getCurrentState() != LoadState.NORMAL) {
            finalHeight = iHeaderHeight;
        }
        //标记当前是heardview在移动
        iScrollWhich = SCROLL_HEADER;
        //inalHeight - height, finalHeight代表heardview抬起的时候高度-heardview初始化的高度=heardview在y轴上移动的距离,280毫秒内移动完毕
        //int startX,开始的位置
        // int mLastY,y位置开始的位置
        // int dx, x滑动的距离
        // int dy, y滑动的距离
        // int duration执行完毕需要的时间
        mScroller.startScroll(0, height, 0, finalHeight - height, 300);
        //手动调用刷新移动
        invalidate();
    }
    /**
     * 设置头部的view的高度,来达到下移的效果
     */
    private void upDataHeardView(float dy) {
        //如果当前的状态不是正在加载中,就改变状态
        if (mHeaderView.getCurrentState() != LoadState.LOADING) {
            //如果当前的heardview的高度大于原始的高度就代表用户下拉刷新了要该表状态,变成下拉刷新的状态
            if (mHeaderView.getHeaderHeight() > iHeaderHeight) {
                mHeaderView.setHeaderState(LoadState.WILL_RELEASE);
            } else {
                mHeaderView.setHeaderState(LoadState.NORMAL);
            }
        }
        //移动距离等于move的距离加上heardview的高度
        mHeaderView.setHeaderHeight((int) (dy + mHeaderView.getHeaderHeight()));
    }

    /**
     * 设置刷新的监听
     */
    public void setXrefershListviewListener(XrefershListviewListener l) {
        mListViewListener = l;
    }
    //每次移动都会调用computeScroll
    @Override
    public void computeScroll() {
        //判断是否滑动结束
        if (mScroller.computeScrollOffset()) {
            //代表是下拉刷新
            if (iScrollWhich == SCROLL_HEADER) {
                //获取到当前滚动的位置,来动态的设置mHeaderView的高度
                mHeaderView.setHeaderHeight(mScroller.getCurrY());
                //代表是上拉加载改变footview
            } else if (iScrollWhich == SCROLL_FOOTER) {
                //不断去改变footview的高度
                mFooterView.setFooterHeight(mScroller.getCurrY());
            }
            invalidate();
        }
    }
    //监听listview的滑动监听
    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {
    }
    //监听listview的滑动监听
    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
    }
}

###上面的注释也非常的清楚,大体思路:

  • 首先我们直接初始化HeaderView和FooterView然后设置她们的默认高度为200dp
  • addHeaderView然后我们添加头部的view
  • 设置addOnGlobalLayoutListener的监听mHeaderView.getMeasuredHeight();在回调的方法中获取测量的heardview的高度
  • mHeaderView.setPadding(0,-iHeaderHeight,0,0);这步我们就隐藏了heardview,设置他距离上面的距离为-iHeaderHeight
  • 然后我们重写onTouchEvent来实现具体的移动的逻辑,首先我们在ACTION_DOWN里面获取当前的坐标,然后在ACTION_MOVE中我们去计算滑动的距离,详情看代码的注释
  • 通过 upDataHeardView(dy*0.7f);这个方法我们实现头部view下拉的效果
 /**
     * 设置头部的view的高度,来达到下移的效果
     */
    private void upDataHeardView(float dy) {
        //如果当前的状态不是正在加载中,就改变状态
        if (mHeaderView.getCurrentState() != LoadState.LOADING) {
            //如果当前的heardview的高度大于原始的高度就代表用户下拉刷新了要该表状态,变成下拉刷新的状态
            if (mHeaderView.getHeaderHeight() > iHeaderHeight) {
                mHeaderView.setHeaderState(LoadState.WILL_RELEASE);
            } else {
                mHeaderView.setHeaderState(LoadState.NORMAL);
            }
        }
        //移动距离等于move的距离加上heardview的高度
        mHeaderView.setHeaderHeight((int) (dy + mHeaderView.getHeaderHeight()));
    }

  • 然后我们在看ACTION_UP,抬起的时候我们去判断当前的是否是下拉刷新,如果是就直接调用onRefresh是刷新界面的数据在resetHeaderHeight我们重置头部view的状态
 /**
     * 重置heardview的状态
     */
    private void resetHeaderHeight() {
        //重置刷新的状态
        mHeaderView.setHeaderState(LoadState.NORMAL);
        //获取测量得到的heardview的高度
        int height = mHeaderView.getMeasuredHeight();
        //如果获取当前的View的高度等于0代表是没有移动就不做处理
        if (height == 0) // not visible.
            return;
        int finalHeight = 0;
        //如果超过HeaderView高度,则回滚到HeaderView高度即可
        if (height > iHeaderHeight && mHeaderView.getCurrentState() != LoadState.NORMAL) {
            finalHeight = iHeaderHeight;
        }
        //标记当前是heardview在移动
        iScrollWhich = SCROLL_HEADER;
        //inalHeight - height, finalHeight代表heardview抬起的时候高度-heardview初始化的高度=heardview在y轴上移动的距离,280毫秒内移动完毕
        //int startX,开始的位置
        // int mLastY,y位置开始的位置
        // int dx, x滑动的距离
        // int dy, y滑动的距离
        // int duration执行完毕需要的时间
        mScroller.startScroll(0, height, 0, finalHeight - height, 300);
        //手动调用刷新移动
        invalidate();
    }

  • 我们需要重写computeScroll这个方法来实现阻尼的效果
	 @Override
    public void computeScroll() {
        //判断是否滑动结束
        if (mScroller.computeScrollOffset()) {
            //代表是下拉刷新
            if (iScrollWhich == SCROLL_HEADER) {
                //获取到当前滚动的位置,来动态的设置mHeaderView的高度
                mHeaderView.setHeaderHeight(mScroller.getCurrY());
                //代表是上拉加载改变footview
            } else if (iScrollWhich == SCROLL_FOOTER) {
                //不断去改变footview的高度
                mFooterView.setFooterHeight(mScroller.getCurrY());
            }
            invalidate();
        }
    }

底部的刷新逻辑和头部的都一样,这样我们就实现了上拉刷新下拉加载的效果了! 代码地址点击这