想必你們也發現,時下的不少App都應用了這個Google出品的SwipeRefreshLayout下拉刷新控件,它以Material Design風格、適用場景普遍,簡單易用等特性而獨步江湖。但在咱們使用的過程當中,不可避免地會發現一些bug,或者須要添加某些特性來知足需求。出現這些問題,最好的方法就是解讀源碼,理解它實現的原理,而且在理解源碼的基礎上修改源碼,達成需求。然而不知爲什麼,至今尚未一篇關於SwipeRefreshLayout源碼解析的文章,因此萌發了要寫一篇這樣的文章。鑑於閱讀技術博文的枯燥,加之仍是篇源碼解析的文章,我不打算一會兒扔出來一大段代碼讓讀者去啃,而是一步一步往下走,揭開SwipeRefreshLayout的神祕面紗。java
爲何源碼廣泛都很難讀,有人甚至談之色變?其實代碼(出自大神之手)生來是易讀的,但代碼多了就變得難讀了。因此閱讀源碼時,要把握住主幹,細枝末節能夠暫時忽略,一路下來理解了程序工做流程後再回過頭來會有一種豁然開朗的感受。
閱讀源碼我仍是選擇Android Studio。這個強大的工具提供了不少快捷鍵,大大地方便了源碼的閱讀。android
在看往下看以前,我但願你瞭解:git
所幸該控件沒有跟系統api耦合,因此能夠直接copy一份代碼到本身的demo工程中,盡情地改。可是hint會理解報出一些錯誤。首先包名要改一下,類名最好也改吧,以避免混淆~其次把CircleImageView和MaterialProgressDrawable這兩個類都copy過來,放在同一個包裏。如圖:
若是嫌麻煩能夠直接fork個人項目。github
咱們朝着未知的黑暗出發。打開SwipeRefreshTestLayout的類文件,看到左邊這麼小的滑塊,其實我一開始是拒絕的~ 感受無從下手啊有沒有… 沉下心來,想一想看看它是繼承於ViewGroup的,因此想一想它必定有兩個很關鍵的方法:onMeasure和onLayout,分別解決了它和它的子View佔多大地和擱到哪。由於它是一個下拉刷新控件,它一定要涉及到事件分發的處理,一樣是兩個關鍵方法:onInterceptTouchEvent和onTouchEvent,分別用於決定是否攔截點擊事件和進行點擊事件的處理。天空瞬間亮了許多…api
@Override public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (mTarget == null) { ensureTarget(); } if (mTarget == null) { return; } //mTarget的尺寸爲match_parent,除去內邊距 mTarget.measure(MeasureSpec.makeMeasureSpec( getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec( getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY)); //設置mCircleView的尺寸 mCircleView.measure(MeasureSpec.makeMeasureSpec(mCircleWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(mCircleHeight, MeasureSpec.EXACTLY)); //若是mOriginalOffsetTop未被初始化而且mUsingCustomStart ?,則將下拉小圓的初始位置設置成默認值 if (!mUsingCustomStart && !mOriginalOffsetCalculated) { mOriginalOffsetCalculated = true; mCurrentTargetOffsetTop = mOriginalOffsetTop = -mCircleView.getMeasuredHeight(); } mCircleViewIndex = -1; // Get the index of the circleview.獲取circleview的索引值,主要是爲了後面重載getChildDrawingOrder時要用 for (int index = 0; index < getChildCount(); index++) { if (getChildAt(index) == mCircleView) { mCircleViewIndex = index; break; } } }
咱們看到,這個方法代碼不長,但卻很關鍵。重寫該方法的做用是設置子View的尺寸。出現mTarget是什麼未知生物?其實就是一個它包裹的子View,一般是ListView等一些可滾動的控件。ensureTarget();保證它非空並存在。若是不當心包裹了多個VIew呢?則mTarget就是其中的最後一個子View。mCircleView又是什麼生物呢?顧名思義,下拉的白色小圓圈,一個ImageView而已。mCurrentTargetOffsetTop 和mOriginalOffsetTop 是兩個很是關鍵的變量,分別表示當前mCircleView的位置(top值)和初始時mCircleView的位置(top值),固然它們初始化都等於mCircleView高度的負數。還有一個mUsingCustomStart 是什麼呢?我當時也不知道。不要緊,Ctrl+F11打個書籤,等讀完再回頭看。或者咱們能夠經過Alt+F7看看它的在哪裏被引用過。
能夠看到,它在setProgressViewOffset被賦值爲true,而該方法是用於設置CircleView初始的位置和刷新停留的位置,Custom是自定義的意思,因此mUsingCustomStart就是一個標誌,表示是否用自定義的起始位置,而默認的起始位置就是CircleView高度的負數。框架
@Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { final int width = getMeasuredWidth(); final int height = getMeasuredHeight(); if (getChildCount() == 0) { return; } if (mTarget == null) { ensureTarget(); } if (mTarget == null) { return; } final View child = mTarget; final int childLeft = getPaddingLeft(); final int childTop = getPaddingTop(); final int childWidth = width - getPaddingLeft() - getPaddingRight(); final int childHeight = height - getPaddingTop() - getPaddingBottom(); //將mTarget放在覆蓋parent的位置(除去內邊距) child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight); //將mCircleView放在mTarget的平面位置上面居中,初始化時是徹底隱藏在屏幕外的 int circleWidth = mCircleView.getMeasuredWidth(); int circleHeight = mCircleView.getMeasuredHeight(); mCircleView.layout((width / 2 - circleWidth / 2), mCurrentTargetOffsetTop, (width / 2 + circleWidth / 2), mCurrentTargetOffsetTop + circleHeight); }
這個方法代碼也不長,很簡單,但卻很關鍵。做用是安排子View的位置。將mTarget填充整個控件,將mCircleView放在mTarget的平面位置上面居中,初始化時是徹底隱藏在屏幕外的。ide
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { ensureTarget(); final int action = MotionEventCompat.getActionMasked(ev); //若是當mCircleView正在返回初始位置的同時手指按下了,將標誌mReturningToStart復位 if (mReturningToStart && action == MotionEvent.ACTION_DOWN) { mReturningToStart = false; } //若是下拉被禁用、mCircleView正在返回初始位置、mTarget沒有到達頂部、 //正在刷新、mNestedScrollInProgress // 不攔截,不處理點擊事件,處理權交還mTarget 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; //若是是滑動動做,將mIsBeingDragged置爲true 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; } //若是正在被拖拽,攔截該系列的點擊事件,並調用本身的onTouchEvent()來處理 return mIsBeingDragged; }
這個方法的邏輯很是清晰。當若是下拉被禁用、mCircleView正在返回初始位置、mTarget沒有到達頂部、
或者正在刷新時, 不攔截,不處理點擊事件,處理權交還mTarget。排除以上狀況後,還須要進一步判斷。
當手指按下時,記錄按下的座標;在MotionEvent.ACTION_MOVE當中,判斷是不是滑動動做,若是是,攔截該系列的點擊事件,並調用本身的onTouchEvent()來處理。函數
重頭戲來了!這個方法是關鍵中的關鍵:工具
@Override public boolean onTouchEvent(MotionEvent ev) { final int action = MotionEventCompat.getActionMasked(ev); int pointerIndex = -1; if (mReturningToStart && action == MotionEvent.ACTION_DOWN) { mReturningToStart = false; } //若是被禁用、CircleView正在復位、沒到達頂部、mNestedScrollInProgress,直接返回,不處理該事件 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: 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) { //spinner可理解爲下拉組件,將spinner移到指定的高度 //很關鍵的方法,進入看看 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; } 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; }
在case MotionEvent.ACTION_MOVE當中,計算下拉的總高度overscrollTop,DRAG_RATE是下拉阻尼,能夠經過改變它的值來改變下拉手感哦~~而後進入到moveSpinner()方法,將spinner移到指定的高度。那麼spinner是啥?其實就是下拉組件的意思。post
/** * 經過調用setTargetOffsetTopAndBottom()方法移動下拉組件Spinner(mCircleView) * 同時更新mProgress(一個drawable)的繪製進度 * @param overscrollTop 下拉高度 */ private void moveSpinner(float overscrollTop) { mProgress.showArrow(true);//顯示Progressbar的箭頭 //通過一系列的計算,spinner控制下拉的最大距離 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; //計算spinner將要(target)被移動到的位置對應的Y座標,當targetY爲0時,小圓圈恰好所有露出來 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-else主要是在經過下拉進度,對mProgress在下拉過程設置顏色透明度,箭頭旋轉角度,縮放大小的控制 //若是下拉高度小於mTotalDragDistance(一個觸發下拉刷新的高度) if (overscrollTop < mTotalDragDistance) { //若是支持下拉小圓圈縮放,設置顏色透明度和縮放大小 if (mScale) { setAnimationProgress(overscrollTop / mTotalDragDistance); } if (mProgress.getAlpha() > STARTING_PROGRESS_ALPHA && !isAnimationRunning(mAlphaStartAnimation)) { // Animate the alpha startProgressAlphaStartAnimation(); } float strokeStart = adjustedPercent * .8f; mProgress.setStartEndTrim(0f, Math.min(MAX_PROGRESS_ANGLE, strokeStart)); mProgress.setArrowScale(Math.min(1f, adjustedPercent)); } else {//下拉高度達到了觸發刷新的高度 if (mProgress.getAlpha() < MAX_ALPHA && !isAnimationRunning(mAlphaMaxAnimation)) { // Animate the alpha startProgressAlphaMaxAnimation(); } ViewCompat.setScaleX(mCircleView, 1f); ViewCompat.setScaleY(mCircleView, 1f); } float rotation = (-0.25f + .4f * adjustedPercent + tensionPercent * 2) * .5f; mProgress.setProgressRotation(rotation); //這是關鍵調用!動態設置mSpinner的高度。進入該函數看看 setTargetOffsetTopAndBottom(targetY - mCurrentTargetOffsetTop, true /* requires update */); }
該方法主要乾的事就是經過調用setTargetOffsetTopAndBottom()方法移動下拉組件Spinner(mCircleView),同時更新mProgress(一個drawable)的繪製進度。其中進行了一些複雜的運算,其實就是在控制下拉的最大高度,避免用戶無限下拉…說明一下,這個mScale,由於我已經添加了javadoc,讀者Ctrl+Q就能夠查看它的相關信息。它以爲spinner下拉過程是否支持縮放,能夠經過setProgressViewEndTarget()和setProgressViewOffset()設置。但我發現一個bug,若是手指下拉過快,小圓就會來不及放到最大…小圓明顯變小了,如圖
好,有改bug的但願就有了繼續閱讀的動力!咱們進入setTargetOffsetTopAndBottom()看看。
/** * 設置mCircleView的偏移量 * 同時更新mCurrentTargetOffsetTop * @param offset 偏移量,可正可負 * @param requiresUpdate 界面是否須要重繪invalidate(); */ private void setTargetOffsetTopAndBottom(int offset, boolean requiresUpdate) { //bringToFront()該方法會調用requestLayout()和invalidate()把view放到前面 //已經重寫了getChildDrawingOrder方法,因此沒有必要再調用該方法了,我我的認爲... //可經過手動調用invalidate()來代替它 // mCircleView.bringToFront(); mCircleView.offsetTopAndBottom(offset); mCurrentTargetOffsetTop = mCircleView.getTop(); if (requiresUpdate /*&& android.os.Build.VERSION.SDK_INT < 11*/) { invalidate(); } }
該方法很短,倒是mCircleView可以下拉的精髓所在啊!offsetTopAndBottom()本質上是調用layout(getLeft(),getTop()+offsetY,getRight(),getBottom()+offsetY);(注意不是onLayout())同時對mCircleView的top和bottom進行偏移,offset是View總體在垂直方向上的偏移量。這裏我把bringToFront()註釋掉,bringToFront()該方法會調用requestLayout()和invalidate()把view放到前面,由於已經重寫了getChildDrawingOrder方法,因此沒有必要再調用該方法了,我我的認爲…可經過手動調用invalidate()來代替它。
到此,咱們已經瞭解過它下拉的的過程,下面進行回溯到onTouchEvent的case MotionEvent.ACTION_UP:
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; }
計算完了鬆開手時下拉的總距離後,交給方法finishSpinner(overscrollTop);處理。進去看看。
/** * 手指鬆開後,處理下拉組件Spinner * 設置開始刷新的動畫,或者 * 將下拉組件Spinner回滾隱藏 * @param overscrollTop 下拉距離 */ private void finishSpinner(float overscrollTop) { if (overscrollTop > mTotalDragDistance) {//下拉距離達到了可觸發刷新的高度 //關鍵方法 setRefreshing(true, true /* notify */); } else {//下拉距離還未達到了可觸發刷新的高度,作一些復位的操做 // cancel refresh mRefreshing = false; mProgress.setStartEndTrim(0f, 0f); //值得關注的是這個回滾動畫 AnimationListener listener = null; if (!mScale) { listener = new AnimationListener() { @Override public void onAnimationEnd(Animation animation) { if (!mScale) { startScaleDownAnimation(null); } } }; } //開始回滾動畫 //這是一個比較複雜的方法,也是比較有用的方法 //其實這個本質上不是開啓一個動畫,而是一個數值產生器 //經過監聽數值變化, //從mCurrentTargetOffsetTop這個高度開始, //調用setTargetOffsetTopAndBottom()慢慢回滾到mOriginalOffsetTop animateOffsetToStartPosition(mCurrentTargetOffsetTop, listener); mProgress.showArrow(false); } }
這裏看到的mTotalDragDistance一樣能夠經過Ctrl+Q查看它的信息。這個方法只有一對大大的if-else,若是下拉距離達到了可觸發刷新的高度,開始刷新;不然開始回滾動畫,將Spinner回滾到開始的位置(也就是mOriginalOffsetTop)。而animateOffsetToStartPosition這個方法是一個內涵很豐富的方法,涉及到多步跳轉才能瞭解完全。你們能夠去github fork下來,找到相應方法Ctrl+左擊進去看看,裏面的方法都添加了詳細的註釋,相信你們必定能看懂。有朋友可能會問,這裏怎麼用視圖動畫而不用屬性動畫呢?其實這裏並非開啓一個真正意義上的動畫,而是一個數值產生器,經過監聽數值變化,從mCurrentTargetOffsetTop這個高度開始,調用setTargetOffsetTopAndBottom()慢慢回滾到mOriginalOffsetTop。
下面咱們一塊兒來看看setRefreshing(true, true /* notify */);
/** * 設置刷新狀態,該方法一般不是由類庫使用者來調用,而是在用戶下拉的時候由SwipeRefreshLayout來調用 * @param refreshing 是否刷新 * @param notify 是否回調onRefresh() */ private void setRefreshing(boolean refreshing, final boolean notify) { if (mRefreshing != refreshing) { mNotify = notify; ensureTarget(); mRefreshing = refreshing; if (mRefreshing) {//啓動刷新 animateOffsetToCorrectPosition(mCurrentTargetOffsetTop, mRefreshListener); } else {//中止刷新 //開始Spinner消失動畫 startScaleDownAnimation(mRefreshListener); } } }
該方法一般不是由類庫使用者來調用,而是在用戶下拉的時候由SwipeRefreshLayout本身來調用,因此它是private的。若是啓動刷新,則調用animateOffsetToCorrectPosition(mCurrentTargetOffsetTop, mRefreshListener);將mCircleView移到正確的高度(也就是mSpinnerFinalOffset),animateOffsetToCorrectPosition()跟上文提到的animateOffsetToStartPosition()方法的實現機理是徹底同樣的。咱們這裏回想一下,剛纔的bug是因爲手指鬆開時mCircleView的Scale值沒有達到1,那麼在這裏咱們就能夠在它的移動到刷新位置的動畫結束時,把它的Scale手動設置爲1。
private AnimationListener mRefreshListener = new AnimationListener() { @Override public void onAnimationEnd(Animation animation) { if (mRefreshing) { //改bug ViewCompat.setScaleX(mCircleView, 1f); ViewCompat.setScaleY(mCircleView, 1f); // Make sure the progress view is fully visible mProgress.setAlpha(MAX_ALPHA); mProgress.start(); if (mNotify) { if (mListener != null) { mListener.onRefresh(); } } } else { mProgress.stop(); mCircleView.setVisibility(View.GONE); setColorViewAlpha(MAX_ALPHA); // Return the circle to its start position if (mScale) { setAnimationProgress(0 /* animation complete and view is hidden */); } else { setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCurrentTargetOffsetTop, true /* requires update */); } } mCurrentTargetOffsetTop = mCircleView.getTop(); } };
效果還可吧!
咱們發現onRefresh()是在這個被回調的,並且僅在這裏被回調。
不知不覺,天亮了~框架脈絡已經很清晰了吧。
還有一些變量或方法的名字帶有NestedScroll沒有提到,其實那是跟嵌套滑動有關的,不知道也不影響源碼的閱讀。
下面說說我遇到過的一個問題,當咱們在Activity的onCreate中
mRefreshLayout = (SwipeRefreshTestLayout) findViewById(R.id.refresh_widget); mRefreshLayout.setRefreshing(true);
new Handler().postDelayed(new Runnable() { @Override public void run() { mRefreshLayout.setRefreshing(true); } },100);
或者
mRefreshLayout.postDelayed(new Runnable() { @Override public void run() { mRefreshLayout.setRefreshing(false); } }, 3000);
@Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); mRefreshLayout.setRefreshing(true); }
public void setRefreshing(final boolean refreshing) { //防止類庫使用者在SwipeRefreshLayout還沒徹底被初始化時調用該方法 //仍是建議使用者重寫Activity的onWindowFocusChanged()方法來調用setRefreshing(true); if (!isShown()&& refreshing){ Log.e("SwipeRefreshLayout", "It's not advisable to invoke setRefreshing() when SwipeRefreshLayout is inVisible."); new Handler().postDelayed(new Runnable() { @Override public void run() { setRefreshing(true); } },50/*該時間能夠任意設置*/); return; } .....省略若干代碼...... }
項目代碼已上傳至Github。—repo
點star和轉發也是一種支持!
若是你發現有什麼不清楚或不妥的地方歡迎留言討論。