下拉重新整理框架其實有很多,而且質量都比較高。但是在日常開發中,每一款產品都會有一套自己獨特的一套重新整理樣式。相信有很多小夥伴在個性化定製中都或多或少的遇到過麻煩。今天我就給大家推薦一個在定製方面很出彩的一個重新整理框架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));
}
}
複製程式碼
原始碼分析到此結束。怎麼樣,是不是很簡單
參考文章 MNSwipeToLoadDemo
連結:https://www.jianshu.com/p/fc8c73db72b3
更多文章
相信自己,沒有做不到的,只有想不到的
如果你覺得此文對您有所幫助,歡迎入群 QQ交流群 :644196190 微信公眾號:終端研發部