Yalantis開源項(xiàng)目Phoenix-Pull-to-Refresh源碼分析

GitHub上有個(gè)非常漂亮的Android下拉刷新框架,是由Yalantis開源的呆躲,看如下效果圖:

Phoenix-Pull-to-Refresh

在我自己做的項(xiàng)目中也用到了這樣的下拉刷新樣式,今天就來分析下它的源碼冀自。
項(xiàng)目地址:https://github.com/Yalantis/Phoenix


看下這個(gè)項(xiàng)目的library結(jié)構(gòu):

Phoenix-Pull-to-Refresh的Labrary結(jié)構(gòu)

除了一個(gè)工具包汽纠,真正涉及到下拉刷新UI邏輯的就只有3個(gè)類:PullToRefreshView吱涉,BaseRefreshVeiw刹泄,SunRefreshView

官方給出的使用demo:

<com.yalantis.phoenix.PullToRefreshView
    android:id="@+id/pull_to_refresh"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ListView
        android:id="@+id/list_view"
        android:divider="@null"
        android:dividerHeight="0dp"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</com.yalantis.phoenix.PullToRefreshView>
mPullToRefreshView = (PullToRefreshView) findViewById(R.id.pull_to_refresh);
mPullToRefreshView.setOnRefreshListener(new PullToRefreshView.OnRefreshListener() {
    @Override
    public void onRefresh() {
        mPullToRefreshView.postDelayed(new Runnable() {
            @Override
            public void run() {
                mPullToRefreshView.setRefreshing(false);
            }
        }, REFRESH_DELAY);
    }
 });

特別簡單怎爵,是不是有種熟悉的感覺特石,基本上就和SwipeRefreshLayout的使用方式一樣。


1.PullToRefreshView

PullToRefreshView繼承自ViewGroup鳖链,那么我們就按照自定義ViewGroup的套路來進(jìn)行分析姆蘸。
構(gòu)造函數(shù)中初始化一些屬性:

public PullToRefreshView(Context context, AttributeSet attrs) {
    super(context, attrs);
    // 自定義屬性(實(shí)際上這個(gè)屬性沒什么用處)
    TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RefreshView);
    final int type = a.getInteger(R.styleable.RefreshView_type, STYLE_SUN);
    a.recycle();
    // 動(dòng)畫插值器
    mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR);
    // 滑動(dòng)觸發(fā)的臨界距離
    mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    // 觸發(fā)下拉刷新拖動(dòng)的總距離
    mTotalDragDistance = Utils.convertDpToPixel(context, DRAG_MAX_DISTANCE);
    // 頭部刷新的ImageView
    mRefreshView = new ImageView(context);
    // 根據(jù)type設(shè)置刷新樣式
    setRefreshStyle(type);
    // 將頭部刷新ImageVeiw添加到當(dāng)前的PullToRefreshView
    addView(mRefreshView);

    setWillNotDraw(false);
    ViewCompat.setChildrenDrawingOrderEnabled(this, true);
}

public void setRefreshStyle(int type) {
    setRefreshing(false);
    switch (type) {
        case STYLE_SUN:
            // new一個(gè)刷新的Drawable
            mBaseRefreshView = new SunRefreshView(getContext(), this);
            break;
        default:
            throw new InvalidParameterException("Type does not exist");
    }
    // 設(shè)置頭部刷新的ImageView(mRefreshView)設(shè)自定義的Drawable(mBaseRefreshView)
    mRefreshView.setImageDrawable(mBaseRefreshView);
}

根據(jù)上文中PullToRefreshView的使用方式墩莫,同時(shí)在構(gòu)造函數(shù)中向PullToRefreshView添加了一個(gè)ImageView(mRefreshView),可以看出整個(gè)PullToRefreshView中就只有兩個(gè)子控件:mRefreshView逞敷,mTarget狂秦。
mRefreshView就是下拉及刷新過程頭部用來展示動(dòng)畫的ImageView
mTarget就是需要刷新的目標(biāo)View推捐,比如RecylerView裂问,ListViewScrollView牛柒。

onMeasure

測量子控件(mTarget堪簿,mRefreshView

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    // 確保需要刷新的子控件Target已經(jīng)添加
    ensureTarget();
    if (mTarget == null) return;
    // 測量mTarget和mRefreshView
    widthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth() - getPaddingRight() - getPaddingLeft(), MeasureSpec.EXACTLY);
    heightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY);
    mTarget.measure(widthMeasureSpec, heightMeasureSpec);
    mRefreshView.measure(widthMeasureSpec, heightMeasureSpec);
}

private void ensureTarget() {
    if (mTarget != null) return;
    if (getChildCount() > 0) {
        // 遍歷子View,找到mTarget
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            if (child != mRefreshView) {
                mTarget = child;
                mTargetPaddingBottom = mTarget.getPaddingBottom();
                mTargetPaddingLeft = mTarget.getPaddingLeft();
                mTargetPaddingRight = mTarget.getPaddingRight();
                mTargetPaddingTop = mTarget.getPaddingTop();
            }
        }
    }
}
onLayout

布局子控件(mTarget皮壁,mRefreshView):

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    ensureTarget();
    if (mTarget == null) return;
    // 獲取PullToRefreshView的寬高以及padding值
    int height = getMeasuredHeight();
    int width = getMeasuredWidth();
    int left = getPaddingLeft();
    int top = getPaddingTop();
    int right = getPaddingRight();
    int bottom = getPaddingBottom();
    // 根據(jù)PullToRefreshView的寬高和內(nèi)邊界來布局mTarget椭更、mRefreshView
    mTarget.layout(left, top + mCurrentOffsetTop, left + width - right, top + height - bottom + mCurrentOffsetTop);
    mRefreshView.layout(left, top, left + width - right, top + height - bottom);
}
onInterceptTouchEvent

重點(diǎn)來了,攔截TouchEvent蛾魄。mTarget一般都是可以滾動(dòng)的虑瀑,要保證下拉刷新滾動(dòng)和子控件mTarget內(nèi)部的滑動(dòng)不沖突,所以就需要重寫攔截邏輯:

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    // enable 或者 mTarget能滑動(dòng) 或者 正在刷新滴须,此時(shí)不攔截舌狗,交給child來分發(fā)ev
    if (!isEnabled() || canChildScrollUp() || mRefreshing) {
        return false;
    }
    // 事件action
    final int action = MotionEventCompat.getActionMasked(ev);
    // 根據(jù)事件類型,處理攔截邏輯
    switch (action) {
        case MotionEvent.ACTION_DOWN:
            // 手指down時(shí)扔水,設(shè)置mTarget的偏移量為0
            // 相當(dāng)于初始化mTarget的mCurrentOffsetTop以及頭部刷新的Drawable(mBaseRefreshView)
            setTargetOffsetTop(0, true);
            // 活動(dòng)手指ID(觸發(fā)拖動(dòng)的手指)
            mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
            // 不是正在被拖動(dòng)
            mIsBeingDragged = false;
            // 活動(dòng)手指初始按下時(shí)的Y坐標(biāo)
            final float initialMotionY = getMotionEventY(ev, mActivePointerId);
            if (initialMotionY == -1) {
                return false;
            }
            mInitialMotionY = initialMotionY;
            break;
        case MotionEvent.ACTION_MOVE:
            if (mActivePointerId == INVALID_POINTER) {
                return false;
            }
            // 獲取活動(dòng)手指的Y坐標(biāo)(當(dāng)前可能有多個(gè)手指在屏幕上move把夸,只需處理活動(dòng)手指即可)
            final float y = getMotionEventY(ev, mActivePointerId);
            if (y == -1) {
                return false;
            }
            // 移動(dòng)的距離yDiff大于臨界值并且當(dāng)前沒有被拖動(dòng)
            // 改變拖動(dòng)的狀態(tài)值mIsBeingDragged為正在拖動(dòng)
            final float yDiff = y - mInitialMotionY;
            if (yDiff > mTouchSlop && !mIsBeingDragged) {
                mIsBeingDragged = true;
            }
            break;
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            // 手指cancel或者up,拖動(dòng)狀態(tài)為false铭污,活動(dòng)手指invalid。
            mIsBeingDragged = false;
            mActivePointerId = INVALID_POINTER;
            break;
        case MotionEventCompat.ACTION_POINTER_UP:
            // 多個(gè)手指在屏幕上膀篮,當(dāng)?shù)诙€(gè)手指抬起時(shí)嘹狞,需要更新活動(dòng)手指
            onSecondaryPointerUp(ev);
            break;
    }
    // 只要當(dāng)前處于拖動(dòng)狀態(tài),就攔截事件誓竿,否則不攔截
    return mIsBeingDragged;
}

/**
 * 當(dāng)有多個(gè)手指在屏幕上時(shí)磅网,有一個(gè)手指抬起時(shí),需要處理的邏輯
 * 多點(diǎn)觸控時(shí)筷屡,手指的down和up之間涧偷,只有通過ID才能識(shí)別手指,當(dāng)有手指抬起時(shí)毙死,需要更新活動(dòng)手指燎潮。
 * @param ev
 */
private void onSecondaryPointerUp(MotionEvent ev) {
    final int pointerIndex = MotionEventCompat.getActionIndex(ev);
    final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
    if (pointerId == mActivePointerId) {
        final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
        mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
    }
}

關(guān)于事件攔截機(jī)制和多點(diǎn)觸控相關(guān)的解析可以參考大牛非著名程序員的博客:http://www.gcssloop.com/

onTouchEvent

攔截到的事件,要在onTouchEvent中來處理扼倘,實(shí)現(xiàn)mTarget拖動(dòng)的UI邏輯确封。

@Override
public boolean onTouchEvent(@NonNull MotionEvent ev) {
    // 當(dāng)前沒有被拖動(dòng),不處理
    if (!mIsBeingDragged) {
        return super.onTouchEvent(ev);
    }
    // 根據(jù)事件action處理不同事件邏輯
    final int action = MotionEventCompat.getActionMasked(ev);
    switch (action) {
        case MotionEvent.ACTION_MOVE: {
            // 獲取當(dāng)前事件中活動(dòng)手指的pointerIndex
            final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
            if (pointerIndex < 0) {
                return false;
            }
            // 根據(jù)活動(dòng)手指pointerIndex獲取Y坐標(biāo)
            // 計(jì)算出手指的移動(dòng)距離yDiff
            final float y = MotionEventCompat.getY(ev, pointerIndex);
            final float yDiff = y - mInitialMotionY;
            // mTarget需要滾動(dòng)的距離scrollTop
            final float scrollTop = yDiff * DRAG_RATE;
            // 當(dāng)前拖動(dòng)百分比mCurrentDragPercent
            mCurrentDragPercent = scrollTop / mTotalDragDistance;
            if (mCurrentDragPercent < 0) {
                return false;
            }
            // 以下邏輯都是根據(jù)當(dāng)前拖動(dòng)百分比和mTarget需要滾動(dòng)的距離來計(jì)算出當(dāng)前move事件中mTarget需要達(dá)到的目標(biāo)Y坐標(biāo)
            // 即每一次移動(dòng)都需要計(jì)算出即將要達(dá)到的位置的Y坐標(biāo),通過該即將到達(dá)的Y坐標(biāo)以及當(dāng)前的偏移量爪喘,
            // 就能計(jì)算出這次手指移動(dòng)時(shí)mTarget所需要的偏移量
            // 做如此處理主要是讓拖動(dòng)距離超過觸發(fā)刷新的距離時(shí)繼續(xù)拖動(dòng)有一個(gè)阻尼效果
            float boundedDragPercent = Math.min(1f, Math.abs(mCurrentDragPercent));
            float extraOS = Math.abs(scrollTop) - mTotalDragDistance;
            float slingshotDist = mTotalDragDistance;
            float tensionSlingshotPercent = Math.max(0,
                    Math.min(extraOS, slingshotDist * 2) / slingshotDist);
            float tensionPercent = (float) ((tensionSlingshotPercent / 4) - Math.pow(
                    (tensionSlingshotPercent / 4), 2)) * 2f;
            float extraMove = (slingshotDist) * tensionPercent / 2;
            // targetY為此次手指移動(dòng)mTarget即將達(dá)到的Y坐標(biāo)
            int targetY = (int) ((slingshotDist * boundedDragPercent) + extraMove);
            // 設(shè)置mBaseRefreshView(頭部刷新Drawable)的百分比颜曾,用以更新刷新動(dòng)畫
            mBaseRefreshView.setPercent(mCurrentDragPercent, true);
            // 設(shè)置mTarget偏移量,實(shí)現(xiàn)下拉
            setTargetOffsetTop(targetY - mCurrentOffsetTop, true);
            break;
        }
        case MotionEventCompat.ACTION_POINTER_DOWN:
            // 新的手指按下時(shí)秉剑,更新觸發(fā)拖動(dòng)活動(dòng)手指
            final int index = MotionEventCompat.getActionIndex(ev);
            mActivePointerId = MotionEventCompat.getPointerId(ev, index);
            break;
        case MotionEventCompat.ACTION_POINTER_UP:
            // 多點(diǎn)觸控泛豪,手指抬起時(shí)更新活動(dòng)手指
            onSecondaryPointerUp(ev);
            break;
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL: {
            if (mActivePointerId == INVALID_POINTER) {
                return false;
            }
            // 手指up或cancel,根據(jù)活動(dòng)手指計(jì)算出mTarget滾動(dòng)的距離overScrollTop
            final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
            final float y = MotionEventCompat.getY(ev, pointerIndex);
            final float overScrollTop = (y - mInitialMotionY) * DRAG_RATE;
            // 改變拖動(dòng)狀體
            mIsBeingDragged = false;
            if (overScrollTop > mTotalDragDistance) {
                // mTarget被拖動(dòng)的距離大于觸發(fā)刷新的拖動(dòng)距離時(shí)侦鹏,設(shè)置當(dāng)前刷新狀態(tài)true
                setRefreshing(true, true);
            } else {
                // 否則诡曙,當(dāng)前刷新狀態(tài)為false,并且通過動(dòng)畫讓mTarget回到最初狀態(tài)
                mRefreshing = false;
                animateOffsetToStartPosition();
            }
            // 活動(dòng)手指invelid
            mActivePointerId = INVALID_POINTER;
            return false;
        }
    }
    // 消費(fèi)掉當(dāng)前Touch事件
    return true;
}

手指在屏幕滑動(dòng)時(shí),mTarget的整個(gè)拖動(dòng)邏輯都是在onTouchEvent中實(shí)現(xiàn)种柑。
setRefershing方法中岗仑,設(shè)置PullToRefreshView當(dāng)前的刷新狀態(tài):
1.通過動(dòng)畫將mTarget偏移到正在刷新的位置
2.通過動(dòng)畫將mTarget偏移到初始位置

/**
 * 設(shè)置PullToRefreshView的刷新狀態(tài)
 *
 * @param refreshing 是否正在刷新
 * @param notify     是否回調(diào)onRefresh
 */
private void setRefreshing(boolean refreshing, final boolean notify) {
    if (mRefreshing != refreshing) {
        mNotify = notify;
        ensureTarget();
        mRefreshing = refreshing;
        if (mRefreshing) {
            // 正在刷新,設(shè)置刷新Drawable(mBaseRefreshView)的percent聚请,用以更新刷新動(dòng)畫
            mBaseRefreshView.setPercent(1f, true);
            // 通過動(dòng)畫讓mTarget偏移到正在刷新的位置荠雕。
            animateOffsetToCorrectPosition();
        } else {
            // 不是正在刷新,通過動(dòng)畫使mTarget偏移到初始位置驶赏。
            animateOffsetToStartPosition();
        }
    }
}

在通過動(dòng)畫來偏移mTarget的邏輯比較簡單炸卑,同樣也是通過動(dòng)畫的執(zhí)行過程來不斷調(diào)用setTargetOffsetTop方法來移動(dòng)mTarget
PullToRefresh中煤傍,主要是處理攔截到的move事件盖文,通過move事件計(jì)算出mTarget所需的偏移量來實(shí)現(xiàn)mTarget的拖動(dòng)。同時(shí)在手指up時(shí)蚯姆,通過當(dāng)前拖動(dòng)偏移量mCurrentOffsetTop五续、觸發(fā)刷新的拖動(dòng)距離mTotalDragDistance比較來決定是通過動(dòng)畫將mTarget偏移到正在刷新的位置和最初始的位置。
總之龄恋,PullToRefershView是通過mTarget的偏移來實(shí)現(xiàn)下拉拖動(dòng)疙驾。mTarget偏移的同時(shí),將當(dāng)前拖動(dòng)百分比mCurrentDragPercent設(shè)置到刷新的Drawable(mBaseRefreshView)中郭毕,更新刷新動(dòng)畫它碎。


2.BaseRefreshVeiw

這個(gè)類是自定義刷新Drawable抽象類,繼承自Drawable显押,并且實(shí)現(xiàn)了Animable接口扳肛。

public abstract class BaseRefreshView extends Drawable implements Drawable.Callback, Animatable {

    private PullToRefreshLayout mRefreshLayout;
    private boolean mEndOfRefreshing;

    public BaseRefreshView(Context context, PullToRefreshLayout layout) {
        mRefreshLayout = layout;
    }

    public Context getContext() {
        return mRefreshLayout != null ? mRefreshLayout.getContext() : null;
    }

    public PullToRefreshLayout getRefreshLayout() {
        return mRefreshLayout;
    }
    /**
    * 設(shè)置拖動(dòng)百分比,用以更新Drawable中的動(dòng)畫
    */
    public abstract void setPercent(float percent, boolean invalidate);

    /**
    * 設(shè)置偏移量乘碑,用以更新Drawable中的動(dòng)畫
    */
    public abstract void offsetTopAndBottom(int offset);

    // ...去掉一些無用代碼...

    @Override
    public int getOpacity() {
        return PixelFormat.TRANSLUCENT;
    }

    @Override
    public void setAlpha(int alpha) {

    }

    @Override
    public void setColorFilter(ColorFilter cf) {

    }

   // ...去掉一些無用代碼...

}

BaseRefreshView作為一個(gè)抽象類挖息,我們可以繼承它來實(shí)現(xiàn)不同樣式的刷新動(dòng)畫。在PullToRefershView中根據(jù)拖動(dòng)實(shí)時(shí)調(diào)用setPercent(float percent, boolean invalidate)蝉仇,offsetTopAndBottom(int offset)這兩個(gè)方法就可以實(shí)時(shí)更新動(dòng)畫旋讹。


3.SunRefreshView

具體實(shí)現(xiàn)就是SunRefreshView殖蚕,繼承自BaseRefreshView,具體的動(dòng)畫邏輯躲在SunRefreshView中實(shí)現(xiàn)沉迹。

@Override
public void setPercent(float percent, boolean invalidate) {
    setPercent(percent);
    if (invalidate) setRotate(percent);
}

@Override
public void offsetTopAndBottom(int offset) {
    mTop += offset;
    invalidateSelf();
}

public void setPercent(float percent) {
    mPercent = percent;
}

public void setRotate(float rotate) {
    mRotate = rotate;
    invalidateSelf();
}

實(shí)現(xiàn)抽象父類BaseRefreshView的兩個(gè)方法睦疫,offsetTopAndBottom(int offset)方法改變mTopsetPercent(float percent, boolean invalidate)方法設(shè)置mPercentmRotate鞭呕,兩個(gè)方法都會(huì)重繪自己蛤育。
PullToRefresh在手指拖動(dòng)過程中不斷調(diào)用這兩個(gè)方法,達(dá)到拖動(dòng)時(shí)Drawable跟隨變化的動(dòng)效葫松。
當(dāng)手指松開時(shí)瓦糕,PullToRefreshView自動(dòng)回到正在刷新的狀態(tài)或者初始狀態(tài),SunRefreshView的動(dòng)效變化是通過調(diào)用start()stop()方法腋么,在start()stop()中開始和結(jié)束mAnimation動(dòng)畫咕娄。

@Override
public void start() {
    mAnimation.reset();
    isRefreshing = true;
    mParent.startAnimation(mAnimation);
}

@Override
public void stop() {
    mParent.clearAnimation();
    isRefreshing = false;
    resetOriginals();
}

private void setupAnimations() {
    mAnimation = new Animation() {
        @Override
        public void applyTransformation(float interpolatedTime, Transformation t) {
            // 根據(jù)動(dòng)畫時(shí)間來設(shè)置(sun)旋轉(zhuǎn),然后重繪
            setRotate(interpolatedTime);
        }
    };
    mAnimation.setRepeatCount(Animation.INFINITE);
    mAnimation.setRepeatMode(Animation.RESTART);
    mAnimation.setInterpolator(LINEAR_INTERPOLATOR);
    mAnimation.setDuration(ANIMATION_DURATION);
}

不管PullToRefreshView調(diào)用setPercent(float percent, boolean invalidate)offsetTopAndBottom(int offset)方法珊擂,還是手指松開時(shí)調(diào)用的start()stop()方法圣勒,最終都是要改變這三個(gè)變量:mPercentmRotate摧扇、mTop圣贸。
因?yàn)檎麄€(gè)下拉刷新的頭部動(dòng)效都是通過SunRefreshView這個(gè)Drawable來不斷重繪自己實(shí)現(xiàn)的。

@Override
public void draw(Canvas canvas) {
    if (mScreenWidth <= 0) return;
    // 保存當(dāng)前畫布狀態(tài)扛稽,然后平移吁峻、裁剪畫布
    final int saveCount = canvas.save();
    canvas.translate(0, mTop);
    canvas.clipRect(0, -mTop, mScreenWidth, mParent.getTotalDragDistance());
    // 繪制sky、sun在张、town
    drawSky(canvas);
    drawSun(canvas);
    drawTown(canvas);
    // 繪制完成后恢復(fù)畫布狀態(tài)
    canvas.restoreToCount(saveCount);
}

繪制過程按照mTop畫布會(huì)進(jìn)行平移和裁剪用含。
繪制sky:
sky的動(dòng)畫過程中由縮放和平移兩個(gè)動(dòng)畫組成“镓遥縮放是通過mPrercent計(jì)算出縮放比例skyScale耕餐,平移是通過縮放比例skyScalePullToRefreshView的拖動(dòng)比例mTotalDragDistance計(jì)算出x和y方向的偏移量。

private void drawSky(Canvas canvas) {
    Matrix matrix = mMatrix;
    matrix.reset();
    // 拖動(dòng)比例
    float dragPercent = Math.min(1f, Math.abs(mPercent));
    float skyScale; // sky縮放比例
    float scalePercentDelta = dragPercent - SCALE_START_PERCENT;
    // SCALE_START_PERCENT = 0.5f辟狈,SKY_INITIAL_SCALE = 1.05f
    // 拖動(dòng)比例大于0.5時(shí),sky縮放比例為SKY_INITIAL_SCALE - (SKY_INITIAL_SCALE - 1.0f) * scalePercent;
    // 拖動(dòng)比例小于0.5時(shí)夏跷,sky縮放比例就為SKY_INITIAL_SCALE
    if (scalePercentDelta > 0) {
        /** Change skyScale between {@link #SKY_INITIAL_SCALE} and 1.0f depending on {@link #mPercent} */
        float scalePercent = scalePercentDelta / (1.0f - SCALE_START_PERCENT);
        skyScale = SKY_INITIAL_SCALE - (SKY_INITIAL_SCALE - 1.0f) * scalePercent;
    } else {
        skyScale = SKY_INITIAL_SCALE;
    }
    // 根據(jù)縮放比例skyScale就算出offsetX和offsetY.
    float offsetX = -(mScreenWidth * skyScale - mScreenWidth) / 2.0f;
    float offsetY = (1.0f - dragPercent) * mParent.getTotalDragDistance() - mSkyTopOffset // Offset canvas moving
            - mSkyHeight * (skyScale - 1.0f) / 2 // Offset sky scaling
            + mSkyMoveOffset * dragPercent; // Give it a little move top -> bottom
    matrix.postScale(skyScale, skyScale);
    matrix.postTranslate(offsetX, offsetY);
    // 繪制sky
    canvas.drawBitmap(mSky, matrix, null);
}

skyScale哼转、x方向偏移量offsetX,y方向偏移量offsetY不斷變化來重繪sky槽华,實(shí)現(xiàn)動(dòng)畫效果壹蔓。
繪制sun:
sun在下拉刷新和釋放時(shí),有三個(gè)動(dòng)畫:上下平移猫态、旋轉(zhuǎn)佣蓉、縮放披摄。

private void drawSun(Canvas canvas) {
    Matrix matrix = mMatrix;
    matrix.reset();
    float dragPercent = mPercent;
    if (dragPercent > 1.0f) { // Slow down if pulling over set height
        dragPercent = (dragPercent + 9.0f) / 10;
    }
    float sunRadius = (float) mSunSize / 2.0f;
    float sunRotateGrowth = SUN_INITIAL_ROTATE_GROWTH;
    // 偏移量offsetX和offsetY決定sun的位置
    // 在重繪的過程中根據(jù)mPercent和mTop實(shí)現(xiàn)上下平移
    float offsetX = mSunLeftOffset;
    float offsetY = mSunTopOffset
            + (mParent.getTotalDragDistance() / 2) * (1.0f - dragPercent) // Move the sun up
            - mTop; // Depending on Canvas position
    // 根據(jù)拖動(dòng)比例mPercent計(jì)算縮放比例
    float scalePercentDelta = dragPercent - SCALE_START_PERCENT;
    if (scalePercentDelta > 0) {
        float scalePercent = scalePercentDelta / (1.0f - SCALE_START_PERCENT);
        float sunScale = 1.0f - (1.0f - SUN_FINAL_SCALE) * scalePercent;
        sunRotateGrowth += (SUN_FINAL_ROTATE_GROWTH - SUN_INITIAL_ROTATE_GROWTH) * scalePercent;

        matrix.preTranslate(offsetX + (sunRadius - sunRadius * sunScale), offsetY * (2.0f - sunScale));
        matrix.preScale(sunScale, sunScale);
        // 縮放的同時(shí)要改變偏移量(保證縮放和上下平移時(shí),sun的中心在豎直方向)
        offsetX += sunRadius;
        offsetY = offsetY * (2.0f - sunScale) + sunRadius * sunScale;
    } else {
        matrix.postTranslate(offsetX, offsetY);
        // 縮放的同時(shí)要改變偏移量(保證縮放和上下平移時(shí)勇凭,sun的中心在豎直方向)
        offsetX += sunRadius;
        offsetY += sunRadius;
    }

    // 根據(jù)mRotate計(jì)算旋轉(zhuǎn)的角度
    // 拖動(dòng)時(shí)旋轉(zhuǎn)方向?yàn)轫槙r(shí)針疚膊,釋放或正在刷新為逆時(shí)針方向。
    // 拖動(dòng)時(shí)或釋放后旋轉(zhuǎn)的角度按照拖動(dòng)的幅度來旋轉(zhuǎn)虾标,正在刷新時(shí)每次繪制旋轉(zhuǎn)1°
    matrix.postRotate(
            (isRefreshing ? -360 : 360) * mRotate * (isRefreshing ? 1 : sunRotateGrowth),
            offsetX,
            offsetY);
    // 繪制sun
    canvas.drawBitmap(mSun, matrix, null);
}

sun的動(dòng)畫相對(duì)來說要復(fù)雜些寓盗,主要邏輯就是根據(jù)mPercent來計(jì)算偏移量和縮放比例,根據(jù)mRotatesunRotateGrowth來計(jì)算旋轉(zhuǎn)角度璧函。
繪制town:
town的繪制邏輯和sky一樣傀蚌,只涉及到平移和縮放。

private void drawTown(Canvas canvas) {
    Matrix matrix = mMatrix;
    matrix.reset();
    float dragPercent = Math.min(1f, Math.abs(mPercent));
    float townScale;
    float townTopOffset;
    float townMoveOffset;
    // 計(jì)算縮放比例
    float scalePercentDelta = dragPercent - SCALE_START_PERCENT;
    if (scalePercentDelta > 0) {
        /**
         * Change townScale between {@link #TOWN_INITIAL_SCALE} and {@link #TOWN_FINAL_SCALE} depending on {@link #mPercent}
         * Change townTopOffset between {@link #mTownInitialTopOffset} and {@link #mTownFinalTopOffset} depending on {@link #mPercent}
         */
        float scalePercent = scalePercentDelta / (1.0f - SCALE_START_PERCENT);
        townScale = TOWN_INITIAL_SCALE + (TOWN_FINAL_SCALE - TOWN_INITIAL_SCALE) * scalePercent;
        townTopOffset = mTownInitialTopOffset - (mTownFinalTopOffset - mTownInitialTopOffset) * scalePercent;
        townMoveOffset = mTownMoveOffset * (1.0f - scalePercent);
    } else {
        float scalePercent = dragPercent / SCALE_START_PERCENT;
        townScale = TOWN_INITIAL_SCALE;
        townTopOffset = mTownInitialTopOffset;
        townMoveOffset = mTownMoveOffset * scalePercent;
    }
    // 計(jì)算平移量
    float offsetX = -(mScreenWidth * townScale - mScreenWidth) / 2.0f;
    float offsetY = (1.0f - dragPercent) * mParent.getTotalDragDistance() // Offset canvas moving
            + townTopOffset
            - mTownHeight * (townScale - 1.0f) / 2 // Offset town scaling
            + townMoveOffset; // Give it a little move

    matrix.postScale(townScale, townScale);
    matrix.postTranslate(offsetX, offsetY);
    // 繪制town
    canvas.drawBitmap(mTown, matrix, null);
}

將sky蘸吓、sun善炫、town繪制完成。在繪制過程中與平移量库继、縮放比例箩艺、旋轉(zhuǎn)角度有關(guān)的mTopmPercent制跟、mRotate這三個(gè)變量都是在PullToRefreshView下拉刷新過程中不斷改變的舅桩。sky、sun雨膨、town組合在一起伴隨著下拉刷新的過程不斷重繪擂涛,從而實(shí)現(xiàn)刷新動(dòng)畫。


最后

PullToRefreshView主要是實(shí)現(xiàn)mTarget的拖動(dòng)并解決mTarget內(nèi)部滑動(dòng)時(shí)的沖突聊记。
SunRefreshView主要是在PullToRefresh有變化時(shí)不斷重繪自己實(shí)現(xiàn)動(dòng)畫效果撒妈。

Yalantis還有另外兩個(gè)下拉刷新的開源項(xiàng)目Pull-to-Refresh.ToursPull-To-Make-Soup,里面的PullToRefreshView都是和Phoenix-Pull-to-Refresh中的一樣排监,只是自定義了不同BaseRefreshView狰右,我們也可以根據(jù)這個(gè)框架的PullToRefreshView來自定義自己的下拉刷新Drawable舆床,實(shí)現(xiàn)自己的下拉刷新樣式。


文中可能有理解有誤或疏漏之處腾夯,歡迎大家指正饥漫,謝謝!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末证膨,一起剝皮案震驚了整個(gè)濱河市澳化,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌希痴,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,589評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡桶略,警方通過查閱死者的電腦和手機(jī)惶翻,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,615評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鹅心,“玉大人吕粗,你說我怎么就攤上這事⌒窭ⅲ” “怎么了颅筋?”我有些...
    開封第一講書人閱讀 165,933評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長输枯。 經(jīng)常有香客問我议泵,道長,這世上最難降的妖魔是什么桃熄? 我笑而不...
    開封第一講書人閱讀 58,976評(píng)論 1 295
  • 正文 為了忘掉前任先口,我火速辦了婚禮,結(jié)果婚禮上蜻拨,老公的妹妹穿的比我還像新娘池充。我一直安慰自己,他們只是感情好缎讼,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,999評(píng)論 6 393
  • 文/花漫 我一把揭開白布收夸。 她就那樣靜靜地躺著,像睡著了一般血崭。 火紅的嫁衣襯著肌膚如雪卧惜。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,775評(píng)論 1 307
  • 那天夹纫,我揣著相機(jī)與錄音咽瓷,去河邊找鬼。 笑死舰讹,一個(gè)胖子當(dāng)著我的面吹牛茅姜,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播月匣,決...
    沈念sama閱讀 40,474評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼钻洒,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼奋姿!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起素标,我...
    開封第一講書人閱讀 39,359評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤称诗,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后头遭,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體寓免,經(jīng)...
    沈念sama閱讀 45,854評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,007評(píng)論 3 338
  • 正文 我和宋清朗相戀三年计维,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了袜香。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,146評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡享潜,死狀恐怖困鸥,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情剑按,我是刑警寧澤疾就,帶...
    沈念sama閱讀 35,826評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站艺蝴,受9級(jí)特大地震影響猬腰,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜猜敢,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,484評(píng)論 3 331
  • 文/蒙蒙 一姑荷、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧缩擂,春花似錦鼠冕、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,029評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至博脑,卻和暖如春憎乙,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背叉趣。 一陣腳步聲響...
    開封第一講書人閱讀 33,153評(píng)論 1 272
  • 我被黑心中介騙來泰國打工泞边, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人疗杉。 一個(gè)月前我還...
    沈念sama閱讀 48,420評(píng)論 3 373
  • 正文 我出身青樓阵谚,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子梢什,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,107評(píng)論 2 356

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

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,193評(píng)論 25 707
  • 內(nèi)容抽屜菜單ListViewWebViewSwitchButton按鈕點(diǎn)贊按鈕進(jìn)度條TabLayout圖標(biāo)下拉刷新...
    皇小弟閱讀 46,769評(píng)論 22 665
  • 20160715day1: 5點(diǎn)浦東機(jī)場集合 7:10~9:14飛往桂林 9:30~12:00乘車前往龍勝 12:...
    常拓閱讀 86評(píng)論 0 0
  • 隨著移動(dòng)數(shù)碼設(shè)備的流行绳矩,作為移動(dòng)播放器、手機(jī)玖翅、平板等數(shù)碼產(chǎn)品的搭檔翼馆,耳機(jī)的應(yīng)用也是越來越廣泛。在大街上隨處可以看到...
    呂艷朋閱讀 2,145評(píng)論 0 51
  • 十四天營銷訓(xùn)練Session2作業(yè):觀察你的企業(yè)或者身邊企業(yè)的廣告金度,哪些做得好应媚,哪些做得差甚至錯(cuò)誤 公司/團(tuán)隊(duì)/項(xiàng)...
    征兒閱讀 246評(píng)論 0 0