GitHub上有个非常漂亮的Android下拉刷新框架,是由Yalantis开源的,看如下效果图:
在我自己做的项目中也用到了这样的下拉刷新样式,今天就来分析下它的源码。
项目地址: https://github.com/Yalantis/Phoenix
看下这个项目的library结构:
除了一个工具包,真正涉及到下拉刷新UI逻辑的就只有3个类:
PullToRefreshView
,
BaseRefreshVeiw
,
SunRefreshView
。
官方给出的使用demo:
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
中就只有两个子控件:mRefreshView
,mTarget
。
mRefreshView
就是下拉及刷新过程头部用来展示动画的ImageView
。
mTarget
就是需要刷新的目标View
,比如RecylerView
,ListView
,ScrollView
。
onMeasure
测量子控件(mTarget
,mRefreshView
)
@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
布局子控件(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();
// 根据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
重点来了,拦截TouchEvent
。mTarget
一般都是可以滚动的,要保证下拉刷新滚动和子控件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);
}
}
关于事件拦截机制和多点触控相关的解析可以参考大牛非著名程序员的博客:http://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)
方法改变mTop
,setPercent(float percent, boolean invalidate)
方法设置mPercent
和mRotate
,两个方法都会重绘自己。
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()
方法,最终都是要改变这三个变量:mPercent
、mRotate
、mTop
。
因为整个下拉刷新的头部动效都是通过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
,平移是通过缩放比例skyScale
和 PullToRefreshView
的拖动比例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
来计算偏移量和缩放比例,根据mRotate
和sunRotateGrowth
来计算旋转角度。
绘制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绘制完成。在绘制过程中与平移量、缩放比例、旋转角度有关的mTop
、mPercent
、mRotate
这三个变量都是在PullToRefreshView
下拉刷新过程中不断改变的。sky、sun、town组合在一起伴随着下拉刷新的过程不断重绘,从而实现刷新动画。
最后
PullToRefreshView
主要是实现mTarget
的拖动并解决mTarget
内部滑动时的冲突。
SunRefreshView
主要是在PullToRefresh
有变化时不断重绘自己实现动画效果。
Yalantis还有另外两个下拉刷新的开源项目Pull-to-Refresh.Tours和Pull-To-Make-Soup,里面的PullToRefreshView
都是和Phoenix-Pull-to-Refresh
中的一样,只是自定义了不同BaseRefreshView
,我们也可以根据这个框架的PullToRefreshView
来自定义自己的下拉刷新Drawable
,实现自己的下拉刷新样式。
文中可能有理解有误或疏漏之处,欢迎大家指正,谢谢!