Android下拉刷新控件SwipeRefreshLayout源碼解析
SwipeRefreshLayout是Android官方的下拉刷新控件,使用簡單,界面美觀,不熟悉的朋友可以隨便搜索了解一下,這里就不廢話了,直接進(jìn)入正題。
首先給張流程圖吧,標(biāo)出了幾個主要方法的作用,可以結(jié)合著看一下哈。
這種下拉刷新控件的原理不難,基本就是監(jiān)聽手指的運動,獲取手指的坐標(biāo),通過計算判斷出是哪種操作,然后就是回調(diào)相應(yīng)的接口了。SwipeRefreshLayout是繼承自ViewGroup的,根據(jù)Android的事件分發(fā)機制,觸摸事件應(yīng)該是先傳遞到ViewGroup,根據(jù)onInterceptTouchEvent的返回值決定是否攔截事件的,那么就onInterceptTouchEvent出發(fā):
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
ensureTarget();
final int action = MotionEventCompat.getActionMasked(ev);
if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
mReturningToStart = false;
}
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:
setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCircleView.getTop(), true);
mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
mIsBeingDragged = false;
final float initialDownY = getMotionEventY(ev, mActivePointerId);
if (initialDownY == -1) {
return false;
}
mInitialDownY = initialDownY;
break;
case MotionEvent.ACTION_MOVE:
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;
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;
}
是否攔截的情況有很多種,這里如果滿足五個條件之一就直接返回false,使用時觸摸事件發(fā)生沖突的話就可以從這里出發(fā)分析,這里也不具體展開了。簡單看一下,在ACTION_DOWN中記錄下手指坐標(biāo),ACTION_MOVE中計算出移動的距離,并且判斷是否大于閾值,是的話就將mIsBeingDragged標(biāo)志位設(shè)為true,ACTION_UP中則將mIsBeingDragged設(shè)為false。最后返回的是mIsBeingDragged。
SwipeRefreshLayout一般是嵌套可滾動的View使用的,正常滾動時會滿足前面的條件,這時不進(jìn)行攔截,只有當(dāng)滾動到頂部才會進(jìn)入后面action的判斷。在手指按下和抬起期間mIsBeingDragged為true,也就是說進(jìn)行攔截,接下來就是如何處理了,看看onTouchEvent:
@Override
public boolean onTouchEvent(MotionEvent ev) {
....
switch (action) {
case MotionEvent.ACTION_DOWN:
mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
mIsBeingDragged = false;
break;
case MotionEvent.ACTION_MOVE: {
pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
if (pointerIndex < 0) {
Log.e(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id.");
return false;
}
final float y = MotionEventCompat.getY(ev, pointerIndex);
final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
if (mIsBeingDragged) {
if (overscrollTop > 0) {
moveSpinner(overscrollTop);
} else {
return false;
}
}
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;
}
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;
}
這里省略了一些代碼,前面還有幾行跟上面的類似,也是在滿足其中一個條件時直接返回;switch中也還有幾行處理多指觸控的,這些都略過了??匆幌翧CTION_MOVE中計算了手指移動的距離,這時的mIsBeingDragged正常情況下應(yīng)為true,當(dāng)距離大于零就會執(zhí)行moveSpinner。在ACTION_UP中則會執(zhí)行finishSpinner,到這里就可以猜出,執(zhí)行刷新的邏輯主要就在這兩個方法中。
看這兩個方法前,要知道兩個重要的成員變量:一個是mCircleView,是CircleImageView的實例,繼承了ImageView,主要繪制進(jìn)度圈的背景;另一個是mProgress,是MaterialProgressDrawable的實例,繼承自Drawable且實現(xiàn)Animatable接口,主要繪制進(jìn)度圈,SwipeRefreshLayout正是通過調(diào)用其方法來繪制動畫。接下來就先看一下moveSpinner:
<span style="font-size:18px;">private void moveSpinner(float overscrollTop) {
mProgress.showArrow(true);
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();
}
}
float strokeStart = adjustedPercent * .8f;
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 */);
}</span>
showArrow是顯示箭頭,中間那一坨主要也是一些math和設(shè)置進(jìn)度圈的樣式,倒數(shù)第二行執(zhí)行了setProgressRotation,傳入的是經(jīng)過一堆計算后的rotation,這堆計算主要是優(yōu)化效果,比如在剛開始移動時增長比較快,超過刷新的距離后就增長比較慢。傳入該方法后,mProgress就根據(jù)它來繪制進(jìn)度圈,因此主要的動畫就應(yīng)該在這個方法內(nèi)。最后一行執(zhí)行setTargetOffsetTopAndBottom,我們來看一下:
<span style="font-size:18px;">private void setTargetOffsetTopAndBottom(int offset, boolean requiresUpdate) {
mCircleView.bringToFront();
mCircleView.offsetTopAndBottom(offset);
mCurrentTargetOffsetTop = mCircleView.getTop();
if (requiresUpdate && android.os.Build.VERSION.SDK_INT < 11) {
invalidate();
}
}</span>
比較簡單,就是調(diào)整進(jìn)度圈的位置并進(jìn)行記錄。最后來看一下finishSpinner:
<span style="font-size:18px;">private void finishSpinner(float overscrollTop) {
if (overscrollTop > mTotalDragDistance) {
setRefreshing(true, true /* notify */);
} else {
// cancel refresh
mRefreshing = false;
mProgress.setStartEndTrim(0f, 0f);
Animation.AnimationListener listener = null;
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) {
}
};
}
animateOffsetToStartPosition(mCurrentTargetOffsetTop, listener);
mProgress.showArrow(false);
}
}</span>
邏輯也很簡單,當(dāng)移動的距離超過設(shè)定值時就執(zhí)行setRefreshing(true,true),在該方法里更新一些成員變量的值后會執(zhí)行animateOffsetToCorrectPosition,由名字就知道是執(zhí)行動畫將進(jìn)度圈移動到正確位置的(也就是頭部)。如果移動的距離沒有超過設(shè)定值,就會執(zhí)行animateOffsetToStartPosition。一起看一下animateOffsetToCorrectPosition和animateOffsetToStartPosition這兩個方法:
<span style="font-size:18px;">private void animateOffsetToCorrectPosition(int from, AnimationListener listener) {
mFrom = from;
mAnimateToCorrectPosition.reset();
mAnimateToCorrectPosition.setDuration(ANIMATE_TO_TRIGGER_DURATION);
mAnimateToCorrectPosition.setInterpolator(mDecelerateInterpolator);
if (listener != null) {
mCircleView.setAnimationListener(listener);
}
mCircleView.clearAnimation();
mCircleView.startAnimation(mAnimateToCorrectPosition);
}
private void animateOffsetToStartPosition(int from, AnimationListener listener) {
if (mScale) {
// Scale the item back down
startScaleDownReturnToStartAnimation(from, listener);
} else {
mFrom = from;
mAnimateToStartPosition.reset();
mAnimateToStartPosition.setDuration(ANIMATE_TO_START_DURATION);
mAnimateToStartPosition.setInterpolator(mDecelerateInterpolator);
if (listener != null) {
mCircleView.setAnimationListener(listener);
}
mCircleView.clearAnimation();
mCircleView.startAnimation(mAnimateToStartPosition);
}
}</span>
邏輯基本相同,進(jìn)行一些設(shè)置后,最后都會執(zhí)行mCircleView的startAnimation,只是傳入的值以及監(jiān)聽器不同。
如果是要執(zhí)行刷新的操作,傳入的值是頭部高度,監(jiān)聽器為:
<span style="font-size:18px;">private Animation.AnimationListener mRefreshListener = new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationRepeat(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
if (mRefreshing) {
// Make sure the progress view is fully visible
mProgress.setAlpha(MAX_ALPHA);
mProgress.start();
if (mNotify) {
if (mListener != null) {
mListener.onRefresh();
}
}
mCurrentTargetOffsetTop = mCircleView.getTop();
} else {
reset();
}
}
};</span>
動畫完成后,也就是進(jìn)度圈移動到頭部后,會執(zhí)行mProgress.start();這里執(zhí)行的就是在刷新時進(jìn)度圈轉(zhuǎn)啊轉(zhuǎn)的動畫。接下來注意到如果mListener不為空就會執(zhí)行onRefresh方法,這個mListener其實就是執(zhí)行setOnRefreshListener所設(shè)置的監(jiān)聽器,因此在這里完成刷新。如果是執(zhí)行回到初始位置的操作,傳入的值為初始高度(也就是頂部之上),監(jiān)聽器為
<span style="font-size:18px;">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) {
}
};</span>
移動到初始位置后會執(zhí)行startScaleDownAnimation,也就是消失的動畫了,到這里整個刷新流程就結(jié)束了。
這樣就基本把SwipeRefreshLayout的流程過了一遍,但是要實現(xiàn)這樣一個控件還是有很多小問題需要考慮的,這里主要是把思路理清,知道如果出現(xiàn)問題該怎樣解決。另外從源碼也可以看出swipeRefreshLayout的定制性是比較差的,也不知道google是不是故意這樣希望以后全都用這種統(tǒng)一樣式的下拉刷新。。當(dāng)然有一些第三方下拉刷新的定制性還是比較好的,使用上也不難。但是有些人(比如我)是比較傾向于使用官方的控件的,不到萬不得已都不想用第三方工具。下次會寫一篇探討一下用swipeRefreshLayout實現(xiàn)自定義樣式的文章~
后續(xù)還有一篇從修改swipeRefreshLayout的源碼出發(fā)自定義樣式高仿微信朋友圈的下拉刷新效果的文章,有興趣可以看一下哈http://www.dhdzp.com/article/89311.htm
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- Android 使用SwipeRefreshLayout控件仿抖音做的視頻下拉刷新效果
- Android SwipeRefreshLayout仿抖音app靜態(tài)刷新
- android使用SwipeRefreshLayout實現(xiàn)ListView下拉刷新上拉加載
- Android SwipereFreshLayout下拉刷新
- Android SwipeRefreshLayout下拉刷新組件示例
- android組件SwipeRefreshLayout下拉小球式刷新效果
- Android實現(xiàn)SwipeRefreshLayout首次進(jìn)入自動刷新
- Android SwipeRefreshLayout下拉刷新源碼解析
- Android下拉刷新SwipeRefreshLayout控件使用方法
- Android官方下拉刷新控件SwipeRefreshLayout使用詳解
- Android自定義SwipeRefreshLayout高仿微信朋友圈下拉刷新
- SwipeRefreshLayout+RecyclerView實現(xiàn)上拉刷新和下拉刷新功能
相關(guān)文章
記錄Android studio JNI開發(fā)的三種方式(推薦)
JNI (Java Native Interface)是一套編程接口,用來實現(xiàn)Java代碼和其他語言(c、C++或匯編)進(jìn)行交互。下面通過本文給大家講解Android studio JNI開發(fā)的三種方式,需要的朋友參考下吧2017-12-12
條件數(shù)據(jù)庫Android:sqllite的簡單使用
條件數(shù)據(jù)庫Android:sqllite的簡單使用,需要的朋友可以參考一下2013-05-05
Android Studio 新建項目通過git上傳到碼云圖文教程詳解
本文通過圖文并茂的方式給大家介紹了Android Studio 新建項目通過git上傳到碼云的方法,需要的朋友可以參考下2017-11-11
Android控件ListView用法(讀取聯(lián)系人示例代碼)
本文以一個讀取聯(lián)系人的代碼為大家講解下Android控件中ListView的使用方法,這個listView有個setAdapter 適配器,里面可以直接實現(xiàn)接口,或者寫個類2013-06-06
Kotlin1.6.20新功能Context?Receivers使用技巧揭秘
這篇文章主要為大家介紹了Kotlin1.6.20功能Context?Receivers使用揭秘,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06

