Phoenix Pull-to-Refresh 下拉刷新框架源码分析

Phoenix Pull-to-Refresh是一个简洁且美观的Android下拉刷新框架,看它的源码对熟悉View事件传递很有帮助。Phoenix的源码很短,其中关于下拉刷新就是PullToRefreshView这个类,因此我会尽可能说的详细点。

PullToRefreshView类

下拉刷新的核心类。

先看它的初始化:

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

    mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR);
    mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
        mTotalDragDistance = Utils.convertDpToPixel(context, DRAG_MAX_DISTANCE);

    mRefreshView = new ImageView(context);
    mRefreshView.setImageResource(R.drawable.buildings);
    addView(mRefreshView);

    setWillNotDraw(false);
    setChildrenDrawingOrderEnabled(true);
}

mDecelerateInterpolator是动画的差值器。mTouchSlop是一个距离,表示滑动的时候,手的移动要大于这个距离才开始移动控件,在这里用于判断是否可以滑动。mTotalDragDistance是可下拉的最大距离。这三个都是常量。
mRefreshView用于填充下拉弹性区域的内容,为SunRefreshDrawable提供了载体。
对于ViewGroup,如果要执行onDraw,需要去掉其WILL_NOT_DRAW的Flag,这里其实没有要onDraw,这段代码可以去掉。setChildrenDrawingOrderEnabled设置绘制顺序可重定义,需要重写getChildDrawingOrder来变化绘制顺序,这里也可以不要。

onMeasure和onLayout过程:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    ensureTarget();
    if (mTarget == null)
        return;

    widthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth() - mTargetPaddingLeft - mTargetPaddingRight, MeasureSpec.EXACTLY);
    heightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight() - mTargetPaddingTop - mTargetPaddingBottom, MeasureSpec.EXACTLY);
    mTarget.measure(widthMeasureSpec, heightMeasureSpec);
    mRefreshView.measure(widthMeasureSpec, heightMeasureSpec);
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    ensureTarget();
    if (mTarget == null)
        return;

    int width = getMeasuredWidth();
    int height = getMeasuredHeight();
    int left = getPaddingLeft();
    int top = getPaddingTop();
    int right = getPaddingRight();
    int bottom = getPaddingBottom();


    mTarget.layout(left, top + mCurrentOffsetTop, left + width - right, top + height - bottom + mCurrentOffsetTop);
    mRefreshView.layout(left, top, left + width - right, top + height - bottom);
}

mTarget就是下拉的内容对象。onMeasure将mTarget和mRefreshView设置成相同的高度和宽度。onLayout中把mRefreshView的位置固定,通过改变mCurrentOffsetTop值来实现滑动mTarget的效果。

onInterceptTouchEvent过程:

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (!isEnabled() || canChildScrollUp() || mRefreshing)
        return false;

    final int action = ev.getAction();

    switch (action) {
        case MotionEvent.ACTION_DOWN:
            setTargetOffsetTop(0, true);
            mActivePointerId = ev.getPointerId(0);
            mIsBeingDragged = false;
            final float initialMotionY = getMotionEventYByIndex(ev);
            if (initialMotionY == -1)
                return false;
            mInitialMotionY = initialMotionY;
            break;
        case MotionEvent.ACTION_MOVE:
            if (mActivePointerId == INVALID_POINTER)
                return false;
            final float y = getMotionEventYByIndex(ev);
            if (y == -1)
                return false;
            final float yDiff = y - mInitialMotionY;
            if (yDiff > mTouchSlop && !mIsBeingDragged)
                mIsBeingDragged = true;
            break;
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            mIsBeingDragged = false;
            mActivePointerId = INVALID_POINTER;
            break;
        case MotionEvent.ACTION_POINTER_UP:
            onSecondPointerUp(ev);
            break;
    }

    return mIsBeingDragged;
}

这个过程主要是检测手势是否达到了drag的标准,如果达到了就拦截,将后续的事件序列交给onTouchEvent处理。
当view处于disabled,mTarget的内容可以向上滑动,正在刷新其中的一种情况时,不对触摸事件进行拦截。
当action为ACTION_DOWN:mActivePointerId表示触发down事件的手指的Id,这根手指所触发的整个一轮touch事件,该Id是不变的。其他的代码是完成view下拉状态的初始化。
当action为ACTION_MOVE:getMotionEventYByIndex通过mActivePointerId获取当前move的值,再计算出yDiff,从而判断mIsBeingDragged。
当action为ACTION_POINTER_UP:如果当前触摸到屏幕上的不止一根手指,当其中一根手指抬起时触发该事件,onSecondPointerUp的作用是将mActivePointerId指向剩下的还在屏幕上的手指。
当action为ACTION_UP和ACTION_CANCEL时,恢复初始状态,整个过程中都没有达到drag的标准。

onTouchEvent过程:

@Override
public boolean onTouchEvent(MotionEvent ev) {
    ...
    final int action = ev.getAction();
    switch (action) {
        case MotionEvent.ACTION_MOVE: {
            final int pointerIndex = ev.findPointerIndex(mActivePointerId);
            if (pointerIndex < 0)
                return false;
            final float y = getMotionEventYByIndex(ev);
            final float yDiff = y - mInitialMotionY;
            final float scrollTop = yDiff * DRAG_RATE;
            mCurrentDragPercent = scrollTop / mTotalDragDistance;
            if (mCurrentDragPercent < 0)
                return false;
            ...
            mRefreshDrawable.setPercent(mCurrentDragPercent, true);
            setTargetOffsetTop(targetY - mCurrentOffsetTop, true);
            break;
        }
        case MotionEvent.ACTION_POINTER_DOWN:
            final int index = ev.getActionIndex();
            mActivePointerId = ev.getPointerId(index);
            break;
        case MotionEvent.ACTION_POINTER_UP:
            onSecondPointerUp(ev);
            break;
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL: {
            if (mActivePointerId == INVALID_POINTER)
                return false;
            final float y = getMotionEventYByIndex(ev);
            final float overScrollTop = (y - mInitialMotionY) * DRAG_RATE;
            mIsBeingDragged = false;
            if (overScrollTop > mTotalDragDistance) {
                setRefreshing(true, true);
            }else {
                mRefreshing = false;
                animateOffsetToStartPosition();
            }
            mActivePointerId = INVALID_POINTER;
            return false;
        }
    }
    return true;
}

当手势达到了drag的标准后,view会拦截该事件,并将后续的事件序列都交给onTouchEvent处理而不会再经过onInterceptTouchEvent了。我们再来看这个过程的代码。
当action为ACTION_MOVE时,需要计算在这个事件中mTarget期望被拖动的位置的值即targetY,它的计算过程这里就不讨论了。mCurrentOffsetTop为当前mTarget的top值,这个在setTargetOffsetTop函数中可以知道。二者的差值就是这个事件mTarget需要滑动的值。
当action为ACTION_POINTER_DOWN和ACTION_POINTER_UP时,主要作用同之前在onInterceptTouchEvent的ACTION_POINTER_UP相同,都是为了确保mActivePointerId是指向还留在触摸屏上的其中一根手指的Id。
当action为ACTION_UP和MotionEvent.ACTION_CANCEL时,获取此时mTarget滑动的值即overScrollTop,同mTotalDragDistance比较,当overScrollTop大于mTotalDragDistance时,触发刷新动画,mTarget会先利用动画mAnimateToCorrectPosition弹回到mTotalDragDistance的位置,再在刷新好后利用动画mAnimateToStartPosition弹回初始位置;当overScrollTop小于mTotalDragDistance时,不触发刷新直接弹回初始位置。

关于PullToRefreshView的源码分析就到这,上面贴的代码可能跟原始项目中的代码有些不同,因为我把MotionEventCompat这个类直接用MotionEvent替换了,其他是一样的。分析的过程中如有错误的地方,还请指出。

最后是Phoenix Pull-to-Refresh项目的地址:https://github.com/Yalantis/Phoenix

你可能感兴趣的:(Phoenix Pull-to-Refresh 下拉刷新框架源码分析)