RecyclerView已經寫過兩篇文章了,分別是Android 5.X新特性之RecyclerView基本解析及無限複用 和 Android 5.X新特性之爲RecyclerView添加HeaderView和FooterView,既然來到這裏還沒學習的,先去學習下吧。html
今天咱們的主題是學習爲RecyclerView添加下拉刷新和上拉加載功能。python
首先,咱們先來學習下拉刷新,google公司已經爲咱們提供的一個很好的包裝類,那就是SwipeRefreshLayout,這個類能夠支持咱們向下滑動並進行監聽。那麼咱們先了解一些基本知識,而後再從源碼的角度來解析它。android
A. SwipeRefreshLayout 是一個容器,直接繼承於ViewGroup。微信
從其源碼中咱們能夠直接看出,它是直接繼承於ViewGroup的,因此它是一個容器,既然是一個容器,那麼咱們就能夠向其中添加View。
B. SwipeRefreshLayout 封裝了一些列的方法供咱們使用,其中較經常使用的包括如下幾個。ide
1. setColorSchemeResources: 刷新時動畫的顏色,能夠設置4個 2. setProgressBackgroundColorSchemeResource: 設置刷新時進度圓環的背景顏色 3. setOnRefreshListener(SwipeRefreshLayout.OnRefreshListener listener): 設置手勢滑動監聽器。 4. setRefreshing(Boolean refreshing): 設置組件的刷洗狀態。 5. setSize(int size):設置進度圈的大小,只有兩個值:DEFAULT、LARGE
其中最主要的是setOnRefreshListener,它是用來監聽咱們下拉手勢的回調方法。佈局
C. 接下來咱們再從源碼的角度來了解這個類:post
SwipeRefreshLayout 是一個ViewGroup容器,那在向它添加子View的時候,那首先會去測量各個子View的大小來肯定自己的大小,而且還會制定子View的座標位置,最後繪製View並顯示出來。針對ViewGroup的繪製我以前有寫過一篇博文,你們能夠去參考下Android自定義控件之繼承ViewGroup建立新容器(四) ,裏面有詳細的講解。而咱們今天所要講解的是從SwipeRefreshLayout 的事件機制來講起,也更符合咱們下拉刷新的主題。學習
在SwipeRefreshLayout 的事件攔截分發器onInterceptTouchEvent中,它是這麼定製的,源碼以下:動畫
@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; }
它最終返回的是表明是否滑動的mIsBeingDragged布爾值。在咱們按下,擡起,或取消時mIsBeingDragged的值是false,意思是在這幾個動做中,SwipeRefreshLayout 自己是不攔截事件的,而是傳遞給父類,讓父類進行處理。而咱們主要來看MotionEvent.ACTION_MOVE:這個動做,它首先判斷是不是可用的活動id: mActivePointerId,而後根據獲得mActivePointerId來獲取滑動的中座標距離值:Y,而後作出判斷:若是Y==-1就表明沒滑動,因此直接返回false表示不攔截;若是Y值大於規定的最小滑動距離mTouchSlop值,而且!mIsBeingDragged爲真,那麼就讓mIsBeingDragged == true;並返回,也就是在這種狀況下,SwipeRefreshLayout 它本身消化了事件,而不是傳遞給父類。所以,當咱們在向下滑動了必定的距離時,SwipeRefreshLayout 就是捕捉到當前的事件。ui
那麼咱們再來看看它是怎麼處理當前捕捉到的事件的。請看源碼:
@Override public boolean onTouchEvent(MotionEvent ev) { final int action = MotionEventCompat.getActionMasked(ev); int pointerIndex = -1; 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: 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 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; }
一樣的道理在MotionEvent.ACTION_DOWN和case MotionEvent.ACTION_CANCEL時不處理事件,交給父類處理。而在MotionEvent.ACTION_MOVE:中獲取到與頂端窗口的overscrollTop,若是overscrollTop值大於0就調用moveSpinner(overscrollTop);方法來初始化mCircleView旋轉的。最後在MotionEvent.ACTION_UP:擡起事件中,一樣獲取overscrollTop,且調用finishSpinner(overscrollTop);方法來完成mCircleView的旋轉事件並回復一些屬性配置值。
而後咱們再來看看finishSpinner(overscrollTop);方法中是怎麼處理的。
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); } }
方法裏面很簡單,if (overscrollTop > mTotalDragDistance) 就調用setRefreshing(true, true /* notify */);用來設置刷新事件的,不然就回復初始前的屬性配置值。
再來看看setRefreshing(true, true)方法:
private void setRefreshing(boolean refreshing, final boolean notify) { if (mRefreshing != refreshing) { mNotify = notify; ensureTarget(); mRefreshing = refreshing; if (mRefreshing) { animateOffsetToCorrectPosition(mCurrentTargetOffsetTop, mRefreshListener); } else { startScaleDownAnimation(mRefreshListener); } } }
也很好理解,由於傳進來的refreshing值爲true,因此它會調用animateOffsetToCorrectPosition(mCurrentTargetOffsetTop, mRefreshListener);來開啓mCircleView的動畫展現,並傳進了mRefreshListener監聽器,這個監聽器是什麼呢?來看看
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(); } } };
它是一個動畫監聽器,在動畫結束時調用mListener.onRefresh();而mListener是一個接口,裏面封裝了一個onRefresh()的方法,而且它暴露了對外調用的方法setOnRefreshListener(),因此咱們能夠在Activity中調用該方法能夠實現咱們本身的邏輯業務。
ok,到這裏,相信你們都知道了wipeRefreshLayout.setOnRefreshListener();的工做原理,那麼咱們如今來實現咱們的刷新功能吧;
首先,咱們的佈局文件先把RecyclerView放到SwipeRefreshLayout容器中:
recycer_view.xml文件:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:custom="http://schemas.android.com/apk/res-auto" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.v4.widget.SwipeRefreshLayout android:id="@+id/srl_refresh" android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.v7.widget.RecyclerView android:id="@+id/recycler_view" custom:listDividerSize="2dp" custom:listDividerBackgroundColor="#FF0000" android:layout_width="match_parent" android:layout_height="match_parent"> </android.support.v7.widget.RecyclerView> </android.support.v4.widget.SwipeRefreshLayout> </LinearLayout>
而後RecycerActivity中配置一些SwipeRefreshLayout屬性值,並調用setOnRefreshListener方法並在onRefresh()實現本身的邏輯業務:
srl_refresh.setColorSchemeResources(android.R.color.holo_blue_light, android.R.color.holo_red_light,android.R.color.holo_orange_light, android.R.color.holo_green_light); srl_refresh.setProgressBackgroundColorSchemeResource(android.R.color.white); srl_refresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { @Override public void onRefresh() { new Handler().postDelayed(new Runnable() { @Override public void run() { List<String> newDatas = new ArrayList<String>(); for (int i = 0; i <5; i++) { int index = i + 1; newDatas.add("new item" + index); } mBaseRecyclerAdapter.addDatas(newDatas); srl_refresh.setRefreshing(false); Toast.makeText(RecycerActivity.this, "更新了五條數據...", Toast.LENGTH_SHORT).show(); } }, 5000); } });
來看看結果吧
好了,RecyclerView利用SwipeRefreshLayout實現上拉刷新咱們已經實現了,而且也帶你們看過它的實現原理了,相信你們必定能更好的掌握它了,那麼接下來咱們就來實現上拉加載了。
在上一講中,咱們已經實現了在底部添加上了一個FooterView,那麼咱們如今能夠利用它來實現咱們的上拉加載。
其思想咱們能夠這樣設計,當咱們滑動到最後一個ItemView時,讓它去加載數據,那怎麼獲取到列表的最後一個ItemView呢?所幸的是,在RecyclerView中封裝的LayoutManger子類中有這樣的方法能夠供咱們獲取到最後一個ItemView,該方法是findLastVisibleItemPosition();那咱們又該怎麼監聽RecyclerView滑動呢?能夠調用它的addOnScrollListener()方法,由此咱們找到了解決方案
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); if(newState == RecyclerView.SCROLL_STATE_IDLE && lastVisibleItem + 1 == mBaseRecyclerAdapter.getItemCount()){ mBaseRecyclerAdapter.changeStatus(BaseRecyclerAdapter.LOADING_MORE); new Handler().postDelayed(new Runnable() { @Override public void run() { List<String> newDatas = new ArrayList<String>(); for (int i = 0; i< 5; i++) { int index = i +1; newDatas.add("more item" + index); } if(newDatas == null){ mBaseRecyclerAdapter.changeStatus(BaseRecyclerAdapter.LOADED_MORE); return; } mBaseRecyclerAdapter.addMoreDatas(newDatas); mBaseRecyclerAdapter.changeStatus(BaseRecyclerAdapter.LOAD_MORE); Toast.makeText(RecycerActivity.this,"已加載了數據", Toast.LENGTH_SHORT).show(); } },1000); } } @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); lastVisibleItem = linearLayoutManger.findLastVisibleItemPosition(); } });
代碼解釋:首先咱們會在onScrolled方法中回去到最後一行的ItenView,而後再onScrollStateChanged方法中進行必要的判斷,若是lastVisibleItem + 1 == mBaseRecyclerAdapter.getItemCount(),那麼就能夠肯定給ItemView是最後一個ItemView,而後就能夠用來實現咱們的業務邏輯了,在這裏我讓它新加了5條數據,而後更新Adapter。
最後在onBindViewHolder稍做修改,以下
@Override public void onBindViewHolder(BaseViewHolderHelper holder, int position) { //把每個itemView設置一個標籤,方便之後根據標籤獲取到該itemView以便作其餘事項,比較點擊事件 if(getItemViewType(position) == TYPE_HEADER){ return; }else if(getItemViewType(position) == TYPE_FOOTER){ FooterViewHolder footViewHolder=(FooterViewHolder)holder; footViewHolder.footView.setText("上拉加載更多..."); switch (status){ case LOAD_MORE: footViewHolder.footView.setText("上拉加載更多..."); break; case LOADING_MORE: footViewHolder.footView.setText("正在加載中..."); break; case LOADED_MORE: footViewHolder.footView.setText("已加載完畢"); break; } } else{ ... } }
ok,來看看結果吧:
好了,已經實現了上拉加載的功能了,相信你們也均可以作不少事情了。
總結:本節主題是爲RecyclerView添加下拉刷新和上拉加載的功能,基本的思路也都已講清楚了,並且着重的講解了一下利用SwipeRefreshLayout實現下拉刷新的實現原理,相信你們經過這節更能學到一些原理性的東西,ok,今天就講到這裏吧。祝你們學習愉快。
更多資訊請關注微信平臺,有博客更新會及時通知。愛學習愛技術。