Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android 5.X新特性之為RecyclerView添加下拉刷新和上拉加載及SwipeRefreshLayout實現原理

Android 5.X新特性之為RecyclerView添加下拉刷新和上拉加載及SwipeRefreshLayout實現原理

編輯:關於Android編程

RecyclerView已經寫過兩篇文章了,分別是Android 5.X新特性之RecyclerView基本解析及無限復用 和 Android 5.X新特性之為RecyclerView添加HeaderView和FooterView,既然來到這裡還沒學習的,先去學習下吧。

今天我們的主題是學習為RecyclerView添加下拉刷新和上拉加載功能。

首先,我們先來學習下拉刷新,google公司已經為我們提供的一個很好的包裝類,那就是SwipeRefreshLayout,這個類可以支持我們向下滑動並進行監聽。那麼我們先了解一些基本知識,然後再從源碼的角度來解析它。

A. SwipeRefreshLayout 是一個容器,直接繼承於ViewGroup。

從其源碼中我們可以直接看出,它是直接繼承於ViewGroup的,所以它是一個容器,既然是一個容器,那麼我們就可以向其中添加View。

B. SwipeRefreshLayout 封裝了一些列的方法供我們使用,其中較常用的包括以下幾個。

1. setColorSchemeResources: 刷新時動畫的顏色,可以設置4個
2. setProgressBackgroundColorSchemeResource: 設置刷新時進度圓環的背景顏色
3. setOnRefreshListener(SwipeRefreshLayout.OnRefreshListener listener): 設置手勢滑動監聽器。
4. setRefreshing(Boolean refreshing): 設置組件的刷洗狀態。
5. setSize(int size):設置進度圈的大小,只有兩個值:DEFAULT、LARGE

其中最主要的是setOnRefreshListener,它是用來監聽我們下拉手勢的回調方法。

C. 接下來我們再從源碼的角度來了解這個類:

SwipeRefreshLayout 是一個ViewGroup容器,那在向它添加子View的時候,那首先會去測量各個子View的大小來確定本身的大小,並且還會制定子View的坐標位置,最後繪制View並顯示出來。而我們今天所要講解的是從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 就是捕捉到當前的事件。

那麼我們再來看看它是怎麼處理當前捕捉到的事件的。請看源碼:

    @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文件:


    

        
        
    

然後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 newDatas = new ArrayList();
                        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 newDatas = new ArrayList();
                            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,今天就講到這裡吧。祝大家學習愉快。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved