咱们在做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测量函数

    @Overrideprotected 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();// 画阴影的paintmShadowPaint = 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);}@Overridepublic 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()函数了。

    @Overridepublic 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 trianglemArrowPaint.setColor(mCurrentColor);c.rotate(startAngle + sweepAngle - ARROW_OFFSET_ANGLE, bounds.exactCenterX(), bounds.exactCenterY());c.drawPath(mArrow, mArrowPaint);}}

画圆环,画圆环上面的三角形箭头有了吧。到现在图形是有了,但是啥时候开始转圈啥时候停止转圈动画呢。看MaterialProgressDrawable的start()和stop()函数,对应的就是开始结束转圈的动画。看看start()里面到底做的是写啥。

    // MaterialProgressDrawable释放的时候开始转圈动画,没转一圈换一个颜色@Overridepublic 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() {@Overridepublic 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() {@Overridepublic void onAnimationStart(Animation animation) {mRotationCount = 0;}@Overridepublic void onAnimationEnd(Animation animation) {// do nothing}@Overridepublic 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);// 一次动画要转的rotationfloat 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 Viewif (mTarget == null) {for (int i = 0; i < getChildCount(); i++) {View child = getChildAt(i);if (!child.equals(mCircleView)) {mTarget = child;break;}}}}@Overridepublic 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的indexmCircleViewIndex = -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中控件的布局规则

    @Overrideprotected 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);}}@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {ensureTarget();final int action = MotionEventCompat.getActionMasked(ev);// mReturningToStart好像没啥作用,一直是falseif (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对具体的事件都在这个函数里面了。

    @Overridepublic 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都设置为1ViewCompat.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() {@Overridepublic void onAnimationStart(Animation animation) {}@Overridepublic void onAnimationEnd(Animation animation) {if (!mScale) {// 如果小圆圈没有设置缩放,当会到了初始位置之后scale缩小为0,不可见startScaleDownAnimation(null);}}@Overridepublic 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:分析的比较简单,希望对大家能有一点帮助。

Android SwipeRefreshLayout下拉刷新控件源码简单分析相关推荐

  1. Android 解决下拉刷新控件和ScrollVIew的滑动冲突问题。

    最近项目要实现ScrollView中嵌套广告轮播图+RecyleView卡片布局,并且RecyleView按照header和内容的排列样式,因为RecyleView的可扩展性很强,所以我毫无疑问的选择 ...

  2. Android SwipeRefreshLayout 官方下拉刷新控件介绍

    转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/24521483 下面App基本都有下拉刷新的功能,以前基本都使用XListView ...

  3. android webview 下拉刷新页面,Android 下拉刷新控件SwipeRefreshLayout结合WebView使用

    SwipeRefreshLayout 是谷歌官方下拉刷新控件,4.0如下的版本须要用到 android-support-v4.jar包才能用到html android-support-v4.jar 包 ...

  4. android google 下拉刷新 csdn,android SwipeRefreshLayout google官方下拉刷新控件

    下拉刷新功能之前一直使用的是XlistView很方便我前面的博客有介绍 SwipeRefreshLayout是google官方推出的下拉刷新控件使用方法也比较简单 今天就来使用下SwipeRefres ...

  5. android 下拉刷新 组件,android系统自带下拉刷新控件的实现

    android系统自带的下拉刷新控件SwipeRefreshLayout位于android.support.v4.widget包下,实现步骤如下: 1.在布局文件中添加该控件,该控件一般作为父控件,而 ...

  6. android多个下拉控件,Android实现支持所有View的通用的下拉刷新控件

    下拉刷新对于一个app来说是必不可少的一个功能,在早期大多数使用的是chrisbanes的PullToRefresh,或是修改自该框架的其他库.而到现在已经有了更多的选择,github上还是有很多体验 ...

  7. Android 怎么实现支持所有View的通用的下拉刷新控件

    转载请标明出处: http://blog.csdn.net/u010386612/article/details/51372696 本文出自:[AItsuki的博客] 下拉刷新对于一个app来说是必不 ...

  8. android禁止下拉刷新,Android开发之无痕过渡下拉刷新控件的实现思路详解

    相信大家已经对下拉刷新熟悉得不能再熟悉了,市面上的下拉刷新琳琅满目,然而有很多在我看来略有缺陷,接下来我将说明一下存在的缺陷问题,然后提供一种思路来解决这一缺陷,废话不多说!往下看嘞! 1.市面一些下 ...

  9. Android自定义控件实战——下拉刷新控件终结者:PullToRefreshLayout

    说到下拉刷新控件,网上版本有很多,很多软件也都有下拉刷新功能.有一个叫XListView的,我看别人用过,没看过是咋实现的,看这名字估计是继承自ListView修改的,不过效果看起来挺丑的,也没什么扩 ...

最新文章

  1. 条形码?二维码?生成、解析都在这里!
  2. js中Array数组中的常用方法汇总
  3. html画线需要适应不同屏幕,hr标签不止创建html水平线也可以画圆噢
  4. java inflaterinputstream_java.util.zip.InflaterInputStream.available()方法示例
  5. phpcms如何给已有的模块添加新功能?
  6. 2019matlab中的floyd,基于matlab的floyd算法详解
  7. 周期均方根和有效值的区别_买羊肉,“羔羊肉”和“羊肉”有啥区别?口感差别大,别再乱买了...
  8. three.js 笑脸雨
  9. 【并查集】NOI2015 洛谷 P1955 程序自动分析
  10. A2Billing 代码分析
  11. Redis ZADD命令
  12. linux 进程 ksoftirqd/n 占用cpu 100%
  13. java执行shell命令,chmod 777 xxx,改变权限无效的解决办法。
  14. oracle 误删 log文件,Redo log文件被删除恢复
  15. 计算机操作系统之进程与线程
  16. 电影赏析 001《全民目击》
  17. Boost 入门02(字符串操作)
  18. 寄存器某一位置位或者清零
  19. 不足一年下跌830 麒麟970+128G 2400万像素手机加速清仓!
  20. 游戏制作入门小知识------3ds Max

热门文章

  1. 自我认知是智能最高维度的智力活动
  2. CleanMyMac序列号秘钥下载安装教程
  3. dogpile.cache-用于会话和缓存的WSGI中间件(beaker的下一代从产品)
  4. python 当前时间字符串,Python常用时间操作总结【取得当前时间、时间函数、应用等】...
  5. Sql Server 中 Order by排序(升序,降序)
  6. 计算机的领域展望,计算机应用基础领域展望
  7. Nginx高级之Rewrite规则
  8. ShaderToy(四)画更好的笑脸
  9. ClickHouse特性及底层存储原理
  10. authentication with the app store