首先呈上效果图
当今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第一个Item private 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; } @Override public void onScrollStateChanged(AbsListView view, int scrollState) { if (scrollButtomListener != null) { if (scrollState == OnScrollListener.SCROLL_STATE_IDLE) { if (view.getLastVisiblePosition() == view.getAdapter().getCount() - 1) { scrollButtomListener.onScrollToButtom(); } } } } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { firstItemIndex = firstVisibleItem; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); mark.onDraw(canvas); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int width = MeasureSpec.getSize(widthMeasureSpec); mark.setCenterX(width / 2); super.onMeasure(widthMeasureSpec, heightMeasureSpec); } @Override public 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; } } @Override public 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{ @Override public 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() { @Override public void onRefresh() { new Handler().postDelayed(new Runnable() { @Override public void run() { listView.stopRefresh(); } }, 2000); } });
两个源代码文件就搞定了,demo工程就不提供了,很简单的。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持小牛知识库。
本文向大家介绍Android自定义listview布局实现上拉加载下拉刷新功能,包括了Android自定义listview布局实现上拉加载下拉刷新功能的使用技巧和注意事项,需要的朋友参考一下 listview实现上拉加载以及下拉刷新的方式有很多。下面是我写的一种自定义的布局,复用性也比较的强。首先就是继承的listview的自定义view。 AutoListView.Java:
本文向大家介绍Android自定义控件实现下拉刷新效果,包括了Android自定义控件实现下拉刷新效果的使用技巧和注意事项,需要的朋友参考一下 app开发中下拉刷新是最常接触到的一个功能,也有很多开源的框架,封装的非常棒。前段时间了解了一下ViewDragHelper,遂用它实现了下拉刷新的功能。 大概和我之前的ViewDragHelper之拖动加载(类似淘宝)这篇代码类似。只是做了相关改动。具体
本文向大家介绍Android自定义控件开发实战之实现ListView下拉刷新实例代码,包括了Android自定义控件开发实战之实现ListView下拉刷新实例代码的使用技巧和注意事项,需要的朋友参考一下 这篇博客为大家介绍一个android常见的功能——ListView下拉刷新: 首先下拉未松手时候手机显示这样的界面: 下面的代码是自定的扎样的控件: 接下来需要自定义自己的ListView继承与a
本文向大家介绍Android中ListView下拉刷新的实现方法,包括了Android中ListView下拉刷新的实现方法的使用技巧和注意事项,需要的朋友参考一下 ListView中的下拉刷新是非常常见的,也是经常使用的,看到有很多同学想要,那我就整理一下,供大家参考。那我就不解释,直接上代码了。 这里需要自己重写一下ListView,重写代码如下: 重写完ListView之后,在布局文件中是这么
本文向大家介绍Android自定义下拉刷新上拉加载,包括了Android自定义下拉刷新上拉加载的使用技巧和注意事项,需要的朋友参考一下 本文实例为大家分享了Android自定义下拉刷新上拉加载的具体实现步骤,供大家参考,具体内容如下 实现的方式是SwipeRefreshLayout + RecyclerView 的VIewType 首先看效果: 总的思路: 布局文件 下拉刷新的实现思路 用于测试的
本文向大家介绍Android自定义下拉刷新控件RefreshableView,包括了Android自定义下拉刷新控件RefreshableView的使用技巧和注意事项,需要的朋友参考一下 这是在了解下拉刷新功能原理下的产物,下拉刷新可以说是国产APP里面必有的功能,连Google都为此出了SwipeRefreshLayout,一种MD风格的下拉刷新。 不过,MD风格在国内似乎很是艰难,不单单是国内