Android下拉刷新控件SwipeRefreshLayout源码解析

作者:u011443509 时间:2023-04-03 20:42:16 

SwipeRefreshLayout是Android官方的下拉刷新控件,使用简单,界面美观,不熟悉的朋友可以随便搜索了解一下,这里就不废话了,直接进入正题。 

首先给张流程图吧,标出了几个主要方法的作用,可以结合着看一下哈。

Android下拉刷新控件SwipeRefreshLayout源码解析 

这种下拉刷新控件的原理不难,基本就是监听手指的运动,获取手指的坐标,通过计算判断出是哪种操作,然后就是回调相应的接口了。SwipeRefreshLayout是继承自ViewGroup的,根据Android的事件分发机制,触摸事件应该是先传递到ViewGroup,根据onInterceptTouchEvent的返回值决定是否拦截事件的,那么就onInterceptTouchEvent出发: 


@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
 ensureTarget();

final int action = MotionEventCompat.getActionMasked(ev);

if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
  mReturningToStart = false;
 }

if (!isEnabled() || mReturningToStart || canChildScrollUp()
   || mRefreshing || mNestedScrollInProgress) {
  // Fail fast if we're not in a state where a swipe is possible
  return false;
 }

switch (action) {
  case MotionEvent.ACTION_DOWN:
   setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCircleView.getTop(), true);
   mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
   mIsBeingDragged = false;
   final float initialDownY = getMotionEventY(ev, mActivePointerId);
   if (initialDownY == -1) {
    return false;
   }
   mInitialDownY = initialDownY;
   break;

case MotionEvent.ACTION_MOVE:
   if (mActivePointerId == INVALID_POINTER) {
    Log.e(LOG_TAG, "Got ACTION_MOVE event but don't have an active pointer id.");
    return false;
   }

final float y = getMotionEventY(ev, mActivePointerId);
   if (y == -1) {
    return false;
   }
   final float yDiff = y - mInitialDownY;
   if (yDiff > mTouchSlop && !mIsBeingDragged) {
    mInitialMotionY = mInitialDownY + mTouchSlop;
    mIsBeingDragged = true;
    mProgress.setAlpha(STARTING_PROGRESS_ALPHA);
   }
   break;

case MotionEventCompat.ACTION_POINTER_UP:
   onSecondaryPointerUp(ev);
   break;

case MotionEvent.ACTION_UP:
  case MotionEvent.ACTION_CANCEL:
   mIsBeingDragged = false;
   mActivePointerId = INVALID_POINTER;
   break;
 }

return mIsBeingDragged;
}

是否拦截的情况有很多种,这里如果满足五个条件之一就直接返回false,使用时触摸事件发生冲突的话就可以从这里出发分析,这里也不具体展开了。简单看一下,在ACTION_DOWN中记录下手指坐标,ACTION_MOVE中计算出移动的距离,并且判断是否大于阈值,是的话就将mIsBeingDragged标志位设为true,ACTION_UP中则将mIsBeingDragged设为false。最后返回的是mIsBeingDragged。

SwipeRefreshLayout一般是嵌套可滚动的View使用的,正常滚动时会满足前面的条件,这时不进行拦截,只有当滚动到顶部才会进入后面action的判断。在手指按下和抬起期间mIsBeingDragged为true,也就是说进行拦截,接下来就是如何处理了,看看onTouchEvent:


@Override
public boolean onTouchEvent(MotionEvent ev) {

....

switch (action) {
  case MotionEvent.ACTION_DOWN:
   mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
   mIsBeingDragged = false;
   break;

case MotionEvent.ACTION_MOVE: {
   pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
   if (pointerIndex < 0) {
    Log.e(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id.");
    return false;
   }

final float y = MotionEventCompat.getY(ev, pointerIndex);
   final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
   if (mIsBeingDragged) {
    if (overscrollTop > 0) {
     moveSpinner(overscrollTop);
    } else {
     return false;
    }
   }
   break;
  }
  ....
  case MotionEvent.ACTION_UP: {
   pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
   if (pointerIndex < 0) {
    Log.e(LOG_TAG, "Got ACTION_UP event but don't have an active pointer id.");
    return false;
   }

final float y = MotionEventCompat.getY(ev, pointerIndex);
   final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
   mIsBeingDragged = false;
   finishSpinner(overscrollTop);
   mActivePointerId = INVALID_POINTER;
   return false;
  }
  case MotionEvent.ACTION_CANCEL:
   return false;
 }

return true;
}

这里省略了一些代码,前面还有几行跟上面的类似,也是在满足其中一个条件时直接返回;switch中也还有几行处理多指触控的,这些都略过了。看一下ACTION_MOVE中计算了手指移动的距离,这时的mIsBeingDragged正常情况下应为true,当距离大于零就会执行moveSpinner。在ACTION_UP中则会执行finishSpinner,到这里就可以猜出,执行刷新的逻辑主要就在这两个方法中。 

看这两个方法前,要知道两个重要的成员变量:一个是mCircleView,是CircleImageView的实例,继承了ImageView,主要绘制进度圈的背景;另一个是mProgress,是MaterialProgressDrawable的实例,继承自Drawable且实现Animatable接口,主要绘制进度圈,SwipeRefreshLayout正是通过调用其方法来绘制动画。接下来就先看一下moveSpinner:


<span style="font-size:18px;">private void moveSpinner(float overscrollTop) {
 mProgress.showArrow(true);
 float originalDragPercent = overscrollTop / mTotalDragDistance;

float dragPercent = Math.min(1f, Math.abs(originalDragPercent));
 float adjustedPercent = (float) Math.max(dragPercent - .4, 0) * 5 / 3;
 float extraOS = Math.abs(overscrollTop) - mTotalDragDistance;
 float slingshotDist = mUsingCustomStart ? mSpinnerFinalOffset - mOriginalOffsetTop
   : mSpinnerFinalOffset;
 float tensionSlingshotPercent = Math.max(0, Math.min(extraOS, slingshotDist * 2)
   / slingshotDist);
 float tensionPercent = (float) ((tensionSlingshotPercent / 4) - Math.pow(
   (tensionSlingshotPercent / 4), 2)) * 2f;
 float extraMove = (slingshotDist) * tensionPercent * 2;

int targetY = mOriginalOffsetTop + (int) ((slingshotDist * dragPercent) + extraMove);
 // where 1.0f is a full circle
 if (mCircleView.getVisibility() != View.VISIBLE) {
  mCircleView.setVisibility(View.VISIBLE);
 }
 if (!mScale) {
  ViewCompat.setScaleX(mCircleView, 1f);
  ViewCompat.setScaleY(mCircleView, 1f);
 }

if (mScale) {
  setAnimationProgress(Math.min(1f, overscrollTop / mTotalDragDistance));
 }
 if (overscrollTop < mTotalDragDistance) {
  if (mProgress.getAlpha() > STARTING_PROGRESS_ALPHA
    && !isAnimationRunning(mAlphaStartAnimation)) {
   // Animate the alpha
   startProgressAlphaStartAnimation();
  }
 } else {
  if (mProgress.getAlpha() < MAX_ALPHA && !isAnimationRunning(mAlphaMaxAnimation)) {
   // Animate the alpha
   startProgressAlphaMaxAnimation();
  }
 }
 float strokeStart = adjustedPercent * .8f;
 mProgress.setStartEndTrim(0f, Math.min(MAX_PROGRESS_ANGLE, strokeStart));
 mProgress.setArrowScale(Math.min(1f, adjustedPercent));

float rotation = (-0.25f + .4f * adjustedPercent + tensionPercent * 2) * .5f;
 mProgress.setProgressRotation(rotation);
 setTargetOffsetTopAndBottom(targetY - mCurrentTargetOffsetTop, true /* requires update */);
}</span>

showArrow是显示箭头,中间那一坨主要也是一些math和设置进度圈的样式,倒数第二行执行了setProgressRotation,传入的是经过一堆计算后的rotation,这堆计算主要是优化效果,比如在刚开始移动时增长比较快,超过刷新的距离后就增长比较慢。传入该方法后,mProgress就根据它来绘制进度圈,因此主要的动画就应该在这个方法内。最后一行执行setTargetOffsetTopAndBottom,我们来看一下:


<span style="font-size:18px;">private void setTargetOffsetTopAndBottom(int offset, boolean requiresUpdate) {
 mCircleView.bringToFront();
 mCircleView.offsetTopAndBottom(offset);
 mCurrentTargetOffsetTop = mCircleView.getTop();
 if (requiresUpdate && android.os.Build.VERSION.SDK_INT < 11) {
  invalidate();
 }
}</span>

 比较简单,就是调整进度圈的位置并进行记录。最后来看一下finishSpinner:


<span style="font-size:18px;">private void finishSpinner(float overscrollTop) {
 if (overscrollTop > mTotalDragDistance) {
  setRefreshing(true, true /* notify */);
 } else {
  // cancel refresh
  mRefreshing = false;
  mProgress.setStartEndTrim(0f, 0f);
  Animation.AnimationListener listener = null;
  if (!mScale) {
   listener = new Animation.AnimationListener() {

@Override
    public void onAnimationStart(Animation animation) {
    }

@Override
    public void onAnimationEnd(Animation animation) {
     if (!mScale) {
      startScaleDownAnimation(null);
     }
    }

@Override
    public void onAnimationRepeat(Animation animation) {
    }

};
  }
  animateOffsetToStartPosition(mCurrentTargetOffsetTop, listener);
  mProgress.showArrow(false);
 }
}</span>

 逻辑也很简单,当移动的距离超过设定值时就执行setRefreshing(true,true),在该方法里更新一些成员变量的值后会执行animateOffsetToCorrectPosition,由名字就知道是执行动画将进度圈移动到正确位置的(也就是头部)。如果移动的距离没有超过设定值,就会执行animateOffsetToStartPosition。一起看一下animateOffsetToCorrectPosition和animateOffsetToStartPosition这两个方法:


<span style="font-size:18px;">private void animateOffsetToCorrectPosition(int from, AnimationListener listener) {
 mFrom = from;
 mAnimateToCorrectPosition.reset();
 mAnimateToCorrectPosition.setDuration(ANIMATE_TO_TRIGGER_DURATION);
 mAnimateToCorrectPosition.setInterpolator(mDecelerateInterpolator);
 if (listener != null) {
  mCircleView.setAnimationListener(listener);
 }
 mCircleView.clearAnimation();
 mCircleView.startAnimation(mAnimateToCorrectPosition);
}

private void animateOffsetToStartPosition(int from, AnimationListener listener) {
 if (mScale) {
  // Scale the item back down
  startScaleDownReturnToStartAnimation(from, listener);
 } else {
  mFrom = from;
  mAnimateToStartPosition.reset();
  mAnimateToStartPosition.setDuration(ANIMATE_TO_START_DURATION);
  mAnimateToStartPosition.setInterpolator(mDecelerateInterpolator);
  if (listener != null) {
   mCircleView.setAnimationListener(listener);
  }
  mCircleView.clearAnimation();
  mCircleView.startAnimation(mAnimateToStartPosition);
 }
}</span>

逻辑基本相同,进行一些设置后,最后都会执行mCircleView的startAnimation,只是传入的值以及 * 不同。 

如果是要执行刷新的操作,传入的值是头部高度, * 为:


<span style="font-size:18px;">private Animation.AnimationListener mRefreshListener = new Animation.AnimationListener() {
 @Override
 public void onAnimationStart(Animation animation) {
 }

@Override
 public void onAnimationRepeat(Animation animation) {
 }

@Override
 public void onAnimationEnd(Animation animation) {
  if (mRefreshing) {
   // Make sure the progress view is fully visible
   mProgress.setAlpha(MAX_ALPHA);
   mProgress.start();
   if (mNotify) {
    if (mListener != null) {
     mListener.onRefresh();
    }
   }
   mCurrentTargetOffsetTop = mCircleView.getTop();
  } else {
   reset();
  }
 }
};</span>

动画完成后,也就是进度圈移动到头部后,会执行mProgress.start();这里执行的就是在刷新时进度圈转啊转的动画。接下来注意到如果mListener不为空就会执行onRefresh方法,这个mListener其实就是执行setOnRefreshListener所设置的 * ,因此在这里完成刷新。如果是执行回到初始位置的操作,传入的值为初始高度(也就是顶部之上), * 为


<span style="font-size:18px;">listener = new Animation.AnimationListener() {

@Override
public void onAnimationStart(Animation animation) {
}

@Override
public void onAnimationEnd(Animation animation) {
 if (!mScale) {
  startScaleDownAnimation(null);
 }
}

@Override
public void onAnimationRepeat(Animation animation) {
}

};</span>

移动到初始位置后会执行startScaleDownAnimation,也就是消失的动画了,到这里整个刷新流程就结束了。

这样就基本把SwipeRefreshLayout的流程过了一遍,但是要实现这样一个控件还是有很多小问题需要考虑的,这里主要是把思路理清,知道如果出现问题该怎样解决。另外从源码也可以看出swipeRefreshLayout的定制性是比较差的,也不知道google是不是故意这样希望以后全都用这种统一样式的下拉刷新。。当然有一些第三方下拉刷新的定制性还是比较好的,使用上也不难。但是有些人(比如我)是比较倾向于使用官方的控件的,不到万不得已都不想用第三方工具。下次会写一篇探讨一下用swipeRefreshLayout实现自定义样式的文章~

后续还有一篇从修改swipeRefreshLayout的源码出发自定义样式 * 微信朋友圈的下拉刷新效果的文章,有兴趣可以看一下哈https://www.jb51.net/article/89311.htm

来源:http://blog.csdn.net/u011443509/article/details/52017355

标签:Android,下拉刷新,控件,SwipeRefreshLayout
0
投稿

猜你喜欢

  • Spring Cloud Gateway 服务网关快速实现解析

    2023-12-19 04:28:33
  • Java使用RedisTemplate模糊删除key操作

    2023-06-24 06:45:25
  • 值得Java开发者关注的7款新工具

    2023-11-02 23:05:31
  • 详解Java中clone的写法

    2023-09-08 17:00:43
  • C语言算法打卡回文串验证算法题解

    2022-05-29 22:54:10
  • Android实现仪表盘效果

    2021-07-26 13:19:37
  • Java 实战项目之CRM客户管理系统的实现流程

    2022-12-01 22:50:54
  •  Java SE 面向对象编程的3个常用接口

    2023-01-14 06:42:32
  • C++实现优先队列的示例详解

    2022-04-06 22:14:20
  • 聊聊如何打印GC日志排查的问题

    2023-01-22 22:10:56
  • 怎么把本地jar包放入本地maven仓库和远程私服仓库

    2023-12-05 20:13:00
  • C#限速下载网络文件的方法实例

    2023-07-01 01:33:15
  • Java冒泡排序及优化介绍

    2023-11-11 13:05:51
  • Java深入探究关键字abstract的使用

    2023-08-03 07:48:20
  • unity 实现摄像机绕某点旋转一周

    2021-06-11 16:48:57
  • Android App后台服务报告工作状态实例

    2023-04-26 19:10:34
  • java IO流 之 输出流 OutputString()的使用

    2023-08-11 23:16:30
  • C#中WebBrowser.DocumentCompleted事件多次调用问题解决方法

    2023-12-05 18:23:15
  • C++ 归并排序(merge sort)案例详解

    2022-03-23 00:00:00
  • java数据结构排序算法之树形选择排序详解

    2022-07-22 23:43:17
  • asp之家 软件编程 m.aspxhome.com