咱们在做Android APP开发的时候经常碰到有下拉刷新和上拉加载跟多的需求,这篇文章咱们先说说下来刷新,咱们就以google的原生的下拉刷新控件SwipeRefreshLayout来看看大概的实现过程。
SwipeRefreshLayout是google自己推出的下拉刷新控件。使用起来也非常的简单,在满足条件的情况下下拉的时候会显示一个圆形的loading的动画效果,然后回调到上层,上层自己做刷新的一系列的处理,处理结束后调用SwipeRefreshLayout的setRefreshing(false)告诉SwipeRefreshLayout完成刷新。具体的效果图如下
那接下来我们就来简单的看下SwipeRefreshLayout内部是怎么实现的下拉刷新。准备从三个CircleImageView,MaterialProgressDrawable,SwipeRefreshLayout相关的类着手来分析下拉刷新代码简单实现。
一. CircleImageView类代码分析
继承自ImageView,CircleImageView是一个圆形的并且底部是有一定阴影效果的ImageView。正如上图中下拉刷新的时候显示的那个白色的小圆。
CircleImageView的具体实现。里面的代码非常的少就干了两件事一个是确定圆形,一个是圆形底部的阴影效果(包括向下兼容的情况)。具体的实现我就干脆写在代码的注释里面了,CircleImageView包所在的路径android.support.v4.widget。
CircleImageView构造函数
public CircleImageView(Context context, int color, final float radius) {
super(context);
final float density = getContext().getResources().getDisplayMetrics().density;
final int diameter = (int) (radius * density * 2);
final int shadowYOffset = (int) (density * Y_OFFSET);
final int shadowXOffset = (int) (density * X_OFFSET);
mShadowRadius = (int) (density * SHADOW_RADIUS);
ShapeDrawable circle;
if (elevationSupported()) {
// 确保是一个圆形
circle = new ShapeDrawable(new OvalShape());
// 如果版本支持阴影的设置,直接调用setElevation函数设置阴影效果
ViewCompat.setElevation(this, SHADOW_ELEVATION * density);
} else {
// 如果版本不支持阴影效果的设置,没办了只能自己去实现一个类似的效果了。
// OvalShadow是继承自OvalShape自定义的一个类,用来实现类似的阴影效果(这个可能是我们的一个学习的点)。
OvalShape oval = new OvalShadow(mShadowRadius, diameter);
circle = new ShapeDrawable(oval);
// 关闭硬件加速,要不绘制的阴影没有效果
ViewCompat.setLayerType(this, ViewCompat.LAYER_TYPE_SOFTWARE, circle.getPaint());
// 设置阴影层,Y方向稍微偏移了一点点
circle.getPaint().setShadowLayer(mShadowRadius, shadowXOffset, shadowYOffset, KEY_SHADOW_COLOR);
final int padding = mShadowRadius;
// 保证接下的内容不会绘制到阴影上面去,但是阴影被覆盖住。
setPadding(padding, padding, padding, padding);
}
circle.getPaint().setColor(color);
setBackgroundDrawable(circle);
}
CircleImageView测量函数
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (!elevationSupported()) {
// 如果不支持阴影效果,把阴影的范围加进去重新设置控件的大小
setMeasuredDimension(getMeasuredWidth() + mShadowRadius * 2, getMeasuredHeight() + mShadowRadius * 2);
}
}
为了向下兼容实现类似阴影效果而自定义的类
/**
* 继承自OvalShape,先保证图像是圆形的。重写draw方法实现一个类似阴影的效果
*/
private class OvalShadow extends OvalShape {
private RadialGradient mRadialGradient;
private Paint mShadowPaint;
private int mCircleDiameter;
public OvalShadow(int shadowRadius, int circleDiameter) {
super();
// 画阴影的paint
mShadowPaint = new Paint();
// 阴影的范围大小
mShadowRadius = shadowRadius;
// 直径
mCircleDiameter = circleDiameter;
// 环形渲染,达到阴影的效果
mRadialGradient = new RadialGradient(mCircleDiameter / 2, mCircleDiameter / 2, mShadowRadius, new int[]{FILL_SHADOW_COLOR,
Color.TRANSPARENT},
null, Shader.TileMode.CLAMP);
mShadowPaint.setShader(mRadialGradient);
}
@Override
public void draw(Canvas canvas, Paint paint) {
final int viewWidth = CircleImageView.this.getWidth();
final int viewHeight = CircleImageView.this.getHeight();
// 先画上阴影效果
canvas.drawCircle(viewWidth / 2, viewHeight / 2, (mCircleDiameter / 2 + mShadowRadius), mShadowPaint);
// 画上内容
canvas.drawCircle(viewWidth / 2, viewHeight / 2, (mCircleDiameter / 2), paint);
}
}
一. MaterialProgressDrawable类代码分析
继承自Drawable,这个Drawable干的事情就是当下拉刷新进入加载的时候显示一个小圆环,并且这个小圆环是可以一直转圈的,正如上文效果图中一直转圈并且颜色不同变化的情况就是通过MaterialProgressDrawable来实现的。
MaterialProgressDrawable继承自Drawable,既然是继承自Drawable那咱们首先关注重写的getIntrinsicHeight() getIntrinsicWidth() draw(Canvas c) setAlpha(int alpha)这些方法。其中getIntrinsicHeight和getIntrinsicWidth用来给依附的view提供测量大小,draw函数就是Drawable具体的内容了,setAlpha设置透明度。我们就直接看draw()函数了。
@Override
public void draw(Canvas c) {
// 自定义Drawable的时候draw函数是关键部分
final Rect bounds = getBounds(); // 获取Drawable的区域
final int saveCount = c.save();
// 旋转mRotation角度
c.rotate(mRotation, bounds.exactCenterX(), bounds.exactCenterY());
// 这个里面就开始画箭头和转圈的小圆环了
mRing.draw(c, bounds);
c.restoreToCount(saveCount);
}
在draw函数中mRing.draw(c, bounds);就是来绘制转圈的那个圆环的。调用的是内部类Ring的draw()函数,进入看下咯。
public void draw(Canvas c, Rect bounds) {
final RectF arcBounds = mTempBounds;
arcBounds.set(bounds);
// 进度条相对于外圈的一个内边距
arcBounds.inset(mStrokeInset, mStrokeInset);
final float startAngle = (mStartTrim + mRotation) * 360;
final float endAngle = (mEndTrim + mRotation) * 360;
float sweepAngle = endAngle - startAngle;
mPaint.setColor(mCurrentColor);
// 画进度圆环(环的宽度setStrokeWidth)
c.drawArc(arcBounds, startAngle, sweepAngle, false, mPaint);
// 如果需要的话,画箭头
drawTriangle(c, startAngle, sweepAngle, bounds);
if (mAlpha < 255) {
// 在上面覆盖一层alpha,达到透明的效果
mCirclePaint.setColor(mBackgroundColor);
mCirclePaint.setAlpha(255 - mAlpha);
c.drawCircle(bounds.exactCenterX(), bounds.exactCenterY(), bounds.width() / 2, mCirclePaint);
}
}
private void drawTriangle(Canvas c, float startAngle, float sweepAngle, Rect bounds) {
if (mShowArrow) {
// 如果现实箭头
if (mArrow == null) {
mArrow = new android.graphics.Path();
mArrow.setFillType(android.graphics.Path.FillType.EVEN_ODD);
} else {
mArrow.reset();
}
// 找到三角形箭头要偏移的位置(x,y方向要偏移的位置)
float inset = (int) mStrokeInset / 2 * mArrowScale;
float x = (float) (mRingCenterRadius * Math.cos(0) + bounds.exactCenterX());
float y = (float) (mRingCenterRadius * Math.sin(0) + bounds.exactCenterY());
// 先确定三角形箭头的三个点,在偏移到0度角的位置,然后再旋转进度条扫过的角度,在封闭形成三角形箭头
mArrow.moveTo(0, 0);
mArrow.lineTo(mArrowWidth * mArrowScale, 0);
mArrow.lineTo((mArrowWidth * mArrowScale / 2), (mArrowHeight * mArrowScale));
mArrow.offset(x - inset, y);
mArrow.close();
// draw a triangle
mArrowPaint.setColor(mCurrentColor);
c.rotate(startAngle + sweepAngle - ARROW_OFFSET_ANGLE, bounds.exactCenterX(), bounds.exactCenterY());
c.drawPath(mArrow, mArrowPaint);
}
}
画圆环,画圆环上面的三角形箭头有了吧。到现在图形是有了,但是啥时候开始转圈啥时候停止转圈动画呢。看MaterialProgressDrawable的start()和stop()函数,对应的就是开始结束转圈的动画。看看start()里面到底做的是写啥。
// MaterialProgressDrawable释放的时候开始转圈动画,没转一圈换一个颜色
@Override
public void start() {
mAnimation.reset();
// 进度圆环保存一些mStartTrim,mEndTrim,mRotation设置信息
mRing.storeOriginals();
if (mRing.getEndTrim() != mRing.getStartTrim()) {
// 有进度圆环的时候,这个时候做的事情会先慢慢的把这个现有的圆环慢慢的变小,然后在开始转圈
mFinishing = true;
mAnimation.setDuration(ANIMATION_DURATION / 2);
mParent.startAnimation(mAnimation);
} else {
// 没有进度圆环的时候,直接开始转圈的动画
mRing.setColorIndex(0);
mRing.resetOriginals();
mAnimation.setDuration(ANIMATION_DURATION);
mParent.startAnimation(mAnimation);
}
}
恩,都是在和mAnimation变量打交道。接着看下mAnimation干啥用的。
private void setupAnimators() {
final MaterialProgressDrawable.Ring ring = mRing;
final Animation animation = new Animation() {
@Override
public void applyTransformation(float interpolatedTime, Transformation t) {
if (mFinishing) {
// 在有进度圆环的时候我们去启动转圈的动画的时候是要先把这个圆环慢慢的变小消失
applyFinishTranslation(interpolatedTime, ring);
} else {
final float minProgressArc = getMinProgressArc(ring);
final float startingEndTrim = ring.getStartingEndTrim();
final float startingTrim = ring.getStartingStartTrim();
final float startingRotation = ring.getStartingRotation();
// 每次repeat的动画在最后的25%的过程中颜色有过渡的效果
updateRingColor(interpolatedTime, ring);
// 每次repeat的动画的前50%的时候圆环的起始角度有一个往前移的动作
if (interpolatedTime <= START_TRIM_DURATION_OFFSET) {
final float scaledTime = (interpolatedTime) / (1.0f - START_TRIM_DURATION_OFFSET);
final float startTrim = startingTrim +
((MAX_PROGRESS_ARC - minProgressArc) * MATERIAL_INTERPOLATOR.getInterpolation(scaledTime));
ring.setStartTrim(startTrim);
}
// 每次repeat的动画的后50%的时候圆环的结束角度有一个往前移的动作
if (interpolatedTime > END_TRIM_START_DELAY_OFFSET) {
final float minArc = MAX_PROGRESS_ARC - minProgressArc;
float scaledTime = (interpolatedTime - START_TRIM_DURATION_OFFSET) / (1.0f - START_TRIM_DURATION_OFFSET);
final float endTrim = startingEndTrim + (minArc * MATERIAL_INTERPOLATOR.getInterpolation(scaledTime));
ring.setEndTrim(endTrim);
}
final float rotation = startingRotation + (0.25f * interpolatedTime);
// 圆环旋转的效果
ring.setRotation(rotation);
float groupRotation = ((FULL_ROTATION / NUM_POINTS) * interpolatedTime) +
(FULL_ROTATION * (mRotationCount / NUM_POINTS));
setRotation(groupRotation);
}
}
};
animation.setRepeatCount(Animation.INFINITE);
animation.setRepeatMode(Animation.RESTART);
animation.setInterpolator(LINEAR_INTERPOLATOR);
animation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
mRotationCount = 0;
}
@Override
public void onAnimationEnd(Animation animation) {
// do nothing
}
@Override
public void onAnimationRepeat(Animation animation) {
// 转一圈圆环换一个颜色
ring.storeOriginals();
ring.goToNextColor();
ring.setStartTrim(ring.getEndTrim());
if (mFinishing) {
// 在SwipeRefreshLayout中调用MaterialProgressDrawable类start函数的时候,
// 如果有圆环第一次动画就是圆环慢慢消失,这里表示消失完成了
mFinishing = false;
animation.setDuration(ANIMATION_DURATION);
ring.setShowArrow(false);
} else {
mRotationCount = (mRotationCount + 1) % (NUM_POINTS);
}
}
});
mAnimation = animation;
}
// 在有进度圆环的时候我们去启动转圈的动画的时候是要先把这个圆环慢慢的变小消失
private void applyFinishTranslation(float interpolatedTime, MaterialProgressDrawable.Ring ring) {
// 进度圆环的颜色有一个过渡的效果
updateRingColor(interpolatedTime, ring);
// 一次动画要转的rotation
float targetRotation = (float) (Math.floor(ring.getStartingRotation() / MAX_PROGRESS_ARC) + 1f);
final float minProgressArc = getMinProgressArc(ring);
final float startTrim = ring.getStartingStartTrim() +
(ring.getStartingEndTrim() - minProgressArc - ring.getStartingStartTrim()) * interpolatedTime;
// 在一圈的过程中进度圆环是慢慢变小的所以setEndTrim是没变化的
ring.setStartTrim(startTrim);
ring.setEndTrim(ring.getStartingEndTrim());
final float rotation = ring.getStartingRotation() + ((targetRotation - ring.getStartingRotation()) * interpolatedTime);
// 在一圈的过程中进度圆环会慢慢往前旋转的
ring.setRotation(rotation);
}
看的出来mAnimation是自定义的一个Animation(自定义Animation的时候重心在applyTransformation函数上面会随着动画的进行不断的回调这个函数)。如果在我们调用MaterialProgressDrawable start()函数的时候如果有小圆圈的显示动画的第一次repeat的时候会把这个小圆圈慢慢的变小从applyFinishTranslation()可以分析得到。然后才开始一圈一圈的转圈并且每次repeat的时候会换一种颜色。
三. SwipeRefreshLayout类代码分析
本文最重要的一个类来了,这个才是下拉刷新接触最多的一个类,下拉刷新打不的逻辑都集中在这个类当中。SwipeRefreshLayout继承自ViewGroup是一个容器控件。既然是一个自定义的容器类那咱们就从onMeasure(),onLayout(),onInterceptTouchEvent(),onTouchEvent()四个函数入手来分析SwipeRefreshLayout的过程。
1). SwipeRefreshLayout类onMeasure()函数
从onMeasure()函数我们可以得到SwipeRefreshLayout中控件的测量规则,看看onMeasure()的具体实现
private void ensureTarget() {
// 取了第一个不是mCircleView的view作为mTarget View
if (mTarget == null) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (!child.equals(mCircleView)) {
mTarget = child;
break;
}
}
}
}
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (mTarget == null) {
ensureTarget();
}
if (mTarget == null) {
return;
}
// mTarget这个就是咱们的内容控件,直接适用了SwipeRefreshLayout的整个大小
mTarget.measure(MeasureSpec.makeMeasureSpec(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY));
// mCircleView这个就是咱们下拉和加载的时候显示的那个小圆圈在构造函数中addView,给了确定的大小,具体可以参SwipeRefreshLayout的构造函数
mCircleView.measure(MeasureSpec.makeMeasureSpec(mCircleWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(mCircleHeight, MeasureSpec.EXACTLY));
// 确定mCircleView初始的偏移位置和当前位置
if (!mUsingCustomStart && !mOriginalOffsetCalculated) {
mOriginalOffsetCalculated = true;
mCurrentTargetOffsetTop = mOriginalOffsetTop = -mCircleView.getMeasuredHeight();
}
// mCircleView在SwipeRefreshLayout中的子View的index
mCircleViewIndex = -1;
// Get the index of the circleview.
for (int index = 0; index < getChildCount(); index++) {
if (getChildAt(index) == mCircleView) {
mCircleViewIndex = index;
break;
}
}
}
从上面代码分析咱可以看得出来SwipeRefreshLayout只关心两个View:mTarget、mCircleView。其中mTarget是内容控件,mCircleView下拉或者刷新过程中显示的小圆控件。同时mTarget的大小设置了整个SwipeRefreshLayout的大小所以咱们在xml中设置的大小应该是不算数的。
2). SwipeRefreshLayout类onLayout()函数
从onLayout()函数我们可以得到SwipeRefreshLayout中控件的布局规则
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
final int width = getMeasuredWidth();
final int height = getMeasuredHeight();
if (getChildCount() == 0) {
return;
}
if (mTarget == null) {
ensureTarget();
}
if (mTarget == null) {
return;
}
final View child = mTarget;
final int childLeft = getPaddingLeft();
final int childTop = getPaddingTop();
final int childWidth = width - getPaddingLeft() - getPaddingRight();
final int childHeight = height - getPaddingTop() - getPaddingBottom();
// 设置mTarget的位置,正常布局没啥看头
child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
int circleWidth = mCircleView.getMeasuredWidth();
int circleHeight = mCircleView.getMeasuredHeight();
// 设置mCircleView,也是正常布局就偏移了mCurrentTargetOffsetTop的高度,这个好理解咱mCircleView是会上下滑动的
mCircleView.layout((width / 2 - circleWidth / 2), mCurrentTargetOffsetTop, (width / 2 + circleWidth / 2),
mCurrentTargetOffsetTop + circleHeight);
}
onLayout()还是中规中矩的,分别布局了mTarget和mCircleView
3). SwipeRefreshLayout类onInterceptTouchEvent()函数
onInterceptTouchEvent()用来对触摸事件做拦截处理。如果拦截了就不会想子View传递了。关于事件的拦截想多说已经如果ACTION_DOWN被拦截下来了那么该事件接下来的ACTION_MOV和EACTION_UP也不会往下传递。
/**
* 就是去判断mTarget是否有向上滑动,有一个向上的scroll。如果有这个时候肯定是不能下拉刷新的吧
*/
public boolean canChildScrollUp() {
if (android.os.Build.VERSION.SDK_INT < 14) {
if (mTarget instanceof AbsListView) {
final AbsListView absListView = (AbsListView) mTarget;
return absListView.getChildCount() > 0 &&
(absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0).getTop() < absListView.getPaddingTop());
} else {
return ViewCompat.canScrollVertically(mTarget, -1) || mTarget.getScrollY() > 0;
}
} else {
return ViewCompat.canScrollVertically(mTarget, -1);
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
ensureTarget();
final int action = MotionEventCompat.getActionMasked(ev);
// mReturningToStart好像没啥作用,一直是false
if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
mReturningToStart = false;
}
// 如果mTarget这个时候有向上滑动有scroll y(这个时候是不满足下拉刷新的条件的),或者正在刷新。事件不拦截个字View去处理。
// 从这里也可以看出当正在刷新的时候子View还是会想要按键事件的。
if (!isEnabled() || mReturningToStart || canChildScrollUp() || mRefreshing) {
// 不拦截
return false;
}
switch (action) {
case MotionEvent.ACTION_DOWN://ACTION_DOWN这个时间是不拦截的
// mCircleView移动到起始位置。(mOriginalOffsetTop设置的初始位置+mCircleView设置的top位置)
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;
// y方向有滑动
if (yDiff > mTouchSlop && !mIsBeingDragged) {
mInitialMotionY = mInitialDownY + mTouchSlop;
// 下拉开始,从这个时候开始当前事件一直到ACTION_UP之间的事件我们是会拦截下来的
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;
}
先判断是否满足下拉刷新的条件,同时咱也看的出来ACTION_DOWN不去做拦截处理。主要的拦截在ACTION_MOVE里面。当满足下拉刷新的条件并且下拉了那不好意思这次的时间我SwipeRefreshLayout要强行插手处理了。接下来就得去onTouchEvent()函数了。
4). SwipeRefreshLayout类onTouchEvent()函数
onTouchEvent()SwipeRefreshLayout对具体的事件都在这个函数里面了。
@Override
public boolean onTouchEvent(MotionEvent ev) {
final int action = MotionEventCompat.getActionMasked(ev);
if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
mReturningToStart = false;
}
// 如果mTarget这个时候有向上滑动有scroll, SwipeRefreshLayout不对该事件做处理
if (!isEnabled() || mReturningToStart || canChildScrollUp()) {
return false;
}
switch (action) {
case MotionEvent.ACTION_DOWN:
mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
mIsBeingDragged = false;
break;
case MotionEvent.ACTION_MOVE: {
final int 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) {
// 可以处理下拉了,mCircleView会随着手指往下移动了
moveSpinner(overscrollTop);
} else {
return false;
}
}
break;
}
case MotionEventCompat.ACTION_POINTER_DOWN: {
final int index = MotionEventCompat.getActionIndex(ev);
mActivePointerId = MotionEventCompat.getPointerId(ev, index);
break;
}
case MotionEventCompat.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL: {
if (mActivePointerId == INVALID_POINTER) {
if (action == MotionEvent.ACTION_UP) {
Log.e(LOG_TAG, "Got ACTION_UP event but don't have an active pointer id.");
}
return false;
}
final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
final float y = MotionEventCompat.getY(ev, pointerIndex);
final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
mIsBeingDragged = false;
// 是否触摸的时候,mCircleView会到指定的位置,必要的话进入刷新的状态
finishSpinner(overscrollTop);
mActivePointerId = INVALID_POINTER;
return false;
}
}
return true;
}
重心在MotionEvent.ACTION_MOVE和MotionEvent.ACTION_UP上面正好对应了moveSpinner()和finishSpinner()函数。咱们先分析moveSpinner()这个函数做的事情就是随着手指的下拉mCircleView做相应的位移操作并且mCircleView里面的mProgress(MaterialProgressDrawable)做相应的动态变化。
/**
* 下拉过程中调用该函数
* @param overscrollTop:表示y轴上下拉的距离
*/
private void moveSpinner(float overscrollTop) {
mProgress.showArrow(true);
// 相对于刷新距离滑动了百分之多少(注意如果超过了刷新的距离这个值会大于1的)
float originalDragPercent = overscrollTop / mTotalDragDistance;
// 控制最大值为1 dragPercent == 1 表示滑动距离已经到了刷新的条件了
float dragPercent = Math.min(1f, Math.abs(originalDragPercent));
// 调整下百分比(小于0.4的情况下设置为0)
float adjustedPercent = (float) Math.max(dragPercent - .4, 0) * 5 / 3;
// 相对于进入刷新的位置的偏移量,注意这个值可能是负数。负数表示还没有达到刷新的距离
float extraOS = Math.abs(overscrollTop) - mTotalDragDistance;
// 这里去计算小圆圈在Y轴上面可以滑动到的距离(targetY)为啥要这样算就没搞明白
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);
// 在手指滑动的过程中mCircleView小圆圈是可见的
if (mCircleView.getVisibility() != View.VISIBLE) {
mCircleView.setVisibility(View.VISIBLE);
}
if (!mScale) {
// 在滑动过程中小圆圈设置不缩放,x,y scale都设置为1
ViewCompat.setScaleX(mCircleView, 1f);
ViewCompat.setScaleY(mCircleView, 1f);
}
if (overscrollTop < mTotalDragDistance) {
// 还没达到刷新的距离的时候
if (mScale) {
// 如果设置了小圆圈在滑动的过程中可以缩放,scale慢慢的变大
setAnimationProgress(overscrollTop / mTotalDragDistance);
}
if (mProgress.getAlpha() > STARTING_PROGRESS_ALPHA && !isAnimationRunning(mAlphaStartAnimation)) {
// 其实这里也可以看出来,在没有达到刷新距离的时候,alpha会尽量保持是STARTING_PROGRESS_ALPHA的(相对来说模糊点)
startProgressAlphaStartAnimation();
}
float strokeStart = adjustedPercent * .8f;
// 设置小圆圈里面进度条的开始和结束位置(在还没有达到刷新距离的时候小圆圈里面进度条是慢慢变大的,最多达到80%的圈)
mProgress.setStartEndTrim(0f, Math.min(MAX_PROGRESS_ANGLE, strokeStart));
// 设置mCircleView小圆圈里面进度条箭头的缩放大小(在还没有达到刷新距离的时候小圆圈进度条箭头是慢慢变大的)
mProgress.setArrowScale(Math.min(1f, adjustedPercent));
} else {
// 达到了刷新的距离的时候(注意这个时候小圆圈里面进度条占80%,并且是可见的)
if (mProgress.getAlpha() < MAX_ALPHA && !isAnimationRunning(mAlphaMaxAnimation)) {
// 其实这里也可以看出来,在达到刷新距离的时候,alpha会尽量保持是MAX_ALPHA的(完全显示)
startProgressAlphaMaxAnimation();
}
}
float rotation = (-0.25f + .4f * adjustedPercent + tensionPercent * 2) * .5f;
// 设置小圆圈进度条的旋转角度,在下拉的过程中mCircleView小圆圈是一点一点往前旋转的
mProgress.setProgressRotation(rotation);
// mCircleView会随着手指往下移动
setTargetOffsetTopAndBottom(targetY - mCurrentTargetOffsetTop, true /* requires update */);
}
/**
* mCircleView做缩放操作
*/
private void setAnimationProgress(float progress) {
if (isAlphaUsedForScale()) {
setColorViewAlpha((int) (progress * MAX_ALPHA));
} else {
ViewCompat.setScaleX(mCircleView, progress);
ViewCompat.setScaleY(mCircleView, progress);
}
}
// 启动一个alpha变化的动画,从当前值到STARTING_PROGRESS_ALPHA的变化
private void startProgressAlphaStartAnimation() {
mAlphaStartAnimation = startAlphaAnimation(mProgress.getAlpha(), STARTING_PROGRESS_ALPHA);
}
当然里面涉及到的东西比较都,直接一笔带过了哦。
接下来咱来看看都手指松开的时候调用的finishSpinner()函数。
/**
* 下拉结束的时候调用该函数
* @param overscrollTop: 表示y轴上下拉的距离
*/
private void finishSpinner(float overscrollTop) {
if (overscrollTop > mTotalDragDistance) {
// 下拉结束的时候达到了刷新的距离,这个时候就要告诉上层该进入刷新了
setRefreshing(true, true /* notify */);
} else {
// 下拉结束的时候还没有达到刷新的距离
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) {
// 如果小圆圈没有设置缩放,当会到了初始位置之后scale缩小为0,不可见
startScaleDownAnimation(null);
}
}
@Override
public void onAnimationRepeat(Animation animation) {
}
};
}
// 小圆圈从当前位置返回到初始位置
animateOffsetToStartPosition(mCurrentTargetOffsetTop, listener);
// 小圆圈里面进度条不显示箭头了
mProgress.showArrow(false);
}
}
准备进入刷新状态的时候调用的是setRefreshing()函数。
/**
* 是指是否进入刷新状态
* @param refreshing: 是否进入刷新状态
* @param notify:是否通知上层,SwipeRefreshLayout的时候定义OnRefreshListener的监听
*/
private void setRefreshing(boolean refreshing, final boolean notify) {
if (mRefreshing != refreshing) {
// 当前状态不相同
mNotify = notify;
ensureTarget();
mRefreshing = refreshing;
if (mRefreshing) {
// 进入刷新状态,
animateOffsetToCorrectPosition(mCurrentTargetOffsetTop, mRefreshListener);
} else {
// 进入非刷新状态,直接scale缩小为0了
startScaleDownAnimation(mRefreshListener);
}
}
}
咱还是看进入刷新状态的情况,调用的是animateOffsetToCorrectPosition()函数。两个参数一个是mCurrentTargetOffsetTop:mCircleView的当前top位置,一个是mRefreshListener:动画开始,结束,重复的监听。animateOffsetToCorrectPosition()函数的启动一个动画引导mCircleView到指定的位置,并且在动画结束的时候会进入到刷新的状态OnRefreshListener。动画的具体实现也比较的简单咱就不具体的贴出来了。
ps:分析的比较简单,希望对大家能有一点帮助。