SwipeRefreshLayout源码解析,体验谷歌原生的艺术

前言

相信SwipeRefreshLayout对大家都不陌生,SwipeRefreshLayout是谷歌官方推出的下拉刷新控件,因为简洁,美观,使用起来简单(相信都有被第三方坑过的小码农,个人是比较偏向使用谷歌原生的东西,现在谷歌推出的东西,至少bug少,且也不失美观),深受各位开发人员的喜爱(掘金app也在使用)。

OK,下面进入正题。

SwipeRefreshLayout一般内部都是嵌套着RecyclerView,ListView,ScrollView 等一些可滚动的view,所以SwipeRefreshLayout无疑是继承了ViewGroup,OK,先让我们看看它的构造方法

public SwipeRefreshLayout(Context context, AttributeSet attrs) {
    super(context, attrs);

   //....此部分代码省略,初始化了一些参数及设置(包括获得滑动多少像素才能滑动view的阈值,动画时长,背景宽高等)
  
  
    createProgressView();
  

    mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);

    mNestedScrollingChildHelper = new NestedScrollingChildHelper(this);

}

我们可以先看NestedScrollingParentHelper,NestedScrollingChildHelper这两个类,是Android在support.v4包中为大家提供的两个嵌套滚动帮助类,这里不深究,要是想了解的童鞋们,可以看下这篇洋神写的Android NestedScrolling机制完全解析 带你玩转嵌套滑动,OK,那我们继续往下看,createProgressView(),从名字上看大概就知道是创建了那个转啊转刷新的View,那我们点进去,看下创建了什么。

private void createProgressView() {
    mCircleView = new CircleImageView(getContext(), CIRCLE_BG_LIGHT, CIRCLE_DIAMETER/2);
    mProgress = new MaterialProgressDrawable(getContext(), this);
    mProgress.setBackgroundColor(CIRCLE_BG_LIGHT);
    mCircleView.setImageDrawable(mProgress);
    mCircleView.setVisibility(View.GONE);
    addView(mCircleView);
}

咦,我们这里可以看到创建了两个对象,CircleImageViewMaterialProgressDrawable,那这两个到底是什么呢,其实CircleImageView就是刷新的自定义view,它继承了ImageView,里面利用OvalShape绘制了一个圆形,然后设置成了CircleImageView的背景,那指示器的去哪了呢,看上面的代码,mCircleView.setImageDrawable(mProgress),它将MaterialProgressDrawable以Drawable的形式设置给了CircleImageView,那MaterialProgressDrawable又是何方神圣,其实它就是这个进度条的样式,继承于Drawable,也是绘制出来的,那我们看下它的draw()到底做了什么

public void draw(Canvas c) {
    final Rect bounds = getBounds();
    final int saveCount = c.save();
    c.rotate(mRotation, bounds.exactCenterX(), bounds.exactCenterY());
    mRing.draw(c, bounds);
    c.restoreToCount(saveCount);
}

这里保存了画布的状态,让画布rotate,而且还把画布传给了mRing,mRing是MaterialProgressDrawable的内部类Ring,进度圈其实就是由Ring绘制的,代码如下

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);
    c.drawArc(arcBounds, startAngle, sweepAngle, false, mPaint);

    drawTriangle(c, startAngle, sweepAngle, bounds);

    if (mAlpha < 255) {
        mCirclePaint.setColor(mBackgroundColor);
        mCirclePaint.setAlpha(255 - mAlpha);
        c.drawCircle(bounds.exactCenterX(), bounds.exactCenterY(), bounds.width() / 2,
                mCirclePaint);
    }
}

它绘制了箭头和进度圈,还有中间部分的背景圆,OK,如何创建ProgressView大概就是这样了,接下来看它是如何监听手势的,根据事件分发的机制,触摸事件应该是先传递到ViewGroup,根据onInterceptTouchEvent的返回值决定是否拦截事件的,那么就onInterceptTouchEvent出发:

public boolean onInterceptTouchEvent(MotionEvent ev) {
    
    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:
   
            final float y = getMotionEventY(ev, mActivePointerId);
          
            final float yDiff = y - mInitialDownY;
            if (yDiff > mTouchSlop && !mIsBeingDragged) {
                mInitialMotionY = mInitialDownY + mTouchSlop;
                mIsBeingDragged = true;
                mProgress.setAlpha(STARTING_PROGRESS_ALPHA);
            }
            break;

  
    return mIsBeingDragged;
}


这里我省略了一部分代码,这里其实做了不少处理,但是为了简洁,看的舒服,我把大部分return false的都给剔除掉了,我们主要看他如何出现下拉的,所以只看它拦截部分和关键部分,这里ACTION_DOWN时记录了initialDownY,也就是落指的Y坐标,接着看ACTION_MOVE,这里我也省了一部分代码,主要看mIsBeingDragged返回true的条件,当手指滑动的距离大于要滑动View的阈值(也就是要想使view滚动的条件,必须滑动了多少距离的意思),那么就会return ture,拦截后时间就是自己处理,那接下来就是看onTouchEvent

public boolean onTouchEvent(MotionEvent ev) {
    switch (action) {
        
        case MotionEvent.ACTION_MOVE: {
            pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
            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);
            final float y = MotionEventCompat.getY(ev, pointerIndex);
            final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
            mIsBeingDragged = false;
            finishSpinner(overscrollTop);
            mActivePointerId = INVALID_POINTER;
            return false;
        }
    }

    return true;
}

同样,看关键部分,ACTION_MOVE先计算出手指移动距离,然后大于0的话,就调用moveSpinner(overscrollTop),而ACTION_UP一开始代码逻辑一样,然后调用了finishSpinner(overscrollTop),看来这两句就应该是主要的关键部分,让我们先来看下moveSpinner方法

private void moveSpinner(float overscrollTop) {
    mProgress.showArrow(true);
    //...省略了计算 移动距离的逻辑代码
    int targetY = mOriginalOffsetTop + (int) ((slingshotDist * dragPercent) + extraMove);
    // where 1.0f is a full circle
    if (mCircleView.getVisibility() != View.VISIBLE) {
        mCircleView.setVisibility(View.VISIBLE);
    }

    //...省略了进入动画的逻辑代码

    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 */);
}

这里主要根据你的移动距离,然后让其进度圈view显示,其中了还有比较多的细节(如控制移动速度,超过刷新距离后移动变慢)。倒数第二行执行了setProgressRotation,传入的是经过一堆计算后的rotation,传入该方法后,mProgress也就是MaterialProgressDrawable就根据它来绘制进度圈。

最后一行执行setTargetOffsetTopAndBottom,里面的代码比较简单,就是设置进度圈的位置

接着我们来看下ACTION_UP里的finishSpinner(overscrollTop)

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);
    }
}


代码比较简单,我就全贴了,
如果移动的距离没有大于设定值(如果有印象的童鞋应该会记得如果滑动一点点,那进度圈就会收回去),执行startScaleDownAnimation,也就是消失的动画,

如果移动的距离大于设定值,就执行setRefreshing(true,true),该方法会调用animateOffsetToCorrectPosition,代码如下

private void setRefreshing(boolean refreshing, final boolean notify) {
    if (mRefreshing != refreshing) {
        mNotify = notify;
        ensureTarget();
        mRefreshing = refreshing;
        if (mRefreshing) {
            animateOffsetToCorrectPosition(mCurrentTargetOffsetTop, mRefreshListener);
        } else {
            startScaleDownAnimation(mRefreshListener);
        }
    }
}

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);
}


animateOffsetToCorrectPosition()的方法也是全贴的了,对mCircleView(CircleImageView)的一些属性重置,然后调用mCircleView.startAnimation(),如果我们有传入过listener的话,就会执行onRefresh方法,刷新完再setRefreshing(false),就会再执行startScaleDownAnimation()收回去了。

到这里,相信大家也会比较清楚SwipeRefreshLayout的流程了, 希望看官老爷们喜欢,也希望我们大家在使用的同时,更能了解源码,知道如果出现问题该如何解决,如有写的不好的地方,还请见谅、指点,


更多精彩文章请关注微信公众号"Android经验分享":这里将长期为您分享Android高手经验、中外开源项目、源码解析、框架设计和Android好文推荐!QQ交流群:Android经验分享一区 386067289

SwipeRefreshLayout源码解析,体验谷歌原生的艺术_第1张图片
二维码

你可能感兴趣的:(SwipeRefreshLayout源码解析,体验谷歌原生的艺术)