在上一篇文章《有阻尼下拉刷新列表的实现》中,我解析了如何基于重载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<getChildCount();i++) { View child = getChildAt(i); drawChild(canvas, child, getDrawingTime()); } canvas.restore();
为了优化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<getChildCount();i++) { View child = getChildAt(i); drawChild(canvas, child, getDrawingTime()); } canvas.restore(); 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(); } } /** * 下拉结束时进行回滚动画并执行刷新动作 * @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; } }