Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android自定義ListView實現下拉刷新

Android自定義ListView實現下拉刷新

編輯:關於Android編程

首先呈上效果圖


當今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工程就不提供了,很簡單的。

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持本站。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved