編輯:關於Android編程
本文站在巨人的肩膀上 自我感覺又進了一步而成。
基於翔神的大作基礎之上寫的一個為RecyclerView添加HeaderView FooterView 的另一種解決方案,
上次翔神發表這篇文章時,我就提了個問題:說headerView和FooterView都是強引用在Adapter中的,這樣即使他所屬的ViewHolder被回收復用,但是View本身的實例還是在被強引用,內存空間也無法釋放的。 這樣做雖然速度沒任何問題,(甚至還有小小提升,但是HeaderView過大內存空間就會吃緊了吧) 所以我想了好久 改寫了一下,換了種思路,給RecyclerView提供數據和布局,UI的創建 和 數據的綁定分開來做,都交由Adapter維護。
牆裂建議大家先閱讀翔神文章後 再立刻閱讀此文,威力翻倍。這樣對本文使用到的一些吊炸天的東西就不會陌生了,例如通用的CommonAdapter和ViewHolder。
敲黑板,如果只是伸手黨,建議直接看 【2 使用方法】,並直接到文末下載鏈接裡的工程,拷貝recyclerview包下的幾個文件即可使用。
工程裡已經參考解決,HeaderView適配GridLayoutManager 和StaggeredGridLayoutManager。
========================================================================
【1 引言】
眾所周知,RecyclerView已經是主流,ListView已經成為過去式,而兩者之間有些許的不同,其中比較重要的一點就是ListView自帶addHeaderView,addFooterView方法,而RecyclerView並沒有提供。So,我們開發者要自己想辦法實現這個功能。
市面上大多為RecyclerView添加HeaderView的方案,都是在使用RecyclerView的類中(Activity Fragment)裡構建一個View,並綁定好數據,然後通過XXXAdapter提供的addHeaderView方法,將這個View set進Adapter裡。
Adapter內部使用ArrayList、或者翔神使用的是SparseArray存儲這個View,並為HeaderView FooterView分配不同的itemViewType,然後Adapter在onCreateViewHolder和onBindViewHolder方法裡,根據ViewType的不同來判斷這是HeaderView 還是普通item。
這種方法目前為止我只發現一個弊端(也是本文改進的地方),就是這個HeaderView由於在Adapter裡是被ArrayList、SparseArray強引用的,就算其所屬的RecyclerView.ViewHolder被回收服用了,但是這個View會因為被ArrayList等強引用著,依然停留在內存中。所以該HeaderView並沒有被回收,只是被復用了。而普通的item都只有數據和layoutId被保存在Adapter中,並沒有View的實例。
一般情況下 這並沒有任何問題,因為普通項目的HeaderView也不大,但是若HeaderView過於龐大,(就像我司的項目,動辄HeaderView就三個屏幕長度,三屏之後才是普通的item),在這個頁面已經往下滑了很多距離,浏覽了很多內容,HeaderView早已不可見,此時按照RecyclerView的思路,這個龐大的HeaderView所屬的VIewHolder應該被系統回收復用,以騰出空間,ok,那麼RecyclerView做了它該做的事,回收了這個HeaderView寄身的VIewHolder給其他類型的Item使用了,可惜上文提到,此時HeaderView被強引用住,被回收復用的只是其所屬的那個ViewHolder,這個龐大的VIew所占的內存空間依然沒有被釋放。
其實我們仔細想一想,RecyclerView Adapter裡是不保存View對象的,它保存的只是數據和layout,而我們也應該遵循此原則 為其添加HeaderView(FooterView)。
(題外話,和ListView相比,RecyclerView更是進一步的 將 UI的創建 和數據的綁定 分成了兩步,(oncreateViewHolder,onBindViewHolder))
敲黑板,本文就參考翔神的裝飾者模式,為RecyclerView 添加 HeaderView(FooterView),並且將HeaderView的UI創建,和數據綁定強制分開,令HeaderView實例在Adapter中不再被強引用,讓HeaderView和普通的ItemView沒有兩樣~。
先上預覽動圖:
========================================================================
【2 使用方法】
//HeaderView使用方法小窺: 以下為Rv添加兩個HeaderView mHeaderAdapter = new HeaderRecyclerAndFooterWrapperAdapter(mAdapter) { @Override protected void onBindHeaderHolder(ViewHolder holder, int headerPos, int layoutId, Object o) { switch (layoutId) { case R.layout.item_header_1: TestHeader1 header1 = (TestHeader1) o; holder.setText(R.id.tv, header1.getText()); break; case R.layout.item_header_2: TestHeader2 header2 = (TestHeader2) o; holder.setText(R.id.tv1, header2.getTxt1()); holder.setText(R.id.tv2, header2.getTxt2()); break; default: break; } } }; mHeaderAdapter.addHeaderView(R.layout.item_header_1,new TestHeader1("第一個HeaderView")); mHeaderAdapter.addHeaderView(R.layout.item_header_2,new TestHeader2("第二個","HeaderView")); mRv.setAdapter(mHeaderAdapter);粗略這麼一看,我擦 什麼辣雞,比翔神那個真是差十萬八千裡,人家只要4行代碼就加一個HeaderView,而且還不用實現父類Adapter的方法,你這還要switch case 看起來就一坨好麻煩的樣子,走了走了。
客官留步留步,如果客官有這種想法,先冷靜一下,裡聽我港。
這個寫法猛地看起來是略復雜了一些,但是它強制的讓我們將UI的創建和數據的綁定分開了,我們重寫的onBindHeaderHolder()方法,就是數據的綁定過程, 試想一下,基本上每個帶HeaderView的頁面都有下拉刷新功能,如果你使用傳統方法添加HeaderView,那麼你必須要持有HeaderView的引用才能在數據刷新時改變頭部數據,而且那些煩人的set方法一樣是要寫一遍,你可能需要將 寫在Activity(Fragment)裡的 創建HeaderView時的set數據方法抽成一個函數,再調用一遍。所以工作量是一點沒減少的。
所以我們這種做法,你的工作量也是一點沒增加滴!反而還是方便滴!優雅滴!
(躲開丟過來的雞蛋)廢話不多說,用法已經看到,下面看我們是怎麼實現的。 如果伸手黨看到這裡覺得已經夠了,那麼就可以去文末直接下載源碼copy使用了,裡面使用的幾個類版權大多歸翔神所有。
========================================================================
【三,實現】
直接貼出核心代碼:
private static final int BASE_ITEM_TYPE_HEADER = 1000000;//headerview的viewtype基准值
//存放HeaderViews的layoudID和data,key是viewType,value 是 layoudID和data, // 在createViewHOlder裡根據layoutId創建UI, // 在onbindViewHOlder裡依據這個data渲染UI,同時也將layoutId回傳出去用於判斷何種Header private SparseArrayCompatmHeaderDatas = new SparseArrayCompat ();
@Override public int getItemViewType(int position) { if (isHeaderViewPos(position)) { return mHeaderDatas.keyAt(position); } return super.getItemViewType(position - getHeaderViewCount()); } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (mHeaderDatas.get(viewType) != null) {//不為空,說明是headerview return ViewHolder.get(parent.getContext(), null, parent, mHeaderDatas.get(viewType).keyAt(0), -1); } return mInnerAdapter.onCreateViewHolder(parent, viewType); } protected abstract void onBindHeaderHolder(ViewHolder holder, int headerPos, int layoutId, Object o);//多回傳一個layoutId出去,用於判斷是第幾個headerview @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { if (isHeaderViewPos(position)) { int layoutId = mHeaderDatas.get(getItemViewType(position)).keyAt(0); onBindHeaderHolder((ViewHolder) holder, position, layoutId, mHeaderDatas.get(getItemViewType(position)).get(layoutId)); return; } //舉例子,2個header,0 1是頭,2是開始,2-2 = 0 mInnerAdapter.onBindViewHolder(holder, position - getHeaderViewCount()); }
/** * 添加HeaderView * * @param layoutId headerView 的LayoutId * @param data headerView 的data(可能多種不同類型的header 只能用Object了) */ public void addHeaderView(int layoutId, Object data) { //mHeaderViews.put(mHeaderViews.size() + BASE_ITEM_TYPE_HEADER, v); SparseArrayCompat headerContainer = new SparseArrayCompat(); headerContainer.put(layoutId, data); mHeaderDatas.put(mHeaderDatas.size() + BASE_ITEM_TYPE_HEADER, headerContainer); }首先,定義一個很大的int,作為HeaderView的viewType的基准值(我這裡是100W),
然後定義一個SparseArray
外層的SparseArray的Key用來存放HeaderView的ViewType,value存放的是這個ViewType對應的HeaderView的布局layoutId 和 數據data,
即內層SparseArray的key是layoutId,value是Object類型的數據data(因為每個HeaderView的數據類型不同,所以這裡我只能想到用Obejct類型)。
在getItemViewType()方法中,我們先根據postion判斷是否是HeaderView,如果是,那麼返回該HeaderView的viewtype(SparseArray的key)。
在onCreateViewHolder()方法裡,根據ViewType判斷是否是HeaderView,如果是HeaderView ,那麼創建一個該HeaderView的ViewHolder(我這裡使用的是翔神的通用ViewHolder)。
在onBindViewHolder()方法中,先根據postion判斷是否是HeaderView,如果是,先從mHeaderDatas裡取出這個HeaderView的layoutId,並將這個HeaderView的數據也一並取出,回調一個 abstract 的 onBindHeaderHolder()的方法,將這些參數都傳入,交由子類去自由處理。 子類在這個方法裡 完成數據的綁定即可。
========================================================================
【四,完整代碼】
這份代碼FooterView並沒有用此方法實現,是“強引用VIew方法實現的”。
理由:
1 因為FooterView往往是一個LoadMore相關的提示控件,內存占用很有限。
2 LoadMore相關提示的控件 是需要強引用在Fragment Activity 或者相關類中,即使我在Adapter類裡將其引用釋放,這個View在內存的空間依然是無法被釋放的。
3 兩種實現方法都放上來,大家可以根據本文描述的方法,自行嘗試將FooterView也改寫,可以和我討論,稍後我也會附加上我修改的版本。
package com.example.headerrv.recyclerview; import android.support.v4.util.SparseArrayCompat; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.StaggeredGridLayoutManager; import android.view.View; import android.view.ViewGroup; /** * 介紹:一個給RecyclerView添加HeaderView FooterView的裝飾Adapter類 * 重點哦~ RecyclerView的HeaderView將可以被系統回收,不像老版的HeaderView是一個強引用在內存裡 * 作者:zhangxutong * 郵箱:[email protected] * 時間: 2016/8/2. */ public abstract class HeaderRecyclerAndFooterWrapperAdapter extends RecyclerView.Adapter{ private static final int BASE_ITEM_TYPE_HEADER = 1000000;//headerview的viewtype基准值 private static final int BASE_ITEM_TYPE_FOOTER = 2000000;//footerView的ViewType基准值 //存放HeaderViews的layoudID和data,key是viewType,value 是 layoudID和data, // 在createViewHOlder裡根據layoutId創建UI, // 在onbindViewHOlder裡依據這個data渲染UI,同時也將layoutId回傳出去用於判斷何種Header private SparseArrayCompat mHeaderDatas = new SparseArrayCompat (); private SparseArrayCompat mFooterViews = new SparseArrayCompat<>();//存放FooterViews,key是viewType protected RecyclerView.Adapter mInnerAdapter;//內部的的普通Adapter public HeaderRecyclerAndFooterWrapperAdapter(RecyclerView.Adapter mInnerAdapter) { this.mInnerAdapter = mInnerAdapter; } public int getHeaderViewCount() { return mHeaderDatas.size(); } public int getFooterViewCount() { return mFooterViews.size(); } private int getInnerItemCount() { return mInnerAdapter != null ? mInnerAdapter.getItemCount() : 0; } /** * 傳入position 判斷是否是headerview * * @param position * @return */ public boolean isHeaderViewPos(int position) {// 舉例, 2 個頭,pos 0 1,true, 2+ false return getHeaderViewCount() > position; } /** * 傳入postion判斷是否是footerview * * @param position * @return */ public boolean isFooterViewPos(int position) {//舉例, 2個頭,2個inner,pos 0 1 2 3 ,false,4+true return position >= getHeaderViewCount() + getInnerItemCount(); } /** * 添加HeaderView * * @param layoutId headerView 的LayoutId * @param data headerView 的data(可能多種不同類型的header 只能用Object了) */ public void addHeaderView(int layoutId, Object data) { //mHeaderViews.put(mHeaderViews.size() + BASE_ITEM_TYPE_HEADER, v); SparseArrayCompat headerContainer = new SparseArrayCompat(); headerContainer.put(layoutId, data); mHeaderDatas.put(mHeaderDatas.size() + BASE_ITEM_TYPE_HEADER, headerContainer); } /** * 設置(更新)某個layoutId的HeaderView的數據 * * @param layoutId * @param data */ public void setHeaderView(int layoutId, Object data) { boolean isFinded = false; for (int i = 0; i < mHeaderDatas.size(); i++) { SparseArrayCompat sparse = mHeaderDatas.valueAt(i); if (layoutId == sparse.keyAt(0)) { sparse.setValueAt(0, data); isFinded = true; } } if (!isFinded) {//沒發現 說明是addHeaderView addHeaderView(layoutId, data); } } /** * 設置某個位置的HeaderView * * @param headerPos 從0開始,如果pos過大 就是addHeaderview * @param layoutId * @param data */ public void setHeaderView(int headerPos, int layoutId, Object data) { if (mHeaderDatas.size() > headerPos) { SparseArrayCompat headerContainer = new SparseArrayCompat(); headerContainer.put(layoutId, data); mHeaderDatas.setValueAt(headerPos, headerContainer); } else if (mHeaderDatas.size() == headerPos) {//調用addHeaderView addHeaderView(layoutId, data); } else { // addHeaderView(layoutId, data); } } /** * 添加FooterView * * @param v */ public void addFooterView(View v) { mFooterViews.put(mFooterViews.size() + BASE_ITEM_TYPE_FOOTER, v); } /** * 清空HeaderView數據 */ public void clearHeaderView() { mHeaderDatas.clear(); } public void clearFooterView() { mFooterViews.clear(); } public void setFooterView(View v) { clearFooterView(); addFooterView(v); } @Override public int getItemViewType(int position) { if (isHeaderViewPos(position)) { return mHeaderDatas.keyAt(position); } else if (isFooterViewPos(position)) {//舉例:header 2, innter 2, 0123都不是,4才是,4-2-2 = 0,ok。 return mFooterViews.keyAt(position - getHeaderViewCount() - getInnerItemCount()); } return super.getItemViewType(position - getHeaderViewCount()); } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (mHeaderDatas.get(viewType) != null) {//不為空,說明是headerview //return new ViewHolder(parent.getContext(), mHeaderViews.get(viewType)); //return createHeader(parent, mHeaderViews.indexOfKey(viewType)); 第一種方法是讓子類實現這個方法 構建ViewHolder return ViewHolder.get(parent.getContext(), null, parent, mHeaderDatas.get(viewType).keyAt(0), -1); } else if (mFooterViews.get(viewType) != null) {//不為空,說明是footerview return new ViewHolder(parent.getContext(), mFooterViews.get(viewType)); } return mInnerAdapter.onCreateViewHolder(parent, viewType); } //protected abstract RecyclerView.ViewHolder createHeader(ViewGroup parent, int headerPos); protected abstract void onBindHeaderHolder(ViewHolder holder, int headerPos, int layoutId, Object o);//多回傳一個layoutId出去,用於判斷是第幾個headerview @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { if (isHeaderViewPos(position)) { int layoutId = mHeaderDatas.get(getItemViewType(position)).keyAt(0); onBindHeaderHolder((ViewHolder) holder, position, layoutId, mHeaderDatas.get(getItemViewType(position)).get(layoutId)); return; } else if (isFooterViewPos(position)) { return; } //舉例子,2個header,0 1是頭,2是開始,2-2 = 0 mInnerAdapter.onBindViewHolder(holder, position - getHeaderViewCount()); } @Override public int getItemCount() { return getInnerItemCount() + getHeaderViewCount() + getFooterViewCount(); } @Override public void onAttachedToRecyclerView(RecyclerView recyclerView) { mInnerAdapter.onAttachedToRecyclerView(recyclerView); //為了兼容GridLayout RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); if (layoutManager instanceof GridLayoutManager) { final GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager; final GridLayoutManager.SpanSizeLookup spanSizeLookup = gridLayoutManager.getSpanSizeLookup(); gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { int viewType = getItemViewType(position); if (mHeaderDatas.get(viewType) != null) { return gridLayoutManager.getSpanCount(); } else if (mFooterViews.get(viewType) != null) { return gridLayoutManager.getSpanCount(); } if (spanSizeLookup != null) return spanSizeLookup.getSpanSize(position); return 1; } }); gridLayoutManager.setSpanCount(gridLayoutManager.getSpanCount()); } } @Override public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) { mInnerAdapter.onViewAttachedToWindow(holder); int position = holder.getLayoutPosition(); if (isHeaderViewPos(position) || isFooterViewPos(position)) { ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams(); if (lp != null && lp instanceof StaggeredGridLayoutManager.LayoutParams) { StaggeredGridLayoutManager.LayoutParams p = (StaggeredGridLayoutManager.LayoutParams) lp; p.setFullSpan(true); } } } }
在消息通知的時候,我們經常用到兩個控件Notification和Toast。特別是重要的和需要長時間顯示的信
Android接口回調方法處處涉及到,比如常用的Button點擊事件就是一個接口回調,可見掌握熟練使用接口回調方法的重要性。接口回調的簡單解釋就是:比如我這個類實現了一個
Activity中有7個與生命周期有關的函數。其中onCreated()是activity第一次被啟動時執行的,主要是初始化一些變量,onRestart()是
在上篇文章的例子中,我們使用了一張圖片和一個文本作為每一行的數據,發現效果已經完全達到了,而且沒出現什麼問題。但如果我們將Item的數量調大,比如調到1000、10000