1. 程式人生 > >RecyclerView實現addHeadView的三種方法原理說明和利弊分析(footHead同理)

RecyclerView實現addHeadView的三種方法原理說明和利弊分析(footHead同理)

介紹

上一篇部落格我分析了ListView的原始碼看Google是怎麼樣實現addHeadView的,原始碼的思路是對繫結在ListView的Adapter做轉換,在我們呼叫addHeadView的時候把已經寫好的BaseAdapter轉換成HeaderViewListAdapter這一元件,在程式碼內部呼叫BaseAdapter.getView方法。這樣寫的好處是解耦和不影響我們原有程式碼的前提下做好轉換。這是最好的解決方案。這幾天我從網路上看到很多人對RecyclerView新增HeadView的理解。整理如下。

直接修改RecyclerView,Adapter

這是原文連結還有這篇

原文連結感謝這兩位提供的部落格
他們的思路都是修改了adapter的三個主要方法

  • getItemViewType 返回檢視型別
  • onCreateViewHolder 建立ViewHolder
  • onBindViewHolder 繫結ViewHolder內容

然後新增addHeadView和addFootView方法
在不同的position位置上做型別判斷,返回不同型別結果。

特別說明

當使用StaggeredGridLayoutManager實現瀑布流效果時需要呼叫setFullSpan才能讓某個位置的View佔滿格,下面是使用示例程式碼。

//能夠讓某個view滿格的 setFullSpan 方法
ViewGroup.LayoutParams layoutParams=holder.mView.getLayoutParams(); if (layoutParams!=null &&layoutParams instanceof StaggeredGridLayoutManager.LayoutParams){ StaggeredGridLayoutManager.LayoutParams params= (StaggeredGridLayoutManager.LayoutParams) layoutParams; params.setFullSpan(holder.getLayoutPosition()==0
); }

這樣實現比較簡單。也比較能夠理解。但缺點是耦合的太高,比較影響現有程式碼。比如我已經實現了adapter這樣的話就需要對整體的adapter程式碼進行重構。並且這個adapter的作用也被侷限在是一個專門處理addHeadView的RecyclerView,Adapter。這裡有個GitHub地址是封裝好的Adapter

總結:耦合度太高

巢狀RecyclerView

  • 就是給RecyclerView巢狀上可滑動的父檢視,然後程式碼控制Head和Foot的顯示。
  • 我簡單的嘗試一下給RecyclerView巢狀ScrollingView,程式碼執行效率奇低,只要滑動就會不停的呼叫RecyclerView,Adapter的onCreateViewHolder產生無數個ViewHolder根本沒有Recycler的複用機制,主要是ScrollingView影響了子檢視的顯示問題。
  • 當然也有大神解決了這個問題,用其他的思路實現巢狀
    –比如這個Github地址在RecyclerView上套上FrameLayout,然後新增準備好的HeadView檢視,監聽滑動。合適的同學可以傳送過去看看給大神star。缺點是隻實現了addHeadView。
    –還有這個用CoordinatorLayout 把 header 抽離出 RecyclerView,也算是很奇特的思路。

總結:實現複雜,可能產生滑動衝突,導致問題更加複雜

重點介紹

上面總結怎麼多,重點終於來了。前面提到Google是用一個內部HeaderViewListAdapter替換我們的adapter,實現addHeadView並且程式碼解耦不影響現有程式碼。本來想自己寫的,後來在Github上看到已經有位大神實現了這個思路的解決方案。哪就不重複造輪子了。
隆重介紹—>GitHub地址
下面直接上原始碼,—>原始碼在這裡

關鍵程式碼

/**
     * 設定adapter 得到繫結的adapter 賦值給內部變數adapter
     * @param adapter
     */
    public void setAdapter(RecyclerView.Adapter<RecyclerView.ViewHolder> adapter) {

        if (adapter != null) {
            if (!(adapter instanceof RecyclerView.Adapter))
                throw new RuntimeException("your adapter must be a RecyclerView.Adapter");
        }

        if (mInnerAdapter != null) {
            notifyItemRangeRemoved(getHeaderViewsCount(), mInnerAdapter.getItemCount());
            mInnerAdapter.unregisterAdapterDataObserver(mDataObserver);
        }

        this.mInnerAdapter = adapter;
        mInnerAdapter.registerAdapterDataObserver(mDataObserver);
        notifyItemRangeInserted(getHeaderViewsCount(), mInnerAdapter.getItemCount());
    }
/**
     * 根據 viewType 和 headerViewsCountCount 的數量 
     * 決定建立的 ViewHolder是 使用List<View> mHeaderViews 還是內部adapter的 onCreateViewHolder
     * @param parent
     * @param viewType
     * @return
     */
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        int headerViewsCountCount = getHeaderViewsCount();
        if (viewType < TYPE_HEADER_VIEW + headerViewsCountCount) {
            return new ViewHolder(mHeaderViews.get(viewType - TYPE_HEADER_VIEW));
        } else if (viewType >= TYPE_FOOTER_VIEW && viewType < Integer.MAX_VALUE / 2) {
            return new ViewHolder(mFooterViews.get(viewType - TYPE_FOOTER_VIEW));
        } else {
            return mInnerAdapter.onCreateViewHolder(parent, viewType - Integer.MAX_VALUE / 2);
        }
    }


/**
     * head和foot的檢視不復用 不需要特別的 onBindViewHolder
     * 只是在 使用StaggeredGridLayoutManager 瀑布流時候 讓head和foot 檢視佔據滿格 setFullSpan(true)
     * @param holder
     * @param position
     */
    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        int headerViewsCountCount = getHeaderViewsCount();
        if (position >= headerViewsCountCount && position < headerViewsCountCount + mInnerAdapter.getItemCount()) {
            mInnerAdapter.onBindViewHolder(holder, position - headerViewsCountCount);
        } else {
            ViewGroup.LayoutParams layoutParams = holder.itemView.getLayoutParams();
            if(layoutParams instanceof StaggeredGridLayoutManager.LayoutParams) {
                ((StaggeredGridLayoutManager.LayoutParams) layoutParams).setFullSpan(true);
            }
        }
    }

程式碼太多,只說明關鍵部分。實現的思路我在 上一篇部落格 有相同的分析的原始碼的思路,看不懂的點選連結看部落格。
我做了部分修改 ,符合我的專案使用,主要就是添加了部分功能的呼叫,這樣我adapter才能得到方法呼叫。否則我的很多adapter方法無效。

修改部分程式碼


    @Override
    public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {
        super.onViewAttachedToWindow(holder);
        //新增程式碼 需要呼叫內部adapter 才能收到通知
        mInnerAdapter.onViewAttachedToWindow(holder);

    }

    @Override
    public void onViewRecycled(RecyclerView.ViewHolder holder) {
        super.onViewRecycled(holder);
        //新增程式碼 需要呼叫內部adapter 才能收到通知
        mInnerAdapter.onViewRecycled(holder);
    }

使用說明

看完上面程式碼,怎麼使用你心裡也應該有底了,就是和系統幾乎一樣。來自GitHub

mHeaderAndFooterRecyclerViewAdapter = new HeaderAndFooterRecyclerViewAdapter(mDataAdapter);
        mRecyclerView.setAdapter(mHeaderAndFooterRecyclerViewAdapter);

        mRecyclerView.setLayoutManager(new LinearLayoutManager(this));

        //add a HeaderView
        RecyclerViewUtils.setHeaderView(mRecyclerView, new SampleHeader(this));

        //add a FooterView
        RecyclerViewUtils.setFooterView(mRecyclerView, new SampleFooter(this));

總結:這就是我要的方式,程式碼解耦,不影響現有程式碼結構。

我的使用心得

當時看到程式碼很激動,但是直接使用我的專案App會啟動崩潰。丟擲ClassCastException型別轉換異常。

分析

這是因為解耦的關鍵,我的專案Adapter和HeaderAndFooterRecyclerViewAdapter沒有直接繼承關鍵,都是
extends RecyclerView.Adapter<RecyclerView.ViewHolder>
繼承自系統,算是兄弟類關係。這在ListView上就是這樣沒有問題,但是RecyclerView添加了ViewHolder,直接呼叫方法通知使用ViewHolder類會型別轉換異常。
道理是這樣的:

繼承中,子類可以自動轉型為父類,但是父類強制轉換為子類時只有當引用型別真正的身份為子類時才會強制轉換成功,否則失敗

解決辦法

這是我原來的Adapter類結構

public class RecyclerCardAdapter extends RecyclerView.Adapter<RecyclerCardAdapter.ViewHolder>

修改後

public class RecyclerHeadCardAdapter extends RecyclerView.Adapter

相應的類的方法也得修改

@Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 
        ViewHolderGeneral holder = null;//ViewHolder的子類

        View view = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.cardview_item_image, parent, false);
        holder = new ViewHolderGeneral(view);//使用子類初始化ViewHolder 
        //子類可以自動轉型為父類
        return holder;
    }
@Override
    public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) {
         ViewHolder viewHolder = (ViewHolder) holder;
         //強制轉化,父類轉子類
         //當父類的引用型別真正的身份為子類時才會強制轉換成功
         //因為在onCreateViewHolder中是用子類初始化的父類 所以能成功
        onBindData(viewHolder, bean); //繫結操作    
    }
@Override
    public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {
        super.onViewAttachedToWindow(holder);

       //這是新增headView後 需要修正的position位置
        mAdapterPosition = RecyclerViewUtils.getAdapterPosition(mRecyclerView, holder);
    }

總結

  1. 這篇部落格是我對RecyclerView的理解和使用,addHeadView這個方法我從ListView上找思路,在GitHub上看到一模一樣的實現。感謝Cundong大神的無私貢獻。
  2. 這個專案我花了我很多心思,希望能通過這專案提高我的程式碼水平。快畢業了希望能夠找到一份好的Android開發工作。