在上一篇文章《有阻尼下拉刷新列表的实现》中,我解析了如何基于重载dispatchDraw方法重画子View和重载onTouchEvent方法监控受试来实现下拉刷新列表,而在这篇文章中,我将会基于上一篇文章介绍的技术,在下拉刷新列表PullToRefreshListVIew的基础上,加上有阻尼上拉刷新功能。上一篇文章《有阻尼下拉刷新列表的实现》的链接如下。
http://blog.csdn.net/ivan_zgj/article/details/50664780
好,我们还是先来看看效果。
在PullToRefreshListView中,我们通过onScrollListener回调来监控PullToRefreshListView是否已经滚动到顶部,当时的代码是这样的:
setOnScrollListener(new OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
// 没有子view的时候(没有数据,或者被拉到看不到子view),意味着该listView滚动到顶部
if (getChildCount() == 0) {
isTop = true;
return;
}
if (firstVisibleItem == 0) {
View firstView = getChildAt(0);
if (firstView.getTop() + distanceY >= 0) {
// 第一个view可见且其相对parent(该listView)的顶部距离大于等于0,意味着该listView也是滚动到顶部
isTop = true;
return;
}
}
isTop = false;
}
});
当时我们通过判断firstVisibleItem==0以及第一个子View是否完全可见,从而确定PullToRefreshListView是否滚动到顶部。现在,我们加入以下代码来判断PullToRefreshListView是否滚动到底部。
if (firstVisibleItem + visibleItemCount == totalItemCount) {
View firstView = getChildAt(visibleItemCount - 1);
if (firstView.getBottom() + distanceY <= getHeight()) {
// 最后一个view可见且其相对parent(该listView)的底部距离小于等于listView高度,意味着该listView也是滚动到底部
isBottom = true;
return;
}
}
isBottom = false;
在onScroll方法中,fisrstVisibleItem是指列表当前可见的第一个子View在其adapter中的position,而visibleItemCount是指可见的子View的个数,totalItemCount是指adapter总共有多少个View。因此,我们可以很容易得出结论,当:
firstVisibleItem + visibleItemCount == totalItemCount
成立时,PullToRefreshListView就已经滚动到底部了。
在PullToRefreshListView中,我们通过重载onTouchEvent方法来监控用户手势,从而判断PullToRefreshListView是否要进行下拉,当时的代码是这样的:
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (lastAction == -1 && ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
// 按下的时候
lastAction = MotionEvent.ACTION_DOWN;
cancelAnimating();
L.d(TAG, "touch down");
} else if (lastAction == MotionEvent.ACTION_MOVE && ev.getActionMasked() == MotionEvent.ACTION_UP) {
// 放开手指,开始回滚
isPulling = false;
lastAction = -1;
startAnimating();
L.d(TAG, "touch up");
} else if (lastAction == MotionEvent.ACTION_DOWN) {
if (ev.getActionMasked() == MotionEvent.ACTION_MOVE) {
// 在按下手指的基础上,开始滑动
if (isTop && !isPulling) {
// listView在顶部而且不处于下拉刷新状态,开始下拉
pullStartY = ev.getY();
lastAction = MotionEvent.ACTION_MOVE;
isPulling = true;
}
}
} else if (lastAction == MotionEvent.ACTION_MOVE) {
if (isTop) {
// 下拉
distanceY = ev.getY() - pullStartY;
L.d(TAG, distanceY + "");
if (distanceY > 0) {
distanceY = (float) (Math.exp(-ev.getY() / pullStartY / 40) * distanceY);
// 在下拉状态时取消系统对move动作的响应,完全由本类响应
ev.setAction(MotionEvent.ACTION_DOWN);
} else {
distanceY = 0;
// 在下拉过程中往上拉动该listView使得其回到顶部位置,则将该move动作交由系统进行响应
ev.setAction(MotionEvent.ACTION_MOVE);
}
} else {
// 在下拉过程中往上拉动listView使listView往下滚动到其没有滚动到顶部,则取消其下拉状态,回到手指按下的初始状态
lastAction = MotionEvent.ACTION_DOWN;
isPulling = false;
distanceY = 0;
}
}
return super.onTouchEvent(ev);
}
if (isBottom) {
// 上拉
configureHeader(false);
distanceY = ev.getY() - pullStartY;
L.d(TAG, distanceY + "");
if (distanceY < 0) {
distanceY = (float) (Math.exp(-pullStartY / ev.getY() / 40) * distanceY);
// 在上拉状态时取消系统对move动作的响应,完全由本类响应
ev.setAction(MotionEvent.ACTION_DOWN);
} else {
distanceY = 0;
// 在上拉过程中往上拉动该listView使得其回到顶部位置,则将该move动作交由系统进行响应
ev.setAction(MotionEvent.ACTION_MOVE);
}
}
很明显,这个判断是在执行这样一个动作,如果PullToRefreshListView已经滚动到底部,那么就根据用户手指滑动的距离计算PullToRefreshListView上拉的距离,这一段代码与处理下拉的代码是基本一样的。
在PullToRefreshListView的基础上,我们将重画下拉的子View的代码替换成以下代码。
// 重画子view
int left = getPaddingLeft();
int top = getPaddingTop();
int bottom = getPaddingBottom();
canvas.save();
if (distanceY > 0) {
canvas.translate(left, top + distanceY);
} else {
canvas.translate(left, -bottom + distanceY);
}
for (int i=0;i
为了优化UI和给用户做出提示,我这次还加入刷新头,就是那个提示下拉刷新,释放以刷新blablabla的东西。同样的,我也是在dispatchDraw方法里面实现的。具体代码如下。
canvas.save();
// 画刷新头
View header;
if (distanceY > 0) {
int whereToLoad = dp2px(onLoadCallBack.whereToLoad(true));
if (distanceY > whereToLoad) {
topRefreshHeaderView.setText(HeaderView.STATE_CAN_RELEASE);
topRefreshHeaderView.setIcon(HeaderView.STATE_CAN_RELEASE);
} else if (distanceY < whereToLoad) {
topRefreshHeaderView.setText(HeaderView.STATE_PULLING_DOWN);
topRefreshHeaderView.setIcon(HeaderView.STATE_PULLING_DOWN);
}
header = topRefreshHeaderView.getView();
canvas.translate(left, top + distanceY - header.getHeight() - offset);
} else {
int whereToLoad = dp2px(onLoadCallBack.whereToLoad(false));
if (-distanceY > whereToLoad) {
bottomRefreshHeaderView.setText(HeaderView.STATE_CAN_RELEASE);
bottomRefreshHeaderView.setIcon(HeaderView.STATE_CAN_RELEASE);
} else if (-distanceY < whereToLoad) {
bottomRefreshHeaderView.setText(HeaderView.STATE_PULLING_UP);
bottomRefreshHeaderView.setIcon(HeaderView.STATE_PULLING_UP);
}
header = bottomRefreshHeaderView.getView();
canvas.translate(left, -bottom + distanceY + getHeight() + offset);
}
drawChild(canvas, header, getDrawingTime());
canvas.restore();
我们看到这里有两个判断,第一个是下拉刷新头,第二个是上拉刷新头,其实原理也是一样,将canvas平移到合适的位置,然后调用drawChild方法就可以了。
这里要简单地说一说topRefreshHeaderView和bottomRefreshHeaderView这两个东西,其实它们是我设计的一个抽象类实现对象,该抽象类如下。
/**
* 刷新头的抽象类
*/
public abstract class HeaderView {
private View headerView;
public static final int STATE_PULLING_UP = 0x30;
public static final int STATE_PULLING_DOWN = 0x31;
public static final int STATE_CAN_RELEASE = 0x32;
public static final int STATE_REFRESHING = 0x33;
public HeaderView(View headerView) {
this.headerView = headerView;
}
/**
* 获得刷新头的View
* @return 实例化是给予的一个自定义View
*/
protected View getView() {
return headerView;
}
/**
* 根据状态设置显示内容
* @param state 状态
*/
protected abstract void setText(int state);
/**
* 根据状态设置显示图标
* @param state 状态
*/
protected abstract void setIcon(int state);
}
根据这个抽象类的设计,我们可以知道,该抽象类为刷新头的外观提供了一个标准,它应该有一个图标和一个提示的文字内容。当然,PullToRefreshListView有一个默认的刷新头。
最后就是大家最喜欢的源码了。
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.AbsListView;
import android.widget.ListView;
import android.widget.TextView;
import com.ivan.healthcare.healthcare_android.log.L;
/**
* 支持下拉刷新的的listView
* Created by Ivan on 16/2/14.
*/
public class PullToRefreshListView extends ListView {
private final String TAG = "PullToRefreshListView";
/**
* 默认回滚速度,millis/100dp
*/
private final int DEFAULT_BASE_ANIMATING_TIME_PER_100DP = 100;
/**
* 默认刷新背景高度
*/
public static final int DEFAULT_WHERE_TO_LOAD = 80;
/**
* 记录上一个手势动作
*/
private int lastAction = -1;
/**
* 下拉起始位置
*/
private float pullStartY = -1;
/**
* 是否处于“滚动到顶部”状态
*/
private boolean isTop = true;
/**
* 是否处于“滚动到底部”状态
*/
private boolean isBottom = true;
/**
* 下拉距离
*/
private float distanceY = 0;
/**
* 是否处于下拉状态
*/
private boolean isPulling = false;
/**
* 回滚动画控制器
*/
private ValueAnimator pullCancelAnimator;
private Context context;
/**
* 刷新背景
*/
private Drawable refreshDrawable;
/**
* 下拉刷新头
*/
private HeaderView topRefreshHeaderView;
/**
* 上拉刷新头
*/
private HeaderView bottomRefreshHeaderView;
/**
* 刷新头位置偏移
*/
private int offset;
/**
* 刷新回调
*/
private OnLoadCallBack onLoadCallBack = new OnLoadCallBack(this) {
@Override
public void onLoad(boolean topOrBottom) {
}
@Override
public void cancelLoad(boolean topOrBottom) {
}
};
public PullToRefreshListView(Context context) {
super(context);
initView(context);
}
public PullToRefreshListView(Context context, AttributeSet attrs) {
super(context, attrs);
initView(context);
}
private void initView(Context context) {
this.context = context;
offset = dp2px(10);
setOnScrollListener(new OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
// 没有子view的时候(没有数据,或者被拉到看不到子view),意味着该listView滚动到顶部/底部
if (getChildCount() == 0) {
isTop = true;
isBottom = true;
return;
}
if (firstVisibleItem == 0) {
View firstView = getChildAt(0);
if (firstView.getTop() + distanceY >= 0) {
// 第一个view可见且其相对parent(该listView)的顶部距离大于等于0,意味着该listView也是滚动到顶部
isTop = true;
return;
}
}
isTop = false;
if (firstVisibleItem + visibleItemCount == totalItemCount) {
View firstView = getChildAt(visibleItemCount - 1);
if (firstView.getBottom() + distanceY <= getHeight()) {
// 最后一个view可见且其相对parent(该listView)的底部距离小于等于listView高度,意味着该listView也是滚动到底部
isBottom = true;
return;
}
}
isBottom = false;
}
});
}
/**
* 配置刷新头,当开始下拉/上拉时会执行该方法
* @param topOrBottom true for top, false for bottom
*/
private void configureHeader(boolean topOrBottom) {
final TextView header;
if (topOrBottom) {
if (topRefreshHeaderView == null) {
topRefreshHeaderView = onLoadCallBack.getHeaderView(true);
if (topRefreshHeaderView != null) {
return;
}
header = new TextView(context);
topRefreshHeaderView = new HeaderView(header) {
@Override
public void setText(int state) {
switch (state) {
case STATE_PULLING_DOWN:
header.setText("下拉刷新");
break;
case STATE_PULLING_UP:
header.setText("上拉刷新");
break;
case STATE_CAN_RELEASE:
header.setText("释放以刷新");
break;
case STATE_REFRESHING:
header.setText("刷新中...");
break;
default:
break;
}
}
@Override
public void setIcon(int state) {
}
};
} else {
return;
}
header.setText("下拉刷新");
} else {
if (bottomRefreshHeaderView == null) {
bottomRefreshHeaderView = onLoadCallBack.getHeaderView(true);
if (bottomRefreshHeaderView != null) {
return;
}
header = new TextView(context);
bottomRefreshHeaderView = new HeaderView(header) {
@Override
public void setText(int state) {
switch (state) {
case STATE_PULLING_DOWN:
header.setText("下拉刷新");
break;
case STATE_PULLING_UP:
header.setText("上拉刷新");
break;
case STATE_CAN_RELEASE:
header.setText("释放以刷新");
break;
case STATE_REFRESHING:
header.setText("刷新中...");
break;
default:
break;
}
}
@Override
public void setIcon(int state) {
}
};
} else {
return;
}
header.setText("上拉刷新");
}
header.setTextSize(20);
header.setTextColor(Color.WHITE);
header.setGravity(Gravity.CENTER);
LayoutParams layoutParams = (LayoutParams) header.getLayoutParams();
if (layoutParams == null) {
layoutParams = (LayoutParams) generateDefaultLayoutParams();
header.setLayoutParams(layoutParams);
}
int heightMode = MeasureSpec.getMode(layoutParams.height);
int heightSize = MeasureSpec.getSize(layoutParams.height);
if (heightMode == MeasureSpec.UNSPECIFIED) heightMode = MeasureSpec.EXACTLY;
int maxHeight = getHeight() - getListPaddingTop() - getListPaddingBottom();
if (heightSize > maxHeight) heightSize = maxHeight;
// measure & layout
int ws = MeasureSpec.makeMeasureSpec(getWidth() - getListPaddingLeft() - getListPaddingRight(), MeasureSpec.EXACTLY);
int hs = MeasureSpec.makeMeasureSpec(heightSize, heightMode);
header.measure(ws, hs);
header.layout(0, 0, header.getMeasuredWidth(), header.getMeasuredHeight());
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (lastAction == -1 && ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
// 按下的时候
lastAction = MotionEvent.ACTION_DOWN;
cancelAnimating();
L.d(TAG, "touch down");
} else if (lastAction == MotionEvent.ACTION_MOVE && ev.getActionMasked() == MotionEvent.ACTION_UP) {
// 放开手指,开始回滚
isPulling = false;
lastAction = -1;
startAnimating(distanceY>0);
L.d(TAG, "touch up");
} else if (lastAction == MotionEvent.ACTION_DOWN) {
if (ev.getActionMasked() == MotionEvent.ACTION_MOVE) {
// 在按下手指的基础上,开始滑动
if ((isBottom || isTop) && !isPulling) {
// listView在顶部/底部而且不处于下拉刷新状态,开始下拉/上拉
pullStartY = ev.getY();
lastAction = MotionEvent.ACTION_MOVE;
isPulling = true;
}
}
} else if (lastAction == MotionEvent.ACTION_MOVE) {
if (isTop) {
// 下拉
configureHeader(true);
distanceY = ev.getY() - pullStartY;
L.d(TAG, distanceY + "");
if (distanceY > 0) {
distanceY = (float) (Math.exp(-ev.getY() / pullStartY / 40) * distanceY);
// 在下拉状态时取消系统对move动作的响应,完全由本类响应
ev.setAction(MotionEvent.ACTION_DOWN);
} else {
distanceY = 0;
// 在下拉过程中往上拉动该listView使得其回到顶部位置,则将该move动作交由系统进行响应
ev.setAction(MotionEvent.ACTION_MOVE);
}
}
if (isBottom) {
// 上拉
configureHeader(false);
distanceY = ev.getY() - pullStartY;
L.d(TAG, distanceY + "");
if (distanceY < 0) {
distanceY = (float) (Math.exp(-pullStartY / ev.getY() / 40) * distanceY);
// 在上拉状态时取消系统对move动作的响应,完全由本类响应
ev.setAction(MotionEvent.ACTION_DOWN);
} else {
distanceY = 0;
// 在上拉过程中往上拉动该listView使得其回到顶部位置,则将该move动作交由系统进行响应
ev.setAction(MotionEvent.ACTION_MOVE);
}
}
if (!isTop && !isBottom){
// 在下拉过程中往上拉动listView使listView往下滚动到其没有滚动到顶部,则取消其下拉状态,回到手指按下的初始状态
lastAction = MotionEvent.ACTION_DOWN;
isPulling = false;
distanceY = 0;
}
}
return super.onTouchEvent(ev);
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (distanceY != 0) {
if (refreshDrawable == null) {
refreshDrawable = onLoadCallBack.refreshDrawable();
}
if (refreshDrawable == null) {
canvas.drawColor(Color.GRAY);
} else {
int left = getPaddingLeft();
int top = getPaddingTop();
refreshDrawable.setBounds(left, top, getWidth()+left, getHeight()+top);
refreshDrawable.draw(canvas);
}
// 重画子view
int left = getPaddingLeft();
int top = getPaddingTop();
int bottom = getPaddingBottom();
canvas.save();
if (distanceY > 0) {
canvas.translate(left, top + distanceY);
} else {
canvas.translate(left, -bottom + distanceY);
}
for (int i=0;i 0) {
int whereToLoad = dp2px(onLoadCallBack.whereToLoad(true));
if (distanceY > whereToLoad) {
topRefreshHeaderView.setText(HeaderView.STATE_CAN_RELEASE);
topRefreshHeaderView.setIcon(HeaderView.STATE_CAN_RELEASE);
} else if (distanceY < whereToLoad) {
topRefreshHeaderView.setText(HeaderView.STATE_PULLING_DOWN);
topRefreshHeaderView.setIcon(HeaderView.STATE_PULLING_DOWN);
}
header = topRefreshHeaderView.getView();
canvas.translate(left, top + distanceY - header.getHeight() - offset);
} else {
int whereToLoad = dp2px(onLoadCallBack.whereToLoad(false));
if (-distanceY > whereToLoad) {
bottomRefreshHeaderView.setText(HeaderView.STATE_CAN_RELEASE);
bottomRefreshHeaderView.setIcon(HeaderView.STATE_CAN_RELEASE);
} else if (-distanceY < whereToLoad) {
bottomRefreshHeaderView.setText(HeaderView.STATE_PULLING_UP);
bottomRefreshHeaderView.setIcon(HeaderView.STATE_PULLING_UP);
}
header = bottomRefreshHeaderView.getView();
canvas.translate(left, -bottom + distanceY + getHeight() + offset);
}
drawChild(canvas, header, getDrawingTime());
canvas.restore();
}
}
/**
* 下拉结束时进行回滚动画并执行刷新动作
* @param topOrBottom true for top, false for bottom
*/
private void startAnimating(final boolean topOrBottom) {
int whereToLoad = dp2px(onLoadCallBack.whereToLoad(topOrBottom));
final boolean toLoad;
if (distanceY > whereToLoad && topOrBottom) {
// 下拉
pullCancelAnimator = ValueAnimator.ofFloat(distanceY, whereToLoad);
toLoad = true;
} else if (-distanceY > whereToLoad && !topOrBottom) {
// 上拉
pullCancelAnimator = ValueAnimator.ofFloat(distanceY, -whereToLoad);
toLoad = true;
} else {
// 回滚
pullCancelAnimator = ValueAnimator.ofFloat(distanceY, 0);
toLoad = false;
}
pullCancelAnimator.setDuration((long) (DEFAULT_BASE_ANIMATING_TIME_PER_100DP*px2dp(Math.abs(distanceY))/100));
pullCancelAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
pullCancelAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
distanceY = (float) animation.getAnimatedValue();
ViewCompat.postInvalidateOnAnimation(PullToRefreshListView.this);
}
});
pullCancelAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
post(new Runnable() {
@Override
public void run() {
if (topOrBottom) {
topRefreshHeaderView.setText(HeaderView.STATE_REFRESHING);
topRefreshHeaderView.setIcon(HeaderView.STATE_REFRESHING);
} else {
bottomRefreshHeaderView.setText(HeaderView.STATE_REFRESHING);
bottomRefreshHeaderView.setIcon(HeaderView.STATE_REFRESHING);
}
pullCancelAnimator = null;
if (toLoad) {
onLoadCallBack.onLoad(topOrBottom);
}
}
});
}
@Override
public void onAnimationCancel(Animator animation) {
post(new Runnable() {
@Override
public void run() {
pullCancelAnimator = null;
if (toLoad) {
onLoadCallBack.cancelLoad(topOrBottom);
}
}
});
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
pullCancelAnimator.start();
}
/**
* 取消回滚动画
*/
private void cancelAnimating() {
if (pullCancelAnimator != null) {
pullCancelAnimator.cancel();
}
}
private float px2dp(float pxvalue) {
return (pxvalue - 0.5f) /context.getResources().getDisplayMetrics().density;
}
private int dp2px(float dpvalue) {
return (int) (dpvalue * context.getResources().getDisplayMetrics().density + 0.5f);
}
/**
* 下拉刷新的回调
*/
public static abstract class OnLoadCallBack {
private PullToRefreshListView listView;
public OnLoadCallBack(PullToRefreshListView lv) {
this.listView = lv;
}
/**
* 下拉结束后将listView定位到哪个位置等待刷新完成
* @param topOrBottom true for top, false for bottom
* @return listView的定位y坐标值,in dp
*/
public int whereToLoad(boolean topOrBottom) {
return DEFAULT_WHERE_TO_LOAD;
}
/**
* 下拉结束后进行刷新的回调
* @param topOrBottom true for top, false for bottom
*/
public abstract void onLoad(boolean topOrBottom);
/**
* 取消刷新
* @param topOrBottom true for top, false for bottom
*/
public abstract void cancelLoad(boolean topOrBottom);
/**
* 下拉刷新的背景
* @return 背景drawable
*/
public Drawable refreshDrawable() {
return new ColorDrawable(Color.GRAY);
}
/**
* 自定义刷新头
* @param topOrBottom true for top, false for bottom
*/
public HeaderView getHeaderView(boolean topOrBottom) {
if (topOrBottom) {
listView.destroyTopRefreshHeaderView();
} else {
listView.destroyBottomRefreshHeaderView();
}
return null;
}
}
/**
* 设置下拉刷新回调
* @param cb 回调
*/
public void setOnLoadCallBack(OnLoadCallBack cb) {
this.onLoadCallBack = cb;
}
/**
* 刷新动作结束后调用该方法结束刷新,使得listView回滚到顶部
*/
public void setLoadingFinish() {
startAnimating(distanceY>0);
}
/**
* 刷新头的抽象类
*/
public abstract class HeaderView {
private View headerView;
public static final int STATE_PULLING_UP = 0x30;
public static final int STATE_PULLING_DOWN = 0x31;
public static final int STATE_CAN_RELEASE = 0x32;
public static final int STATE_REFRESHING = 0x33;
public HeaderView(View headerView) {
this.headerView = headerView;
}
/**
* 获得刷新头的View
* @return 实例化是给予的一个自定义View
*/
protected View getView() {
return headerView;
}
/**
* 根据状态设置显示内容
* @param state 状态
*/
protected abstract void setText(int state);
/**
* 根据状态设置显示图标
* @param state 状态
*/
protected abstract void setIcon(int state);
}
protected void destroyTopRefreshHeaderView() {
topRefreshHeaderView = null;
}
protected void destroyBottomRefreshHeaderView() {
bottomRefreshHeaderView = null;
}
}