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

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

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

这种下拉刷新控件的原理不难,基本就是监听手指的运动,获取手指的坐标,通过计算判断出是哪种操作,然后就是回调相应的接口了。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的源码出发自定义样式高仿微信朋友圈的下拉刷新效果的文章,有兴趣可以看一下哈http://www.jb51.net/article/89311.htm

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

(0)

相关推荐

  • Android自定义控件开发实战之实现ListView下拉刷新实例代码

    这篇博客为大家介绍一个android常见的功能--ListView下拉刷新: 首先下拉未松手时候手机显示这样的界面: 下面的代码是自定的扎样的控件: <span style="font-family: comic sans ms,sans-serif; font-size: 16px;">package com.dhsr.smartID.view; import android.content.Context; import android.util.AttributeSe

  • Android自定义View控件实现刷新效果

    三种得到LinearInflater的方法 a. LayoutInflater inflater = getLayoutInflater(); b. LayoutInflater localinflater = (LayoutInflater)context.getSystemService (Context.LAYOUT_INFLATER_SERVICE); c. LayoutInflater inflater = LayoutInflater.from(context); onDraw 方法

  • Android PullToRefreshLayout下拉刷新控件的终结者

    说到下拉刷新控件,网上版本有很多,很多软件也都有下拉刷新功能.有一个叫XListView的,我看别人用过,没看过是咋实现的,看这名字估计是继承自ListView修改的,不过效果看起来挺丑的,也没什么扩展性,太单调了.看了QQ2014的列表下拉刷新,发现挺好看的,我喜欢,贴一下图看一下qq的下拉刷新效果: 不错吧?嗯,是的.一看就知道实现方式不一样.咱们今天就来实现一个下拉刷新控件.由于有时候不仅仅是ListView需要下拉刷新,ExpandableListView和GridView也有这个需求,

  • Android控件RefreshableView实现下拉刷新

    需求:自定义一个ViewGroup,实现可以下拉刷新的功能.下拉一定距离后(下拉时显示的界面可以自定义任何复杂的界面)释放手指可以回调刷新的功能,用户处理完刷新的内容后,可以调用方法onCompleteRefresh()通知刷新完毕,然后回归正常状态.效果如下: 源代码:RefreshableView(https://github.com/wangjiegulu/RefreshableView) 分析: 我们的目的是不管什么控件,只要在xml中外面包一层标签,那这个标签下面的所有子标签所在的控件

  • Android自定义组合控件之自定义下拉刷新和左滑删除实例代码

    绪论 最近项目里面用到了下拉刷新和左滑删除,网上找了找并没有可以用的,有比较好的左滑删除,但是并没有和下拉刷新上拉加载结合到一起,要不就是一些比较水的结合,并不能在项目里面使用,小编一着急自己组合了一个,做完了和QQ的对比了一下,并没有太大区别,今天分享给大家,其实并不难,但是不知道为什么网上没有比较好的Demo,当你的项目真的很急的时候,又没有比较好的Demo,那么"那条友谊的小船儿真是说翻就翻啊",好了,下面先来具体看一下实现后的效果吧: 代码已经上传到Github上了,小伙伴们记

  • Android开发之无痕过渡下拉刷新控件的实现思路详解

    相信大家已经对下拉刷新熟悉得不能再熟悉了,市面上的下拉刷新琳琅满目,然而有很多在我看来略有缺陷,接下来我将说明一下存在的缺陷问题,然后提供一种思路来解决这一缺陷,废话不多说!往下看嘞! 1.市面一些下拉刷新控件普遍缺陷演示 以直播吧APP为例: 第1种情况: 滑动控件在初始的0位置时,手势往下滑动然后再往上滑动,可以看到滑动到初始位置时滑动控件不能滑动. 原因: 下拉刷新控件响应了触摸事件,后续的一系列事件都由它来处理,当滑动控件到顶端的时候,滑动事件都被下拉刷新控件消费掉了,传递不到它的子控件

  • Android官方下拉刷新控件SwipeRefreshLayout使用详解

    可能开发安卓的人大多数都用过很多下拉刷新的开源组件,但是今天用了官方v4支持包的SwipeRefreshLayout觉得效果也蛮不错的,特拿出来分享. 简介: SwipeRefreshLayout组件只接受一个子组件:即需要刷新的那个组件.它使用一个侦听机制来通知拥有该组件的监听器有刷新事件发生,换句话说我们的Activity必须实现通知的接口.该Activity负责处理事件刷新和刷新相应的视图.一旦监听者接收到该事件,就决定了刷新过程中应处理的地方.如果要展示一个"刷新动画",它必须

  • Android下拉刷新上拉加载控件(适用于所有View)

    前面写过一篇关于下拉刷新控件的文章下拉刷新控件终结者:PullToRefreshLayout,后来看到好多人还有上拉加载更多的需求,于是就在前面下拉刷新控件的基础上进行了改进,加了上拉加载的功能.不仅如此,我已经把它改成了对所有View都通用!可以随心所欲使用这两个功能~~ 我做了一个大集合的demo,实现了ListView.GridView.ExpandableListView.ScrollView.WebView.ImageView.TextView的下拉刷新和上拉加载.后面会提供demo的

  • 亲自动手编写Android通用刷新控件

    项目中我们经常有上拉.下拉刷新的需求,几乎所有的listView.RecyclerView都会伴随着上拉.下拉刷新的需求,如果我们使用一些开源控件,换了控件我们就要更新,现在我们自己撸起袖子写一个通用的刷新控件 项目地址:https://git.oschina.net/qiangshen/commentview.git 思路: 写一个继承RelativeLayout的RefreshLayout 添加头尾控件作为刷新控件 通过事件分发来进行刷新操作 通过动画来控制控件移动 目的:让他的所有子控件都

  • Android实现支持所有View的通用的下拉刷新控件

    下拉刷新对于一个app来说是必不可少的一个功能,在早期大多数使用的是chrisbanes的PullToRefresh,或是修改自该框架的其他库.而到现在已经有了更多的选择,github上还是有很多体验不错的下拉刷新. 而下拉刷新主要有两种实现方式: 1. 在ListView中添加header和footer,监听ListView的滑动事件,动态设置header/footer的高度,但是这种方式只适用于ListView,RecyclerView. 2. 第二种方式则是继承ViewGroup或其子类,

随机推荐