GitHub上有個(gè)非常漂亮的Android下拉刷新框架,是由Yalantis開源的呆躲,看如下效果圖:
在我自己做的項(xiàng)目中也用到了這樣的下拉刷新樣式,今天就來分析下它的源碼冀自。
項(xiàng)目地址:https://github.com/Yalantis/Phoenix
看下這個(gè)項(xiàng)目的library結(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
裂问,ListView
,ScrollView
牛柒。
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)
方法改變mTop
,setPercent(float percent, boolean invalidate)
方法設(shè)置mPercent
和mRotate
鞭呕,兩個(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è)變量:mPercent
、mRotate
摧扇、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
耕餐,平移是通過縮放比例skyScale
和 PullToRefreshView
的拖動(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ù)mRotate
和sunRotateGrowth
來計(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)的mTop
、mPercent
制跟、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.Tours和Pull-To-Make-Soup,里面的PullToRefreshView
都是和Phoenix-Pull-to-Refresh
中的一樣排监,只是自定義了不同BaseRefreshView
狰右,我們也可以根據(jù)這個(gè)框架的PullToRefreshView
來自定義自己的下拉刷新Drawable
舆床,實(shí)現(xiàn)自己的下拉刷新樣式。
文中可能有理解有誤或疏漏之處腾夯,歡迎大家指正饥漫,謝謝!