代碼版本:support-v4 24.1.1 共1165行
將SwipeRefreshLayout在本文中簡(jiǎn)稱(chēng)為SRL
涉及到的知識(shí)點(diǎn)
從控件的使用效果來(lái)看,我們可以了解到關(guān)鍵的知識(shí)點(diǎn)如下:
- 自定義ViewGroup:可以學(xué)習(xí)一下ViewGroup以及View的一些關(guān)鍵方法的使用
- 滑動(dòng)事件:觸摸事件傳遞規(guī)則涯捻,簡(jiǎn)單的多點(diǎn)觸控相關(guān)
- 動(dòng)畫(huà)
本篇也從這三個(gè)方面來(lái)解讀柿祈。
涉及到的關(guān)鍵方法
SwipeRefreshLayout(Context context, AttributeSet attrs)
onMeasure (int widthMeasureSpec, int heightMeasureSpec)
onLayout(boolean changed, int left, int top, int right, int bottom)
onInterceptTouchEvent(MotionEvent ev)
onTouchEvent(MotionEvent ev)
moveSpinner(float overscrollTop)
我們可以把整個(gè)下拉刷新過(guò)程分為幾個(gè)關(guān)鍵的過(guò)程部分:
- 下拉過(guò)程
- 回彈過(guò)程
- 轉(zhuǎn)圈刷新過(guò)程
常量和方法
通過(guò)一些常量的定義我們可以認(rèn)識(shí)到SRL的View組成部分氓奈,以及一些關(guān)鍵的時(shí)間點(diǎn)。
CircleImageView mCircleView
(繼承自:android.support.v7.widget.AppCompatImageView): 即我們下拉刷新的過(guò)程中可以見(jiàn)到的圈圈控件,在構(gòu)造方法中為其設(shè)置了一系列屬性.
MaterialProgressDrawable mProgress
(繼承自:Drawable):這是圓圈控件CircleImageView 的內(nèi)容稚伍,在構(gòu)造方法中可以看到:mCircleView.setImageDrawable(mProgress);對(duì)于圓圈內(nèi)容的控制基本上都是通過(guò)MaterialProgressDrawable 來(lái)實(shí)現(xiàn)的轧叽。
View mTarget
:一般即SRL的直接子View比如你嵌入的RecyclerView苗沧;在ensureTarget()方法中完成對(duì)其賦值刊棕。
float mTotalDragDistance
:超過(guò)這個(gè)距離值后即認(rèn)定為下拉刷新;也就是觸發(fā)下拉刷新的距離待逞;也就是最終停下來(lái)轉(zhuǎn)圈圈的位置甥角。
int mCurrentTargetOffsetTo
:CircleView實(shí)時(shí)距離初始位置滑過(guò)的距離。
private void setTargetOffsetTopAndBottom(int offset, boolean requiresUpdate)
:通過(guò)調(diào)用mCircleView的offsetTopAndBottom來(lái)移動(dòng)CircleView本身识樱。
public boolean canChildScrollUp()
:判斷SRL的內(nèi)容是否滑動(dòng)到頂部嗤无,return false 說(shuō)明滑動(dòng)到頂部可以觸發(fā)下拉刷新;return true說(shuō)明未滑動(dòng)到頂部不觸發(fā)下拉刷新怜庸。該方法在onTouchEvent和onInterceptTouchEvent方法中均被調(diào)用來(lái)判斷当犯。
初始及化繪制過(guò)程:
構(gòu)造方法
初始化各種常量字段,諸如mCircleView的大小割疾,動(dòng)畫(huà)插值器嚎卫,mTotalDragDistance,創(chuàng)建并添加mCircleView宏榕,設(shè)置一些屬性拓诸,比如setWillNotDraw(false)可以提高效率。
繪制布局
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//確保mTarget不為空
if (mTarget == null) {
ensureTarget();
}
if (mTarget == null) {
return;
}
//測(cè)量mTarget的寬高麻昼,去掉padding
mTarget.measure(MeasureSpec.makeMeasureSpec(
getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(
getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY));
//測(cè)量mCircleView的寬高
mCircleView.measure(MeasureSpec.makeMeasureSpec(mCircleWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(mCircleHeight, MeasureSpec.EXACTLY));
//如果沒(méi)有使用自定義的起始位置奠支,并且起始位置沒(méi)有被計(jì)算過(guò)(一般第一次onMeaure的時(shí)候會(huì)被調(diào)用)
if (!mUsingCustomStart && !mOriginalOffsetCalculated) {
mOriginalOffsetCalculated = true;
//計(jì)算出當(dāng)前CircleView移動(dòng)的位置,即CircleView的自然高度的負(fù)值抚芦,
//也就是說(shuō)CircleView正好在屏幕上邊倍谜,我們看不到它
mCurrentTargetOffsetTop = mOriginalOffsetTop = -mCircleView.getMeasuredHeight();
}
mCircleViewIndex = -1;
// Get the index of the circleview.
// 獲取到圓圈view在當(dāng)前view中的位置;一般情況下mCircleViewIndex都為0燕垃;
// 這個(gè)值會(huì)在getChildDrawingOrder被調(diào)用
for (int index = 0; index < getChildCount(); index++) {
if (getChildAt(index) == mCircleView) {
mCircleViewIndex = index;
break;
}
}
}
onMeasure的代碼和解釋如上枢劝,做了兩件事: 完成子View的大小測(cè)量,對(duì)變量進(jìn)行正確賦值卜壕。
接下來(lái)在onLayout方法中來(lái)確定在mTarget和mCircleView在SRL中的位置您旁,onLayout的代碼清晰明了,就做了兩件事:對(duì)mTarget調(diào)用layout方法確定其位置轴捎,對(duì)CircleView調(diào)用layout方法確定其位置鹤盒,在這里就不貼代碼啦。
滑動(dòng)事件處理:
首先我們知道對(duì)于ViewGroup的滑動(dòng)處理流程的偽代碼如下:
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean result = false; // 默認(rèn)狀態(tài)為沒(méi)有消費(fèi)過(guò)
if (!onInterceptTouchEvent(ev)) { // 如果沒(méi)有攔截交給子View
result = child.dispatchTouchEvent(ev);
}
if (!result) { // 如果事件沒(méi)有被消費(fèi),詢(xún)問(wèn)自身onTouchEvent
result = onTouchEvent(ev);
}
return result;
}
在SRL中重寫(xiě)了onInterceptTouchEvent和onTouchEvent方法侦副,通過(guò)完成一次完整的下拉刷新侦锯,打印Log,我們發(fā)現(xiàn)調(diào)用過(guò)程如下:
下面看onInterceptTouchEvent的處理秦驯,這里它會(huì)返回一個(gè)mIsBeingDragged (是否被拖拽的布爾值)尺碰,返回true則正在被拖拽,這時(shí)就會(huì)把事件分發(fā)到事件本身的onTouchEvent中,反之就會(huì)交給子View去處理亲桥。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
ensureTarget();
final int action = MotionEventCompat.getActionMasked(ev);
// 設(shè)置準(zhǔn)備開(kāi)始的狀態(tài)
if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
mReturningToStart = false;
}
// 判斷一系列狀態(tài)洛心,決定是否可以下拉刷新,注意canChildScrollUp()方法题篷,
// 該方法決定了只有滑動(dòng)到頂部繼續(xù)下來(lái)才能觸發(fā)下拉刷新
if (!isEnabled() || mReturningToStart || canChildScrollUp()
|| mRefreshing || mNestedScrollInProgress) {
// Fail fast if we're not in a state where a swipe is possible
return false;
}
switch (action) {
case MotionEvent.ACTION_DOWN:
// 默認(rèn)情況下offset為0词身,不移動(dòng);
// 若初始指定了mOriginalOffsetTop 的大小則意味著番枚,按下的一刻法严,被移動(dòng)到了指定位置。
setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCircleView.getTop(), true);
mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
mIsBeingDragged = false;
final float initialDownY = getMotionEventY(ev, mActivePointerId);
if (initialDownY == -1) {
// 不在屏幕范圍內(nèi)不處理
return false;
}
// mInitialDownY 在ActionMove中用于與移動(dòng)距離比較葫笼,判斷是否被拖拽深啤。
mInitialDownY = initialDownY;
//可以看到在當(dāng)前動(dòng)作下,設(shè)置了CircleView的初始位置渔欢;獲取到了多點(diǎn)觸控相關(guān)的手指id墓塌;拖拽狀態(tài)置為false;賦值初始按下的位置值:mInitialDownY
break;
case MotionEvent.ACTION_MOVE:
// 排除無(wú)效觸摸情況
if (mActivePointerId == INVALID_POINTER) {
Log.e(LOG_TAG, "Got ACTION_MOVE event but don't have an active pointer id.");
return false;
}
final float y = getMotionEventY(ev, mActivePointerId);
if (y == -1) {
return false;
}
final float yDiff = y - mInitialDownY;
// 如果滑動(dòng)的距離大于被視為滑動(dòng)的最小距離奥额,并且之前的狀態(tài)為沒(méi)有被拖動(dòng);
// 這時(shí)把拖拽狀態(tài)mIsBeingDragged置為true访诱;同時(shí)記下按下的位置:mInitialMotionY
//如果mIsBeingDragged為true就會(huì)return true垫挨;根據(jù)事件處理偽代碼那么接下來(lái)的操作就交給onTouchEvent處理了
if (yDiff > mTouchSlop && !mIsBeingDragged) {
mInitialMotionY = mInitialDownY + mTouchSlop;
mIsBeingDragged = true;
mProgress.setAlpha(STARTING_PROGRESS_ALPHA);
}
break;
case MotionEventCompat.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mIsBeingDragged = false;
mActivePointerId = INVALID_POINTER;
break;
}
return mIsBeingDragged;
}
如上就是onInterceptTouchEvent所做的工作,設(shè)置初始位置触菜,進(jìn)行相關(guān)賦值操作九榔,根據(jù)滑動(dòng)的實(shí)際情況來(lái)決定是否把進(jìn)一步操作轉(zhuǎn)交給onTouchEvent。
public boolean onTouchEvent(MotionEvent ev) {
final int action = MotionEventCompat.getActionMasked(ev);
int pointerIndex = -1;
// 首先是根據(jù)狀態(tài)進(jìn)行攔截
if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
mReturningToStart = false;
}
if (!isEnabled() || mReturningToStart || canChildScrollUp() || mNestedScrollInProgress) {
// Fail fast if we're not in a state where a swipe is possible
return false;
}
switch (action) {
case MotionEvent.ACTION_DOWN:
// 獲取觸摸id涡相,設(shè)置拖拽狀態(tài)
mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
mIsBeingDragged = false;
break;
case MotionEvent.ACTION_MOVE: {
pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
//排除無(wú)效狀態(tài)
if (pointerIndex < 0) {
Log.e(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id.");
return false;
}
//獲取滑動(dòng)位置: y哲泊,y減去初始按下的位置在撐拖拽比率才是CircleView真正要?jiǎng)澾^(guò)的距離,
//DRAG_RATE為0.5催蝗,所以CircleView滑過(guò)的距離切威,要比你手指移動(dòng)的距離短。
final float y = MotionEventCompat.getY(ev, pointerIndex);
final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
if (mIsBeingDragged) {
if (overscrollTop > 0) {
//滑動(dòng)距離大于0就去移動(dòng)CircleView丙号,下面這個(gè)方法很重要先朦,就是依靠它來(lái)完成CircleView的狀態(tài)變化和移動(dòng)的
moveSpinner(overscrollTop);
} else {
return false;
}
}
break;
}
case MotionEventCompat.ACTION_POINTER_DOWN: {
pointerIndex = MotionEventCompat.getActionIndex(ev);
if (pointerIndex < 0) {
Log.e(LOG_TAG, "Got ACTION_POINTER_DOWN event but have an invalid action index.");
return false;
}
mActivePointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
break;
}
case MotionEventCompat.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
break;
case MotionEvent.ACTION_UP: {
pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
if (pointerIndex < 0) {
Log.e(LOG_TAG, "Got ACTION_UP event but don't have an active pointer id.");
return false;
}
// 當(dāng)結(jié)束下拉之后這時(shí)把拖拽狀態(tài)置為false,通過(guò)finishSpinner來(lái)進(jìn)行刷新操作或者是不到刷新距離回彈到初始位置的操作
final float y = MotionEventCompat.getY(ev, pointerIndex);
final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
mIsBeingDragged = false;
finishSpinner(overscrollTop);
mActivePointerId = INVALID_POINTER;
return false;
}
case MotionEvent.ACTION_CANCEL:
return false;
}
return true;
}
onTouchEvent分析到這里犬缨,我們知道了在這個(gè)方法中根據(jù)用戶(hù)真實(shí)的滑動(dòng)狀況來(lái)調(diào)用相關(guān)方法:moveSpinner方法完成CircleView及其內(nèi)容的變化喳魏,finishSpinner完成手指抬起后的刷新操作,接下來(lái)我們看這兩個(gè)方法
private void moveSpinner(float overscrollTop) {
mProgress.showArrow(true);// ture,設(shè)置下拉過(guò)程中展示小箭頭
//移動(dòng)的距離除以刷新位置的距離得出一個(gè)拖拽比率:originalDragPercent
float originalDragPercent = overscrollTop / mTotalDragDistance;
float dragPercent = Math.min(1f, Math.abs(originalDragPercent));
float adjustedPercent = (float) Math.max(dragPercent - .4, 0) * 5 / 3;
float extraOS = Math.abs(overscrollTop) - mTotalDragDistance;
float slingshotDist = mUsingCustomStart ? mSpinnerFinalOffset - mOriginalOffsetTop
: mSpinnerFinalOffset;
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;
int targetY = mOriginalOffsetTop + (int) ((slingshotDist * dragPercent) + extraMove);
// where 1.0f is a full circle
if (mCircleView.getVisibility() != View.VISIBLE) {
mCircleView.setVisibility(View.VISIBLE);
}
if (!mScale) {
ViewCompat.setScaleX(mCircleView, 1f);
ViewCompat.setScaleY(mCircleView, 1f);
}
if (mScale) {
setAnimationProgress(Math.min(1f, overscrollTop / mTotalDragDistance));
}
if (overscrollTop < mTotalDragDistance) {
if (mProgress.getAlpha() > STARTING_PROGRESS_ALPHA
&& !isAnimationRunning(mAlphaStartAnimation)) {
// Animate the alpha
startProgressAlphaStartAnimation();
}
} else {
if (mProgress.getAlpha() < MAX_ALPHA && !isAnimationRunning(mAlphaMaxAnimation)) {
// Animate the alpha
startProgressAlphaMaxAnimation();
}
}
//設(shè)置內(nèi)部圈圈開(kāi)始出現(xiàn)時(shí)的大小
float strokeStart = adjustedPercent * .8f;
//設(shè)置小箭頭的樣式
mProgress.setStartEndTrim(0f, Math.min(MAX_PROGRESS_ANGLE, strokeStart));
mProgress.setArrowScale(Math.min(1f, adjustedPercent));
float rotation = (-0.25f + .4f * adjustedPercent + tensionPercent * 2) * .5f;
mProgress.setProgressRotation(rotation);
setTargetOffsetTopAndBottom(targetY - mCurrentTargetOffsetTop, true /* requires update */);
}
在moveSpinner方法中通過(guò)對(duì)overscrollTop(CircileView移動(dòng)的距離)和mTotalDragDistance(刷新位置的距離)進(jìn)行一系列的計(jì)算得出在下拉過(guò)程中其他元素的變化程度怀薛。最后通過(guò)setTargetOffsetTopAndBottom方法來(lái)真是移動(dòng)CircleView本身刺彩。
接下來(lái)我們看finishSpinner(float overscrollTop)方法,在onTouchEvent中攔截到ACTION_UP手勢(shì)后調(diào)用到了 finishSpinner(overscrollTop);傳入的overscrollTop為CircileView移動(dòng)的距離。
private void finishSpinner(float overscrollTop) {
if (overscrollTop > mTotalDragDistance) {
//如果超過(guò)了CircleView最終下來(lái)刷新位置的距離后則認(rèn)定為觸發(fā)下拉刷新調(diào)用setRefreshing方法
setRefreshing(true, true /* notify */);
} else {
//否則的話取消本次刷新動(dòng)作
// cancel refresh
mRefreshing = false;
mProgress.setStartEndTrim(0f, 0f);
Animation.AnimationListener listener = null;
//mScale默認(rèn)為false 在這里監(jiān)聽(tīng)動(dòng)畫(huà)結(jié)束來(lái)進(jìn)一步操作
if (!mScale) {
listener = new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
if (!mScale) {
startScaleDownAnimation(null);
}
}
@Override
public void onAnimationRepeat(Animation animation) {
}
};
}
// 在這里開(kāi)始真正的回彈動(dòng)畫(huà)
animateOffsetToStartPosition(mCurrentTargetOffsetTop, listener);
mProgress.showArrow(false);
}
}
在流程上的分析基本上到這里就結(jié)束了创倔,其中一些自定義view和繪制的方法還需要進(jìn)一步理解三热。
動(dòng)畫(huà)
在SRL中的動(dòng)畫(huà)實(shí)現(xiàn)方式都是如下套路:
首先直接繼承View動(dòng)畫(huà)Animation來(lái)實(shí)現(xiàn)在動(dòng)畫(huà)過(guò)程中的相應(yīng)操作。
private final Animation mAnimateToStartPosition = new Animation() {
@Override
public void applyTransformation(float interpolatedTime, Transformation t) {
int targetTop = 0;
targetTop = (mFrom + (int) ((mOriginalOffsetTop - mFrom) * interpolatedTime));
int offset = targetTop - mCircleView.getTop();
//基于Z軸三幻,放到最上層
mCircleView.bringToFront();
mCircleView.offsetTopAndBottom(offset);
mCurrentTargetOffsetTop = mCircleView.getTop();
if (requiresUpdate && android.os.Build.VERSION.SDK_INT < 11) {
//將觸發(fā)onDraw方法
invalidate();
}
}
};
然后設(shè)置動(dòng)畫(huà)的相關(guān)屬性:
mAnimateToStartPosition.reset();
mAnimateToStartPosition.setDuration(ANIMATE_TO_START_DsURATION);
mAnimateToStartPosition.setInterpolator(mDecelerateInterpolator); //減速返回
最后給View設(shè)置動(dòng)畫(huà)
mCircleView.clearAnimation();
mCircleView.startAnimation(mAnimateToStartPosition);
源碼中的動(dòng)畫(huà)實(shí)現(xiàn)都是這一個(gè)套路就不多說(shuō)了就漾。
其他
我們看到SwipeRefreshLayout還實(shí)現(xiàn)了兩個(gè)接口:NestedScrollingParent 和NestedScrollingChild來(lái)處理嵌套滑動(dòng),關(guān)于這個(gè)知識(shí)點(diǎn)打算再寫(xiě)一篇博客念搬。
總結(jié)
到這里我們已經(jīng)把SRL的基本實(shí)現(xiàn)方式和調(diào)用細(xì)節(jié)大致分析了一遍抑堡,主要的知識(shí)點(diǎn)還是自定義ViewGroup、 觸摸事件的處理和動(dòng)畫(huà)的使用朗徊。另外關(guān)于CircleImageView和MaterialProgressDrawable的實(shí)現(xiàn)感興趣的話還值得深入挖掘首妖。