下拉刷新框架其實有很多,而且質量都比較高。但是在日常開發中,每一款產品都會有一套自己獨特的一套刷新樣式。相信有很多小伙伴在個性化定制中都或多或少的遇到過麻煩。今天我就給大家推薦一個在定制方面很出彩的一個刷新框架SwipeToLoadLayout,該框架自身完成了下拉刷新與上拉加載功能,同時將頂部視圖與底部視圖的UI定制功能通過接口很方便的提供給使用者自行定義。
相關代碼已經上傳到github上,歡迎star、fork
基本流程
先簡單了解一下SwipeToLoadLayout的使用流程,以下拉刷新為例:
- 完成Header部分,實現SwipeRefreshTrigger與SwipeRefreshTrigger接口
- 完成activity或fragment的布局,在SwipeToLoadLayout節點下配置好Header與下拉目標組件(如RecyclerView等)
這里還是要稍微說一下,因為這個布局過程還是有一定的規則的
首先布局的id是固定的,這個我們在ids.xml中就能看出。框架提供三個View:Header、Target、Footer,分別對應三個位置的View
<?xml version="1.0" encoding="utf-8"?> <resources> <item name="swipe_target" type="id" /> <item name="swipe_refresh_header" type="id" /> <item name="swipe_load_more_footer" type="id" /> </resources>
其次onFinishInflate()方法告訴我們,最多只能同時存在這三個View,不能有更多的子View了
@Override protected void onFinishInflate() { super.onFinishInflate(); final int childNum = getChildCount(); if (childNum == 0) { // no child return return; } else if (0 < childNum && childNum < 4) { mHeaderView = findViewById(R.id.swipe_refresh_header); mTargetView = findViewById(R.id.swipe_target); mFooterView = findViewById(R.id.swipe_load_more_footer); } else { // more than three children: unsupported! throw new IllegalStateException("Children num must equal or less than 3"); } if (mTargetView == null) { return; } if (mHeaderView != null && mHeaderView instanceof SwipeTrigger) { mHeaderView.setVisibility(GONE); } if (mFooterView != null && mFooterView instanceof SwipeTrigger) { mFooterView.setVisibility(GONE); } }
這樣你就能得出下一步該怎么來實現了吧?沒錯肯定是這樣的
<?xml version="1.0" encoding="utf-8"?> <com.aspsine.swipetoloadlayout.SwipeToLoadLayout > <View android:id="@id/swipe_refresh_header" /> <android.support.v7.widget.RecyclerView android:id="@id/swipe_target" /> <View android:id="@id/swipe_load_more_footer" /> </com.aspsine.swipetoloadlayout.SwipeToLoadLayout>
Header的部分尤為重要。我們需在Header上實現SwipeTrigger與SwipeRefreshTrigger接口,接口中的方法分別對應滑動刷新在各個狀態下的回調。它們分別為
onPrepare:代表下拉刷新開始的狀態
onMove:代表正在滑動過程中的狀態
onRelease:代表手指松開后,下拉刷新進入松開刷新的狀態
onComplete:代表下拉刷新完成的狀態
onReset:代表下拉刷新重置恢復的狀態
onRefresh:代表正在刷新中的狀態
有了這幾個接口,我們就可以完成Header部分的任何動畫效果了。當然上拉加載更多的場景,只是把SwipeRefreshTrigger接口換成SwipeLoadMoreTrigger接口而已,其他跟下拉刷新情況完全相同
- 在activity或fragment中配置下拉監聽事件,並在數據獲取完成后主動觸發刷新swipeToLoadLayout.setRefreshing(false);完成功能
更深入的部分我們放到源碼分析里面再說
看起來好像很簡單,那么我們就通過幾個小Demo了解一下如何使用吧
仿新浪微博
之所以第一個范例選擇新浪微博,是因為它是最傳統刷新風格:根據箭頭和文字的不同來表明當前不同的狀態

如果你在早期研究過PullToRefresh,那么很容易在這個框架基礎上實現相應的視圖更新功能
先完成頭部的定義。WeiboRefreshHeaderView作為頭,其實際為一個LinearLayout
class WeiboRefreshHeaderView : LinearLayout, SwipeTrigger, SwipeRefreshTrigger
頭部布局很簡單
<?xml version="1.0" encoding="utf-8"?> <com.renyu.swipetoloadlayoutdemo.view.WeiboRefreshHeaderView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="60dip" android:gravity="center" android:orientation="horizontal"> <RelativeLayout android:layout_width="wrap_content" android:layout_height="wrap_content"> <ProgressBar android:id="@+id/pb_weibo" style="?android:attr/progressBarStyleSmallInverse" android:layout_centerInParent="true" android:visibility="gone" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <ImageView android:id="@+id/iv_weibo" android:src="@mipmap/tableview_pull_refresh_arrow_down" android:layout_centerInParent="true" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </RelativeLayout> <TextView android:id="@+id/tv_weibo" android:layout_marginStart="10dip" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="下拉刷新"/> </com.renyu.swipetoloadlayoutdemo.view.WeiboRefreshHeaderView>
activity的布局也很簡單,把頭跟身子一起加在SwipeToLoadLayout里
<?xml version="1.0" encoding="utf-8"?> <com.aspsine.swipetoloadlayout.SwipeToLoadLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/swipe_weibo"> <include layout="@layout/header_weibo" android:id="@id/swipe_refresh_header" /> <TextView android:id="@id/swipe_target" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:text="下拉刷新"/> </com.aspsine.swipetoloadlayout.SwipeToLoadLayout>
下面就是完成頭部動畫效果了。新浪微博的這個效果就是視圖被下拉到頭部高度之后,將箭頭位置旋轉一下同時更換文字,刷新時展現progressbar即可
class WeiboRefreshHeaderView : LinearLayout, SwipeTrigger, SwipeRefreshTrigger { var pb_weibo: ProgressBar? = null var iv_weibo: ImageView? = null var tv_weibo: TextView? = null // 是否發生旋轉 var rotated = false private val rotate_up: Animation by lazy { AnimationUtils.loadAnimation(context, R.anim.rotate_up) } private val rotate_down: Animation by lazy { AnimationUtils.loadAnimation(context, R.anim.rotate_down) } constructor(context: Context) : super(context) constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet) constructor(context: Context, attributeSet: AttributeSet, defStyleAttr: Int) : super(context, attributeSet, defStyleAttr) override fun onFinishInflate() { super.onFinishInflate() pb_weibo = findViewById(R.id.pb_weibo) iv_weibo = findViewById(R.id.iv_weibo) tv_weibo = findViewById(R.id.tv_weibo) } override fun onReset() { pb_weibo?.visibility = View.GONE iv_weibo?.visibility = View.VISIBLE tv_weibo?.text = "下拉刷新" } override fun onComplete() { tv_weibo?.text = "刷新完成" pb_weibo?.visibility = View.GONE } override fun onRelease() { } override fun onMove(p0: Int, p1: Boolean, p2: Boolean) { if (p0 > SizeUtils.dp2px(60f)) { if (!rotated) { rotated = true tv_weibo?.text = "釋放更新" iv_weibo?.clearAnimation() iv_weibo?.startAnimation(rotate_up) } } else { if (rotated) { rotated = false tv_weibo?.text = "下拉刷新" iv_weibo?.clearAnimation() iv_weibo?.startAnimation(rotate_down) } } } override fun onPrepare() { } override fun onRefresh() { tv_weibo?.text = "加載中" iv_weibo?.clearAnimation() iv_weibo?.visibility = View.GONE pb_weibo?.visibility = View.VISIBLE } }

對照一下上文的刷新周期,應該很好理解
美團外賣
美團外賣是利用ImageView直接播放一段animation直到刷新完成停止。在下拉過程中,該ImageView隨着位移的距離變化而發生相應的大小變化

美團外賣動畫效果是由一系列的圖片組成的,所以與新浪微博效果相比更為簡單一些

一樣要完成頭部視圖的定義
class MTRefreshHeaderView : LinearLayout, SwipeTrigger, SwipeRefreshTrigger
<?xml version="1.0" encoding="utf-8"?> <com.renyu.swipetoloadlayoutdemo.view.MTRefreshHeaderView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" android:padding="10dip"> <ImageView android:id="@+id/iv_mt" android:layout_width="112dp" android:layout_height="44dp" android:background="@drawable/animation_list_refresh_mt" android:transformPivotX="56dp" android:transformPivotY="22dp" android:scaleY="0.3" android:scaleX="0.3"/> </com.renyu.swipetoloadlayoutdemo.view.MTRefreshHeaderView>
剩下就是完成動畫的播放與縮放的處理了
class MTRefreshHeaderView : LinearLayout, SwipeTrigger, SwipeRefreshTrigger { var iv_mt: ImageView? = null val animationDrawable: AnimationDrawable by lazy { iv_mt?.background as AnimationDrawable } constructor(context: Context) : super(context) constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet) constructor(context: Context, attributeSet: AttributeSet, defStyleAttr: Int) : super(context, attributeSet, defStyleAttr) override fun onFinishInflate() { super.onFinishInflate() iv_mt = findViewById(R.id.iv_mt) } override fun onReset() { } override fun onComplete() { animationDrawable.stop() } override fun onRelease() { } override fun onMove(p0: Int, p1: Boolean, p2: Boolean) { val percent = if (p0 * 1.0f / SizeUtils.dp2px(44f) > 1) 1f else p0 * 1.0f / SizeUtils.dp2px(44f) iv_mt?.scaleY = (0.3f + 0.7 * percent).toFloat() iv_mt?.scaleX = (0.3f + 0.7 * percent).toFloat() } override fun onPrepare() { if (!animationDrawable.isRunning) { animationDrawable.start() } iv_mt?.scaleY = 0.3f iv_mt?.scaleX = 0.3f } override fun onRefresh() { if (!animationDrawable.isRunning) { animationDrawable.start() } iv_mt?.scaleY = 1f iv_mt?.scaleX = 1f } }

代碼都很簡單,很容易理解
餓了么
餓了么的效果是通過SVG來實現的

餓了么app對資源進行了混淆,所以我拿不到圖片,只能隨便從其他地方找一個了
一樣是Header的編寫,這里面有一點不同,我用android-pathview這個開源框架實現SVG播放進度控制功能
我需要將這個動畫效果在下拉刷新的過程中實現

class ElemeRefreshHeaderView : LinearLayout, SwipeTrigger, SwipeRefreshTrigger
<?xml version="1.0" encoding="utf-8"?> <com.renyu.swipetoloadlayoutdemo.view.ElemeRefreshHeaderView xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center"> <com.eftimoff.androipathview.PathView xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/pathView_ele" android:layout_width="58dp" android:layout_height="58dp" app:pathColor="@android:color/black" app:svg="@raw/issues" app:pathWidth="2dp"/> </com.renyu.swipetoloadlayoutdemo.view.ElemeRefreshHeaderView>
下面就是根據滑動偏移量來處理SVG播放的進度
class ElemeRefreshHeaderView : LinearLayout, SwipeTrigger, SwipeRefreshTrigger { var pathView_ele: PathView? = null constructor(context: Context) : super(context) constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet) constructor(context: Context, attributeSet: AttributeSet, defStyleAttr: Int) : super(context, attributeSet, defStyleAttr) override fun onFinishInflate() { super.onFinishInflate() pathView_ele = findViewById(R.id.pathView_ele) } override fun onReset() { } override fun onComplete() { pathView_ele?.setPercentage(1f) } override fun onRelease() { } override fun onMove(p0: Int, p1: Boolean, p2: Boolean) { val percent = 1 - (SizeUtils.dp2px(58f) - p0) * 1.0f / SizeUtils.dp2px(58f) val value = if (percent >= 1) 1f else percent pathView_ele?.setPercentage(value) } override fun onPrepare() { pathView_ele?.setPercentage(0f) } override fun onRefresh() { pathView_ele?.setPercentage(1f) } }

這里你會發出一個疑問,怎么效果與餓了么有的差距?餓了么是滑動到Header完成展開之后就不再繼續下滑了,那咱們這個怎么實現呢?那我只能說不好意思,在現有條件下咱們實現不了,只能通過改源碼完成
那我們就順帶來閱讀源碼,看看這個地方怎么改進吧?
源碼分析
之前的onFinishInflate咱們就不說了,那個就是告訴我們只能有三個View,分別是Header、Target、Footer
然后是測量階段,在測量階段可以得到兩個重要的變量mHeaderHeight與mFooterHeight,他們分別代表Header與Footer的高度。同時如果定義的mRefreshTriggerOffset(松開刷新的高度)比Header或Footer的高度小,則修正這個刷新位置
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); // header if (mHeaderView != null) { final View headerView = mHeaderView; measureChildWithMargins(headerView, widthMeasureSpec, 0, heightMeasureSpec, 0); MarginLayoutParams lp = ((MarginLayoutParams) headerView.getLayoutParams()); mHeaderHeight = headerView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; if (mRefreshTriggerOffset < mHeaderHeight) { mRefreshTriggerOffset = mHeaderHeight; } } // target if (mTargetView != null) { final View targetView = mTargetView; measureChildWithMargins(targetView, widthMeasureSpec, 0, heightMeasureSpec, 0); } // footer if (mFooterView != null) { final View footerView = mFooterView; measureChildWithMargins(footerView, widthMeasureSpec, 0, heightMeasureSpec, 0); MarginLayoutParams lp = ((MarginLayoutParams) footerView.getLayoutParams()); mFooterHeight = footerView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; if (mLoadMoreTriggerOffset < mFooterHeight) { mLoadMoreTriggerOffset = mFooterHeight; } } }
在onLayout中對三個視圖進行布局
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { layoutChildren(); mHasHeaderView = (mHeaderView != null); mHasFooterView = (mFooterView != null); }
這里有一個重要的方法layoutChildren,這個方法就是改變三個視圖的位置的。當然這個位置要根據不同的類型來處理,默認情況下我們都是STYLE.CLASSIC類型。
private void layoutChildren() { final int width = getMeasuredWidth(); final int height = getMeasuredHeight(); final int paddingLeft = getPaddingLeft(); final int paddingTop = getPaddingTop(); final int paddingRight = getPaddingRight(); final int paddingBottom = getPaddingBottom(); if (mTargetView == null) { return; } // layout header if (mHeaderView != null) { final View headerView = mHeaderView; MarginLayoutParams lp = (MarginLayoutParams) headerView.getLayoutParams(); final int headerLeft = paddingLeft + lp.leftMargin; final int headerTop; switch (mStyle) { case STYLE.CLASSIC: // classic headerTop = paddingTop + lp.topMargin - mHeaderHeight + mHeaderOffset; break; case STYLE.ABOVE: // classic headerTop = paddingTop + lp.topMargin - mHeaderHeight + mHeaderOffset; break; case STYLE.BLEW: // blew headerTop = paddingTop + lp.topMargin; break; case STYLE.SCALE: // scale headerTop = paddingTop + lp.topMargin - mHeaderHeight / 2 + mHeaderOffset / 2; break; case STYLE.BLEW2CLASSIC: // blew2classic if (mHeaderOffset > mHeaderHeight) { headerTop = paddingTop + lp.topMargin; } else { headerTop = paddingTop + lp.topMargin - mHeaderHeight + mHeaderOffset; } break; default: // classic headerTop = paddingTop + lp.topMargin - mHeaderHeight + mHeaderOffset; break; } final int headerRight = headerLeft + headerView.getMeasuredWidth(); final int headerBottom = headerTop + headerView.getMeasuredHeight(); headerView.layout(headerLeft, headerTop, headerRight, headerBottom); } // layout target if (mTargetView != null) { final View targetView = mTargetView; MarginLayoutParams lp = (MarginLayoutParams) targetView.getLayoutParams(); final int targetLeft = paddingLeft + lp.leftMargin; final int targetTop; switch (mStyle) { case STYLE.CLASSIC: // classic targetTop = paddingTop + lp.topMargin + mTargetOffset; break; case STYLE.ABOVE: // above targetTop = paddingTop + lp.topMargin; break; case STYLE.BLEW: // classic targetTop = paddingTop + lp.topMargin + mTargetOffset; break; case STYLE.SCALE: // classic targetTop = paddingTop + lp.topMargin + mTargetOffset; break; case STYLE.BLEW2CLASSIC: // classic targetTop = paddingTop + lp.topMargin + mTargetOffset; break; default: // classic targetTop = paddingTop + lp.topMargin + mTargetOffset; break; } final int targetRight = targetLeft + targetView.getMeasuredWidth(); final int targetBottom = targetTop + targetView.getMeasuredHeight(); targetView.layout(targetLeft, targetTop, targetRight, targetBottom); } // layout footer if (mFooterView != null) { final View footerView = mFooterView; MarginLayoutParams lp = (MarginLayoutParams) footerView.getLayoutParams(); final int footerLeft = paddingLeft + lp.leftMargin; final int footerBottom; switch (mStyle) { case STYLE.CLASSIC: // classic footerBottom = height - paddingBottom - lp.bottomMargin + mFooterHeight + mFooterOffset; break; case STYLE.ABOVE: // classic footerBottom = height - paddingBottom - lp.bottomMargin + mFooterHeight + mFooterOffset; break; case STYLE.BLEW: // blew footerBottom = height - paddingBottom - lp.bottomMargin; break; case STYLE.SCALE: // scale footerBottom = height - paddingBottom - lp.bottomMargin + mFooterHeight / 2 + mFooterOffset / 2; break; case STYLE.BLEW2CLASSIC: // blew2classic if (mFooterOffset > mFooterHeight) { footerBottom = height - paddingBottom - lp.bottomMargin; } else { footerBottom = height - paddingBottom - lp.bottomMargin + mFooterHeight + mFooterOffset; } break; default: // classic footerBottom = height - paddingBottom - lp.bottomMargin + mFooterHeight + mFooterOffset; break; } final int footerTop = footerBottom - footerView.getMeasuredHeight(); final int footerRight = footerLeft + footerView.getMeasuredWidth(); footerView.layout(footerLeft, footerTop, footerRight, footerBottom); } if (mStyle == STYLE.CLASSIC || mStyle == STYLE.ABOVE) { if (mHeaderView != null) { mHeaderView.bringToFront(); } if (mFooterView != null) { mFooterView.bringToFront(); } } else if (mStyle == STYLE.BLEW || mStyle == STYLE.SCALE || mStyle == STYLE.BLEW2CLASSIC) { if (mTargetView != null) { mTargetView.bringToFront(); } } }
以下拉刷新為例,看這行代碼。
paddingTop與lp.topMargin都是0,mHeaderHeight是Header的高度,mHeaderOffset就是手指滑動的距離(這個稍后會有說明)。在下拉過程中,mHeaderOffset的值會越來越大,所以headerTop的值是從-mHeaderHeight開始逐漸增大的,所以headerView會向下逐步移動
headerTop = paddingTop + lp.topMargin - mHeaderHeight + mHeaderOffset
而Target更為簡單,你手指滑動多少它就跟着滑動多少
targetTop = paddingTop + lp.topMargin + mTargetOffset;
這樣能夠想象出餓了么滑動到mHeaderHeight高度之后如何處理的吧,請參考我自己定義的style--BLEW2CLASSIC
if (mHeaderOffset > mHeaderHeight) { headerTop = paddingTop + lp.topMargin; } else { headerTop = paddingTop + lp.topMargin - mHeaderHeight + mHeaderOffset; }

繼續往下來到事件分發部分了
@Override public boolean dispatchTouchEvent(MotionEvent ev) { final int action = MotionEventCompat.getActionMasked(ev); switch (action) { case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: // swipeToRefresh -> finger up -> finger down if the status is still swipeToRefresh // in onInterceptTouchEvent ACTION_DOWN event will stop the scroller // if the event pass to the child view while ACTION_MOVE(condition is false) // in onInterceptTouchEvent ACTION_MOVE the ACTION_UP or ACTION_CANCEL will not be // passed to onInterceptTouchEvent and onTouchEvent. Instead It will be passed to // child view's onTouchEvent. So we must deal this situation in dispatchTouchEvent onActivePointerUp(); break; } return super.dispatchTouchEvent(ev); }
獲取事件之后,在手指釋放的時候執行onActivePointerUp(),咱們來看看。分別判斷了當前是處在下拉以刷新、上拉以加載更多、松開以刷新、松開以加載更多,然后滾動到響應的位置上去。注意在松開狀態時,執行了onRelease()回調
private void onActivePointerUp() {
if (STATUS.isSwipingToRefresh(mStatus)) { // simply return scrollSwipingToRefreshToDefault(); } else if (STATUS.isSwipingToLoadMore(mStatus)) { // simply return scrollSwipingToLoadMoreToDefault(); } else if (STATUS.isReleaseToRefresh(mStatus)) { // return to header height and perform refresh mRefreshCallback.onRelease(); scrollReleaseToRefreshToRefreshing(); } else if (STATUS.isReleaseToLoadMore(mStatus)) { // return to footer height and perform loadMore mLoadMoreCallback.onRelease(); scrollReleaseToLoadMoreToLoadingMore(); } }
隨后就是事件攔截的判斷。只要你向下滑動時Target確實不能再向下移動了或者向上滑動時Target確實不能再向上移動了,那么SwipeRefreshLayout就把事件攔截,執行onTouchEvent里面的位移操作了
@Override public boolean onInterceptTouchEvent(MotionEvent event) { final int action = MotionEventCompat.getActionMasked(event); switch (action) { case MotionEvent.ACTION_DOWN: mActivePointerId = MotionEventCompat.getPointerId(event, 0); mInitDownY = mLastY = getMotionEventY(event, mActivePointerId); mInitDownX = mLastX = getMotionEventX(event, mActivePointerId); // if it isn't an ing status or default status if (STATUS.isSwipingToRefresh(mStatus) || STATUS.isSwipingToLoadMore(mStatus) || STATUS.isReleaseToRefresh(mStatus) || STATUS.isReleaseToLoadMore(mStatus)) { // abort autoScrolling, not trigger the method #autoScrollFinished() mAutoScroller.abortIfRunning(); if (mDebug) { Log.i(TAG, "Another finger down, abort auto scrolling, let the new finger handle"); } } if (STATUS.isSwipingToRefresh(mStatus) || STATUS.isReleaseToRefresh(mStatus) || STATUS.isSwipingToLoadMore(mStatus) || STATUS.isReleaseToLoadMore(mStatus)) { return true; } // let children view handle the ACTION_DOWN; // 1\. children consumed: // if at least one of children onTouchEvent() ACTION_DOWN return true. // ACTION_DOWN event will not return to SwipeToLoadLayout#onTouchEvent(). // but the others action can be handled by SwipeToLoadLayout#onInterceptTouchEvent() // 2\. children not consumed: // if children onTouchEvent() ACTION_DOWN return false. // ACTION_DOWN event will return to SwipeToLoadLayout's onTouchEvent(). // SwipeToLoadLayout#onTouchEvent() ACTION_DOWN return true to consume the ACTION_DOWN event. // anyway: handle action down in onInterceptTouchEvent() to init is an good option break; case MotionEvent.ACTION_MOVE: if (mActivePointerId == INVALID_POINTER) { return false; } float y = getMotionEventY(event, mActivePointerId); float x = getMotionEventX(event, mActivePointerId); final float yInitDiff = y - mInitDownY; final float xInitDiff = x - mInitDownX; mLastY = y; mLastX = x; boolean moved = Math.abs(yInitDiff) > Math.abs(xInitDiff) && Math.abs(yInitDiff) > mTouchSlop; boolean triggerCondition = // refresh trigger condition (yInitDiff > 0 && moved && onCheckCanRefresh()) || //load more trigger condition (yInitDiff < 0 && moved && onCheckCanLoadMore()); if (triggerCondition) { // if the refresh's or load more's trigger condition is true, // intercept the move action event and pass it to SwipeToLoadLayout#onTouchEvent() return true; } break; case MotionEvent.ACTION_POINTER_UP: { onSecondaryPointerUp(event); mInitDownY = mLastY = getMotionEventY(event, mActivePointerId); mInitDownX = mLastX = getMotionEventX(event, mActivePointerId); break; } case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: mActivePointerId = INVALID_POINTER; break; } return super.onInterceptTouchEvent(event); }
下面就是位移過程。
如果當期處於初始STATUS_DEFAULT狀態,則進入STATUS_SWIPING_TO_REFRESH,同時回調onPrepare()方法
如果在下拉刷新流程中向上滑動並且滑動偏移量小於0,為了不讓Target部分移動到屏幕之外,則將體系流程恢復到初始STATUS_DEFAULT狀態,同時使用fixCurrentStatusLayout()方法調整三個View的位置。上拉加載更多流程同理
在正常下拉刷新流程中,如果當期狀態是STATUS_SWIPING_TO_REFRESH或者是STATUS_RELEASE_TO_REFRESH,即處於下拉以刷新、松開以刷新狀態,如果下拉的距離超過mRefreshTriggerOffset,則進入松開以刷新狀態,反之則進入下拉以刷新狀態。上拉加載更多流程同理
這時候會觸發位移發生fingerScroll()
@Override public boolean onTouchEvent(MotionEvent event) { final int action = MotionEventCompat.getActionMasked(event); switch (action) { case MotionEvent.ACTION_DOWN: mActivePointerId = MotionEventCompat.getPointerId(event, 0); return true; case MotionEvent.ACTION_MOVE: // take over the ACTION_MOVE event from SwipeToLoadLayout#onInterceptTouchEvent() // if condition is true final float y = getMotionEventY(event, mActivePointerId); final float x = getMotionEventX(event, mActivePointerId); final float yDiff = y - mLastY; final float xDiff = x - mLastX; mLastY = y; mLastX = x; if (Math.abs(xDiff) > Math.abs(yDiff) && Math.abs(xDiff) > mTouchSlop) { return true; } if (STATUS.isStatusDefault(mStatus)) { if (yDiff > 0 && onCheckCanRefresh()) { mRefreshCallback.onPrepare(); setStatus(STATUS.STATUS_SWIPING_TO_REFRESH); } else if (yDiff < 0 && onCheckCanLoadMore()) { mLoadMoreCallback.onPrepare(); setStatus(STATUS.STATUS_SWIPING_TO_LOAD_MORE); } } else if (STATUS.isRefreshStatus(mStatus)) { if (mTargetOffset <= 0) { setStatus(STATUS.STATUS_DEFAULT); fixCurrentStatusLayout(); return true; } } else if (STATUS.isLoadMoreStatus(mStatus)) { if (mTargetOffset >= 0) { setStatus(STATUS.STATUS_DEFAULT); fixCurrentStatusLayout(); return true; } } if (STATUS.isRefreshStatus(mStatus)) { if (STATUS.isSwipingToRefresh(mStatus) || STATUS.isReleaseToRefresh(mStatus)) { if (mTargetOffset >= mRefreshTriggerOffset) { setStatus(STATUS.STATUS_RELEASE_TO_REFRESH); } else { setStatus(STATUS.STATUS_SWIPING_TO_REFRESH); } fingerScroll(yDiff); } } else if (STATUS.isLoadMoreStatus(mStatus)) { if (STATUS.isSwipingToLoadMore(mStatus) || STATUS.isReleaseToLoadMore(mStatus)) { if (-mTargetOffset >= mLoadMoreTriggerOffset) { setStatus(STATUS.STATUS_RELEASE_TO_LOAD_MORE); } else { setStatus(STATUS.STATUS_SWIPING_TO_LOAD_MORE); } fingerScroll(yDiff); } } return true; case MotionEvent.ACTION_POINTER_DOWN: { final int pointerIndex = MotionEventCompat.getActionIndex(event); final int pointerId = MotionEventCompat.getPointerId(event, pointerIndex); if (pointerId != INVALID_POINTER) { mActivePointerId = pointerId; } mInitDownY = mLastY = getMotionEventY(event, mActivePointerId); mInitDownX = mLastX = getMotionEventX(event, mActivePointerId); break; } case MotionEvent.ACTION_POINTER_UP: { onSecondaryPointerUp(event); mInitDownY = mLastY = getMotionEventY(event, mActivePointerId); mInitDownX = mLastX = getMotionEventX(event, mActivePointerId); break; } case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: if (mActivePointerId == INVALID_POINTER) { return false; } mActivePointerId = INVALID_POINTER; break; default: break; } return super.onTouchEvent(event); }
位移無非就是對mTargetOffset進行賦值,同時調整三個View的位置。注意這里調用了onMove()回調
private void fingerScroll(final float yDiff) { float ratio = mDragRatio; float yScrolled = yDiff * ratio; // make sure (targetOffset>0 -> targetOffset=0 -> default status) // or (targetOffset<0 -> targetOffset=0 -> default status) // forbidden fling (targetOffset>0 -> targetOffset=0 ->targetOffset<0 -> default status) // or (targetOffset<0 -> targetOffset=0 ->targetOffset>0 -> default status) // I am so smart :) float tmpTargetOffset = yScrolled + mTargetOffset; if ((tmpTargetOffset > 0 && mTargetOffset < 0) || (tmpTargetOffset < 0 && mTargetOffset > 0)) { yScrolled = -mTargetOffset; } if (mRefreshFinalDragOffset >= mRefreshTriggerOffset && tmpTargetOffset > mRefreshFinalDragOffset) { yScrolled = mRefreshFinalDragOffset - mTargetOffset; } else if (mLoadMoreFinalDragOffset >= mLoadMoreTriggerOffset && -tmpTargetOffset > mLoadMoreFinalDragOffset) { yScrolled = -mLoadMoreFinalDragOffset - mTargetOffset; } if (STATUS.isRefreshStatus(mStatus)) { mRefreshCallback.onMove(mTargetOffset, false, false); } else if (STATUS.isLoadMoreStatus(mStatus)) { mLoadMoreCallback.onMove(mTargetOffset, false, false); } updateScroll(yScrolled); } private void updateScroll(final float yScrolled) { if (yScrolled == 0) { return; } mTargetOffset += yScrolled; if (STATUS.isRefreshStatus(mStatus)) { mHeaderOffset = mTargetOffset; mFooterOffset = 0; } else if (STATUS.isLoadMoreStatus(mStatus)) { mFooterOffset = mTargetOffset; mHeaderOffset = 0; } if (mDebug) { Log.i(TAG, "mTargetOffset = " + mTargetOffset); } layoutChildren(); invalidate(); }
最后就是執行結束刷新操作,完成閉環。結束的時候,refreshing值為false,執行onComplete()回調,同時回滾到初始位置
public void setRefreshing(boolean refreshing) { if (!isRefreshEnabled() || mHeaderView == null) { return; } this.mAutoLoading = refreshing; if (refreshing) { if (STATUS.isStatusDefault(mStatus)) { setStatus(STATUS.STATUS_SWIPING_TO_REFRESH); scrollDefaultToRefreshing(); } } else { if (STATUS.isRefreshing(mStatus)) { mRefreshCallback.onComplete(); postDelayed(new Runnable() { @Override public void run() { scrollRefreshingToDefault(); } }, mRefreshCompleteDelayDuration); } } }
這里還有一個補充,關於自動滑動方面。自動滾動一般都是通過AutoScroller類,調用其autoScroll()方法來完成,而實際上也是調用Scroller.startScroll()。但是不知道你有沒有注意到post(this),它在反復調用這個Runnable的run()來判斷滑動是否已經結束。如果沒有結束,則通過autoScroll()方法來調用move()回調;如果已經結束,則通過autoScrollFinished()方法來判斷下一步應該到達何種狀態
private class AutoScroller implements Runnable { private Scroller mScroller; private int mmLastY; private boolean mRunning = false; private boolean mAbort = false; public AutoScroller() { mScroller = new Scroller(getContext()); } @Override public void run() { boolean finish = !mScroller.computeScrollOffset() || mScroller.isFinished(); int currY = mScroller.getCurrY(); int yDiff = currY - mmLastY; if (finish) { finish(); } else { mmLastY = currY; SwipeToLoadLayout.this.autoScroll(yDiff); post(this); } } /** * remove the post callbacks and reset default values */ private void finish() { mmLastY = 0; mRunning = false; removeCallbacks(this); // if abort by user, don't call if (!mAbort) { autoScrollFinished(); } } /** * abort scroll if it is scrolling */ public void abortIfRunning() { if (mRunning) { if (!mScroller.isFinished()) { mAbort = true; mScroller.forceFinished(true); } finish(); mAbort = false; } } /** * The param yScrolled here isn't final pos of y. * It's just like the yScrolled param in the * {@link #updateScroll(float yScrolled)} * * @param yScrolled * @param duration */ private void autoScroll(int yScrolled, int duration) { removeCallbacks(this); mmLastY = 0; if (!mScroller.isFinished()) { mScroller.forceFinished(true); } mScroller.startScroll(0, 0, 0, yScrolled, duration); post(this); mRunning = true; } }
如果是松開以刷新,則進入刷新狀態,同時回調onRefresh()方法
如果是正在刷新狀態,則復原,執行onReset()方法
如果是松開以刷新並且通過setRefresh(true)方法進來的,則進入正在刷新狀態,執行onRefresh()方法;反之則執行復原操作,執行onReset()方法。
上拉加載更多流程同理
private void autoScrollFinished() { int mLastStatus = mStatus; if (STATUS.isReleaseToRefresh(mStatus)) { setStatus(STATUS.STATUS_REFRESHING); fixCurrentStatusLayout(); mRefreshCallback.onRefresh(); } else if (STATUS.isRefreshing(mStatus)) { setStatus(STATUS.STATUS_DEFAULT); fixCurrentStatusLayout(); mRefreshCallback.onReset(); } else if (STATUS.isSwipingToRefresh(mStatus)) { if (mAutoLoading) { mAutoLoading = false; setStatus(STATUS.STATUS_REFRESHING); fixCurrentStatusLayout(); mRefreshCallback.onRefresh(); } else { setStatus(STATUS.STATUS_DEFAULT); fixCurrentStatusLayout(); mRefreshCallback.onReset(); } } else if (STATUS.isStatusDefault(mStatus)) { } else if (STATUS.isSwipingToLoadMore(mStatus)) { if (mAutoLoading) { mAutoLoading = false; setStatus(STATUS.STATUS_LOADING_MORE); fixCurrentStatusLayout(); mLoadMoreCallback.onLoadMore(); } else { setStatus(STATUS.STATUS_DEFAULT); fixCurrentStatusLayout(); mLoadMoreCallback.onReset(); } } else if (STATUS.isLoadingMore(mStatus)) { setStatus(STATUS.STATUS_DEFAULT); fixCurrentStatusLayout(); mLoadMoreCallback.onReset(); } else if (STATUS.isReleaseToLoadMore(mStatus)) { setStatus(STATUS.STATUS_LOADING_MORE); fixCurrentStatusLayout(); mLoadMoreCallback.onLoadMore(); } else { throw new IllegalStateException("illegal state: " + STATUS.getStatus(mStatus)); } if (mDebug) { Log.i(TAG, STATUS.getStatus(mLastStatus) + " -> " + STATUS.getStatus(mStatus)); } }
源碼分析到此結束。怎么樣,是不是很簡單
鏈接:https://www.jianshu.com/p/fc8c73db72b3
更多文章
相信自己,沒有做不到的,只有想不到的
如果你覺得此文對您有所幫助,歡迎入群 QQ交流群 :644196190
微信公眾號:終端研發部
