代码版本:support-v4 24.1.1 共1165行
将SwipeRefreshLayout在本文中简称为SRL
涉及到的知识点
从控件的使用效果来看,我们可以了解到关键的知识点如下:
- 自定义ViewGroup:可以学习一下ViewGroup以及View的一些关键方法的使用
- 滑动事件:触摸事件传递规则,简单的多点触控相关
- 动画
本篇也从这三个方面来解读。
涉及到的关键方法
SwipeRefreshLayout(Context context, AttributeSet attrs)
onMeasure (int widthMeasureSpec, int heightMeasureSpec)
onLayout(boolean changed, int left, int top, int right, int bottom)
onInterceptTouchEvent(MotionEvent ev)
onTouchEvent(MotionEvent ev)
moveSpinner(float overscrollTop)
我们可以把整个下拉刷新过程分为几个关键的过程部分:
- 下拉过程
- 回弹过程
- 转圈刷新过程
常量和方法
通过一些常量的定义我们可以认识到SRL的View组成部分,以及一些关键的时间点。
CircleImageView mCircleView
(继承自:android.support.v7.widget.AppCompatImageView): 即我们下拉刷新的过程中可以见到的圈圈控件,在构造方法中为其设置了一系列属性.
MaterialProgressDrawable mProgress
(继承自:Drawable):这是圆圈控件CircleImageView 的内容,在构造方法中可以看到:mCircleView.setImageDrawable(mProgress);对于圆圈内容的控制基本上都是通过MaterialProgressDrawable 来实现的。
View mTarget
:一般即SRL的直接子View比如你嵌入的RecyclerView;在ensureTarget()方法中完成对其赋值。
float mTotalDragDistance
:超过这个距离值后即认定为下拉刷新;也就是触发下拉刷新的距离;也就是最终停下来转圈圈的位置。
int mCurrentTargetOffsetTo
:CircleView实时距离初始位置滑过的距离。
private void setTargetOffsetTopAndBottom(int offset, boolean requiresUpdate)
:通过调用mCircleView的offsetTopAndBottom来移动CircleView本身。
public boolean canChildScrollUp()
:判断SRL的内容是否滑动到顶部,return false 说明滑动到顶部可以触发下拉刷新;return true说明未滑动到顶部不触发下拉刷新。该方法在onTouchEvent和onInterceptTouchEvent方法中均被调用来判断。
初始及化绘制过程:
构造方法
初始化各种常量字段,诸如mCircleView的大小,动画插值器,mTotalDragDistance,创建并添加mCircleView,设置一些属性,比如setWillNotDraw(false)可以提高效率。
绘制布局
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//确保mTarget不为空
if (mTarget == null) {
ensureTarget();
}
if (mTarget == null) {
return;
}
//测量mTarget的宽高,去掉padding
mTarget.measure(MeasureSpec.makeMeasureSpec(
getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(
getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY));
//测量mCircleView的宽高
mCircleView.measure(MeasureSpec.makeMeasureSpec(mCircleWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(mCircleHeight, MeasureSpec.EXACTLY));
//如果没有使用自定义的起始位置,并且起始位置没有被计算过(一般第一次onMeaure的时候会被调用)
if (!mUsingCustomStart && !mOriginalOffsetCalculated) {
mOriginalOffsetCalculated = true;
//计算出当前CircleView移动的位置,即CircleView的自然高度的负值,
//也就是说CircleView正好在屏幕上边,我们看不到它
mCurrentTargetOffsetTop = mOriginalOffsetTop = -mCircleView.getMeasuredHeight();
}
mCircleViewIndex = -1;
// Get the index of the circleview.
// 获取到圆圈view在当前view中的位置;一般情况下mCircleViewIndex都为0;
// 这个值会在getChildDrawingOrder被调用
for (int index = 0; index < getChildCount(); index++) {
if (getChildAt(index) == mCircleView) {
mCircleViewIndex = index;
break;
}
}
}
onMeasure的代码和解释如上,做了两件事: 完成子View的大小测量,对变量进行正确赋值。
接下来在onLayout方法中来确定在mTarget和mCircleView在SRL中的位置,onLayout的代码清晰明了,就做了两件事:对mTarget调用layout方法确定其位置,对CircleView调用layout方法确定其位置,在这里就不贴代码啦。
滑动事件处理:
首先我们知道对于ViewGroup的滑动处理流程的伪代码如下:
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean result = false; // 默认状态为没有消费过
if (!onInterceptTouchEvent(ev)) { // 如果没有拦截交给子View
result = child.dispatchTouchEvent(ev);
}
if (!result) { // 如果事件没有被消费,询问自身onTouchEvent
result = onTouchEvent(ev);
}
return result;
}
在SRL中重写了onInterceptTouchEvent和onTouchEvent方法,通过完成一次完整的下拉刷新,打印Log,我们发现调用过程如下:
下面看onInterceptTouchEvent的处理,这里它会返回一个mIsBeingDragged (是否被拖拽的布尔值),返回true则正在被拖拽,这时就会把事件分发到事件本身的onTouchEvent中,反之就会交给子View去处理。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
ensureTarget();
final int action = MotionEventCompat.getActionMasked(ev);
// 设置准备开始的状态
if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
mReturningToStart = false;
}
// 判断一系列状态,决定是否可以下拉刷新,注意canChildScrollUp()方法,
// 该方法决定了只有滑动到顶部继续下来才能触发下拉刷新
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:
// 默认情况下offset为0,不移动;
// 若初始指定了mOriginalOffsetTop 的大小则意味着,按下的一刻,被移动到了指定位置。
setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCircleView.getTop(), true);
mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
mIsBeingDragged = false;
final float initialDownY = getMotionEventY(ev, mActivePointerId);
if (initialDownY == -1) {
// 不在屏幕范围内不处理
return false;
}
// mInitialDownY 在ActionMove中用于与移动距离比较,判断是否被拖拽。
mInitialDownY = initialDownY;
//可以看到在当前动作下,设置了CircleView的初始位置;获取到了多点触控相关的手指id;拖拽状态置为false;赋值初始按下的位置值:mInitialDownY
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;
// 如果滑动的距离大于被视为滑动的最小距离,并且之前的状态为没有被拖动;
// 这时把拖拽状态mIsBeingDragged置为true;同时记下按下的位置:mInitialMotionY
//如果mIsBeingDragged为true就会return true;根据事件处理伪代码那么接下来的操作就交给onTouchEvent处理了
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;
}
如上就是onInterceptTouchEvent所做的工作,设置初始位置,进行相关赋值操作,根据滑动的实际情况来决定是否把进一步操作转交给onTouchEvent。
public boolean onTouchEvent(MotionEvent ev) {
final int action = MotionEventCompat.getActionMasked(ev);
int pointerIndex = -1;
// 首先是根据状态进行拦截
if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
mReturningToStart = false;
}
if (!isEnabled() || mReturningToStart || canChildScrollUp() || mNestedScrollInProgress) {
// Fail fast if we're not in a state where a swipe is possible
return false;
}
switch (action) {
case MotionEvent.ACTION_DOWN:
// 获取触摸id,设置拖拽状态
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;
}
//获取滑动位置: y,y减去初始按下的位置在撑拖拽比率才是CircleView真正要划过的距离,
//DRAG_RATE为0.5,所以CircleView滑过的距离,要比你手指移动的距离短。
final float y = MotionEventCompat.getY(ev, pointerIndex);
final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
if (mIsBeingDragged) {
if (overscrollTop > 0) {
//滑动距离大于0就去移动CircleView,下面这个方法很重要,就是依靠它来完成CircleView的状态变化和移动的
moveSpinner(overscrollTop);
} else {
return false;
}
}
break;
}
case MotionEventCompat.ACTION_POINTER_DOWN: {
pointerIndex = MotionEventCompat.getActionIndex(ev);
if (pointerIndex < 0) {
Log.e(LOG_TAG, "Got ACTION_POINTER_DOWN event but have an invalid action index.");
return false;
}
mActivePointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
break;
}
case MotionEventCompat.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
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;
}
// 当结束下拉之后这时把拖拽状态置为false,通过finishSpinner来进行刷新操作或者是不到刷新距离回弹到初始位置的操作
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;
}
onTouchEvent分析到这里,我们知道了在这个方法中根据用户真实的滑动状况来调用相关方法:moveSpinner方法完成CircleView及其内容的变化,finishSpinner完成手指抬起后的刷新操作,接下来我们看这两个方法
private void moveSpinner(float overscrollTop) {
mProgress.showArrow(true);// ture,设置下拉过程中展示小箭头
//移动的距离除以刷新位置的距离得出一个拖拽比率:originalDragPercent
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 */);
}
在moveSpinner方法中通过对overscrollTop(CircileView移动的距离)和mTotalDragDistance(刷新位置的距离)进行一系列的计算得出在下拉过程中其他元素的变化程度。最后通过setTargetOffsetTopAndBottom方法来真是移动CircleView本身。
接下来我们看finishSpinner(float overscrollTop)方法,在onTouchEvent中拦截到ACTION_UP手势后调用到了 finishSpinner(overscrollTop);传入的overscrollTop为CircileView移动的距离。
private void finishSpinner(float overscrollTop) {
if (overscrollTop > mTotalDragDistance) {
//如果超过了CircleView最终下来刷新位置的距离后则认定为触发下拉刷新调用setRefreshing方法
setRefreshing(true, true /* notify */);
} else {
//否则的话取消本次刷新动作
// cancel refresh
mRefreshing = false;
mProgress.setStartEndTrim(0f, 0f);
Animation.AnimationListener listener = null;
//mScale默认为false 在这里监听动画结束来进一步操作
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);
}
}
在流程上的分析基本上到这里就结束了,其中一些自定义view和绘制的方法还需要进一步理解。
动画
在SRL中的动画实现方式都是如下套路:
首先直接继承View动画Animation来实现在动画过程中的相应操作。
private final Animation mAnimateToStartPosition = new Animation() {
@Override
public void applyTransformation(float interpolatedTime, Transformation t) {
int targetTop = 0;
targetTop = (mFrom + (int) ((mOriginalOffsetTop - mFrom) * interpolatedTime));
int offset = targetTop - mCircleView.getTop();
//基于Z轴,放到最上层
mCircleView.bringToFront();
mCircleView.offsetTopAndBottom(offset);
mCurrentTargetOffsetTop = mCircleView.getTop();
if (requiresUpdate && android.os.Build.VERSION.SDK_INT < 11) {
//将触发onDraw方法
invalidate();
}
}
};
然后设置动画的相关属性:
mAnimateToStartPosition.reset();
mAnimateToStartPosition.setDuration(ANIMATE_TO_START_DsURATION);
mAnimateToStartPosition.setInterpolator(mDecelerateInterpolator); //减速返回
最后给View设置动画
mCircleView.clearAnimation();
mCircleView.startAnimation(mAnimateToStartPosition);
源码中的动画实现都是这一个套路就不多说了。
其他
我们看到SwipeRefreshLayout还实现了两个接口:NestedScrollingParent 和NestedScrollingChild来处理嵌套滑动,关于这个知识点打算再写一篇博客。
总结
到这里我们已经把SRL的基本实现方式和调用细节大致分析了一遍,主要的知识点还是自定义ViewGroup、 触摸事件的处理和动画的使用。另外关于CircleImageView和MaterialProgressDrawable的实现感兴趣的话还值得深入挖掘。