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

Phoenix Pull-to-Refresh是一個(gè)簡(jiǎn)潔且美觀的Android下拉刷新框架刨晴,看它的源碼對(duì)熟悉View事件傳遞很有幫助卸留。Phoenix的源碼很短空镜,其中關(guān)于下拉刷新就是PullToRefreshView這個(gè)類染厅,因此我會(huì)盡可能說(shuō)的詳細(xì)點(diǎn)桑腮。

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是動(dòng)畫的差值器。mTouchSlop是一個(gè)距離破讨,表示滑動(dòng)的時(shí)候丛晦,手的移動(dòng)要大于這個(gè)距離才開(kāi)始移動(dòng)控件,在這里用于判斷是否可以滑動(dòng)提陶。mTotalDragDistance是可下拉的最大距離烫沙。這三個(gè)都是常量。
mRefreshView用于填充下拉彈性區(qū)域的內(nèi)容隙笆,為SunRefreshDrawable提供了載體锌蓄。
對(duì)于ViewGroup,如果要執(zhí)行onDraw撑柔,需要去掉其WILL_NOT_DRAW的Flag瘸爽,這里其實(shí)沒(méi)有要onDraw,這段代碼可以去掉铅忿。setChildrenDrawingOrderEnabled設(shè)置繪制順序可重定義剪决,需要重寫getChildDrawingOrder來(lái)變化繪制順序,這里也可以不要。

onMeasure和onLayout過(guò)程:

@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就是下拉的內(nèi)容對(duì)象昼捍。onMeasure將mTarget和mRefreshView設(shè)置成相同的高度和寬度。onLayout中把mRefreshView的位置固定肢扯,通過(guò)改變mCurrentOffsetTop值來(lái)實(shí)現(xiàn)滑動(dòng)mTarget的效果妒茬。

onInterceptTouchEvent過(guò)程:

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

這個(gè)過(guò)程主要是檢測(cè)手勢(shì)是否達(dá)到了drag的標(biāo)準(zhǔn),如果達(dá)到了就攔截蔚晨,將后續(xù)的事件序列交給onTouchEvent處理乍钻。
當(dāng)view處于disabled,mTarget的內(nèi)容可以向上滑動(dòng)铭腕,正在刷新其中的一種情況時(shí)银择,不對(duì)觸摸事件進(jìn)行攔截。
當(dāng)action為ACTION_DOWN:mActivePointerId表示觸發(fā)down事件的手指的Id累舷,這根手指所觸發(fā)的整個(gè)一輪touch事件浩考,該Id是不變的。其他的代碼是完成view下拉狀態(tài)的初始化被盈。
當(dāng)action為ACTION_MOVE:getMotionEventYByIndex通過(guò)mActivePointerId獲取當(dāng)前move的值析孽,再計(jì)算出yDiff,從而判斷mIsBeingDragged只怎。
當(dāng)action為ACTION_POINTER_UP:如果當(dāng)前觸摸到屏幕上的不止一根手指袜瞬,當(dāng)其中一根手指抬起時(shí)觸發(fā)該事件,onSecondPointerUp的作用是將mActivePointerId指向剩下的還在屏幕上的手指身堡。
當(dāng)action為ACTION_UP和ACTION_CANCEL時(shí)邓尤,恢復(fù)初始狀態(tài),整個(gè)過(guò)程中都沒(méi)有達(dá)到drag的標(biāo)準(zhǔn)贴谎。

onTouchEvent過(guò)程:

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

當(dāng)手勢(shì)達(dá)到了drag的標(biāo)準(zhǔn)后汞扎,view會(huì)攔截該事件,并將后續(xù)的事件序列都交給onTouchEvent處理而不會(huì)再經(jīng)過(guò)onInterceptTouchEvent了擅这。我們?cè)賮?lái)看這個(gè)過(guò)程的代碼佩捞。
當(dāng)action為ACTION_MOVE時(shí),需要計(jì)算在這個(gè)事件中mTarget期望被拖動(dòng)的位置的值即targetY蕾哟,它的計(jì)算過(guò)程這里就不討論了一忱。mCurrentOffsetTop為當(dāng)前mTarget的top值,這個(gè)在setTargetOffsetTop函數(shù)中可以知道谭确。二者的差值就是這個(gè)事件mTarget需要滑動(dòng)的值帘营。
當(dāng)action為ACTION_POINTER_DOWN和ACTION_POINTER_UP時(shí),主要作用同之前在onInterceptTouchEvent的ACTION_POINTER_UP相同逐哈,都是為了確保mActivePointerId是指向還留在觸摸屏上的其中一根手指的Id芬迄。
當(dāng)action為ACTION_UP和MotionEvent.ACTION_CANCEL時(shí),獲取此時(shí)mTarget滑動(dòng)的值即overScrollTop昂秃,同mTotalDragDistance比較禀梳,當(dāng)overScrollTop大于mTotalDragDistance時(shí)杜窄,觸發(fā)刷新動(dòng)畫,mTarget會(huì)先利用動(dòng)畫mAnimateToCorrectPosition彈回到mTotalDragDistance的位置算途,再在刷新好后利用動(dòng)畫mAnimateToStartPosition彈回初始位置塞耕;當(dāng)overScrollTop小于mTotalDragDistance時(shí),不觸發(fā)刷新直接彈回初始位置嘴瓤。

關(guān)于PullToRefreshView的源碼分析就到這扫外,上面貼的代碼可能跟原始項(xiàng)目中的代碼有些不同,因?yàn)槲野袽otionEventCompat這個(gè)類直接用MotionEvent替換了廓脆,其他是一樣的筛谚。分析的過(guò)程中如有錯(cuò)誤的地方,還請(qǐng)指出停忿。

最后是Phoenix Pull-to-Refresh項(xiàng)目的地址:https://github.com/Yalantis/Phoenix

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末驾讲,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子席赂,更是在濱河造成了極大的恐慌蝎毡,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,718評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件氧枣,死亡現(xiàn)場(chǎng)離奇詭異沐兵,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)便监,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,683評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門扎谎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人烧董,你說(shuō)我怎么就攤上這事毁靶。” “怎么了逊移?”我有些...
    開(kāi)封第一講書人閱讀 158,207評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵预吆,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我胳泉,道長(zhǎng)拐叉,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 56,755評(píng)論 1 284
  • 正文 為了忘掉前任扇商,我火速辦了婚禮凤瘦,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘案铺。我一直安慰自己蔬芥,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,862評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著笔诵,像睡著了一般返吻。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上乎婿,一...
    開(kāi)封第一講書人閱讀 50,050評(píng)論 1 291
  • 那天测僵,我揣著相機(jī)與錄音,去河邊找鬼次酌。 笑死恨课,一個(gè)胖子當(dāng)著我的面吹牛舆乔,可吹牛的內(nèi)容都是我干的岳服。 我是一名探鬼主播,決...
    沈念sama閱讀 39,136評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼希俩,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼吊宋!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起颜武,我...
    開(kāi)封第一講書人閱讀 37,882評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤璃搜,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后鳞上,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體这吻,經(jīng)...
    沈念sama閱讀 44,330評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,651評(píng)論 2 327
  • 正文 我和宋清朗相戀三年篙议,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了唾糯。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,789評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡鬼贱,死狀恐怖移怯,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情这难,我是刑警寧澤舟误,帶...
    沈念sama閱讀 34,477評(píng)論 4 333
  • 正文 年R本政府宣布,位于F島的核電站姻乓,受9級(jí)特大地震影響嵌溢,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蹋岩,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,135評(píng)論 3 317
  • 文/蒙蒙 一堵腹、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧星澳,春花似錦疚顷、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,864評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)阀坏。三九已至,卻和暖如春笆檀,著一層夾襖步出監(jiān)牢的瞬間忌堂,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,099評(píng)論 1 267
  • 我被黑心中介騙來(lái)泰國(guó)打工酗洒, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留士修,地道東北人喘垂。 一個(gè)月前我還...
    沈念sama閱讀 46,598評(píng)論 2 362
  • 正文 我出身青樓俱恶,卻偏偏與公主長(zhǎng)得像篡石,于是被迫代替她去往敵國(guó)和親优幸。 傳聞我的和親對(duì)象是個(gè)殘疾皇子蜕径,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,697評(píng)論 2 351

推薦閱讀更多精彩內(nèi)容