[置顶] Android自定义ListView实现下拉刷新,效果仿SwipeRefreshLayout

首先呈上效果图


当今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,自定义控件,下拉刷新)