Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 拆解輪子之XRecyclerView

拆解輪子之XRecyclerView

編輯:關於Android編程

簡介

這個輪子是對RecyclerView的封裝,主要完成了下拉刷新上拉加載更多RecyclerView頭部。在我的Material Design學習項目中使用到了項目地址,感覺還不錯。趁著畢業答辯還有2個星期,先把這個輪子拆了看看,這個項目地址在XRecyclerView,先貼個效果圖,更多效果圖請進入項目中查看。
這裡寫圖片描述

使用

使用起來也比較簡單,首先向普通RecyclerView那樣:

LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());
layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
mRecyclerView.setLayoutManager(layoutManager);
mRecyclerView.setAdapter(mAdapter);

下拉刷新和加載更多需要實現其接口即可:

 mRecyclerView.setLoadingListener(new XRecyclerView.LoadingListener() {
    @Override
    public void onRefresh() {
       //refresh data here
    }

    @Override
    public void onLoadMore() {
       // load more data here
    }
});

這裡要注意的是需要人為的通知刷新和加載都已經完成,通過如下代碼

mRecyclerView.refreshComplete(); //下拉刷新完成
mRecyclerView.loadMoreComplete();//加載更多完成

類關系圖

首先梳理了一下框架,用UML圖畫了這個輪子的結構,這樣有利於幫助我理解,右擊-查看圖像 可以查看清晰大圖)

這裡寫圖片描述

可以看出主要的類只有3個 XRecyclerView,LoadingMoreFooter,ArrowRefreshHeader,而AVLoadingIncatorViewSimpleViewSwitcher是用來輔助刷新或者加載時候的動畫。

下面分析源碼時限於篇幅原因只展現出關鍵代碼,具體可以參考項目源碼。

XRecyclerView的實現
 
XRecyclerView 的head和footer的view實現
XRecyclerView在RecyclerView的基礎上做了進一步的工作因而需要繼承RecyclerView,由於支持RecyclerView Header而不同的header可以自己實現,因此需要對外暴露,而footerView則是固定的,因此在init初始化時候直接初始化了。此外這裡使用了兩個ArrayList存儲不同的view,並且記錄了viewType
    private ArrayList mHeaderViews = new ArrayList<>();
    private ArrayList mFootViews = new ArrayList<>();
    ……
    private void init() {
        if (pullRefreshEnabled) {
            //若支持下拉刷新則加入Headerview列表,設置加載圖標
            ArrowRefreshHeader refreshHeader = new ArrowRefreshHeader(getContext());
            mHeaderViews.add(0, refreshHeader);//從這裡看出headerView可以添加多個
            mRefreshHeader = refreshHeader;
            mRefreshHeader.setProgressStyle(mRefreshProgressStyle);
        }
        //加載更多無需觸發
        LoadingMoreFooter footView = new LoadingMoreFooter(getContext());
        footView.setProgressStyle(mLoadingMoreProgressStyle);
        addFootView(footView);//加入footerView
        mFootViews.get(0).setVisibility(GONE);
    }
    ……
    /**
     * @param view 對外提供添加header的方法
     */
    public void addHeaderView(View view) {
        if (pullRefreshEnabled && !(mHeaderViews.get(0) instanceof ArrowRefreshHeader)) {
            ArrowRefreshHeader refreshHeader = new ArrowRefreshHeader(getContext());
            mHeaderViews.add(0, refreshHeader);
            mRefreshHeader = refreshHeader;
            mRefreshHeader.setProgressStyle(mRefreshProgressStyle);
        }
        mHeaderViews.add(view);
        sHeaderTypes.add(HEADER_INIT_INDEX + mHeaderViews.size());//記錄viewType
    }

但是這樣僅僅只是存儲了View,那麼實現的地方在哪裡呢?數據展現很顯然是在dapater中,但是在使用RecycleView時需要展示item數據,那麼header和footer如何加載?這裡就需要對傳入的數據adapter再做一層封裝。

    @Override
    public void setAdapter(Adapter adapter) {
        mWrapAdapter = new WrapAdapter(adapter);//對傳入的adapter做封裝
        super.setAdapter(mWrapAdapter);
        adapter.registerAdapterDataObserver(mDataObserver);
        mDataObserver.onChanged();
    }

由於RecycleView支持LinearLayoutManager、GridLayoutManager、StaggeredGridLayoutManager,而GridLayoutManagerStaggeredGridLayoutManager在添加header時候需要注意橫跨整個屏幕寬度即:
GridLayoutManager 是要設置SpanSize每行的占位大小
StaggerLayoutManager 就是要獲取StaggerLayoutManager的LayoutParams 的setFullSpan 方法來設置占位寬度,因此在WrapAdapter中做了針對性處理

        @Override
        public void onAttachedToRecyclerView(RecyclerView recyclerView) {
            super.onAttachedToRecyclerView(recyclerView);
            RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();
            if (manager instanceof GridLayoutManager) {
                final GridLayoutManager gridManager = ((GridLayoutManager) manager);
                gridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
                    @Override
                    public int getSpanSize(int position) {
                        return (isHeader(position) || isFooter(position))
                                ? gridManager.getSpanCount() : 1;
                    }
                });
            }
        }

        @Override
        public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {
            super.onViewAttachedToWindow(holder);
            ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
            if (lp != null
                    && lp instanceof StaggeredGridLayoutManager.LayoutParams
                    && (isHeader(holder.getLayoutPosition()) || isFooter(holder.getLayoutPosition()))) {
                StaggeredGridLayoutManager.LayoutParams p = (StaggeredGridLayoutManager.LayoutParams) lp;
                p.setFullSpan(true);
            }
        }

到此只是為展示head提供了必要條件,具體展示還是要靠WrapAdapter的 onCreateViewHolder配合getItemViewType方法,根據viewtype從對應的ArrayList中取出view來展示

        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            if (viewType == TYPE_REFRESH_HEADER) {
                mCurrentPosition++;
                return new SimpleViewHolder(mHeaderViews.get(0));
            } else if (isContentHeader(mCurrentPosition)) {
                if (viewType == sHeaderTypes.get(mCurrentPosition - 1)) {
                    mCurrentPosition++;
                    return new SimpleViewHolder(mHeaderViews.get(headerPosition++));
                }
            } else if (viewType == TYPE_FOOTER) {
                return new SimpleViewHolder(mFootViews.get(0));
            }
            return adapter.onCreateViewHolder(parent, viewType);
        }
    ……
     @Override
        public int getItemViewType(int position) {
            if (isRefreshHeader(position)) {
                return TYPE_REFRESH_HEADER;
            }
            if (isHeader(position)) {
                position = position - 1;
                return sHeaderTypes.get(position);
            }
            if (isFooter(position)) {
                return TYPE_FOOTER;
            }
            int adjPosition = position - getHeadersCount();
            int adapterCount;
            if (adapter != null) {
                adapterCount = adapter.getItemCount();
                if (adjPosition < adapterCount) {
                    return adapter.getItemViewType(adjPosition);
                }
            }
            return TYPE_NORMAL;
        }

ok,到這裡就完成了head和footer的view顯示,

上拉滑動中的下拉刷新和釋放後刷新界面的緩慢消失的實現
上拉刷新分為兩部分,首先是手指滑動,刷新條慢慢顯示出來(而且顯示的大小跟滑動距離有關);釋放後刷新界面慢慢隱藏,這裡刷新的動畫部分後面分析。
先看刷新條隨著手指滑動慢慢顯示
涉及到滑動需要重寫onTouchEvent,特別是針對MotionEvent.ACTION_MOVE處理
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        //通過處理onTouchEvent處理下拉刷新
        if (mLastY == -1) {
            mLastY = ev.getRawY();
        }
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastY = ev.getRawY();
                break;
            case MotionEvent.ACTION_MOVE:
                final float deltaY = ev.getRawY() - mLastY;
                mLastY = ev.getRawY();
                if (isOnTop() && pullRefreshEnabled) {
                    mRefreshHeader.onMove(deltaY / DRAG_RATE);//顯示刷新的關鍵代碼
                    if (mRefreshHeader.getVisibleHeight() > 0 && mRefreshHeader.getState() < ArrowRefreshHeader.STATE_REFRESHING) {
//                        Log.i("getVisibleHeight", "getVisibleHeight = " + mRefreshHeader.getVisibleHeight());
//                        Log.i("getVisibleHeight", " mRefreshHeader.getState() = " + mRefreshHeader.getState());
                        return false;
                    }
                }
                break;
            default:
                mLastY = -1; // reset
                if (isOnTop() && pullRefreshEnabled) {
                    if (mRefreshHeader.releaseAction()) {
                        if (mLoadingListener != null) {
                            mLoadingListener.onRefresh();
                        }
                    }
                }
                break;
        }
        return super.onTouchEvent(ev);
    }

onMove在ArrowRefreshHeader中實現,這裡多插一句getRawY():獲取點擊事件相對整個屏幕頂邊的y軸坐標,即點擊事件距離整個屏幕頂邊的距離注意與getY()區別。

    @Override
    public void onMove(float delta) {
        //由於下拉時候區域是動態變化因此需要動態設置
        if (getVisibleHeight() > 0 || delta > 0) {
            setVisibleHeight((int) delta + getVisibleHeight());
            if (mState <= STATE_RELEASE_TO_REFRESH) { // 未處於刷新狀態,更新箭頭
                if (getVisibleHeight() > mMeasuredHeight) {
                    setState(STATE_RELEASE_TO_REFRESH);
                } else {
                    setState(STATE_NORMAL);
                }
            }
        }
    }

在onMove方法輸入參數中可以看出手指滑動距離的1/3作為刷新顯示的高度,由於init方法初始化時將刷新顯示高度設置為0,同樣在ArrowRefreshHeader中

addView(mContainer, new LayoutParams(LayoutParams.MATCH_PARENT, 0));//初始化時候高度設置為0,通過後面setVisibleHeight設置可見高度
 ……
     /**
     * 設置可見高度
     *
     * @param height
     */
    public void setVisibleHeight(int height) {
        if (height < 0) height = 0;
        LayoutParams lp = (LayoutParams) mContainer.getLayoutParams();
        lp.height = height;
        mContainer.setLayoutParams(lp);
    }

這樣就不難理解onMove方法為何可以在下拉時慢慢出現下拉刷新.
在看釋放是刷新界面慢慢變為0
同樣在在XrecycleView中的onTouch方法中:
default分支:

 default:
                mLastY = -1; // reset
                if (isOnTop() && pullRefreshEnabled) {
                    if (mRefreshHeader.releaseAction()) {//上彈關鍵代碼
                        if (mLoadingListener != null) {
                            mLoadingListener.onRefresh();
                        }
                    }
                }
                break;

mRefreshHeader.releaseAction()中處理了手指釋放後即刷新慢慢向上隱藏的動作,該接口在
ArrowRefreshHeader中實現

    @Override
    public boolean releaseAction() {
        //釋放動作,此時需要處理緩慢回到頂部
        boolean isOnRefresh = false;
        int height = getVisibleHeight();
        if (height == 0) // not visible.
            isOnRefresh = false;

        if (getVisibleHeight() > mMeasuredHeight && mState < STATE_REFRESHING) {
            setState(STATE_REFRESHING);
            isOnRefresh = true;
        }
        // refreshing and header isn't shown fully. do nothing.
        if (mState == STATE_REFRESHING && height <= mMeasuredHeight) {
            //return;
        }
        int destHeight = 0; // default: scroll back to dismiss header.
        // is refreshing, just scroll back to show all the header.
        if (mState == STATE_REFRESHING) {
            destHeight = mMeasuredHeight;
        }
        smoothScrollTo(destHeight);

        return isOnRefresh;
    }
    ……
        private void smoothScrollTo(int destHeight) {
        ValueAnimator animator = ValueAnimator.ofInt(getVisibleHeight(), destHeight);
        animator.setDuration(300).start();
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                setVisibleHeight((int) animation.getAnimatedValue());
            }
        });
        animator.start();
    }

其中主要是通過smoothScrollTo的屬性動畫+setVisibleHeight函數來實現刷新部分慢慢隱藏

加載更多實現
通常實現該功能是在手指滑動停止後進行加載,在XRecyclerView中重寫了onScrollStateChange方法,加載更多主要是需要獲得最後可見的位置即lastVisibleItem,如下所示
 @Override
    public void onScrollStateChanged(int state) {
        super.onScrollStateChanged(state);
        //重寫該方法主要是在IDLE態即手指滑動停止後處理加載更多

        if (state == RecyclerView.SCROLL_STATE_IDLE && mLoadingListener != null && !isLoadingData && loadingMoreEnabled) {
            LayoutManager layoutManager = getLayoutManager();
            int lastVisibleItemPosition;
            if (layoutManager instanceof GridLayoutManager) {
                lastVisibleItemPosition = ((GridLayoutManager) layoutManager).findLastVisibleItemPosition();
            } else if (layoutManager instanceof StaggeredGridLayoutManager) {
                //瀑布流布局發現最後可見的item位置
                int[] into = new int[((StaggeredGridLayoutManager) layoutManager).getSpanCount()];
                ((StaggeredGridLayoutManager) layoutManager).findLastVisibleItemPositions(into);
                lastVisibleItemPosition = findMax(into);
            } else {
                lastVisibleItemPosition = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition();
            }

            if (layoutManager.getChildCount() > 0
                    && lastVisibleItemPosition >= layoutManager.getItemCount() - 1 && layoutManager.getItemCount() > layoutManager.getChildCount() && !isNoMore && mRefreshHeader.getState() < ArrowRefreshHeader.STATE_REFRESHING) {

                View footView = mFootViews.get(0);
                isLoadingData = true;
                if (footView instanceof LoadingMoreFooter) {
                    ((LoadingMoreFooter) footView).setState(LoadingMoreFooter.STATE_LOADING);
                } else {
                    footView.setVisibility(View.VISIBLE);
                }
                mLoadingListener.onLoadMore();
            }
        }
    }
空白數據處理
數據變化時處理布局,這裡主要通過AapterDataObserver監聽數據變化以此來更換布局
 @Override
        public void onChanged() {
            //重寫該方法是在數據發生變化時更換布局
            Adapter adapter = getAdapter();
            if (adapter != null && mEmptyView != null) {
                int emptyCount = 0;
                if (pullRefreshEnabled) {
                    emptyCount++;
                }
                if (loadingMoreEnabled) {
                    emptyCount++;
                }
                if (adapter.getItemCount() == emptyCount) {
                    mEmptyView.setVisibility(View.VISIBLE);
                    XRecyclerView.this.setVisibility(View.GONE);
                } else {
                    mEmptyView.setVisibility(View.GONE);
                    XRecyclerView.this.setVisibility(View.VISIBLE);
                }
            }
            if (mWrapAdapter != null) {
                mWrapAdapter.notifyDataSetChanged();
            }
        }
ArrowRefreshHeader與LoadingMoreFooter
這倆個都是繼承viewgroup的自定義控件,前者要比後者稍微復雜一些,先揀軟柿子捏,看看LoadingMoreFooter:
主要功能就是初始化好加載更多的動畫view和家在文字,然後通過state統統暴露在setState函數中供外界調用
    public void  setState(int state) {
        switch(state) {
            case STATE_LOADING:
                progressCon.setVisibility(View.VISIBLE);
                mText.setText(getContext().getText(R.string.listview_loading));
                this.setVisibility(View.VISIBLE);
                    break;
            case STATE_COMPLETE:
                mText.setText(getContext().getText(R.string.listview_loading));
                this.setVisibility(View.GONE);
                break;
            case STATE_NOMORE:
                mText.setText(getContext().getText(R.string.nomore_loading));
                progressCon.setVisibility(View.GONE);
                this.setVisibility(View.VISIBLE);
                break;
        }

    }

可以看出,通過不同的狀態來處理文字和加載動畫。

在看ArrowRefreshHeader,稍微復雜點,主要是要處理隨著手指滑動刷新界面慢慢顯示和釋放釋放手指刷新界面慢慢返回,刷新完成後的狀態重置,這些都實現接口BaseRefreshHeader處理。其中該自定義控件在初始化時候將高度設置為0,通過setVisibleHeight來設置高度,這樣就可以處理刷新高度的動態變化,在介紹XRecyclerView中已經對這幾個接口方法做了詳細介紹了,這裡就不贅述了。這裡處理方式與LoadingMoreFooter相同,根據不同刷新狀態來處理控件的顯示狀態。
抽象來看,這兩個控件的核心就是使用 SimpleViewSwitcher做中轉將AVLoadingIndicatorView不同的加載動畫呈現的過稱。
其中 SimpleViewSwitcher比較簡單就是一個設置view的很普通的自定義viewgroup,而AVLoadingIndicatorView則是另一個加載動畫庫了github項目地址這次就不分析了。
到此基本上這個輪子就大致分析完了。

尾聲

作為一個android彩筆,還是應該多讀讀源碼,包括android源碼和github上的一些多星的優秀項目的源碼,通過拆這個輪子,可以收獲到:

熟悉uml拆分框架 recycleView針對不同布局(如StaggeredGridLayoutManager)獲取findLastVisibleItemPositions和header的處理方式 下拉刷新手指滑動距離與刷新高度變化、釋放後刷新頭部自動消失(onTouch) 改變不同不通布局的方式,根據狀態設置empytyview可見還是 recycleView可見與否 recycleView增加頭部底部後使用對傳入的數據adapter來進行二次封裝 自定義viewgroup的使用 屬性動畫的簡單使用 view坐標系 熟悉了設計模式的裡氏替換、接口隔離、依賴倒置原則
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved