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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,837评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,551评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,417评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,448评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,524评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,554评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,569评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,316评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,766评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,077评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,240评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,912评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,560评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,176评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,425评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,114评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,114评论 2 352

推荐阅读更多精彩内容