修改SwipeRefreshLayout源码实现自定义Header的下拉刷新控件

下拉刷新是应用中比较常见的功能,最开始下拉刷新主要局限于ListView,网上也有很多成熟的ListView的下拉刷新和上拉加载更多的控件,但如果我要下拉刷新的是WebView这种控件呢?网上也有一些,不过其实谷歌自己也提供了,谷歌提供的这种控件叫SwipeRefreshLayout,它可以实现WebView这类控件的下拉刷新,不过它有一个问题是,它的刷新的样式是固定的,就是在顶部有几个不同颜色的类似进度条的显示(高版本也有类似ProgressBar那样旋转的进度框的样式),如果我想自己定义Header的话,就要改写SwipeRefreshLayout的源码了,先来看看效果图:


好吧,我承认我这个自定义的Header很丑,不过这不是本文的关键,本文的关键是来描述如何改造SwipeRefreshLayout,只要学会了如何改造,Header想弄成什么样子,价格gif动画什么的,都是很简单的,首先来看看SwipeRefreshLayout的源码,地址在:SDK\sources\android-20\android\support\v4\widget,这里要找到自己的SDK的目录 ,另外我这里用的是20版本进行改造的,其它版本我没看,原理是一样的,把这个目录下的SwipeRefreshLayout.java拷贝出来,发现它还用到了SwipeProgressBar.java和BakedBezierInterpolator.java,这2个文件就是SwipeRefreshLayout默认的下拉刷新的进度条和动画,我们改造后可以把它们删除,不需要。


先来初略看下SwipeRefreshLayout的源码:

public class SwipeRefreshLayout extends ViewGroup {
    private static final String LOG_TAG = SwipeRefreshLayout.class.getSimpleName();

    private static final long RETURN_TO_ORIGINAL_POSITION_TIMEOUT = 300;
    private static final float ACCELERATE_INTERPOLATION_FACTOR = 1.5f;
    private static final float DECELERATE_INTERPOLATION_FACTOR = 2f;
    private static final float PROGRESS_BAR_HEIGHT = 4;
    private static final float MAX_SWIPE_DISTANCE_FACTOR = .6f;
    private static final int REFRESH_TRIGGER_DISTANCE = 120;
    private static final int INVALID_POINTER = -1;

    private SwipeProgressBar mProgressBar; //the thing that shows progress is going
    private View mTarget; //the content that gets pulled down
    private int mOriginalOffsetTop;
    private OnRefreshListener mListener;
    private int mFrom;
    private boolean mRefreshing = false;
    private int mTouchSlop;
    private float mDistanceToTriggerSync = -1;
    private int mMediumAnimationDuration;
    private float mFromPercentage = 0;
    private float mCurrPercentage = 0;
    private int mProgressBarHeight;
    private int mCurrentTargetOffsetTop;

    private float mInitialMotionY;
    private float mLastMotionY;
    private boolean mIsBeingDragged;
    private int mActivePointerId = INVALID_POINTER;

    // Target is returning to its start offset because it was cancelled or a
    // refresh was triggered.
    private boolean mReturningToStart;
    private final DecelerateInterpolator mDecelerateInterpolator;
    private final AccelerateInterpolator mAccelerateInterpolator;
    private static final int[] LAYOUT_ATTRS = new int[] {
        android.R.attr.enabled
    };

    private final Animation mAnimateToStartPosition = new Animation() {
        @Override
        public void applyTransformation(float interpolatedTime, Transformation t) {
            int targetTop = 0;
            if (mFrom != mOriginalOffsetTop) {
                targetTop = (mFrom + (int)((mOriginalOffsetTop - mFrom) * interpolatedTime));
            }
            int offset = targetTop - mTarget.getTop();
            final int currentTop = mTarget.getTop();
            if (offset + currentTop < 0) {
                offset = 0 - currentTop;
            }
            setTargetOffsetTopAndBottom(offset);
        }
    };

    private Animation mShrinkTrigger = new Animation() {
        @Override
        public void applyTransformation(float interpolatedTime, Transformation t) {
            float percent = mFromPercentage + ((0 - mFromPercentage) * interpolatedTime);
            mProgressBar.setTriggerPercentage(percent);
        }
    };

    private final AnimationListener mReturnToStartPositionListener = new BaseAnimationListener() {
        @Override
        public void onAnimationEnd(Animation animation) {
            // Once the target content has returned to its start position, reset
            // the target offset to 0
            mCurrentTargetOffsetTop = 0;
        }
    };

    private final AnimationListener mShrinkAnimationListener = new BaseAnimationListener() {
        @Override
        public void onAnimationEnd(Animation animation) {
            mCurrPercentage = 0;
        }
    };

    private final Runnable mReturnToStartPosition = new Runnable() {

        @Override
        public void run() {
            mReturningToStart = true;
            animateOffsetToStartPosition(mCurrentTargetOffsetTop + getPaddingTop(),
                    mReturnToStartPositionListener);
        }

    };

    // Cancel the refresh gesture and animate everything back to its original state.
    private final Runnable mCancel = new Runnable() {

        @Override
        public void run() {
            mReturningToStart = true;
            // Timeout fired since the user last moved their finger; animate the
            // trigger to 0 and put the target back at its original position
            if (mProgressBar != null) {
                mFromPercentage = mCurrPercentage;
                mShrinkTrigger.setDuration(mMediumAnimationDuration);
                mShrinkTrigger.setAnimationListener(mShrinkAnimationListener);
                mShrinkTrigger.reset();
                mShrinkTrigger.setInterpolator(mDecelerateInterpolator);
                startAnimation(mShrinkTrigger);
            }
            animateOffsetToStartPosition(mCurrentTargetOffsetTop + getPaddingTop(),
                    mReturnToStartPositionListener);
        }

    };

    /**
     * Simple constructor to use when creating a SwipeRefreshLayout from code.
     * @param context
     */
    public SwipeRefreshLayout(Context context) {
        this(context, null);
    }

    /**
     * Constructor that is called when inflating SwipeRefreshLayout from XML.
     * @param context
     * @param attrs
     */
    public SwipeRefreshLayout(Context context, AttributeSet attrs) {
        super(context, attrs);

        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();

        mMediumAnimationDuration = getResources().getInteger(
                android.R.integer.config_mediumAnimTime);

        setWillNotDraw(false);
        mProgressBar = new SwipeProgressBar(this);
        final DisplayMetrics metrics = getResources().getDisplayMetrics();
        mProgressBarHeight = (int) (metrics.density * PROGRESS_BAR_HEIGHT);
        mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR);
        mAccelerateInterpolator = new AccelerateInterpolator(ACCELERATE_INTERPOLATION_FACTOR);

        final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS);
        setEnabled(a.getBoolean(0, true));
        a.recycle();
    }

    @Override
    public void onAttachedToWindow() {
        super.onAttachedToWindow();
        removeCallbacks(mCancel);
        removeCallbacks(mReturnToStartPosition);
    }

    @Override
    public void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        removeCallbacks(mReturnToStartPosition);
        removeCallbacks(mCancel);
    }

    private void animateOffsetToStartPosition(int from, AnimationListener listener) {
        mFrom = from;
        mAnimateToStartPosition.reset();
        mAnimateToStartPosition.setDuration(mMediumAnimationDuration);
        mAnimateToStartPosition.setAnimationListener(listener);
        mAnimateToStartPosition.setInterpolator(mDecelerateInterpolator);
        mTarget.startAnimation(mAnimateToStartPosition);
    }

    /**
     * Set the listener to be notified when a refresh is triggered via the swipe
     * gesture.
     */
    public void setOnRefreshListener(OnRefreshListener listener) {
        mListener = listener;
    }

    private void setTriggerPercentage(float percent) {
        if (percent == 0f) {
            // No-op. A null trigger means it's uninitialized, and setting it to zero-percent
            // means we're trying to reset state, so there's nothing to reset in this case.
            mCurrPercentage = 0;
            return;
        }
        mCurrPercentage = percent;
        mProgressBar.setTriggerPercentage(percent);
    }

    /**
     * Notify the widget that refresh state has changed. Do not call this when
     * refresh is triggered by a swipe gesture.
     *
     * @param refreshing Whether or not the view should show refresh progress.
     */
    public void setRefreshing(boolean refreshing) {
        if (mRefreshing != refreshing) {
            ensureTarget();
            mCurrPercentage = 0;
            mRefreshing = refreshing;
            if (mRefreshing) {
                mProgressBar.start();
            } else {
                mProgressBar.stop();
            }
        }
    }

    /**
     * @deprecated Use {@link #setColorSchemeResources(int, int, int, int)}
     */
    @Deprecated
    public void setColorScheme(int colorRes1, int colorRes2, int colorRes3, int colorRes4) {
        setColorSchemeResources(colorRes1, colorRes2, colorRes3, colorRes4);
    }

    /**
     * Set the four colors used in the progress animation from color resources.
     * The first color will also be the color of the bar that grows in response
     * to a user swipe gesture.
     */
    public void setColorSchemeResources(int colorRes1, int colorRes2, int colorRes3,
            int colorRes4) {
        final Resources res = getResources();
        setColorSchemeColors(res.getColor(colorRes1), res.getColor(colorRes2),
                res.getColor(colorRes3), res.getColor(colorRes4));
    }

    /**
     * Set the four colors used in the progress animation. The first color will
     * also be the color of the bar that grows in response to a user swipe
     * gesture.
     */
    public void setColorSchemeColors(int color1, int color2, int color3, int color4) {
        ensureTarget();
        mProgressBar.setColorScheme(color1, color2, color3, color4);
    }

    /**
     * @return Whether the SwipeRefreshWidget is actively showing refresh
     *         progress.
     */
    public boolean isRefreshing() {
        return mRefreshing;
    }

    private void ensureTarget() {
        // Don't bother getting the parent height if the parent hasn't been laid out yet.
        if (mTarget == null) {
            if (getChildCount() > 1 && !isInEditMode()) {
                throw new IllegalStateException(
                        "SwipeRefreshLayout can host only one direct child");
            }
            mTarget = getChildAt(0);
            mOriginalOffsetTop = mTarget.getTop() + getPaddingTop();
        }
        if (mDistanceToTriggerSync == -1) {
            if (getParent() != null && ((View)getParent()).getHeight() > 0) {
                final DisplayMetrics metrics = getResources().getDisplayMetrics();
                mDistanceToTriggerSync = (int) Math.min(
                        ((View) getParent()) .getHeight() * MAX_SWIPE_DISTANCE_FACTOR,
                                REFRESH_TRIGGER_DISTANCE * metrics.density);
            }
        }
    }

    @Override
    public void draw(Canvas canvas) {
        super.draw(canvas);
        mProgressBar.draw(canvas);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        final int width =  getMeasuredWidth();
        final int height = getMeasuredHeight();
        mProgressBar.setBounds(0, 0, width, mProgressBarHeight);
        if (getChildCount() == 0) {
            return;
        }
        final View child = getChildAt(0);
        final int childLeft = getPaddingLeft();
        final int childTop = mCurrentTargetOffsetTop + getPaddingTop();
        final int childWidth = width - getPaddingLeft() - getPaddingRight();
        final int childHeight = height - getPaddingTop() - getPaddingBottom();
        child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
    }

    @Override
    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (getChildCount() > 1 && !isInEditMode()) {
            throw new IllegalStateException("SwipeRefreshLayout can host only one direct child");
        }
        if (getChildCount() > 0) {
            getChildAt(0).measure(
                    MeasureSpec.makeMeasureSpec(
                            getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
                            MeasureSpec.EXACTLY),
                    MeasureSpec.makeMeasureSpec(
                            getMeasuredHeight() - getPaddingTop() - getPaddingBottom(),
                            MeasureSpec.EXACTLY));
        }
    }

    /**
     * @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 (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 mTarget.getScrollY() > 0;
            }
        } else {
            return ViewCompat.canScrollVertically(mTarget, -1);
        }
    }

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

        final int action = MotionEventCompat.getActionMasked(ev);

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

        if (!isEnabled() || mReturningToStart || canChildScrollUp()) {
            // Fail fast if we're not in a state where a swipe is possible
            return false;
        }

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mLastMotionY = mInitialMotionY = ev.getY();
                mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
                mIsBeingDragged = false;
                mCurrPercentage = 0;
                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 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 yDiff = y - mInitialMotionY;
                if (yDiff > mTouchSlop) {
                    mLastMotionY = y;
                    mIsBeingDragged = true;
                }
                break;

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

            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mIsBeingDragged = false;
                mCurrPercentage = 0;
                mActivePointerId = INVALID_POINTER;
                break;
        }

        return mIsBeingDragged;
    }

    @Override
    public void requestDisallowInterceptTouchEvent(boolean b) {
        // Nope.
    }

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

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

        if (!isEnabled() || mReturningToStart || canChildScrollUp()) {
            // Fail fast if we're not in a state where a swipe is possible
            return false;
        }

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mLastMotionY = mInitialMotionY = ev.getY();
                mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
                mIsBeingDragged = false;
                mCurrPercentage = 0;
                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 yDiff = y - mInitialMotionY;

                if (!mIsBeingDragged && yDiff > mTouchSlop) {
                    mIsBeingDragged = true;
                }

                if (mIsBeingDragged) {
                    // User velocity passed min velocity; trigger a refresh
                    if (yDiff > mDistanceToTriggerSync) {
                        // User movement passed distance; trigger a refresh
                        startRefresh();
                    } else {
                        // Just track the user's movement
                        setTriggerPercentage(
                                mAccelerateInterpolator.getInterpolation(
                                        yDiff / mDistanceToTriggerSync));
                        updateContentOffsetTop((int) (yDiff));
                        if (mLastMotionY > y && mTarget.getTop() == getPaddingTop()) {
                            // If the user puts the view back at the top, we
                            // don't need to. This shouldn't be considered
                            // cancelling the gesture as the user can restart from the top.
                            removeCallbacks(mCancel);
                        } else {
                            updatePositionTimeout();
                        }
                    }
                    mLastMotionY = y;
                }
                break;

            case MotionEventCompat.ACTION_POINTER_DOWN: {
                final int index = MotionEventCompat.getActionIndex(ev);
                mLastMotionY = MotionEventCompat.getY(ev, index);
                mActivePointerId = MotionEventCompat.getPointerId(ev, index);
                break;
            }

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

            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mIsBeingDragged = false;
                mCurrPercentage = 0;
                mActivePointerId = INVALID_POINTER;
                return false;
        }

        return true;
    }

    private void startRefresh() {
        removeCallbacks(mCancel);
        mReturnToStartPosition.run();
        setRefreshing(true);
        mListener.onRefresh();
    }

    private void updateContentOffsetTop(int targetTop) {
        final int currentTop = mTarget.getTop();
        if (targetTop > mDistanceToTriggerSync) {
            targetTop = (int) mDistanceToTriggerSync;
        } else if (targetTop < 0) {
            targetTop = 0;
        }
        setTargetOffsetTopAndBottom(targetTop - currentTop);
    }

    private void setTargetOffsetTopAndBottom(int offset) {
        mTarget.offsetTopAndBottom(offset);
        mCurrentTargetOffsetTop = mTarget.getTop();
    }

    private void updatePositionTimeout() {
        removeCallbacks(mCancel);
        postDelayed(mCancel, RETURN_TO_ORIGINAL_POSITION_TIMEOUT);
    }

    private void onSecondaryPointerUp(MotionEvent ev) {
        final int pointerIndex = MotionEventCompat.getActionIndex(ev);
        final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
        if (pointerId == mActivePointerId) {
            // This was our active pointer going up. Choose a new
            // active pointer and adjust accordingly.
            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
            mLastMotionY = MotionEventCompat.getY(ev, newPointerIndex);
            mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
        }
    }

    /**
     * Classes that wish to be notified when the swipe gesture correctly
     * triggers a refresh should implement this interface.
     */
    public interface OnRefreshListener {
        public void onRefresh();
    }

    /**
     * Simple AnimationListener to avoid having to implement unneeded methods in
     * AnimationListeners.
     */
    private class BaseAnimationListener implements AnimationListener {
        @Override
        public void onAnimationStart(Animation animation) {
        }

        @Override
        public void onAnimationEnd(Animation animation) {
        }

        @Override
        public void onAnimationRepeat(Animation animation) {
        }
    }
}

洋洋洒洒几百行,还是有点多的,不过里面有不少是默认的刷新动画相关的东西,我们都可以忽略掉,只看核心的部分:

1. 测量

@Override
    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (getChildCount() > 1 && !isInEditMode()) {
            throw new IllegalStateException("SwipeRefreshLayout can host only one direct child");
        }
        if (getChildCount() > 0) {
            getChildAt(0).measure(
                    MeasureSpec.makeMeasureSpec(
                            getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
                            MeasureSpec.EXACTLY),
                    MeasureSpec.makeMeasureSpec(
                            getMeasuredHeight() - getPaddingTop() - getPaddingBottom(),
                            MeasureSpec.EXACTLY));
        }
    }

这是测量控件的大小,首先,判断了 SwipeRefreshLayout的child的个数,可以看到如果多于1个,就直接抛出异常了,默认只允许有一个child,完后对child的长宽进行了测量,这里没什么好说的,就是用长宽值减去了padding的值,最后得到控件实际的长宽


2. 摆放

@Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        final int width =  getMeasuredWidth();
        final int height = getMeasuredHeight();
        mProgressBar.setBounds(0, 0, width, mProgressBarHeight);
        if (getChildCount() == 0) {
            return;
        }
        final View child = getChildAt(0);
        final int childLeft = getPaddingLeft();
        final int childTop = mCurrentTargetOffsetTop + getPaddingTop();
        final int childWidth = width - getPaddingLeft() - getPaddingRight();
        final int childHeight = height - getPaddingTop() - getPaddingBottom();
        child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
    }

量完了控件实际的长宽,自然要把它摆放在正确的位置了, mProgressBar我们不管,这是默认的进度,我们会删掉,完后计算左下右上的位置,并调用layout方法将其放置在相应的位置上,这应该都看得懂


3. 初始化滑动控件

private void ensureTarget() {
        // Don't bother getting the parent height if the parent hasn't been laid out yet.
        if (mTarget == null) {
            if (getChildCount() > 1 && !isInEditMode()) {
                throw new IllegalStateException(
                        "SwipeRefreshLayout can host only one direct child");
            }
            mTarget = getChildAt(0);
            mOriginalOffsetTop = mTarget.getTop() + getPaddingTop();
        }
        if (mDistanceToTriggerSync == -1) {
            if (getParent() != null && ((View)getParent()).getHeight() > 0) {
                final DisplayMetrics metrics = getResources().getDisplayMetrics();
                mDistanceToTriggerSync = (int) Math.min(
                        ((View) getParent()) .getHeight() * MAX_SWIPE_DISTANCE_FACTOR,
                                REFRESH_TRIGGER_DISTANCE * metrics.density);
            }
        }
    }

这个方法用来确认要滑动的控件, 将滑动控件赋值给mTarget,并定义了 mOriginalOffsetTop变量,它是控件当前顶部位置加上SwipeRefreshLayout的paddingTop的值,也就是控件的顶部坐标了,下面的 mDistanceToTriggerSync这个变量是滑动多少距离后触发刷新,可以看到它是通过父控件的高度乘以一个因子,或者从默认的最大滑动距离两个值取小

4. 事件拦截

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

        final int action = MotionEventCompat.getActionMasked(ev);

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

        if (!isEnabled() || mReturningToStart || canChildScrollUp()) {
            // Fail fast if we're not in a state where a swipe is possible
            return false;
        }

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mLastMotionY = mInitialMotionY = ev.getY();
                mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
                mIsBeingDragged = false;
                mCurrPercentage = 0;
                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 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 yDiff = y - mInitialMotionY;
                if (yDiff > mTouchSlop) {
                    mLastMotionY = y;
                    mIsBeingDragged = true;
                }
                break;

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

            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mIsBeingDragged = false;
                mCurrPercentage = 0;
                mActivePointerId = INVALID_POINTER;
                break;
        }

        return mIsBeingDragged;
    }


 

这里是对用户的操作进行拦截,首先调用了第3步中的ensureTarget来确保滑动控件被初始化了,完后可以看到获取了ACTION,这里跟平时直接用getAction方法来获取还是有些区别的,这里用到的getActionMasked方法可以捕获到多个手指的事件,这个后面会说,下面的mReturningToStart这个是布尔变量,初始为false,用来判断滑动控件是否在执行滑动回顶点的动画,在动画开始的时候,会将这个值赋值为true,完后这里判断如果值为true,并且为按下事件,则赋值为false,表示是一次新的滑动了。

if (!isEnabled() || mReturningToStart || canChildScrollUp())

 

这3个分别是判断是否可用,是否正在执行动画,以及是否能滚动,如果不满足条件,直接返回false,就不往下执行了。

ACTION_DOWN,mLastMotionY是最后滑动的y坐标,mInitialMotionY是按下时初始的y坐标,mActivePointerId是第一个按下手指的id,mIsBeingDragged是一个布尔变量,表示是否正在拖动,mCurrPercentage是默认动画相关的百分比,改造后用不到
ACTION_MOVE,首先判断了id是否为-1,是的话直接报错返回(UP或CANCEL时会赋值为-1),完后获取现在的y坐标,并计算与初始y坐标的差是否大于mTouchSlop,这个值是ViewConfiguration类中定义的一个常量,它定义了最小滑动距离,小于这个值表示不是正常的滑动,一般你随便滑动一下肯定超过这个值,ViewConfiguration中定义了很多这种标准值,有兴趣的可以自己去看看,如果大于mTouchSlop,则将mLastMotionY赋值为新值,并且将mIsBeingDragged赋值为true,表示控件开始随手指滑动了
ACTION_UP/ACTION_CANCEL,这里很简单,不说了
最后还有一个ACTION_POINTER_UP,这里调用了onSecondaryPointerUp方法,这个具体到onTouchEvent中再说

5. 事件处理

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

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

        if (!isEnabled() || mReturningToStart || canChildScrollUp()) {
            // Fail fast if we're not in a state where a swipe is possible
            return false;
        }

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mLastMotionY = mInitialMotionY = ev.getY();
                mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
                mIsBeingDragged = false;
                mCurrPercentage = 0;
                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 yDiff = y - mInitialMotionY;

                if (!mIsBeingDragged && yDiff > mTouchSlop) {
                    mIsBeingDragged = true;
                }

                if (mIsBeingDragged) {
                    // User velocity passed min velocity; trigger a refresh
                    if (yDiff > mDistanceToTriggerSync) {
                        // User movement passed distance; trigger a refresh
                        startRefresh();
                    } else {
                        // Just track the user's movement
                        setTriggerPercentage(
                                mAccelerateInterpolator.getInterpolation(
                                        yDiff / mDistanceToTriggerSync));
                        updateContentOffsetTop((int) (yDiff));
                        if (mLastMotionY > y && mTarget.getTop() == getPaddingTop()) {
                            // If the user puts the view back at the top, we
                            // don't need to. This shouldn't be considered
                            // cancelling the gesture as the user can restart from the top.
                            removeCallbacks(mCancel);
                        } else {
                            updatePositionTimeout();
                        }
                    }
                    mLastMotionY = y;
                }
                break;

            case MotionEventCompat.ACTION_POINTER_DOWN: {
                final int index = MotionEventCompat.getActionIndex(ev);
                mLastMotionY = MotionEventCompat.getY(ev, index);
                mActivePointerId = MotionEventCompat.getPointerId(ev, index);
                break;
            }

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

            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mIsBeingDragged = false;
                mCurrPercentage = 0;
                mActivePointerId = INVALID_POINTER;
                return false;
        }

        return true;
    }

onInterceptTouchEvent如果返回false,是不会进入到onTouchEvent的,反过来说,如果返回true,则进入到onTouchEvent进行处理,这个最开始跟上面一样的,主要看看ACTION_MOVE,如果yDiff大于mDistanceToTriggerSync,也就是滑动的距离大于了设置的最大滑动距离,就直接触发刷新,如果小于,先是执行setTriggerPercentage方法,这个是默认动画的处理,不管,后面的updateContentOffsetTop((int) (yDiff));就是改变控件的位置,实现控件随手指下拉的效果,具体实现为

private void updateContentOffsetTop(int targetTop) {
        final int currentTop = mTarget.getTop();
        if (targetTop > mDistanceToTriggerSync) {
            targetTop = (int) mDistanceToTriggerSync;
        } else if (targetTop < 0) {
            targetTop = 0;
        }
        setTargetOffsetTopAndBottom(targetTop - currentTop);
    }

    private void setTargetOffsetTopAndBottom(int offset) {
        mTarget.offsetTopAndBottom(offset);
        mCurrentTargetOffsetTop = mTarget.getTop();
    }

这里也比较好理解,如果大于了最大值,就赋值为最大值,如果小于0,则赋值为0,保证了滑动不会越界,最终是调用系统的offsetTopAndBottom方法来实现随手指滑动,回到刚才的代码

if (mLastMotionY > y && mTarget.getTop() == getPaddingTop()) {
                            // If the user puts the view back at the top, we
                            // don't need to. This shouldn't be considered
                            // cancelling the gesture as the user can restart from the top.
                            removeCallbacks(mCancel);
                        } else {
                            updatePositionTimeout();
                        }

在滑动的过程中,不断地执行updatePositionTimeout方法,当滑动到顶部时,执行removeCallbacks方法,我们看下updatePositionTimeout的实现

 private void updatePositionTimeout() {
        removeCallbacks(mCancel);
        postDelayed(mCancel, RETURN_TO_ORIGINAL_POSITION_TIMEOUT);
    }

这里的mCancel是一个线程,它就是启动一个滑动回顶部的动画, RETURN_TO_ORIGINAL_POSITION_TIMEOUT默认值是300,也就是0.3秒,所以在手指滑动控件的过程中,是在不断触发滑动回顶部的动画,只不过触发这个动画有0.3秒的延时,在这个时间内滑动又取消了这个线程的执行,所以最终并不会导致动画不停的执行


下面再来看看刚才遗漏的多个手指触屏的问题

第一个手指按下时,会触发ACTION_DOWN,而第二个手指按下时,会触发ACTION_POINTER_DOWN,可以看到,第二个手指按下时,相应的把mLastMotionY和mActivePointerId都赋值成了第二个手指相关的值,这是干什么呢,就比如说你第一个手指向下滑动了20px,完后你第二个手指点到40px的地方,这时就会自动滑动到40px的地方,完后你抬起第二个手指,会调用onSecondaryPointerUp方法

private void onSecondaryPointerUp(MotionEvent ev) {
        final int pointerIndex = MotionEventCompat.getActionIndex(ev);
        final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
        
        if (pointerId == mActivePointerId) {
            // This was our active pointer going up. Choose a new
            // active pointer and adjust accordingly.
            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
            mLastMotionY = MotionEventCompat.getY(ev, newPointerIndex);
            mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
        }
    }

这时它会去判断,你抬起的那个手指是不是最后按下的那个手指,如果是,则会再次改变 mLastMotionY和 mActivePointerId的值为剩下的那个手指的,也就是说会重新滑回到20px处,如果不是最后按下的那个手指,那证明抬起的是第一个手指,那就不用动,因为第二个手指还按在40px处

至此,基本上源码就过了一遍,剩下还有一个mAnimateToStartPosition,这个是滑动回顶部的动画,没什么好说的,还有一个回调接口

public interface OnRefreshListener {
        public void onRefresh();
    }

这个就是在刷新的时候触发,使用SwipeRefreshLayout的用户可以实现这个接口来做自己的处理


SwipeRefreshLayout基本就是这样了,下面我们开始动手改造,实现自定义Header

先上改造后的代码:

public class SwipeRefreshLayout extends ViewGroup {
	private static final String LOG_TAG = SwipeRefreshLayout.class
			.getSimpleName();

	private static final long RETURN_TO_ORIGINAL_POSITION_TIMEOUT = 300;
	private static final float DECELERATE_INTERPOLATION_FACTOR = 2f;
	private static final float MAX_SWIPE_DISTANCE_FACTOR = .6f;
	private static final int REFRESH_TRIGGER_DISTANCE = 120;
	private static final int INVALID_POINTER = -1;

	private View mTarget; // the content that gets pulled down
	private int mOriginalOffsetTop;
	private OnRefreshListener mListener;
	private int mFrom;
	private boolean mRefreshing = false;
	private int mTouchSlop;
	private float mDistanceToTriggerSync = -1;
	private int mMediumAnimationDuration;
	private int mCurrentTargetOffsetTop;

	private float mInitialMotionY;
	private float mLastMotionY;
	private boolean mIsBeingDragged;
	private int mActivePointerId = INVALID_POINTER;

	// Target is returning to its start offset because it was cancelled or a
	// refresh was triggered.
	private boolean mReturningToStart;
	private final DecelerateInterpolator mDecelerateInterpolator;
	private static final int[] LAYOUT_ATTRS = new int[] { android.R.attr.enabled };

	private View mHeaderView;
	private int mHeaderHeight;
	private STATUS mStatus = STATUS.NORMAL;
	private boolean mDisable; // 用来控制控件是否允许滚动

	private enum STATUS {
		NORMAL, LOOSEN, REFRESHING
	}

	private final Animation mAnimateToStartPosition = new Animation() {
		@Override
		public void applyTransformation(float interpolatedTime, Transformation t) {
			int targetTop = 0;
			if (mFrom != mOriginalOffsetTop) {
				targetTop = (mFrom + (int) ((mOriginalOffsetTop - mFrom) * interpolatedTime));
			}

			int offset = targetTop - mTarget.getTop();
			final int currentTop = mTarget.getTop();

			if (offset + currentTop < 0) {
				offset = 0 - currentTop;
			}
			setTargetOffsetTopAndBottom(offset);
		}
	};
	
	private final Animation mAnimateToHeaderPosition = new Animation() {
		@Override
		public void applyTransformation(float interpolatedTime, Transformation t) {
			int targetTop = 0;
			if (mFrom != mHeaderHeight) {
				targetTop = (mFrom + (int) ((mHeaderHeight - mFrom) * interpolatedTime));
			}

			int offset = targetTop - mTarget.getTop();
			final int currentTop = mTarget.getTop();

			if (offset + currentTop < 0) {
				offset = 0 - currentTop;
			}
			setTargetOffsetTopAndBottom(offset);
		}
	};

	private final AnimationListener mReturnToStartPositionListener = new BaseAnimationListener() {
		@Override
		public void onAnimationEnd(Animation animation) {
			// Once the target content has returned to its start position, reset
			// the target offset to 0
			mCurrentTargetOffsetTop = 0;
			mStatus = STATUS.NORMAL;
			mDisable = false;
		}
	};
	
	private final AnimationListener mReturnToHeaderPositionListener = new BaseAnimationListener() {
		@Override
		public void onAnimationEnd(Animation animation) {
			// Once the target content has returned to its start position, reset
			// the target offset to 0
			mCurrentTargetOffsetTop = mHeaderHeight;
			mStatus = STATUS.REFRESHING;
		}
	};

	private final Runnable mReturnToStartPosition = new Runnable() {
		@Override
		public void run() {
			mReturningToStart = true;
			animateOffsetToStartPosition(mCurrentTargetOffsetTop
					+ getPaddingTop(), mReturnToStartPositionListener);
		}
	};
	
	private final Runnable mReturnToHeaderPosition = new Runnable() {
		@Override
		public void run() {
			mReturningToStart = true;
			animateOffsetToHeaderPosition(mCurrentTargetOffsetTop
					+ getPaddingTop(), mReturnToHeaderPositionListener);
		}
	};

	// Cancel the refresh gesture and animate everything back to its original
	// state.
	private final Runnable mCancel = new Runnable() {
		@Override
		public void run() {
			mReturningToStart = true;
			animateOffsetToStartPosition(mCurrentTargetOffsetTop
					+ getPaddingTop(), mReturnToStartPositionListener);
		}
	};

	/**
	 * Simple constructor to use when creating a SwipeRefreshLayout from code.
	 * 
	 * @param context
	 */
	public SwipeRefreshLayout(Context context) {
		this(context, null);
	}

	/**
	 * Constructor that is called when inflating SwipeRefreshLayout from XML.
	 * 
	 * @param context
	 * @param attrs
	 */
	public SwipeRefreshLayout(Context context, AttributeSet attrs) {
		super(context, attrs);

		mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();

		mMediumAnimationDuration = getResources().getInteger(
				android.R.integer.config_mediumAnimTime);

		mDecelerateInterpolator = new DecelerateInterpolator(
				DECELERATE_INTERPOLATION_FACTOR);

		final TypedArray a = context
				.obtainStyledAttributes(attrs, LAYOUT_ATTRS);
		setEnabled(a.getBoolean(0, true));
		a.recycle();
	}

	@Override
	public void onAttachedToWindow() {
		super.onAttachedToWindow();
		removeCallbacks(mCancel);
		removeCallbacks(mReturnToStartPosition);
		removeCallbacks(mReturnToHeaderPosition);
	}

	@Override
	public void onDetachedFromWindow() {
		super.onDetachedFromWindow();
		removeCallbacks(mReturnToStartPosition);
		removeCallbacks(mCancel);
		removeCallbacks(mReturnToHeaderPosition);
	}

	private void animateOffsetToStartPosition(int from,
			AnimationListener listener) {
		mFrom = from;
		mAnimateToStartPosition.reset();
		mAnimateToStartPosition.setDuration(mMediumAnimationDuration);
		mAnimateToStartPosition.setAnimationListener(listener);
		mAnimateToStartPosition.setInterpolator(mDecelerateInterpolator);
		mTarget.startAnimation(mAnimateToStartPosition);
	}
	
	private void animateOffsetToHeaderPosition(int from,
			AnimationListener listener) {
		mFrom = from;
		mAnimateToHeaderPosition.reset();
		mAnimateToHeaderPosition.setDuration(mMediumAnimationDuration);
		mAnimateToHeaderPosition.setAnimationListener(listener);
		mAnimateToHeaderPosition.setInterpolator(mDecelerateInterpolator);
		mTarget.startAnimation(mAnimateToHeaderPosition);
	}

	/**
	 * Set the listener to be notified when a refresh is triggered via the swipe
	 * gesture.
	 */
	public void setOnRefreshListener(OnRefreshListener listener) {
		mListener = listener;
	}

	/**
	 * Notify the widget that refresh state has changed. Do not call this when
	 * refresh is triggered by a swipe gesture.
	 * 
	 * @param refreshing
	 *            Whether or not the view should show refresh progress.
	 */
	public void setRefreshing(boolean refreshing) {
		if (mRefreshing != refreshing) {
			ensureTarget();
			mRefreshing = refreshing;
		}
	}

	/**
	 * @return Whether the SwipeRefreshWidget is actively showing refresh
	 *         progress.
	 */
	public boolean isRefreshing() {
		return mRefreshing;
	}

	private void ensureTarget() {
		// Don't bother getting the parent height if the parent hasn't been laid
		// out yet.
		if (mTarget == null) {
			if (getChildCount() > 2 && !isInEditMode()) {
				throw new IllegalStateException(
						"SwipeRefreshLayout can only host two children");
			}
			mTarget = getChildAt(1);
			
			// 控制是否允许滚动
			mTarget.setOnTouchListener(new View.OnTouchListener() {
				@Override
				public boolean onTouch(View v, MotionEvent event) {
					return mDisable;
				}
			});
			
			mOriginalOffsetTop = mTarget.getTop() + getPaddingTop();
		}
		if (mDistanceToTriggerSync == -1) {
			if (getParent() != null && ((View) getParent()).getHeight() > 0) {
				final DisplayMetrics metrics = getResources()
						.getDisplayMetrics();
				mDistanceToTriggerSync = (int) Math.min(
						((View) getParent()).getHeight()
								* MAX_SWIPE_DISTANCE_FACTOR,
						REFRESH_TRIGGER_DISTANCE * metrics.density);
			}
		}
	}

	@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 || getChildCount() == 1) {
			return;
		}
		final View child = getChildAt(1);
		final int childLeft = getPaddingLeft();
		final int childTop = mCurrentTargetOffsetTop + getPaddingTop();
		final int childWidth = width - getPaddingLeft() - getPaddingRight();
		final int childHeight = height - getPaddingTop() - getPaddingBottom();
		child.layout(childLeft, childTop, childLeft + childWidth, childTop
				+ childHeight);

		mHeaderView.layout(childLeft, childTop - mHeaderHeight, childLeft
				+ childWidth, childTop);
	}

	@Override
	public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		super.onMeasure(widthMeasureSpec, heightMeasureSpec);

		if (getChildCount() <= 1) {
			throw new IllegalStateException(
					"SwipeRefreshLayout must have the headerview and contentview");
		}

		if (getChildCount() > 2 && !isInEditMode()) {
			throw new IllegalStateException(
					"SwipeRefreshLayout can only host two children");
		}

		if (mHeaderView == null) {
			mHeaderView = getChildAt(0);
			measureChild(mHeaderView, widthMeasureSpec, heightMeasureSpec);
			mHeaderHeight = mHeaderView.getMeasuredHeight();

			mDistanceToTriggerSync = mHeaderHeight;
		}

		getChildAt(1).measure(
				MeasureSpec.makeMeasureSpec(getMeasuredWidth()
						- getPaddingLeft() - getPaddingRight(),
						MeasureSpec.EXACTLY),
				MeasureSpec.makeMeasureSpec(getMeasuredHeight()
						- getPaddingTop() - getPaddingBottom(),
						MeasureSpec.EXACTLY));
	}

	/**
	 * @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 (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 mTarget.getScrollY() > 0;
			}
		} else {
			return ViewCompat.canScrollVertically(mTarget, -1);
		}
	}

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

		final int action = MotionEventCompat.getActionMasked(ev);

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

		if (!isEnabled() || mReturningToStart || canChildScrollUp() || mStatus == STATUS.REFRESHING) {
			// Fail fast if we're not in a state where a swipe is possible
			return false;
		}

		switch (action) {
		case MotionEvent.ACTION_DOWN:
			mLastMotionY = mInitialMotionY = ev.getY();
			mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
			mIsBeingDragged = false;
			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 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 yDiff = y - mInitialMotionY;
			if (yDiff > mTouchSlop) {
				mLastMotionY = y;
				mIsBeingDragged = true;
			}
			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;
	}

	@Override
	public void requestDisallowInterceptTouchEvent(boolean b) {
		// Nope.
	}

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

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

		if (!isEnabled() || mReturningToStart || canChildScrollUp() || mStatus == STATUS.REFRESHING) {
			// Fail fast if we're not in a state where a swipe is possible
			return false;
		}

		switch (action) {
		case MotionEvent.ACTION_DOWN:
			mLastMotionY = mInitialMotionY = ev.getY();
			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 yDiff = y - mInitialMotionY;

			if (!mIsBeingDragged && yDiff > mTouchSlop) {
				mIsBeingDragged = true;
			}

			if (mIsBeingDragged) {
				// User velocity passed min velocity; trigger a refresh
				if (yDiff > mDistanceToTriggerSync) {
					if (mStatus == STATUS.NORMAL) {
						mStatus = STATUS.LOOSEN;

						if (mListener != null) {
							mListener.onLoose();
						}
					}
					
					updateContentOffsetTop((int) (yDiff));
				} else {
					if (mStatus == STATUS.LOOSEN) {
						mStatus = STATUS.NORMAL;

						if (mListener != null) {
							mListener.onNormal();
						}
					}

					updateContentOffsetTop((int) (yDiff));
					if (mLastMotionY > y && mTarget.getTop() == getPaddingTop()) {
						// If the user puts the view back at the top, we
						// don't need to. This shouldn't be considered
						// cancelling the gesture as the user can restart from
						// the top.
						removeCallbacks(mCancel);
					}
				}
				mLastMotionY = y;
			}
			break;

		case MotionEventCompat.ACTION_POINTER_DOWN: {
			final int index = MotionEventCompat.getActionIndex(ev);
			mLastMotionY = MotionEventCompat.getY(ev, index);
			mActivePointerId = MotionEventCompat.getPointerId(ev, index);
			break;
		}

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

		case MotionEvent.ACTION_UP:
			if (mStatus == STATUS.LOOSEN) {
				startRefresh();
			} else {
				updatePositionTimeout();
			}

			mIsBeingDragged = false;
			mActivePointerId = INVALID_POINTER;
			return false;
		case MotionEvent.ACTION_CANCEL:
			updatePositionTimeout();

			mIsBeingDragged = false;
			mActivePointerId = INVALID_POINTER;
			return false;
		}

		return true;
	}

	private void startRefresh() {
		removeCallbacks(mCancel);
		mReturnToHeaderPosition.run();
		setRefreshing(true);
		mDisable = true;

		if (mListener != null) {
			mListener.onRefresh();
		}
	}
	
	public void stopRefresh() {
		mReturnToStartPosition.run();
	}

	private void updateContentOffsetTop(int targetTop) {
		final int currentTop = mTarget.getTop();
		if (targetTop > mDistanceToTriggerSync) {
			targetTop = (int) mDistanceToTriggerSync + (int) (targetTop - mDistanceToTriggerSync) / 2; // 超过触发松手刷新的距离后,就只显示滑动一半的距离,避免随手势拉动到最底部,用户体验不好
		} else if (targetTop < 0) {
			targetTop = 0;
		}
		setTargetOffsetTopAndBottom(targetTop - currentTop);
	}

	private void setTargetOffsetTopAndBottom(int offset) {
		mTarget.offsetTopAndBottom(offset);
		mHeaderView.offsetTopAndBottom(offset);
		mCurrentTargetOffsetTop = mTarget.getTop();
		invalidate();
	}

	private void updatePositionTimeout() {
		removeCallbacks(mCancel);
		postDelayed(mCancel, RETURN_TO_ORIGINAL_POSITION_TIMEOUT);
	}

	private void onSecondaryPointerUp(MotionEvent ev) {
		final int pointerIndex = MotionEventCompat.getActionIndex(ev);
		final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);

		if (pointerId == mActivePointerId) {
			// This was our active pointer going up. Choose a new
			// active pointer and adjust accordingly.
			final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
			mLastMotionY = MotionEventCompat.getY(ev, newPointerIndex);
			mActivePointerId = MotionEventCompat.getPointerId(ev,
					newPointerIndex);
		}
	}

	/**
	 * Classes that wish to be notified when the swipe gesture correctly
	 * triggers a normal/ready-refresh/refresh should implement this interface.
	 */
	public interface OnRefreshListener {
		public void onNormal();

		public void onLoose();

		public void onRefresh();
	}

	/**
	 * Simple AnimationListener to avoid having to implement unneeded methods in
	 * AnimationListeners.
	 */
	private class BaseAnimationListener implements AnimationListener {
		@Override
		public void onAnimationStart(Animation animation) {
		}

		@Override
		public void onAnimationEnd(Animation animation) {
		}

		@Override
		public void onAnimationRepeat(Animation animation) {
		}
	}
}


下面具体讲解下,首先,我们的Header有三种状态,需要修改之前的接口OnRefreshListener

public interface OnRefreshListener {
		public void onNormal();

		public void onLoose();

		public void onRefresh();
	}

三种状态分别是下拉刷新状态,松开刷新状态和刷新状态,初始是下拉刷新状态,下拉到我们定义的阀值的距离,就变成松手刷新状态,完后松手,就进入正在刷新状态,刷新完毕,重新变为下拉刷新状态,弄清这几种状态是改造的基础,下面步入正题:

1. 删除原始动画

原始动画主要是SwipeProgressBar相关的内容,直接在SwipeRefreshLayout.java中删除这部分内容,很好早,删掉后代码也精简了不少,也不用再引用SwipeProgressBar.java和BakedBezierInterpolator.java这2个文件了

2. 重构onMeasure

@Override
	public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		super.onMeasure(widthMeasureSpec, heightMeasureSpec);

		if (getChildCount() <= 1) {
			throw new IllegalStateException(
					"SwipeRefreshLayout must have the headerview and contentview");
		}

		if (getChildCount() > 2 && !isInEditMode()) {
			throw new IllegalStateException(
					"SwipeRefreshLayout can only host two children");
		}

		if (mHeaderView == null) {
			mHeaderView = getChildAt(0);
			measureChild(mHeaderView, widthMeasureSpec, heightMeasureSpec);
			mHeaderHeight = mHeaderView.getMeasuredHeight();

			mDistanceToTriggerSync = mHeaderHeight;
		}

		getChildAt(1).measure(
				MeasureSpec.makeMeasureSpec(getMeasuredWidth()
						- getPaddingLeft() - getPaddingRight(),
						MeasureSpec.EXACTLY),
				MeasureSpec.makeMeasureSpec(getMeasuredHeight()
						- getPaddingTop() - getPaddingBottom(),
						MeasureSpec.EXACTLY));
	}

还记得我们上面看源码的时候,如果child多余1个,就要报错了,这里要改造,因为Header也是一个布局,所以这里改成2个,同时header为第一个child,而之前的滑动控件为第二个child,同时要量取Header的长宽,并给相应变量赋值,这里的 mHeaderHeight就是Header的高度, mDistanceToTriggerSync是从下拉刷新变成松手刷新的临界点距离,这里是设成了Header的高度

3. 重构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 || getChildCount() == 1) {
			return;
		}
		final View child = getChildAt(1);
		final int childLeft = getPaddingLeft();
		final int childTop = mCurrentTargetOffsetTop + getPaddingTop();
		final int childWidth = width - getPaddingLeft() - getPaddingRight();
		final int childHeight = height - getPaddingTop() - getPaddingBottom();
		child.layout(childLeft, childTop, childLeft + childWidth, childTop
				+ childHeight);

		mHeaderView.layout(childLeft, childTop - mHeaderHeight, childLeft
				+ childWidth, childTop);
	}

就加了一行,就是Header的位置,Header初始时是在手机屏幕上方,其下边缘挨着手机屏幕的顶部

3. 改造ensureTarget

private void ensureTarget() {
		// Don't bother getting the parent height if the parent hasn't been laid
		// out yet.
		if (mTarget == null) {
			if (getChildCount() > 2 && !isInEditMode()) {
				throw new IllegalStateException(
						"SwipeRefreshLayout can only host two children");
			}
			mTarget = getChildAt(1);
			
			// 控制是否允许滚动
			mTarget.setOnTouchListener(new View.OnTouchListener() {
				@Override
				public boolean onTouch(View v, MotionEvent event) {
					return mDisable;
				}
			});
			
			mOriginalOffsetTop = mTarget.getTop() + getPaddingTop();
		}
		if (mDistanceToTriggerSync == -1) {
			if (getParent() != null && ((View) getParent()).getHeight() > 0) {
				final DisplayMetrics metrics = getResources()
						.getDisplayMetrics();
				mDistanceToTriggerSync = (int) Math.min(
						((View) getParent()).getHeight()
								* MAX_SWIPE_DISTANCE_FACTOR,
						REFRESH_TRIGGER_DISTANCE * metrics.density);
			}
		}
	}

这里改动不大,无外乎就是child变成了2个,target要取第2个,这里还有一个 setOnTouchListener,这个最后来讲,先不管

4. 改造onTouchEvent

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

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

		if (!isEnabled() || mReturningToStart || canChildScrollUp() || mStatus == STATUS.REFRESHING) {
			// Fail fast if we're not in a state where a swipe is possible
			return false;
		}

		switch (action) {
		case MotionEvent.ACTION_DOWN:
			mLastMotionY = mInitialMotionY = ev.getY();
			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 yDiff = y - mInitialMotionY;

			if (!mIsBeingDragged && yDiff > mTouchSlop) {
				mIsBeingDragged = true;
			}

			if (mIsBeingDragged) {
				// User velocity passed min velocity; trigger a refresh
				if (yDiff > mDistanceToTriggerSync) {
					if (mStatus == STATUS.NORMAL) {
						mStatus = STATUS.LOOSEN;

						if (mListener != null) {
							mListener.onLoose();
						}
					}
					
					updateContentOffsetTop((int) (yDiff));
				} else {
					if (mStatus == STATUS.LOOSEN) {
						mStatus = STATUS.NORMAL;

						if (mListener != null) {
							mListener.onNormal();
						}
					}

					updateContentOffsetTop((int) (yDiff));
					if (mLastMotionY > y && mTarget.getTop() == getPaddingTop()) {
						// If the user puts the view back at the top, we
						// don't need to. This shouldn't be considered
						// cancelling the gesture as the user can restart from
						// the top.
						removeCallbacks(mCancel);
					}
				}
				mLastMotionY = y;
			}
			break;

		case MotionEventCompat.ACTION_POINTER_DOWN: {
			final int index = MotionEventCompat.getActionIndex(ev);
			mLastMotionY = MotionEventCompat.getY(ev, index);
			mActivePointerId = MotionEventCompat.getPointerId(ev, index);
			break;
		}

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

		case MotionEvent.ACTION_UP:
			if (mStatus == STATUS.LOOSEN) {
				startRefresh();
			} else {
				updatePositionTimeout();
			}

			mIsBeingDragged = false;
			mActivePointerId = INVALID_POINTER;
			return false;
		case MotionEvent.ACTION_CANCEL:
			updatePositionTimeout();

			mIsBeingDragged = false;
			mActivePointerId = INVALID_POINTER;
			return false;
		}

		return true;
	}

这个是改造的重点,首先,最上面的fast fail加了一个判断条件 mStatus == STATUS.REFRESHING,这意思是说如果正在刷新的话,就不要响应下拉事件了,这主要是考虑到刷新时间可能比较长,如果正在刷新的话,肯定再次下拉刷新没有意义,要屏蔽这种情况。

下面看ACTION_MOVE,如果滑动距离大于阀值

if (mStatus == STATUS.NORMAL) {
						mStatus = STATUS.LOOSEN;

						if (mListener != null) {
							mListener.onLoose();
						}
					}

则判断是否下拉刷新状态,是的话则变成松手刷新状态,且调用回调方法,如果滑动距离在阀值之内

if (mStatus == STATUS.LOOSEN) {
						mStatus = STATUS.NORMAL;

						if (mListener != null) {
							mListener.onNormal();
						}
					}

如果当前为松手刷新状态,则重新变成下拉刷新状态,这种情况是用户下拉超过了阀值,但是没有松手,又上拉回去了,这时也要修改状态并调用回调。另外这里还是通过调用updateContentOffsetTop方法来移动控件的位置,不过我做了一些修改:

private void updateContentOffsetTop(int targetTop) {
		final int currentTop = mTarget.getTop();
		if (targetTop > mDistanceToTriggerSync) {
			targetTop = (int) mDistanceToTriggerSync + (int) (targetTop - mDistanceToTriggerSync) / 2; // 超过触发松手刷新的距离后,就只显示滑动一半的距离,避免随手势拉动到最底部,用户体验不好
		} else if (targetTop < 0) {
			targetTop = 0;
		}
		setTargetOffsetTopAndBottom(targetTop - currentTop);
	}

	private void setTargetOffsetTopAndBottom(int offset) {
		mTarget.offsetTopAndBottom(offset);
		mHeaderView.offsetTopAndBottom(offset);
		mCurrentTargetOffsetTop = mTarget.getTop();
		invalidate();
	}

之前SwipeRefreshLayout如果下拉超过了阀值,就将结果赋值为阀值,因为原始的SwipeRefreshLayout是无法下拉超过阀值的,现在肯定不能这样做,现在阀值只是变成松手刷新状态而已,过了阀值还要能继续下拉,但是如果下拉的跟手指滑动完全一样,那就可以把Header下拉到屏幕最底部了,这显然是不好的,所以这里判断,如果超过了阀值,就只滑动实际滑动距离的一半。


再看ACTION_UP,SwipeRefreshLayout默认是滑动到阀值就触发刷新,但改造后,我们要松手才能触发刷新,所以刷新的逻辑要放在ACTION_UP中

if (mStatus == STATUS.LOOSEN) {
				startRefresh();
			} else {
				updatePositionTimeout();
			}

如果当前是松手刷新状态,则调用startRefresh方法进行刷新,否则直接滑回顶部,看下startRefresh方法

private void startRefresh() {
		removeCallbacks(mCancel);
		mReturnToHeaderPosition.run();
		setRefreshing(true);
		mDisable = true;

		if (mListener != null) {
			mListener.onRefresh();
		}
	}


这里的mReturnToHeaderPosition是一个线程,它用来滑动控件到Header刚好能显示的位置,完后调用了回调方法onRefresh

@Override
	public void onRefresh() {
		mHint.setText("正在刷新,请等待");
		
		new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                // 停止刷新
                mSwipeLayout.setRefreshing(false);
                mSwipeLayout.stopRefresh();
                mHint.setText("下拉刷新");
                mPage.loadUrl("http://wap.163.com");
            }
        }, 3000); // 3秒后发送消息,停止刷新
	}

这里就是将Header的文字内容改成了正在刷新,完后模拟网络加载网页的时间,到了3秒后,刷新结束(实际使用时可以在WebView的onPageFinish方法里调用),这里为了看到刷新效果,将腾讯的网址刷新成了网易的,完后调用了 stopRefresh方法,这里就是将控件从刚好显示Header滑动回刚好隐藏Header

最后说一个刚才遗留的问题,就是在ensureTarget里面加的setOnTouchListener,为什么要加这个呢,我们知道,WebView本身是可以上下拖动的,但如果正在刷新,那我肯定是不希望它还能上下动,也就是正在刷新的时候,要禁用WebView,本来想用系统的setEnable来实现,结果发现没用,只好自己来实现了,不过也蛮简单,就是实现onTouchListener,在onTouch方法里去拦截,onTouch方法是先于WebView本身的onTouchEvent被触发的,如果onTouch这里返回true,就根本不会进入WebView的onTouchEvent了,从而达到禁用WebView的目的,默认情况下,onTouch是返回false的,也就是WebView可用

mTarget.setOnTouchListener(new View.OnTouchListener() {
				@Override
				public boolean onTouch(View v, MotionEvent event) {
					return mDisable;
				}
			});

所以初始情况下, mDisable为false,WebView是可用的,你可以点击,也可以上下滑动它,在startRefresh方法中,我们可以看到mDisable被置为true了,也就是这时WebView不可用,也就是正在刷新的时候禁用WebView,而当控件滑回顶部的时候,会再次将它的值置为false,让WebView再次可用。

所有内容就是这些了,明白了基本的原理,想再实现自定义的Header应该就比较容易了

源码下载

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值