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第一个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工程就不提供了,很简单的。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Android ListView下拉刷新上拉自动加载更多DEMO示例

    代码下载地址已经更新.因为代码很久没更新,已经很落伍了,建议大家使用RecyclerView实现. 参考项目: https://github.com/bingoogolapple/BGARefreshLayout-Android https://github.com/baoyongzhang/android-PullRefreshLayout 下拉刷新,Android中非常普遍的功能.为了方便便重写的ListView来实现下拉刷新,同时添加了上拉自动加载更多的功能.设计最初是参考开源中国的And

  • Android实现上拉加载更多以及下拉刷新功能(ListView)

    首先为大家介绍Andorid5.0原生下拉刷新简单实现. 先上效果图: 相对于上一个19.1.0版本中的横条效果好看了很多.使用起来也很简单. <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/container" and

  • Android自定义listview布局实现上拉加载下拉刷新功能

    listview实现上拉加载以及下拉刷新的方式有很多.下面是我写的一种自定义的布局,复用性也比较的强.首先就是继承的listview的自定义view.   AutoListView.Java: package com.example.mic.testdemo.view; import android.annotation.TargetApi; import android.content.Context; import android.os.Build; import android.os.Bu

  • Android仿XListView支持下拉刷新和上划加载更多的自定义RecyclerView

    首先给大家展示下效果图,感觉还不错,请继续往下阅读: 下拉刷新:        上划加载        在项目更新的过程中,遇到了一个将XListView换成recyclerView的需求,而且更换完之后大体效果不能变,但是对于下拉刷新这样的效果,谷歌给出的解决方案是把RecyclerView放在一个SwipeRefreshLayout中,但是这样其实是拉下一个小圆形控件实现的,和XListView的header效果不同.在网上找了很多的别人代码,都没有实现我想要的效果,于是自己动手写了一个.

  • Android自定义渐变式炫酷ListView下拉刷新动画

    本文实例为大家分享了自定义渐变式炫酷动画的ListView下拉刷新,供大家参考,具体内容如下 主要要点 listview刷新过程中主要有三个步骤当前:状态为下拉刷新,当前状态为下拉刷新,当前状态为放开刷新,当前状态为正在刷新:主要思路为三个步骤分别对应三个自定义的view:即ibuRefreshFirstStepView,ibuRefreshSecondStepView,ibuRefreshThirdStepView. 效果图 ibuRefreshFirstStepView代码,例如: priv

  • Android ListView实现上拉加载下拉刷新和滑动删除功能

    最近项目需要用到可以滑动删除并且带有上拉加载下拉刷新的Listview,查阅了一些资料,大多都是在SwipeMenuListView的基础上去添加头部和底部View,来扩展上拉加载和下拉刷新的功能,不过需要手动的去绘制UI及处理一些动画效果.用起来也不是特别方便.刚好项目中用到PulltorefreshLibrary库,就尝试着扩展了一个PullToRefreshSwipeMenuListView类来实现需求.先看一下效果: 实现步骤 一.组合Pulltorefresh与SwipeMenuLis

  • Android-自定义控件之ListView下拉刷新的实现

    自定义控件学了很久了,发现学了总是忘,于是打算用博客来记录自己学习的知识点. 今天是自定义ListView来实现下拉刷新,这些文章都是借鉴慕课网上的视频来写的. 自定义一个控件,先是看它继承于那个控件,如果我们继承View控件的话,那得让我们写很多关于ListView的功能,这些东西我自己觉得很麻烦,而且也没有那个必要因为我们可以直接继承ListView,在listView的基础上来加一些我们需要的东西. 1.向ListView加Header布局 private void initView(Co

  • Android使用ListView实现下拉刷新及上拉显示更多的方法

    本文实例讲述了Android使用ListView实现下拉刷新及上拉显示更多的方法.分享给大家供大家参考,具体如下: 今天得需求是做listview+上下拉动在header和footer显示progressdialog,但不影响用户操作 直接上代码,我已经加上注释了,自己看. package com.stay.main; import java.net.HttpURLConnection; import java.util.ArrayList; import java.util.HashMap;

  • Android开发之ListView列表刷新和加载更多实现方法

    本文实例讲述了Android开发之ListView列表刷新和加载更多实现方法.分享给大家供大家参考.具体如下: 上下拉实现刷新和加载更多的ListView,如下: package com.sin.android.ui; import android.content.Context; import android.util.AttributeSet; import android.view.Gravity; import android.view.MotionEvent; import andro

  • Android ListView实现上拉加载更多和下拉刷新功能

    本文实例为大家介绍了Android ListView下拉刷新功能的实现方法和功能,供大家参考,具体内容如下 1.ListView优化方式 界面缓存:ViewHolder+convertView 分页加载:上拉刷新 图片缓存 快速滑动ListView禁止刷新 2.效果 3.上拉加载更多原理及实现 当我们手指滑动到listview最后位置的时候,我们触发加载数据的方法.这触发之前我们需要做一些工作,包括: 如何判断滑动到最后? 如何避免重复加载数据? 加载之后如何刷新界面? 1).界面实现AbsLi

  • Android XListView下拉刷新和上拉加载更多

    市面上有好多的类比ListView刷新数据的开源框架,如:v4包自带的SwipeRefreshLayout ,以及集ListView.GridView甚至WebView于一身的Pulltorefresh等等.前述的两个开源框架目前使用也算频繁.有兴趣的读者可以自行搜索,当然有时间一定回来对所有的使用方式做一个汇总和比较.今天介绍的这款框架,专门针对ListView做下拉刷新与上拉加载的,如果单单是ListView就显得更加简单方便易于理解. 1.首先引入xListView_lib库到自己的Dem

随机推荐