不知为何,一直对ListView的下拉刷新情有独钟。或许是在项目开发过程中,使用的频率太多了吧。ListView下拉刷新,和自动加载更多,对于大多数人来说,是一个头疼的问题。很多时候,我们都选择使用框架去实现下拉刷新,加载更多,无奈,很多框架很难修改成自己所需要的效果。如果这时候,自己有一个自己的下拉刷新框架,该有多好!我也曾使用过很多的下拉刷新框架,经过自己对这些框架的使用经历,我发现该如何做一个下拉刷新框架才是最好的。这里,我将介绍如何去简单的定义出一个流畅的下拉刷新ListView。
先预览demo的效果
在介绍ListView编写之前,有一些知识是需要先了解的。
1.我们将使用LinearLayout里面嵌套ListView组合来实现。因此需要熟悉事件分发机制。这里,我们通过重写LinearLayout的 onInterceptTouchEvent 方法,来进行事件的拦截。如果当前LinearLayout需要事件,来进行下拉刷新,则onInterceptTouchEvent返回true;如果当前不需要下拉刷新,则返回false,事件交给ListView处理,此时ListView就可以滑动了。
2.在LinearLayout中,调用scrollTo()方法,对ListView进行滑动。
3.弹性滑动,ListView下拉刷新后,需要弹性回复,使用Scroller来完成该功能。
4.我将使用mScrollY来进行下拉刷新的判断依据。mScollY初始值为0,随着调用scrollTo()下拉,将会逐渐变小。
所以,要完成ListView的下拉刷新,需要掌握android事件分发机制,View的scrollTo()方法,Scroller弹性滑动。
我将通过代码的方式,使用注释来说明:
package com.mjc.pulltorefreshdemo.refresh;
import android.content.Context;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.AbsListView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.Scroller;
import static android.view.GestureDetector.*;
/**
* Created by mjc on 2015/12/26.
*/
public class PullToRefreshViewListView extends LinearLayout implements AbsListView.OnScrollListener {
private static final String TAG = "PullToRefreshViewGroup";
//暂时未用到
private IFooterView mFooterView;
//自定义的一个布局,包含了刷新时,下拉时动画等方法。
private IHeaderView mHeaderView;
//HeaderView的高度
private int mHeaderViewHeight;
private ListView mListView;
//当前listview是否允许下拉
private boolean isPullDownEnable = false;
//按下时的坐标
private float downY = 0;
//阻尼系数
private float rate = 0.4f;
//弹性滑动需要使用Scroller
private Scroller mScroller;
//向下滑动,mScrollY变小;滑动临界值
private int mMaxScrollHeight;
//表示是否正在刷新
private boolean isRefreshing = false;
//记录当前的一个mScrollY的值,做判断
private int mScrollTop;
private int mTouchSlop;
public PullToRefreshViewListView(Context context) {
super(context);
init(context);
}
public PullToRefreshViewListView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
private void init(Context context) {
this.setOrientation(VERTICAL);
mListView = new ListView(context);
LayoutParams listParams = new LayoutParams(-1, -1);
mListView.setLayoutParams(listParams);
mListView.setOnScrollListener(this);
addView(mListView);
//添加所有的子View
mHeaderView = new CustomHeaderView(context);
LayoutParams params = new LayoutParams(-1, -2);
mHeaderView.setLayoutParams(params);
addView(mHeaderView, 0);
//getViewTreeObserver()来获取我们的headerView的高度
getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
//获取各个视图的高度
mHeaderViewHeight = mHeaderView.getHeight();
mHeaderView.getViewTreeObserver()
.removeGlobalOnLayoutListener(this);
//隐藏headerView
HideHeaderView(-mHeaderViewHeight);
//初始化我们需要的当前滑动距离和滑动临界点
mScrollTop = 0;
mMaxScrollHeight = mHeaderViewHeight * 2;
}
});
mScroller = new Scroller(context);
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
}
/**
* 我们需要隐藏HeaderView
*
* @param margin
*/
private void HideHeaderView(int margin) {
LayoutParams params = (LayoutParams) mHeaderView.getLayoutParams();
int topMargin = params.topMargin;
int newMargin = topMargin + margin;
params.topMargin = newMargin;
mHeaderView.requestLayout();
}
public View getRefreshView() {
return mListView;
}
/**
* 用来确定是否允许事件传递给ListView
*
* @param ev
* @return
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//getScrollY()<0,表示当前正在下拉刷新的过程中,事件需要拦截,由LinearLayout处理
boolean isHandled = false;
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
//ACTION_DOWN触发时,无法判断是否拦截事件
downY = ev.getY();
isHandled = false;
break;
case MotionEvent.ACTION_MOVE:
float dY = ev.getY() - downY;
//isPullDownEnable在ListView的onScrollChange方法中,监听,达到了ListView的最顶端,允许拉下
if (isPullDownEnable && dY >= mTouchSlop) {
isHandled = true;
}
if(dY<mTouchSlop){
isHandled =false;
}
}
return isHandled;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
downY = event.getY();
break;
case MotionEvent.ACTION_MOVE:
float moveY = event.getY();
float delta = (moveY - downY);
//获取需要滑动到的位置,mScrollTop为滑动前的位置,其值等于getScrollY()。
delta = mScrollTop - rate * delta;
/**
* della为滑动的目标位置。可以取值范围为, -mHeaderViewHeight - mMaxScrollHeight到0
* 这里,0到-mHeaderViewHeight表示下拉刷新状态
* -mHeaderViewHeight到 -mHeaderViewHeight - mMaxScrollHeight,表示松开刷新状态
* 因为mScrollY向正方向滑动时,是负值,逐渐变小的过程
* 表示为:0 ----下拉刷新状态---- (-mHeaderViewHeight) ----松开刷新--- (-mHeaderViewHeight - mMaxScrollHeight)
*/
//如果目标位置大于0,则让它等于0
if (delta >= 0) {
delta = 0;
}
if (delta <= -mHeaderViewHeight - mMaxScrollHeight) {
delta = -mHeaderViewHeight - mMaxScrollHeight;
}
//滑动到对应位置
scrollTo(0, (int) delta);
//根据位置更新状态
resetRefreshState((int) delta);
return true;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
//记录下次要布局的位置
mScrollTop = getScrollY();
//根据位置处理结果
doReslut(mScrollTop);
break;
}
return true;
}
/**
* 处理松开后的结果
*
* @param mScrollTop -mHeaderViewHeight - mMaxScrollHeight -松开刷新->-mHeaderViewHeight-下拉刷新->0
*/
private void doReslut(int mScrollTop) {
if (mScrollTop < 0 && mScrollTop > -mHeaderViewHeight) {
//当前为下拉刷新状态,弹性回复到初始状态
//todo 弹性回复到正常状态
if (!isRefreshing)
mHeaderView.onNormal();
smoothScrollTo(0);
} else if (mScrollTop >= -mHeaderViewHeight - mMaxScrollHeight && mScrollTop <= -mHeaderViewHeight) {
//todo 弹性滑动到-mHeaderViewHeight位置,然后开始刷新
smoothScrollTo(-mHeaderViewHeight);
//正在刷新
mHeaderView.onRefreshing();
if (isRefreshing) {
//如果当前正在刷新,无作为
} else {
//如果当前没有刷新,刷新
isRefreshing = true;
if (mRefresh != null)
mRefresh.onPullDownRefresh();
}
}
}
/**
* @param delta 值为 -mHeaderViewHeight - mMaxScrollHeight -松开刷新->-mHeaderViewHeight-下拉刷新->0
*/
private void resetRefreshState(float delta) {
if (isRefreshing) {
return;
}
if (delta > -mHeaderViewHeight && delta <= 0) {
//下拉刷新状态
mHeaderView.onPullToRefresh((int) -delta);
Log.v(TAG, "下拉刷新");
} else if (delta >= -mHeaderViewHeight - mMaxScrollHeight && delta <= -mHeaderViewHeight) {
//松开刷新
mHeaderView.onReleaseToRefresh((int) -delta);
Log.v(TAG, "松开刷新");
}
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
//只有当第一个显示,并且getTop==0,才可以下拉
if (mListView.getChildAt(0) == null) {
isPullDownEnable = firstVisibleItem == 0;
} else {
//如果当前第一个可见的为0并且第一个item的gettop>=0
isPullDownEnable = firstVisibleItem == 0 && mListView.getChildAt(0).getTop() >= 0;
}
}
private IRefresh mRefresh;
public void setOnRefreshListener(IRefresh mRefresh) {
this.mRefresh = mRefresh;
}
public void onPullDownRefreshComplete() {
isRefreshing = false;
//todo 弹性从 -mHeaderViewHeight回复到0
smoothScrollTo(0);
}
/**
* 使用Scroller弹性滑动到指定位置
*
* @param scrollValue
*/
private void smoothScrollTo(int scrollValue) {
mScroller.startScroll(0, mScrollTop, 0, scrollValue - mScrollTop, 500);
//主动要求重绘视图
invalidate();
}
/**
* Scroller需要配合重写这个方法才能实现弹性滑动
*/
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
Log.e(TAG, "" + mScroller.getCurrY());
scrollTo(0, mScroller.getCurrY());
mScrollTop = mScroller.getCurrY();
postInvalidate();
}
}
}
整个下拉刷新的流程,其实很简单。当ListView滑动到最顶端,通过判断是否向下滑动,来判断是否拦截事件,如果拦截事件,当前Linearlayout处理滑动;处理滑动,通过监听手指滑动的距离,使用scrollTo()来滑动Linearlayout的内容;并使用Scroller来进行弹性回复。在headerView中,我们可以自定义各种各样的动画,大家可以进行尝试。
最后,我会将代码打包,,上传到csdn。有什么错误,希望大家指正。
源码下载地址:点击打开链接