Yalantis 开源项目 Phoenix-Pull-to-Refresh 源码分析

GitHub上有个非常漂亮的Android下拉刷新框架,是由Yalantis开源的,看下效果图:

《Yalantis 开源项目 Phoenix-Pull-to-Refresh 源码分析》

Phoenix-Pull-to-Refresh

在我自己做的项目中也用到了这样的下拉刷新样式,今天就来分析下它的源码。
项目地址:github.com/Yalantis/Ph…

看下这个项目的library结构:

《Yalantis 开源项目 Phoenix-Pull-to-Refresh 源码分析》

Phoenix-Pull-to-Refresh的Labrary结构

除了一个工具包,真正涉及到下拉刷新UI逻辑的就只有3个类:PullToRefreshViewBaseRefreshVeiwSunRefreshView

官方给出的使用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的套路来进行分析。
构造函数中初始化一些属性:

public PullToRefreshView(Context context, AttributeSet attrs) {
    super(context, attrs);
    // 自定义属性(实际上这个属性没什么用处)
    TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RefreshView);
    final int type = a.getInteger(R.styleable.RefreshView_type, STYLE_SUN);
    a.recycle();
    // 动画插值器
    mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR);
    // 滑动触发的临界距离
    mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    // 触发下拉刷新拖动的总距离
    mTotalDragDistance = Utils.convertDpToPixel(context, DRAG_MAX_DISTANCE);
    // 头部刷新的ImageView
    mRefreshView = new ImageView(context);
    // 根据type设置刷新样式
    setRefreshStyle(type);
    // 将头部刷新ImageVeiw添加到当前的PullToRefreshView
    addView(mRefreshView);

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

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

根据上文中PullToRefreshView的使用方式,同时在构造函数中向PullToRefreshView添加了一个ImageView(mRefreshView),可以看出整个PullToRefreshView中就只有两个子控件:mRefreshViewmTarget
mRefreshView就是下拉及刷新过程头部用来展示动画的ImageView
mTarget就是需要刷新的目标View,比如RecylerViewListViewScrollView

onMeasure

测量子控件(mTargetmRefreshView

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    // 确保需要刷新的子控件Target已经添加
    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

布局子控件(mTargetmRefreshView):

@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();
    // 根据PullToRefreshView的宽高和内边界来布局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

重点来了,拦截TouchEventmTarget一般都是可以滚动的,要保证下拉刷新滚动和子控件mTarget内部的滑动不冲突,所以就需要重写拦截逻辑:

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    // enable 或者 mTarget能滑动 或者 正在刷新,此时不拦截,交给child来分发ev
    if (!isEnabled() || canChildScrollUp() || mRefreshing) {
        return false;
    }
    // 事件action
    final int action = MotionEventCompat.getActionMasked(ev);
    // 根据事件类型,处理拦截逻辑
    switch (action) {
        case MotionEvent.ACTION_DOWN:
            // 手指down时,设置mTarget的偏移量为0
            // 相当于初始化mTarget的mCurrentOffsetTop以及头部刷新的Drawable(mBaseRefreshView)
            setTargetOffsetTop(0, true);
            // 活动手指ID(触发拖动的手指)
            mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
            // 不是正在被拖动
            mIsBeingDragged = false;
            // 活动手指初始按下时的Y坐标
            final float initialMotionY = getMotionEventY(ev, mActivePointerId);
            if (initialMotionY == -1) {
                return false;
            }
            mInitialMotionY = initialMotionY;
            break;
        case MotionEvent.ACTION_MOVE:
            if (mActivePointerId == INVALID_POINTER) {
                return false;
            }
            // 获取活动手指的Y坐标(当前可能有多个手指在屏幕上move,只需处理活动手指即可)
            final float y = getMotionEventY(ev, mActivePointerId);
            if (y == -1) {
                return false;
            }
            // 移动的距离yDiff大于临界值并且当前没有被拖动
            // 改变拖动的状态值mIsBeingDragged为正在拖动
            final float yDiff = y - mInitialMotionY;
            if (yDiff > mTouchSlop && !mIsBeingDragged) {
                mIsBeingDragged = true;
            }
            break;
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            // 手指cancel或者up,拖动状态为false,活动手指invalid。
            mIsBeingDragged = false;
            mActivePointerId = INVALID_POINTER;
            break;
        case MotionEventCompat.ACTION_POINTER_UP:
            // 多个手指在屏幕上,当第二个手指抬起时,需要更新活动手指
            onSecondaryPointerUp(ev);
            break;
    }
    // 只要当前处于拖动状态,就拦截事件,否则不拦截
    return mIsBeingDragged;
}

/** * 当有多个手指在屏幕上时,有一个手指抬起时,需要处理的逻辑 * 多点触控时,手指的down和up之间,只有通过ID才能识别手指,当有手指抬起时,需要更新活动手指。 * @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);
    }
}

关于事件拦截机制和多点触控相关的解析可以参考大牛非著名程序员的博客:www.gcssloop.com/

onTouchEvent

拦截到的事件,要在onTouchEvent中来处理,实现mTarget拖动的UI逻辑。

@Override
public boolean onTouchEvent(@NonNull MotionEvent ev) {
    // 当前没有被拖动,不处理
    if (!mIsBeingDragged) {
        return super.onTouchEvent(ev);
    }
    // 根据事件action处理不同事件逻辑
    final int action = MotionEventCompat.getActionMasked(ev);
    switch (action) {
        case MotionEvent.ACTION_MOVE: {
            // 获取当前事件中活动手指的pointerIndex
            final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
            if (pointerIndex < 0) {
                return false;
            }
            // 根据活动手指pointerIndex获取Y坐标
            // 计算出手指的移动距离yDiff
            final float y = MotionEventCompat.getY(ev, pointerIndex);
            final float yDiff = y - mInitialMotionY;
            // mTarget需要滚动的距离scrollTop
            final float scrollTop = yDiff * DRAG_RATE;
            // 当前拖动百分比mCurrentDragPercent
            mCurrentDragPercent = scrollTop / mTotalDragDistance;
            if (mCurrentDragPercent < 0) {
                return false;
            }
            // 以下逻辑都是根据当前拖动百分比和mTarget需要滚动的距离来计算出当前move事件中mTarget需要达到的目标Y坐标
            // 即每一次移动都需要计算出即将要达到的位置的Y坐标,通过该即将到达的Y坐标以及当前的偏移量,
            // 就能计算出这次手指移动时mTarget所需要的偏移量
            // 做如此处理主要是让拖动距离超过触发刷新的距离时继续拖动有一个阻尼效果
            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为此次手指移动mTarget即将达到的Y坐标
            int targetY = (int) ((slingshotDist * boundedDragPercent) + extraMove);
            // 设置mBaseRefreshView(头部刷新Drawable)的百分比,用以更新刷新动画
            mBaseRefreshView.setPercent(mCurrentDragPercent, true);
            // 设置mTarget偏移量,实现下拉
            setTargetOffsetTop(targetY - mCurrentOffsetTop, true);
            break;
        }
        case MotionEventCompat.ACTION_POINTER_DOWN:
            // 新的手指按下时,更新触发拖动活动手指
            final int index = MotionEventCompat.getActionIndex(ev);
            mActivePointerId = MotionEventCompat.getPointerId(ev, index);
            break;
        case MotionEventCompat.ACTION_POINTER_UP:
            // 多点触控,手指抬起时更新活动手指
            onSecondaryPointerUp(ev);
            break;
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL: {
            if (mActivePointerId == INVALID_POINTER) {
                return false;
            }
            // 手指up或cancel,根据活动手指计算出mTarget滚动的距离overScrollTop
            final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
            final float y = MotionEventCompat.getY(ev, pointerIndex);
            final float overScrollTop = (y - mInitialMotionY) * DRAG_RATE;
            // 改变拖动状体
            mIsBeingDragged = false;
            if (overScrollTop > mTotalDragDistance) {
                // mTarget被拖动的距离大于触发刷新的拖动距离时,设置当前刷新状态true
                setRefreshing(true, true);
            } else {
                // 否则,当前刷新状态为false,并且通过动画让mTarget回到最初状态
                mRefreshing = false;
                animateOffsetToStartPosition();
            }
            // 活动手指invelid
            mActivePointerId = INVALID_POINTER;
            return false;
        }
    }
    // 消费掉当前Touch事件
    return true;
}

手指在屏幕滑动时,mTarget的整个拖动逻辑都是在onTouchEvent中实现。
setRefershing方法中,设置PullToRefreshView当前的刷新状态:
1.通过动画将mTarget偏移到正在刷新的位置
2.通过动画将mTarget偏移到初始位置

/** * 设置PullToRefreshView的刷新状态 * * @param refreshing 是否正在刷新 * @param notify 是否回调onRefresh */
private void setRefreshing(boolean refreshing, final boolean notify) {
    if (mRefreshing != refreshing) {
        mNotify = notify;
        ensureTarget();
        mRefreshing = refreshing;
        if (mRefreshing) {
            // 正在刷新,设置刷新Drawable(mBaseRefreshView)的percent,用以更新刷新动画
            mBaseRefreshView.setPercent(1f, true);
            // 通过动画让mTarget偏移到正在刷新的位置。
            animateOffsetToCorrectPosition();
        } else {
            // 不是正在刷新,通过动画使mTarget偏移到初始位置。
            animateOffsetToStartPosition();
        }
    }
}

在通过动画来偏移mTarget的逻辑比较简单,同样也是通过动画的执行过程来不断调用setTargetOffsetTop方法来移动mTarget
PullToRefresh中,主要是处理拦截到的move事件,通过move事件计算出mTarget所需的偏移量来实现mTarget的拖动。同时在手指up时,通过当前拖动偏移量mCurrentOffsetTop、触发刷新的拖动距离mTotalDragDistance比较来决定是通过动画将mTarget偏移到正在刷新的位置和最初始的位置。
总之,PullToRefershView是通过mTarget的偏移来实现下拉拖动。mTarget偏移的同时,将当前拖动百分比mCurrentDragPercent设置到刷新的Drawable(mBaseRefreshView)中,更新刷新动画。

2.BaseRefreshVeiw

这个类是自定义刷新Drawable抽象类,继承自Drawable,并且实现了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;
    }
    /** * 设置拖动百分比,用以更新Drawable中的动画 */
    public abstract void setPercent(float percent, boolean invalidate);

    /** * 设置偏移量,用以更新Drawable中的动画 */
    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作为一个抽象类,我们可以继承它来实现不同样式的刷新动画。在PullToRefershView中根据拖动实时调用setPercent(float percent, boolean invalidate)offsetTopAndBottom(int offset)这两个方法就可以实时更新动画。

3.SunRefreshView

具体实现就是SunRefreshView,继承自BaseRefreshView,具体的动画逻辑躲在SunRefreshView中实现。

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

实现抽象父类BaseRefreshView的两个方法,offsetTopAndBottom(int offset)方法改变mTopsetPercent(float percent, boolean invalidate)方法设置mPercentmRotate,两个方法都会重绘自己。
PullToRefresh在手指拖动过程中不断调用这两个方法,达到拖动时Drawable跟随变化的动效。
当手指松开时,PullToRefreshView自动回到正在刷新的状态或者初始状态,SunRefreshView的动效变化是通过调用start()stop()方法,在start()stop()中开始和结束mAnimation动画。

@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) {
            // 根据动画时间来设置(sun)旋转,然后重绘
            setRotate(interpolatedTime);
        }
    };
    mAnimation.setRepeatCount(Animation.INFINITE);
    mAnimation.setRepeatMode(Animation.RESTART);
    mAnimation.setInterpolator(LINEAR_INTERPOLATOR);
    mAnimation.setDuration(ANIMATION_DURATION);
}

不管PullToRefreshView调用setPercent(float percent, boolean invalidate)offsetTopAndBottom(int offset)方法,还是手指松开时调用的start()stop()方法,最终都是要改变这三个变量:mPercentmRotatemTop
因为整个下拉刷新的头部动效都是通过SunRefreshView这个Drawable来不断重绘自己实现的。

@Override
public void draw(Canvas canvas) {
    if (mScreenWidth <= 0) return;
    // 保存当前画布状态,然后平移、裁剪画布
    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);
    // 绘制完成后恢复画布状态
    canvas.restoreToCount(saveCount);
}

绘制过程按照mTop画布会进行平移和裁剪。
绘制sky:
sky的动画过程中由缩放和平移两个动画组成。缩放是通过mPrercent计算出缩放比例skyScale,平移是通过缩放比例skyScalePullToRefreshView的拖动比例mTotalDragDistance计算出x和y方向的偏移量。

private void drawSky(Canvas canvas) {
    Matrix matrix = mMatrix;
    matrix.reset();
    // 拖动比例
    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
    // 拖动比例大于0.5时,sky缩放比例为SKY_INITIAL_SCALE - (SKY_INITIAL_SCALE - 1.0f) * scalePercent;
    // 拖动比例小于0.5时,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;
    }
    // 根据缩放比例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,实现动画效果。
绘制sun:
sun在下拉刷新和释放时,有三个动画:上下平移、旋转、缩放。

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的位置
    // 在重绘的过程中根据mPercent和mTop实现上下平移
    float offsetX = mSunLeftOffset;
    float offsetY = mSunTopOffset
            + (mParent.getTotalDragDistance() / 2) * (1.0f - dragPercent) // Move the sun up
            - mTop; // Depending on Canvas position
    // 根据拖动比例mPercent计算缩放比例
    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);
        // 缩放的同时要改变偏移量(保证缩放和上下平移时,sun的中心在竖直方向)
        offsetX += sunRadius;
        offsetY = offsetY * (2.0f - sunScale) + sunRadius * sunScale;
    } else {
        matrix.postTranslate(offsetX, offsetY);
        // 缩放的同时要改变偏移量(保证缩放和上下平移时,sun的中心在竖直方向)
        offsetX += sunRadius;
        offsetY += sunRadius;
    }

    // 根据mRotate计算旋转的角度
    // 拖动时旋转方向为顺时针,释放或正在刷新为逆时针方向。
    // 拖动时或释放后旋转的角度按照拖动的幅度来旋转,正在刷新时每次绘制旋转1°
    matrix.postRotate(
            (isRefreshing ? -360 : 360) * mRotate * (isRefreshing ? 1 : sunRotateGrowth),
            offsetX,
            offsetY);
    // 绘制sun
    canvas.drawBitmap(mSun, matrix, null);
}

sun的动画相对来说要复杂些,主要逻辑就是根据mPercent来计算偏移量和缩放比例,根据mRotatesunRotateGrowth来计算旋转角度。
绘制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;
    // 计算缩放比例
    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;
    }
    // 计算平移量
    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绘制完成。在绘制过程中与平移量、缩放比例、旋转角度有关的mTopmPercentmRotate这三个变量都是在PullToRefreshView下拉刷新过程中不断改变的。sky、sun、town组合在一起伴随着下拉刷新的过程不断重绘,从而实现刷新动画。

最后

PullToRefreshView主要是实现mTarget的拖动并解决mTarget内部滑动时的冲突。
SunRefreshView主要是在PullToRefresh有变化时不断重绘自己实现动画效果。

Yalantis还有另外两个下拉刷新的开源项目Pull-to-Refresh.ToursPull-To-Make-Soup,里面的PullToRefreshView都是和Phoenix-Pull-to-Refresh中的一样,只是自定义了不同BaseRefreshView,我们也可以根据这个框架的PullToRefreshView来自定义自己的下拉刷新Drawable,实现自己的下拉刷新样式。

文中可能有理解有误或疏漏之处,欢迎大家指正,谢谢!

    原文作者:Android源码分析
    原文地址: https://juejin.im/entry/5884ef172f301e006999957e
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞