SwipeRefreshLayout源码解析

SwipeRefreshLayout源码解析

  • SwipeRefreshLayout源码解析
    • SwipeRefreshLayout简介
    • 简单应用
    • 源码解析
      • 构造器
      • onMeasure方法
      • onLayout方法
      • onDraw方法
      • onInterceptTouchEvent方法
      • onTouchEvent方法
    • 总结

SwipeRefreshLayout简介

SwipeRefreshLayout是Android support v4库中的一个控件,用于让开发者快速实现下拉刷新效果。

简单应用

下面是SwipeRefreshLayout的一个简单的应用示例,效果如下:
SwipeRefreshLayout源码解析_第1张图片
示例代码包含两个布局文件以及MainActivity。
activity_main.xml:


<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.swt369.swiperefreshlayouttest.MainActivity">

    <android.support.v4.widget.SwipeRefreshLayout
        android:id="@+id/swipe_refresh_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginLeft="10dp"
        android:layout_marginStart="10dp"
        android:layout_marginTop="5dp"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:layout_alignParentTop="true">

        <ListView
            android:id="@+id/list_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent">

        ListView>

    android.support.v4.widget.SwipeRefreshLayout>
RelativeLayout>

list_item:


<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

TextView>

MainActivity.java:

package com.example.swt369.swiperefreshlayouttest;

import android.os.AsyncTask;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.ArrayAdapter;
import android.widget.ListView;

import java.util.LinkedList;
import java.util.List;

public class MainActivity extends AppCompatActivity {
    private List mList;
    private ArrayAdapter mAdapter;
    private SwipeRefreshLayout refreshLayout;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        refreshLayout = (SwipeRefreshLayout)findViewById(R.id.swipe_refresh_layout);
        ListView listView = (ListView)findViewById(R.id.list_view);

        mList = new LinkedList<>();
        int count = 5;
        while(count-- > 0){
            mList.add(String.valueOf(mList.size()));
        }
        mAdapter = new ArrayAdapter<>(this,R.layout.list_item,mList);
        listView.setAdapter(mAdapter);

        refreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
            @Override
            public void onRefresh() {
                refreshOperation();
            }
        });
    }

    private void refreshOperation(){
        new RefreshTask().execute();
    }

    private class RefreshTask extends AsyncTask<Void,Void,Void>{

        @Override
        protected Void doInBackground(Void... params) {
            try {
                int count = 3;
                while(count-- > 0){
                    mList.add(String.valueOf(mList.size()));
                }
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            return null;
        }

        @Override
        protected void onPostExecute(Void aVoid) {
            super.onPostExecute(aVoid);
            mAdapter.notifyDataSetChanged();
            refreshLayout.setRefreshing(false);
        }
    }
}

总结一下的话,SwipeRefreshLayout的使用分以下几个步骤:
1. 在布局文件中添加SwipeRefreshLayout,并将需要实现下拉刷新的目标控件置于其中。
2. 利用findViewById获取SwipeRefreshLayout与目标控件的引用。
3. 通过SwipeRefreshLayout.setOnRefreshListener()方法为SwipeRefreshLayout提供刷新逻辑。刷新逻辑的结尾要调用SwipeRefreshLayout的setRefreshing(false)方法。

源码解析

下面以源码为基础,对SwipeRefreshLayout的实现做一个解析。

构造器

    public SwipeRefreshLayout(Context context) {
        this(context, null);
    }

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

        //利用ViewConfiguration获取滑动阈值(即判定为滑动所需的距离)
        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();

        //动画持续时间
        mMediumAnimationDuration = getResources().getInteger(
                android.R.integer.config_mediumAnimTime);

        //允许该ViewGroup绘制自身
        setWillNotDraw(false);

        //动画插值器
        mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR);

        //根据设备分辨率确定下拉球的直径
        final DisplayMetrics metrics = getResources().getDisplayMetrics();
        mCircleDiameter = (int) (CIRCLE_DIAMETER * metrics.density);

        //***(1)***
        createProgressView();

        //***(2)***
        ViewCompat.setChildrenDrawingOrderEnabled(this, true);

        //确定正在刷新时下拉球相对View顶部的距离,也就是启动下拉刷新的最小下拉距离
        mSpinnerOffsetEnd = (int) (DEFAULT_CIRCLE_TARGET * metrics.density);
        mTotalDragDistance = mSpinnerOffsetEnd;

        //辅助实现嵌套滑动
        mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
        mNestedScrollingChildHelper = new NestedScrollingChildHelper(this);
        setNestedScrollingEnabled(true);

        //***(3)***
        mOriginalOffsetTop = mCurrentTargetOffsetTop = -mCircleDiameter;
        moveToStart(1.0f);

        //获取唯一的一个自定义属性enabled并设置
        final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS);
        setEnabled(a.getBoolean(0, true));
        a.recycle();
    }

简单的部分直接用注释说明了,下面是几个比较重要的部分的解析:
(1)createProgressView()源码如下:

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

这里创建了示例中提示下拉刷新的那个下拉球,并使用addView()方法将其加入到了View树中。用了两个包级私有的类CircleImageView(继承自ImageView)与MaterialProgressDrawable(继承自Drawable,并实现了Animatable)。mCircleView是球的背景,mProgress是球里面那个环形箭头。
SwipeRefreshLayout还提供了设置它们的颜色的方法:
- setColorSchemeColors(int… colors):设置箭头的颜色。colors数组0位置是初始颜色,后面的用于实现刷新过程中箭头颜色的渐变。
- setProgressBackgroundColorSchemeColor(int color):设置背景色。

(2)setChildrenDrawingOrderEnabled(boolean enabled)是ViewGroup的方法,用于启用/关闭子View绘制顺序调整。实际调整顺序的方法是getChildDrawingOrder(int childCount, int i),同样是ViewGroup的方法。既然用到了setChildrenDrawingOrderEnabled()方法,SwipeRefreshLayout自然也实现了getChildDrawingOrder():

    @Override
    protected int getChildDrawingOrder(int childCount, int i) {
        if (mCircleViewIndex < 0) {
            return i;
        } else if (i == childCount - 1) {
            // Draw the selected child last
            return mCircleViewIndex;
        } else if (i >= mCircleViewIndex) {
            // Move the children after the selected child earlier one
            return i + 1;
        } else {
            // Keep the children before the selected child the same
            return i;
        }
    }

该方法会在ViewGroup循环绘制各个子view的时候被调用,参数i代表当前迭代轮次,返回值代表该轮将被绘制的子view的索引值。方法的逻辑大致如下:
1. 如果下拉球的index小于0则直接返回i,不进行调整;
2. 如果是最后一轮,则绘制下拉球;
3. 对于索引值小于下拉球的子view,按照原顺序绘制。
4. 对于索引值大于下拉球的子view,将它们提前一轮绘制(第i轮就绘制索引值为i+1的的子view)。

方法的意图很明显,就是要保证下拉球是最后一个绘制的,从而使得它不会被其他子View挡住。
(3)首先,两个常量mOriginalOffsetTop与mCurrentTargetOffsetTop的意义分别为下拉球空闲位置的y坐标值以及当前所处的位置的y坐标值(顶部)。
moveToStart()源码:

    void moveToStart(float interpolatedTime) {
        int targetTop = 0;
        targetTop = (mFrom + (int) ((mOriginalOffsetTop - mFrom) * interpolatedTime));
        int offset = targetTop - mCircleView.getTop();
        setTargetOffsetTopAndBottom(offset);
    }

setTargetOffsetTopAndBottom()源码:

    void setTargetOffsetTopAndBottom(int offset) {
        //确保下拉球在所有子view顶部
        mCircleView.bringToFront();
        //根据offset的值竖直移动下拉球
        ViewCompat.offsetTopAndBottom(mCircleView, offset);
        mCurrentTargetOffsetTop = mCircleView.getTop();
    }

可以看出,setTargetOffsetTopAndBottom()是实际移动下拉球的方法,方法中还更新了mCurrentTargetOffsetTop,即下拉球当前位置的值。moveToStart()这个方法是用来将下拉球移动到空闲位置的。至于参数interpolatedTime是为了配合动画使用,取值在0~1之间,代表动画完成的百分比。传入1.0f代表到达结束位置,即让下拉球回到空闲位置。mFrom代表动画开始时下拉球所在的位置,同样是配合动画使用的,用来计算单位时间的位移量。整个动画需要的位移量即为mOriginalOffsetTop - mFrom。

onMeasure()方法

    @Override
    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //以默认方式测量自身尺寸
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        //确保目标view(需要实现下拉刷新的view)存在
        if (mTarget == null) {
            ensureTarget();
        }
        if (mTarget == null) {
            return;
        }

        //测量目标view的尺寸
        mTarget.measure(MeasureSpec.makeMeasureSpec(
                getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
                MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(
                getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY));

       //测量下拉球的尺寸,宽高均为mCircleDiameter,即直径长
mCircleView.measure(MeasureSpec.makeMeasureSpec(mCircleDiameter, MeasureSpec.EXACTLY),
                MeasureSpec.makeMeasureSpec(mCircleDiameter, MeasureSpec.EXACTLY));

        //确定下拉球的view索引值,-1代表未找到
        mCircleViewIndex = -1;
        for (int index = 0; index < getChildCount(); index++) {
            if (getChildAt(index) == mCircleView) {
                mCircleViewIndex = index;
                break;
            }
        }
    }

重点看一下ensureTarget()方法:

    private void ensureTarget() {
        if (mTarget == null) {
            for (int i = 0; i < getChildCount(); i++) {
                View child = getChildAt(i);
                if (!child.equals(mCircleView)) {
                    mTarget = child;
                    break;
                }
            }
        }
    }

可以看出,mTarget保存的就是添加到SwipeRefreshLayout中,需要实现下拉刷新的目标view。方法的逻辑很简单粗暴,直接将不是下拉球的子view认作是目标view。由此可得出结论:使用SwipeRefreshLayout时,应只为它添加一个子view,否则可能导致出错。

onLayout()方法

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        final int width = getMeasuredWidth();
        final int height = getMeasuredHeight();

        //确保目标view(需要实现下拉刷新的view)存在
        if (getChildCount() == 0) {
            return;
        }
        if (mTarget == null) {
            ensureTarget();
        }
        if (mTarget == null) {
            return;
        }

        //布置目标view
        final View child = mTarget;
        final int childLeft = getPaddingLeft();
        final int childTop = getPaddingTop();
        final int childWidth = width - getPaddingLeft() - getPaddingRight();
        final int childHeight = height - getPaddingTop() - getPaddingBottom();
        child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
        int circleWidth = mCircleView.getMeasuredWidth();
        int circleHeight = mCircleView.getMeasuredHeight();

        //布置下拉球的初始位置
        mCircleView.layout((width / 2 - circleWidth / 2), mCurrentTargetOffsetTop,
                (width / 2 + circleWidth / 2), mCurrentTargetOffsetTop + circleHeight);
    }

由于SwipeRefreshLayout仅仅是为给子view附加一个下拉刷新功能而设计的,除了将下拉球放置在中央之外,它的onLayout()方法没做什么特别的事。

onDraw()方法

SwipeRefreshLayout没有覆盖onDraw()方法,显然也没这个必要。

onInterceptTouchEvent()方法

onInterceptTouchEvent()方法是用来判断是否要将触摸事件拦截,交由该ViewGroup处理的方法。为实现下拉刷新的效果,该方法需要做两件事:
- 当目标view未滑动到顶部时,不对滑动事件进行拦截,将其交付给目标view处理;
- 当目标view滑动到顶部,且用户正在进行下拉时,对滑动事件进行拦截,将其交付给自己的事件处理逻辑进行处理。
onInterceptTouchEvent()源码:

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

        final int action = ev.getActionMasked();
        int pointerIndex;

        //如果下拉拉球正在返回途中,并且当前事件是ACTION_DOWN,那么就清除正在返回状态
        if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
            mReturningToStart = false;
        }

        //***(1)***
        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());
                mActivePointerId = ev.getPointerId(0);
                //按下时清除正在下拉状态
                mIsBeingDragged = false;

                pointerIndex = ev.findPointerIndex(mActivePointerId);
                if (pointerIndex < 0) {
                    return false;
                }
                //记录了手指按下的位置的y坐标
                mInitialDownY = ev.getY(pointerIndex);
                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;
                }

                pointerIndex = ev.findPointerIndex(mActivePointerId);
                if (pointerIndex < 0) {
                    return false;
                }
                //***(2)***
                final float y = ev.getY(pointerIndex);
                startDragging(y);
                break;

            case MotionEvent.ACTION_POINTER_UP:
                onSecondaryPointerUp(ev);
                break;

            //***(3)***
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                //
                mIsBeingDragged = false;
                mActivePointerId = INVALID_POINTER;
                break;
        }

        //***(4)***
        return mIsBeingDragged;
    }

注:方法中关于pointerIndex、pointerId的代码都是多点触控相关的,不作为本文的重点。

(1)这里的条件语句是判断SwipeRefreshLayout当前是否处于一个无法进行下拉刷新的状态,如果是的话直接返回false,即不进行事件拦截。判断的几个依据如下:
1. isEnabled():判断SwipeRefreshLayout是否可用;
2. mReturningToStart:下拉球是否正在返回原位的途中;
3. canChildScrollUp(),看下面;
4. mRefreshing:是否正在进行刷新;
5. mNestedScrollInProgress:是否正在进行嵌套滑动。

重点看一下canChildScrollUp()方法:

    /**
     * @return Whether it is possible for the child view of this layout to
     *         scroll up. Override this if the child view is a custom view.
     */
    public boolean canChildScrollUp() {
        if (mChildScrollUpCallback != null) {
            return mChildScrollUpCallback.canChildScrollUp(this, mTarget);
        }
        return ViewCompat.canScrollVertically(mTarget, -1);
    }

官方注释:返回值代表这个布局的子view是否能够进行上滑。如果子view是一个自定义view,那么应当覆盖这个方法。
简单来说,这个方法就是判断当前子view有没有滑到顶。之所以要对这个进行判断,是因为只有子view已经滑到顶的时候才可能进行下拉刷新。
方法中有一个mChildScrollUpCallback,它是SwipeRefreshLayout的一个嵌套的回调接口:

    /**
     * Classes that wish to override {@link SwipeRefreshLayout#canChildScrollUp()} method
     * behavior should implement this interface.
     */
    public interface OnChildScrollUpCallback {
        boolean canChildScrollUp(SwipeRefreshLayout parent, @Nullable View child);
    }

官方注释:想要覆盖SwipeRefreshLayout的canChildScrollUp()方法的类应当实现这个接口。
这个接口的意义是,如果需要添加下拉刷新功能的目标view是一个自定义view,默认的方法可能无法正确判断它当前是否能够上滑,因此就需要开发者提供一个判断的方法。SwipeRefreshLayout提供了setOnChildScrollUpCallback()用来设置该回调接口。
(2)

    final float y = ev.getY(pointerIndex);
    startDragging(y);

首先y中保存了当前触摸位置的y坐标。下面看看startDragging()方法:

    private void startDragging(float y) {
        final float yDiff = y - mInitialDownY;
        if (yDiff > mTouchSlop && !mIsBeingDragged) {
            mInitialMotionY = mInitialDownY + mTouchSlop;
            mIsBeingDragged = true;
            mProgress.setAlpha(STARTING_PROGRESS_ALPHA);
        }
    }

从方法名中就可以判断,该方法是用来启动下拉的。方法进行了两步判断:
首先根据当前y坐标和按下时的y坐标计算出一个差值yDiff(下拉时必然大于0),如果大于滑动阈值则认为用户进行了滑动。之后判断mIsBeingDragged的值,如果为false则当前还未开始下拉。这两个条件都满足之后,就将mIsBeingDragged设置为true,并记录下下拉开始时触摸点的y坐标。
(3)如果触发ACTION_UP(手指抬起)或是ACTION_CANCEL(一般是手指滑到了当前控件的范围之外),那么就将mIsBeingDragged设置为false。
(4)直接将mIsBeingDragged作为返回值返回。意思是,如果下拉已经开始,那么就拦截触摸事件。
总结:
1. 首先判断当前是否有可能进行下拉刷新,若不可能则直接返回false;
2. 手指按下时清除下拉状态,保证了ACTION_DOWN时一定不拦截。
3. 手指滑动时根据滑动距离判断用户的手势是否是下拉,是的话设置下拉状态为true;
4. 手指抬起时清除下拉状态;
5. 根据下拉是否已经开始决定要不要拦截触摸事件。

onTouchEvent()方法

源码如下(删除了部分多点触控相关代码)

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

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

        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:
                mActivePointerId = ev.getPointerId(0);
                mIsBeingDragged = false;
                break;

            case MotionEvent.ACTION_MOVE: {
                pointerIndex = ev.findPointerIndex(mActivePointerId);
                if (pointerIndex < 0) {
                    Log.e(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id.");
                    return false;
                }

                final float y = ev.getY(pointerIndex);
                startDragging(y);

                //***(1)***
                if (mIsBeingDragged) {
                    final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
                    if (overscrollTop > 0) {
                        moveSpinner(overscrollTop);
                    } else {
                        return false;
                    }
                }
                break;
            }

            case MotionEvent.ACTION_UP: {
                pointerIndex = ev.findPointerIndex(mActivePointerId);
                if (pointerIndex < 0) {
                    Log.e(LOG_TAG, "Got ACTION_UP event but don't have an active pointer id.");
                    return false;
                }

                //***(2)***
                if (mIsBeingDragged) {
                    final float y = ev.getY(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()的很多代码和onInterceptTouchEvent()相同,这是因为有的子view可能根本不响应触摸事件。当没有找到能处理触摸事件的子view时,ViewGroup会跳过onInterceptTouchEvent()的判断,直接拦截下所有后续的触摸事件。此时,实现下拉刷新的全部职责就交付给了onTouchEvent()。
下面重点看看onTouchEvent()与onInterceptTouchEvent()不同的地方:
(1)ACTION_MOVE中:

    if (mIsBeingDragged) {
        final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
        if (overscrollTop > 0) {
            moveSpinner(overscrollTop);
        } else {
            return false;
        }
    }

overscrollTop的值是用滑动距离(当前位置 - 起始位置)乘上了一个系数DRAG_RATE,默认是0.5f。这是为了通过让下拉球移动的距离小于手指滑动的距离来产生拉力感。moveSpinner()实现了让下拉球以动画的形式移动到指定位置的功能,这里就不贴代码了。
(2)ACTION_UP中:

    if (mIsBeingDragged) {
        final float y = ev.getY(pointerIndex);
        final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
        mIsBeingDragged = false;
        finishSpinner(overscrollTop);
    }

和上面的区别在于把moveSpinner()换成了finishSpinner()。结合下拉刷新的实际效果,可以猜的到finishSpinner()实现的功能:
1. 首先判断下拉距离是否足够,不够则将下拉球返回原位;
2. 如果下拉距离足够,那么把下拉球放置到刷新时的那个位置上,并启用刷新逻辑。
3. 刷新完毕后将下拉球返回原位。

看看源码:

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

代码比较长,但是逻辑很简单:最外层的if-else将传入的参数overscrollTop与mTotalDragDistance(启动下拉刷新的最小下拉距离)进行比较,如果超过的话就调用setRefreshing()启动刷新,否则让下拉球返回原位。这里就不纠结动画实现的细节部分了。看看setRefreshing()方法:

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

这里传入的两个参数都是true,因此如果当前还未进入刷新状态,就会设置刷新状态为true,并调用animateOffsetToCorrectPosition()方法。这个方法是将下拉球以动画的形式放置到刷新状态的那个位置上。注意方法还传入了一个mRefreshListener,它的源码如下:

    private Animation.AnimationListener mRefreshListener = new Animation.AnimationListener() {
        @Override
        public void onAnimationStart(Animation animation) {
        }

        @Override
        public void onAnimationRepeat(Animation animation) {
        }

        @SuppressLint("NewApi")
        @Override
        public void onAnimationEnd(Animation animation) {
            if (mRefreshing) {
                // Make sure the progress view is fully visible
                mProgress.setAlpha(MAX_ALPHA);
                mProgress.start();
                if (mNotify) {
                    if (mListener != null) {
                        mListener.onRefresh();
                    }
                }
                mCurrentTargetOffsetTop = mCircleView.getTop();
            } else {
                reset();
            }
        }
    };

发现是一个动画监听器。在onAnimationEnd中,mListener.onRefresh()被调用了。mListener就是用setOnRefreshListener设置的监听器,开发者需要将实际的刷新逻辑置于其中。
在开头的示例程序中还提到了一点:在刷新逻辑结束时应手动调用调用SwipeRefreshLayout的setRefreshing(false)方法。这是setRefreshing()的一个重载版本,作为API开放:

    public void setRefreshing(boolean refreshing) {
        if (refreshing && mRefreshing != refreshing) {
            // scale and show
            mRefreshing = refreshing;
            int endTarget = 0;
            if (!mUsingCustomStart) {
                endTarget = mSpinnerOffsetEnd + mOriginalOffsetTop;
            } else {
                endTarget = mSpinnerOffsetEnd;
            }
            setTargetOffsetTopAndBottom(endTarget - mCurrentTargetOffsetTop);
            mNotify = false;
            startScaleUpAnimation(mRefreshListener);
        } else {
            setRefreshing(refreshing, false /* notify */);
        }
    }

分两种情况:
(1)传入值为true:会直接将下拉球放置到刷新位置,并调用startScaleUpAnimation(mRefreshListener)让下拉球以变大的方式出现。注意这里将mNotify设置为false,因此不会调用mListener.onRefresh()方法(看上面mRefreshListener的实现),仅仅是让下拉球出现而已。之所以设置这么一个机制,是为了让开发者制作刷新按钮,这样即便不知道可以用下拉的方式刷新内容的用户也能够操作。下面是一个刷新按钮的点击监听器的实例:

    mySwipeRefreshLayout.setRefreshing(true);

    // This method must calls setRefreshing(false) when it's finished.
    myUpdateOperation();

注意需要在刷新逻辑结束时应手动调用调用SwipeRefreshLayout的setRefreshing(false)方法。
(2)传入值为false:最终会调用到startScaleDownAnimation(mRefreshListener),这个方法是让下拉球以变小的方式消失。mRefreshListener会调用一个reset()方法让动画在结束时将下拉球移回原位。本质上讲,这个方法是用来结束刷新的。

总结

SwipeRefreshLayout的实现逻辑基本上解析完毕了。最后对它的使用方法进行一下总结:
1. 在布局文件中添加SwipeRefreshLayout,并将需要实现下拉刷新的目标控件置于其中。注意只能放一个。
2. 利用findViewById获取SwipeRefreshLayout与目标控件的引用。
3. 通过SwipeRefreshLayout.setOnRefreshListener()方法为SwipeRefreshLayout提供刷新逻辑。刷新逻辑的结尾必须调用SwipeRefreshLayout的setRefreshing(false)方法。
4. 通过setColorSchemeColors(int… colors)与setProgressBackgroundColorSchemeColor(int color)方法设置下拉球箭头和下拉球背景的颜色。
5. (可选)添加一个刷新按钮,按钮中需要调用setRefreshing(true)以及实际执行刷新操作的方法。同样的,结尾必须调用SwipeRefreshLayout的setRefreshing(false)方法。

你可能感兴趣的:(Android,源码解析与应用)