Welcome 微信登录

首页 / 移动开发 / Android / Android自定义ListView实现下拉刷新

首先呈上效果图



当今APP,哪个没有点滑动刷新功能,简直就太落伍了。正因为需求多,因此自然而然开源的也就多。但是若想引用开源库,则很麻烦,比如PullToRefreshView这个库,如果把开源代码都移植到项目中,这是件很繁琐的事,如果用依赖功能的话,对于强迫症的我,又很不爽。现在也有各种自定义ListView实现PullToRefreshListView的控件,无非就是在header加入一个控件,通过setPadding的方式来改变显示效果。效果已经太out了,如意中发现google自带的swiperefreshlayout实现的效果挺不错,但是我发现这个控件在部分手机上的效果不一样,估计和v7包相关。因此就有了这篇文章自定义这个喜欢的效果。
 首先大概描述一下实现原理: 
1、重写ListView的onTouchEvent,在方法中根据手指滑动的距离与临界值判断,决定当前的状态,分为四个状态:RELEASE_TO_REFRESH、PULL_TO_REFRESH、REFRESHING、DONE四个状态,分别代表释放刷新、拉动刷新、正在刷新、默认状态。 
2、重写ListView的onDraw方法,根据不同的状态值,显示不同的图形表示。 
3、根据滑动距离不同,显示不同的透明度、圆弧角度值、整体图形的坐标等等。 
4、图形的变化分为两种:1、手动触发,滑动一点距离就更新一点坐标。比如PULL_TO_REFRESH状态,适合在onTouchEvent中的ACTION_MOVE中触发。2、动画自动触发,比如REFRESHING状态和DONE状态,适合在onTouchEvent中的ACTION_UP方法中触发,手指一松开就自动触发动画效果。 
5、必须在设置了刷新监听器才可以滑动,否则就是一个普通的LIstView。

代码很简单,只有两个文件,并且有很详细的注释:
PullToRefreshListView类:

 package cc.wxf.view.pull; import android.content.Context;import android.graphics.Canvas;import android.util.AttributeSet;import android.view.MotionEvent;import android.view.View;import android.widget.AbsListView;import android.widget.ListView; /** * Created by ccwxf on 2016/3/30. */public class PullToRefreshListView extends ListView implements AbsListView.OnScrollListener { public final static int RELEASE_TO_REFRESH = 0;public final static int PULL_TO_REFRESH = 1;public final static int REFRESHING = 2;public final static int DONE = 3; // 达到刷新条件的滑动距离public final static int TOUCH_SLOP = 160;// 判断是否记录了最开始按下时的Y坐标private boolean isRecored;// 记录最开始按下时的Y坐标private int startY;// ListView第一个Itemprivate int firstItemIndex;// 当前状态private int state;// 是否可刷新,只有设置了监听器才能刷新private boolean isRefreshable;// 刷新标记private PullMark mark; private OnRefreshListener refreshListener;private OnScrollButtomListener scrollButtomListener; public PullToRefreshListView(Context context) {super(context);init(context);} public PullToRefreshListView(Context context, AttributeSet attrs) {super(context, attrs);init(context);} private void init(Context context) {//关闭硬件加速,否则PullMark的阴影不会出现setLayerType(View.LAYER_TYPE_SOFTWARE, null);setOnScrollListener(this);mark = new PullMark(this);state = DONE;isRefreshable = false;} @Overridepublic void onScrollStateChanged(AbsListView view, int scrollState) {if (scrollButtomListener != null) {if (scrollState == OnScrollListener.SCROLL_STATE_IDLE) {if (view.getLastVisiblePosition() == view.getAdapter().getCount() - 1) {scrollButtomListener.onScrollToButtom();}}}} @Overridepublic void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {firstItemIndex = firstVisibleItem;} @Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);mark.onDraw(canvas);} @Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {int width = MeasureSpec.getSize(widthMeasureSpec);mark.setCenterX(width / 2);super.onMeasure(widthMeasureSpec, heightMeasureSpec);} @Overridepublic boolean onTouchEvent(MotionEvent event) {if (!isRefreshable) {return super.onTouchEvent(event);}switch (event.getAction()) {case MotionEvent.ACTION_DOWN:handleActionDown(event);break; case MotionEvent.ACTION_UP:handleActionUp();break; case MotionEvent.ACTION_MOVE:handleActionMove(event);break;default:break;}return super.onTouchEvent(event);} private void handleActionMove(MotionEvent event) {int tempY = (int) event.getY(); if (!isRecored && firstItemIndex == 0) {isRecored = true;startY = tempY;} if (state != REFRESHING && isRecored) {if (state == RELEASE_TO_REFRESH) {setSelection(0);if ((tempY - startY < TOUCH_SLOP) && (tempY - startY) > 0) {state = PULL_TO_REFRESH;}}if (state == PULL_TO_REFRESH) {setSelection(0);if (tempY - startY >= TOUCH_SLOP) {state = RELEASE_TO_REFRESH;} else if (tempY - startY <= 0) {state = DONE;}} if (state == DONE) {if (tempY - startY > 0) {state = PULL_TO_REFRESH;}}mark.change(state, tempY - startY);}} private void handleActionUp() {if (state == PULL_TO_REFRESH) {state = DONE;mark.changeByAnimation(state);} else if (state == RELEASE_TO_REFRESH) {state = REFRESHING;mark.changeByAnimation(state);onRefresh();}isRecored = false;} private void handleActionDown(MotionEvent event) {if (firstItemIndex == 0 && !isRecored) {isRecored = true;startY = (int) event.getY();}} private void onRefresh() {if (refreshListener != null) {refreshListener.onRefresh();}} public void startRefresh() {state = REFRESHING;mark.changeByAnimation(state);onRefresh();} public void stopRefresh() {state = DONE;mark.changeByAnimation(state);} public void setOnRefreshListener(OnRefreshListener refreshListener) {this.refreshListener = refreshListener;isRefreshable = true;} /** * 刷新监听器 */public interface OnRefreshListener {public void onRefresh();} public void setOnScrollButtomListener(OnScrollButtomListener scrollButtomListener) {this.scrollButtomListener = scrollButtomListener;} /** * 滑动到最低端触发监听器 */public interface OnScrollButtomListener {public void onScrollToButtom();} }
刷新标志类:

 package cc.wxf.view.pull; import android.graphics.Canvas;import android.graphics.Color;import android.graphics.Paint;import android.graphics.RectF;import android.os.Handler; /** * Created by ccwxf on 2016/3/30. */public class PullMark {//背景面板的半径、颜色private static final int RADIUS_PAN = 40;private static final int COLOR_PAN = Color.parseColor("#fafafa");//面板阴影的半径、颜色private static final int RADIUS_SHADOW = 5;private static final int COLOR_SHADOW = Color.parseColor("#d9d9d9");//面板中间的圆弧的半径、颜色、粗度、开始绘制角度private static final int RADIUS_ARROWS = 20;private static final int COLOR_ARROWS = Color.GREEN;private static final int BOUND_ARROWS = 6;private static final int START_ANGLE = 0;// 开始绘制角度的变化率、总体绘制角度、总体绘制透明度private static final int RATIO_SATRT_ANGLE = 3;private static final int ALL_ANGLE = 270;private static final int ALL_ALPHA = 255;// 动画的高度渐变比率、时间刷新间隔private static final float RATIO_TOUCH_SLOP = 7f;private static final long RATIO_ANIMATION_DURATION = 10; private PullToRefreshListView listView;// 中点的X、Y坐标、初始隐藏时的Y坐标private float doneCenterY = -(RADIUS_PAN + RADIUS_SHADOW) / 2;private float centerX;private float centerY = doneCenterY;// 开始绘制的角度、需要绘制的角度、透明度private int startAngle = START_ANGLE;private int sweepAngle = startAngle;private int alpha;// 弧度变化比率,根据总体高度与总体弧度角度的比例决定private float radioAngle = ALL_ANGLE * 1.0f / PullToRefreshListView.TOUCH_SLOP;// 透明度变化比率,根据总体高度与总体透明度的比例决定private float radioAlpha = ALL_ALPHA * 1.0f / PullToRefreshListView.TOUCH_SLOP;// PullToRefreshListView的状态private int state;// 当前手指滑动的距离private float mTouchLength;// 是否启动旋转动画private boolean isRotateAnimation = false;// 画笔private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);private Handler handler = new Handler(); public PullMark(PullToRefreshListView listView) {this.listView = listView;} /** * 设置绘制的中点X坐标,在PullToRefreshListView的onMeasure中实现 * @param centerX */public void setCenterX(int centerX){this.centerX = centerX;} /** * 表示一次普通的数据变化,在onTouchEvent中的ACTION_MOVE中触发 * @param state * @param mTouchLength */public void change(int state, float mTouchLength){this.state = state;this.mTouchLength = mTouchLength;// 改变绘制的Y坐标centerY = doneCenterY + mTouchLength;// 改变绘制的透明度alpha = (int) (mTouchLength * radioAlpha);if(alpha > ALL_ALPHA){alpha = ALL_ALPHA;}else if(alpha < 0){alpha = 0;}//改变绘制的起始角度startAngle = startAngle + RATIO_SATRT_ANGLE;if(startAngle >= 360){startAngle = 0;}//改变绘制的弧度角度sweepAngle = (int) (mTouchLength * radioAngle);if(sweepAngle > ALL_ANGLE){sweepAngle = ALL_ANGLE;}else if(sweepAngle < 0){sweepAngle = 0;}listView.invalidate();} /** * 表示一次动画的变化,在onTouchEvent的ACTION_UP中或者手动startRefresh以及手动stopRefresh中触发 * @param state */public void changeByAnimation(final int state){this.state = state;if(state == PullToRefreshListView.DONE){//结束旋转动画(关闭正在刷新的效果)isRotateAnimation = false;}//慢慢变化到起始位置handler.postDelayed(new RunnableMove(state), RATIO_ANIMATION_DURATION);} /** * 启动移动的处理 */public class RunnableMove implements Runnable{ private int state;private int destination;private float slop; public RunnableMove(int state) {this.state = state;if(state == PullToRefreshListView.DONE){destination = 0;slop = RATIO_TOUCH_SLOP;}else if(state == PullToRefreshListView.REFRESHING){destination = PullToRefreshListView.TOUCH_SLOP;slop = RATIO_TOUCH_SLOP * 5;}} @Overridepublic void run() {if(mTouchLength > destination){mTouchLength -= slop;change(state, mTouchLength);handler.postDelayed(this, RATIO_ANIMATION_DURATION);}else{if(state == PullToRefreshListView.DONE){// 直接将坐标初始化,否则会有一点点误差centerY = doneCenterY;listView.invalidate();}else if(state == PullToRefreshListView.REFRESHING){//启动旋转的动画效果isRotateAnimation = true;handler.postDelayed(new RunnableRotate(), RATIO_ANIMATION_DURATION);}}}} /** * 旋转动画的处理 */public class RunnableRotate implements Runnable{ @Overridepublic void run() {if(isRotateAnimation){//启动动画旋转效果startAngle = startAngle + RATIO_SATRT_ANGLE;if(startAngle >= 360){startAngle = 0;}listView.invalidate();handler.postDelayed(this, RATIO_ANIMATION_DURATION);}else{//回到初始位置handler.postDelayed(new RunnableMove(state), RATIO_ANIMATION_DURATION);}}} /** * 绘制刷新图标的标志 * @param mCanvas */public void onDraw(Canvas mCanvas){//绘制背景圆盘和阴影mPaint.setStyle(Paint.Style.FILL);mPaint.setColor(COLOR_PAN);mPaint.setShadowLayer(RADIUS_SHADOW, 0, 0, COLOR_SHADOW);mCanvas.drawCircle(centerX, centerY, RADIUS_PAN, mPaint);//绘制圆弧mPaint.setStyle(Paint.Style.STROKE);mPaint.setColor(COLOR_ARROWS);mPaint.setStrokeWidth(BOUND_ARROWS);mPaint.setAlpha(alpha);mCanvas.drawArc(new RectF(centerX - RADIUS_ARROWS, centerY - RADIUS_ARROWS, centerX + RADIUS_ARROWS, centerY + RADIUS_ARROWS),startAngle, sweepAngle, false, mPaint);}}
使用的时候,必须要设置了监听器才能有效的滑动: 

final PullToRefreshListView listView = (PullToRefreshListView) findViewById(R.id.listView);ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, new String[]{"测试1","测试2","测试3","测试4","测试5","测试6",});listView.setAdapter(adapter);listView.setOnRefreshListener(new PullToRefreshListView.OnRefreshListener() {@Overridepublic void onRefresh() {new Handler().postDelayed(new Runnable() {@Overridepublic void run() {listView.stopRefresh();}}, 2000);}});
 两个源代码文件就搞定了,demo工程就不提供了,很简单的。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。