解读Google官方SwipeRefreshLayout控件源码,带你揭秘Android下拉刷新的实现原理

Posted TellH

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了解读Google官方SwipeRefreshLayout控件源码,带你揭秘Android下拉刷新的实现原理相关的知识,希望对你有一定的参考价值。

前言

想必大家也发现,时下的很多App都应用了这个Google出品的SwipeRefreshLayout下拉刷新控件,它以Material Design风格、适用场景广泛,简单易用等特性而独步江湖。但在我们使用的过程中,不可避免地会发现一些bug,或者需要添加某些特性来满足需求。出现这些问题,最好的方法就是解读源码,理解它实现的原理,并且在理解源码的基础上修改源码,达成需求。然而不知为何,至今还没有一篇关于SwipeRefreshLayout源码解析的文章,所以萌发了要写一篇这样的文章。鉴于阅读技术博文的枯燥,加之还是篇源码解析的文章,我不打算一下子扔出来一大段代码让读者去啃,而是一步一步往下走,揭开SwipeRefreshLayout的神秘面纱。

阅读源码的小技巧

为什么源码普遍都很难读,有人甚至谈之色变?其实代码(出自大神之手)生来是易读的,但代码多了就变得难读了。所以阅读源码时,要把握住主干,细枝末节可以暂时忽略,一路下来理解了程序工作流程后再回过头来会有一种豁然开朗的感觉。
阅读源码我还是选择android Studio。这个强大的工具提供了很多快捷键,大大地方便了源码的阅读。

  • Ctrl+F :在当页查找关键字
  • Alt+F7: 查看方法或变量在哪里被使用过
  • Ctrl+Q:查看java doc,如果该方法或变量有的话javadoc的话就可以更快知道该它的相关信息
  • Ctrl+左击:这个不用说了吧,进入方法体或者查看定义或者查看被使用的地方
  • Ctrl+Shift+i:可以不离开当前阅读的位置,查看指定方法的方法体
  • Ctrl+F11:加BookMark,简直是非常有用的功能,不过需要去设置添加一下跳转下一个书签或上一个书签的快捷键才能发挥出该功能真正强大。
  • Ctrl+F12 : 输入关键字快速定位指定的变量或方法,支持模糊搜索。
  • Ctrl +Alt+左箭头或右箭头:返回前一个或下一个光标的位置,在想回溯阅读位置的时候非常有用
  • 关于阅读源码的快捷键就这些吧,以后想到了再补充…

你应该知道:

在看往下看之前,我希望你了解:

  • 事件分发机制
  • ViewGroup的测量绘制过程

准备工作

所幸该控件没有跟系统api耦合,所以可以直接copy一份代码到自己的demo工程中,尽情地改。但是hint会理解报出一些错误。首先包名要改一下,类名最好也改吧,以免混淆~其次把CircleImageView和MaterialProgressDrawable这两个类都copy过来,放在同一个包里。如图:

如果嫌麻烦可以直接fork我的项目

探究之旅

我们朝着未知的黑暗出发。打开SwipeRefreshTestLayout的类文件,看到左边这么小的滑块,其实我一开始是拒绝的~ 感觉无从下手啊有没有… 沉下心来,想想看看它是继承于ViewGroup的,所以想想它一定有两个很关键的方法:onMeasure和onLayout,分别解决了它和它的子View占多大地和搁到哪。因为它是一个下拉刷新控件,它必定要涉及到事件分发的处理,同样是两个关键方法:onInterceptTouchEvent和onTouchEvent,分别用于决定是否拦截点击事件和进行点击事件的处理。天空瞬间亮了许多…

onMeasure

    @Override
    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) 
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (mTarget == null) 
            ensureTarget();
        
        if (mTarget == null) 
            return;
        
        //mTarget的尺寸为match_parent,除去内边距
        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));
        //如果mOriginalOffsetTop未被初始化并且mUsingCustomStart ?,则将下拉小圆的初始位置设置成默认值
        if (!mUsingCustomStart && !mOriginalOffsetCalculated) 
            mOriginalOffsetCalculated = true;
            mCurrentTargetOffsetTop = mOriginalOffsetTop = -mCircleView.getMeasuredHeight();
        
        mCircleViewIndex = -1;
        // Get the index of the circleview.获取circleview的索引值,主要是为了后面重载getChildDrawingOrder时要用
        for (int index = 0; index < getChildCount(); index++) 
            if (getChildAt(index) == mCircleView) 
                mCircleViewIndex = index;
                break;
            
        
    

我们看到,这个方法代码不长,但却很关键。重写该方法的作用是设置子View的尺寸。出现mTarget是什么未知生物?其实就是一个它包裹的子View,通常是ListView等一些可滚动的控件。ensureTarget();保证它非空并存在。如果不小心包裹了多个VIew呢?则mTarget就是其中的最后一个子View。mCircleView又是什么生物呢?顾名思义,下拉的白色小圆圈,一个ImageView而已。mCurrentTargetOffsetTop 和mOriginalOffsetTop 是两个非常关键的变量,分别表示当前mCircleView的位置(top值)和初始时mCircleView的位置(top值),当然它们初始化都等于mCircleView高度的负数。还有一个mUsingCustomStart 是什么呢?我当时也不知道。没关系,Ctrl+F11打个书签,等读完再回头看。或者我们可以通过Alt+F7看看它的在哪里被引用过。

可以看到,它在setProgressViewOffset被赋值为true,而该方法是用于设置CircleView初始的位置和刷新停留的位置,Custom是自定义的意思,所以mUsingCustomStart就是一个标志,表示是否用自定义的起始位置,而默认的起始位置就是CircleView高度的负数。

onLayout

    @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放在覆盖parent的位置(除去内边距)
        child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
        //将mCircleView放在mTarget的平面位置上面居中,初始化时是完全隐藏在屏幕外的
        int circleWidth = mCircleView.getMeasuredWidth();
        int circleHeight = mCircleView.getMeasuredHeight();
        mCircleView.layout((width / 2 - circleWidth / 2), mCurrentTargetOffsetTop,
                (width / 2 + circleWidth / 2), mCurrentTargetOffsetTop + circleHeight);
    

这个方法代码也不长,很简单,但却很关键。作用是安排子View的位置。将mTarget填充整个控件,将mCircleView放在mTarget的平面位置上面居中,初始化时是完全隐藏在屏幕外的。

onInterceptTouchEvent

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) 
        ensureTarget();

        final int action = MotionEventCompat.getActionMasked(ev);

        //如果当mCircleView正在返回初始位置的同时手指按下了,将标志mReturningToStart复位
        if (mReturningToStart && action == MotionEvent.ACTION_DOWN) 
            mReturningToStart = false;
        

        //如果下拉被禁用、mCircleView正在返回初始位置、mTarget没有到达顶部、
        //正在刷新、mNestedScrollInProgress
        // 不拦截,不处理点击事件,处理权交还mTarget
        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;
                //如果是滑动动作,将mIsBeingDragged置为true
                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;
        

        //如果正在被拖拽,拦截该系列的点击事件,并调用自己的onTouchEvent()来处理
        return mIsBeingDragged;
    

这个方法的逻辑非常清晰。当如果下拉被禁用、mCircleView正在返回初始位置、mTarget没有到达顶部、
或者正在刷新时, 不拦截,不处理点击事件,处理权交还mTarget。排除以上情况后,还需要进一步判断。
当手指按下时,记录按下的坐标;在MotionEvent.ACTION_MOVE当中,判断是否是滑动动作,如果是,拦截该系列的点击事件,并调用自己的onTouchEvent()来处理。

onTouchEvent

重头戏来了!这个方法是关键中的关键:

@Override
    public boolean onTouchEvent(MotionEvent ev) 
        final int action = MotionEventCompat.getActionMasked(ev);
        int pointerIndex = -1;

        if (mReturningToStart && action == MotionEvent.ACTION_DOWN) 
            mReturningToStart = false;
        

        //如果被禁用、CircleView正在复位、没到达顶部、mNestedScrollInProgress,直接返回,不处理该事件
        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:
                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) 
                        //spinner可理解为下拉组件,将spinner移到指定的高度
                        //很关键的方法,进入看看
                        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;
                

                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;
    

在case MotionEvent.ACTION_MOVE当中,计算下拉的总高度overscrollTop,DRAG_RATE是下拉阻尼,可以通过改变它的值来改变下拉手感哦~~然后进入到moveSpinner()方法,将spinner移到指定的高度。那么spinner是啥?其实就是下拉组件的意思。

  • moveSpinner
    /**
     * 通过调用setTargetOffsetTopAndBottom()方法移动下拉组件Spinner(mCircleView)
     * 同时更新mProgress(一个drawable)的绘制进度
     * @param overscrollTop 下拉高度
     */
    private void moveSpinner(float overscrollTop) 
        mProgress.showArrow(true);//显示Progressbar的箭头

        //经过一系列的计算,spinner控制下拉的最大距离
        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;

        //计算spinner将要(target)被移动到的位置对应的Y坐标,当targetY为0时,小圆圈刚好全部露出来
        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-else主要是在通过下拉进度,对mProgress在下拉过程设置颜色透明度,箭头旋转角度,缩放大小的控制
        //如果下拉高度小于mTotalDragDistance(一个触发下拉刷新的高度)
        if (overscrollTop < mTotalDragDistance) 
            //如果支持下拉小圆圈缩放,设置颜色透明度和缩放大小
            if (mScale) 
                setAnimationProgress(overscrollTop / mTotalDragDistance);
            
            if (mProgress.getAlpha() > STARTING_PROGRESS_ALPHA
                    && !isAnimationRunning(mAlphaStartAnimation)) 
                // Animate the alpha
                startProgressAlphaStartAnimation();
            
            float strokeStart = adjustedPercent * .8f;
            mProgress.setStartEndTrim(0f, Math.min(MAX_PROGRESS_ANGLE, strokeStart));
            mProgress.setArrowScale(Math.min(1f, adjustedPercent));
         else //下拉高度达到了触发刷新的高度
            if (mProgress.getAlpha() < MAX_ALPHA && !isAnimationRunning(mAlphaMaxAnimation)) 
                // Animate the alpha
                startProgressAlphaMaxAnimation();
            

            ViewCompat.setScaleX(mCircleView, 1f);
            ViewCompat.setScaleY(mCircleView, 1f);
        
        float rotation = (-0.25f + .4f * adjustedPercent + tensionPercent * 2) * .5f;
        mProgress.setProgressRotation(rotation);

        //这是关键调用!动态设置mSpinner的高度。进入该函数看看
        setTargetOffsetTopAndBottom(targetY - mCurrentTargetOffsetTop, true /* requires update */);
    

该方法主要干的事就是通过调用setTargetOffsetTopAndBottom()方法移动下拉组件Spinner(mCircleView),同时更新mProgress(一个drawable)的绘制进度。其中进行了一些复杂的运算,其实就是在控制下拉的最大高度,避免用户无限下拉…说明一下,这个mScale,因为我已经添加了javadoc,读者Ctrl+Q就可以查看它的相关信息。它觉得spinner下拉过程是否支持缩放,可以通过setProgressViewEndTarget()和setProgressViewOffset()设置。但我发现一个bug,如果手指下拉过快,小圆就会来不及放到最大…小圆明显变小了,如图

好,有改bug的希望就有了继续阅读的动力!我们进入setTargetOffsetTopAndBottom()看看。

  • setTargetOffsetAndBottom
    /**
     * 设置mCircleView的偏移量
     * 同时更新mCurrentTargetOffsetTop
     * @param offset 偏移量,可正可负
     * @param requiresUpdate 界面是否需要重绘invalidate();
     */
    private void setTargetOffsetTopAndBottom(int offset, boolean requiresUpdate) 
        //bringToFront()该方法会调用requestLayout()和invalidate()把view放到前面
        //已经重写了getChildDrawingOrder方法,所以没有必要再调用该方法了,我个人认为...
        //可通过手动调用invalidate()来代替它
//        mCircleView.bringToFront();
        mCircleView.offsetTopAndBottom(offset);
        mCurrentTargetOffsetTop = mCircleView.getTop();
        if (requiresUpdate /*&& android.os.Build.VERSION.SDK_INT < 11*/) 
            invalidate();
        
    

该方法很短,却是mCircleView能够下拉的精髓所在啊!offsetTopAndBottom()本质上是调用layout(getLeft(),getTop()+offsetY,getRight(),getBottom()+offsetY);(注意不是onLayout())同时对mCircleView的top和bottom进行偏移,offset是View整体在垂直方向上的偏移量。这里我把bringToFront()注释掉,bringToFront()该方法会调用requestLayout()和invalidate()把view放到前面,因为已经重写了getChildDrawingOrder方法,所以没有必要再调用该方法了,我个人认为…可通过手动调用invalidate()来代替它。

到此,我们已经了解过它下拉的的过程,下面进行回溯到onTouchEvent的case MotionEvent.ACTION_UP:

            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;
            

计算完了松开手时下拉的总距离后,交给方法finishSpinner(overscrollTop);处理。进去看看。

  • finishSpinner(overscrollTop)
    /**
     * 手指松开后,处理下拉组件Spinner
     * 设置开始刷新的动画,或者
     * 将下拉组件Spinner回滚隐藏
     * @param overscrollTop 下拉距离
     */
    private void finishSpinner(float overscrollTop) 
        if (overscrollTop > mTotalDragDistance) //下拉距离达到了可触发刷新的高度
            //关键方法
            setRefreshing(true, true /* notify */);
         else //下拉距离还未达到了可触发刷新的高度,做一些复位的操作
            // cancel refresh
            mRefreshing = false;
            mProgress.setStartEndTrim(0f, 0f);
            //值得关注的是这个回滚动画
            AnimationListener listener = null;
            if (!mScale) 
                listener = new AnimationListener() 
                    @Override
                    public void onAnimationEnd(Animation animation) 
                        if (!mScale) 
                            startScaleDownAnimation(null);
                        
                    
                ;
            
            //开始回滚动画
            //这是一个比较复杂的方法,也是比较有用的方法
            //其实这个本质上不是开启一个动画,而是一个数值产生器
            //通过监听数值变化,
            //从mCurrentTargetOffsetTop这个高度开始,
            //调用setTargetOffsetTopAndBottom()慢慢回滚到mOriginalOffsetTop
            animateOffsetToStartPosition(mCurrentTargetOffsetTop, listener);
            mProgress.showArrow(false);
        
    

这里看到的mTotalDragDistance同样可以通过Ctrl+Q查看它的信息。这个方法只有一对大大的if-else,如果下拉距离达到了可触发刷新的高度,开始刷新;否则开始回滚动画,将Spinner回滚到开始的位置(也就是mOriginalOffsetTop)。而animateOffsetToStartPosition这个方法是一个内涵很丰富的方法,涉及到多步跳转才能了解彻底。大家可以去github fork下来,找到相应方法Ctrl+左击进去看看,里面的方法都添加了详细的注释,相信大家一定能看懂。有朋友可能会问,这里怎么用视图动画而不用属性动画呢?其实这里并不是开启一个真正意义上的动画,而是一个数值产生器,通过监听数值变化,从mCurrentTargetOffsetTop这个高度开始,调用setTargetOffsetTopAndBottom()慢慢回滚到mOriginalOffsetTop。
下面我们一起来看看setRefreshing(true, true /* notify */);

  • setRefreshing(true, true /* notify */)
    /**
     * 设置刷新状态,该方法通常不是由类库使用者来调用,而是在用户下拉的时候由SwipeRefreshLayout来调用
     * @param refreshing 是否刷新
     * @param notify 是否回调onRefresh()
     */
    private void setRefreshing(boolean refreshing, final boolean notify) 
        if (mRefreshing != refreshing) 
            mNotify = notify;
            ensureTarget();
            mRefreshing = refreshing;
            if (mRefreshing) //启动刷新
                animateOffsetToCorrectPosition(mCurrentTargetOffsetTop, mRefreshListener);
             else //停止刷新
                //开始Spinner消失动画
                startScaleDownAnimation(mRefreshListener);
            
        
    

该方法通常不是由类库使用者来调用,而是在用户下拉的时候由SwipeRefreshLayout自己来调用,所以它是private的。如果启动刷新,则调用animateOffsetToCorrectPosition(mCurrentTargetOffsetTop, mRefreshListener);将mCircleView移到正确的高度(也就是mSpinnerFinalOffset),animateOffsetToCorrectPosition()跟上文提到的animateOffsetToStartPosition()方法的实现机理是完全一样的。我们这里回想一下,刚才的bug是由于手指松开时mCircleView的Scale值没有达到1,那么在这里我们就可以在它的移动到刷新位置的动画结束时,把它的Scale手动设置为1。

    private AnimationListener mRefreshListener = new AnimationListener() 
        @Override
        public void onAnimationEnd(Animation animation) 
            if (mRefreshing) 
                //改bug
                ViewCompat.setScaleX(mCircleView, 1f);
                ViewCompat.setScaleY(mCircleView, 1f);
                // Make sure the progress view is fully visible
                mProgress.setAlpha(MAX_ALPHA);
                mProgress.start();
                if (mNotify) 
                    if (mListener != null) 
                        mListener.onRefresh();
                    
                
             else 
                mProgress.stop();
                mCircleView.setVisibility(View.GONE);
                setColorViewAlpha(MAX_ALPHA);
                // Return the circle to its start position
                if (mScale) 
                    setAnimationProgress(0 /* animation complete and view is hidden */);
                 else 
                    setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCurrentTargetOffsetTop,
                            true /* requires update */);
                
            
            mCurrentTargetOffsetTop = mCircleView.getTop();
        
    ;

效果还可吧!

我们发现onRefresh()是在这个被回调的,而且仅在这里被回调。

不知不觉,天亮了~框架脉络已经很清晰了吧。
还有一些变量或方法的名字带有NestedScroll没有提到,其实那是跟嵌套滑动有关的,不知道也不影响源码的阅读。

下面说说我遇到过的一个问题,当我们在Activity的onCreate中

        mRefreshLayout = (SwipeRefreshTestLayout) findViewById(R.id.refresh_widget);
        mRefreshLayout.setRefreshing(true);
  • 延迟调用
        new Handler().postDelayed(new Runnable() 
            @Override
            public void run() 
                mRefreshLayout.setRefreshing(true);
            
        ,100);

或者

        mRefreshLayout.postDelayed(new Runnable() 
            @Override
            public void run() 
                mRefreshLayout.setRefreshing(false);
            
        , 3000);
  • 改在onWindowFocusChanged当中调用
    因为回调该函数时Activity处于可见状态,注意如果在onResume中调用还是会没有效果的。
    @Override
    public void onWindowFocusChanged(boolean hasFocus) 
        super.onWindowFocusChanged(hasFocus);
        mRefreshLayout.setRefreshing(true);
    
  • 既然我们已经是看过源码的人了,能不能在setRefreshing的源码中解决这个问题呢?
    这是我自己的脑洞方案:
    public void setRefreshing(final boolean refreshing) 
        //防止类库使用者在SwipeRefreshLayout还没完全被初始化时调用该方法
        //还是建议使用者重写Activity的onWindowFocusChanged()方法来调用setRefreshing(true);
        if (!isShown()&& refreshing)
            Log.e("SwipeRefreshLayout", "It's not advisable to invoke setRefreshing() when SwipeRefreshLayout is inVisible.");
            new Handler().postDelayed(new Runnable() 
                @Override
                public void run() 
                    setRefreshing(true);
                
            ,50/*该时间可以任意设置*/);
            return;
        
        .....省略若干代码......
     

项目代码已上传至Github。—repo

点star和转发也是一种支持!

如果你发现有什么不清楚或不妥的地方欢迎留言讨论。

以上是关于解读Google官方SwipeRefreshLayout控件源码,带你揭秘Android下拉刷新的实现原理的主要内容,如果未能解决你的问题,请参考以下文章

详细解读DialogFragment

解读Google开发者大会:你真的了解Android吗?

官方全面解读“5G+工业互联网”

解读阿里官方代码规范

FastAPI 官方文档解读 (一)

FastAPI 官方文档解读 (二)