RecyclerView如何setEmptyView及淺談ListView的setEmptyView原理

沈敏傑發表於2017-12-17

這一年來公司做的專案主要是電商,市場用到了列表的顯示,也遇到了一些坑,今天我們們來聊一下,如何用RecyclerView進行設定空列表介面的提示.

RecyclerView是listView的升級版,其實在日常的開發中,有很多地方我們都在使用RecyclerView,為什麼這樣說,RecyclerView除了列表之外,還能替代ScrollView.RecyclerView實現了NestedScrollingChild,可以配合官方提供的support-design包實現很多很炫的效果。

這幾年google一直推崇其設計理念MaterialDesign,同時也出了許多方便的包,如RecyclerView、CardView、SupportDesign包裡的控制元件,可以說,這兩年google給研發人員帶來了春天,很多很炫的效果,直接拿來就用,這樣說下去就扯遠了,我們們先看看什麼是RecyclerView。RecyclerView在android.support.v7.widget包中。

 compile 'com.android.support:appcompat-v7:24.2.1'
複製程式碼

RecyclerView其實就是listview的升級版,具有更高靈活、擴充套件,但也往往因此失去了listview很多封裝好的一些介面,如setHeader、setFooter、setEmptyView,因此你會發現RecyclerView不支援setEmptyView,我們們先來看一下listView的setEmptyView是怎麼做的,我們們也可以借鑑一下listView的做法做一個。

Paste_Image.png

從上圖所得,ListView 繼承於AbsListView,而AbsListView繼承與AdapterView。 我們可以直接去AdapterView 看其原始碼是怎樣實現setEmptyView的。

/**
 * Sets the view to show if the adapter is empty
 */
@android.view.RemotableViewMethod
public void setEmptyView(View emptyView) {
    //這裡儲存一個emptyView
    mEmptyView = emptyView;
    // If not explicitly specified this view is important for accessibility.
    if (emptyView != null
            && emptyView.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
        emptyView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
    }

    final T adapter = getAdapter();
    //判斷是否adapter中的集合為空
    final boolean empty = ((adapter == null) || adapter.isEmpty());
    updateEmptyStatus(empty);
}

/**
 * Update the status of the list based on the empty parameter.  If empty is true and
 * we have an empty view, display it.  In all the other cases, make sure that the listview
 * is VISIBLE and that the empty view is GONE (if it's not null).
 */
private void updateEmptyStatus(boolean empty) {
    if (isInFilterMode()) {
        empty = false;
    }
    //這裡才是真正判斷進行顯示與否
    if (empty) {
        if (mEmptyView != null) {
            //1.如果ListView中有呼叫setEmptyView,同時adapter中的集合為0的話,顯示emptyView,隱藏listView
            mEmptyView.setVisibility(View.VISIBLE);
            setVisibility(View.GONE);
        } else {
            //如果listView中沒有設定emptyView,讓listView顯示
            // If the caller just removed our empty view, make sure the list view is visible
            setVisibility(View.VISIBLE);
        }

        // We are now GONE, so pending layouts will not be dispatched.
        // Force one here to make sure that the state of the list matches
        // the state of the adapter.
        if (mDataChanged) {           
            this.onLayout(false, mLeft, mTop, mRight, mBottom); 
        }
    } else {
        if (mEmptyView != null) mEmptyView.setVisibility(View.GONE);
        setVisibility(View.VISIBLE);
    }
}

複製程式碼

從原始碼上可得,ListView中的setEmptyView(View),其實只是在內部進行一個判斷,如果Adapter裡面的isEmpty()為true 並且listView裡面的mEmptyView不為空,則顯示mEmptyView,同時隱藏自身的ListView.

從原始碼上看,不就是隱藏顯示嘛,沒錯,但要做到隱藏顯示也不容易,因此,我們對我們的RecyclerView進行一番改造,看一下,是否能達到我們想要的效果。

<ffzxcom.mytest.recyclerviewemptyviewdemo.way1.RecyclerViewEmptySupport
    android:id="@+id/recycler_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
</ffzxcom.mytest.recyclerviewemptyviewdemo.way1.RecyclerViewEmptySupport>

<TextView
    android:id="@+id/empty_view"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_centerInParent="true"
    android:drawablePadding="10dp"
    android:drawableTop="@mipmap/empty_view"
    android:text="@string/no_data_tips"
    android:gravity="center"
    android:visibility="gone" />
複製程式碼

根據ListView的做法,我們可以對RecyclerView進行重寫:

/**
 * Created by shenminjie on 2016/10/19.
 * 用來演示如何在RecyclerView裡面新增setEmptyView
 */

public class RecyclerViewEmptySupport extends RecyclerView {

    /**
     * 當資料為空時展示的View
     */
    private View mEmptyView;

    /**
     * 建立一個觀察者
     * 為什麼要在onChanged裡面寫?
     * 因為每次notifyDataChanged的時候,系統都會呼叫這個觀察者的onChange函式
     * 我們大可以在這個觀察者這裡判斷我們的邏輯,就是顯示隱藏
     */
    private AdapterDataObserver emptyObserver = new AdapterDataObserver() {


        @Override
        public void onChanged() {
            Adapter<?> adapter = getAdapter();
            //這種寫發跟之前我們之前看到的ListView的是一樣的,判斷資料為空否,在進行顯示或者隱藏
            if (adapter != null && mEmptyView != null) {
                if (adapter.getItemCount() == 0) {
                    mEmptyView.setVisibility(View.VISIBLE);
                    RecyclerViewEmptySupport.this.setVisibility(View.GONE);
                } else {
                    mEmptyView.setVisibility(View.GONE);
                    RecyclerViewEmptySupport.this.setVisibility(View.VISIBLE);
                }
            }

        }
    };

    public RecyclerViewEmptySupport(Context context) {
        super(context);
    }

    public RecyclerViewEmptySupport(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public RecyclerViewEmptySupport(Context context, @Nullable AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    /**
     * 依賴注入
     *
     * @param emptyView 展示的空view
     */
    public void setEmptyView(View emptyView) {
        mEmptyView = emptyView;
    }


    @Override
    public void setAdapter(Adapter adapter) {
        super.setAdapter(adapter);

        if (adapter != null) {
            //這裡用了觀察者模式,同時把這個觀察者新增進去,
            // 至於這個模式怎麼用,谷歌一下,不多講了,因為這個涉及到了Adapter的一些原理,感興趣可以點進去看看原始碼,還是受益匪淺的
            adapter.registerAdapterDataObserver(emptyObserver);
        }
        //當setAdapter的時候也調一次
        emptyObserver.onChanged();
    }
}
複製程式碼

我們在RecyclerView裡面新增了一個成員變數emptyObserver,這個作用就是用於觀察每次Adapter進行資料重新整理的時候都呼叫一次觀察者的onChange(),至於為什麼會調,這個就說得有點遠了,建議大家如果想看原始碼,可以參考一下Android原始碼設計模式(何紅輝,關愛民),裡面有很詳細的介紹android的一些設計原理。

回到上面來,我們看到我們的onChange裡面的程式碼,不就是跟ListView裡面的一樣嗎,沒錯,我想說的第一種方式就是借鑑了ListView的做法,當然,還是其他的做法,只是這個思路會有些不一樣,第一種的方法跟listview一樣,在xml裡面寫上emptyview,即便不用在RecyclerView裡寫,相信大家也知道可以在activity裡進行判斷,不就是隱藏顯示嘛,當然,還有第二種做法。

我們知道,RecyclerView的出現,更大程度給了開發者去自定義自己希望的佈局,RecyclerView可以通過引入不同的ViewType進行不同的列表顯示,舉個列子:及時通訊的聊天記錄,一般都會有左邊的佈局跟右邊的佈局,那麼,就有兩個不同的ViewType了,根據不同情況進行引入不同的ViewType。好,那麼,我們也可以根據我們的情況進行引入佈局啊,例如,如果資料為空的時候,那我能不能在我們的Adapter裡面引入一個emptyView這樣的佈局。

/**
 * Created by shenminjie on 2016/10/20.
 * 介面卡,模擬列表
 */

public class EmptyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

    /**
     * 資料
     */
    private List<String> mDatas;

    /**
     * 點選事件回撥
     */
    private OnItemSelectListener mListener;

    /**
     * viewType--分別為item以及空view
     */
    public static final int VIEW_TYPE_ITEM = 1;
    public static final int VIEW_TYPE_EMPTY = 0;

    public EmptyAdapter(List<String> datas) {
        mDatas = datas;
    }

    /**
     * 設定回撥
     *
     * @param listener
     */
    public void setOnItemSelectListener(OnItemSelectListener listener) {
        mListener = listener;
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        //在這裡根據不同的viewType進行引入不同的佈局
        if (viewType == VIEW_TYPE_EMPTY) {
            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_empty_view_layout, parent, false);
            return new RecyclerView.ViewHolder(view) {
            };
        }
        //其他的引入正常的
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_my_list_layout, parent, false);
        return new MyViewHolder(view);
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, final int position) {
        if (holder instanceof MyViewHolder) {
            MyViewHolder viewHolder = (MyViewHolder) holder;
            viewHolder.setData(mDatas.get(position));
            viewHolder.getItemView().setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (mListener != null) {
                        mListener.onItemSelectListener(v, position);
                    }
                }
            });
        }
    }

    @Override
    public int getItemCount() {
        //同時這裡也需要新增判斷,如果mData.size()為0的話,只引入一個佈局,就是emptyView
        //那麼,這個recyclerView的itemCount為1
        if (mDatas.size() == 0) {
            return 1;
        }
        //如果不為0,按正常的流程跑
        return mDatas.size();
    }

    @Override
    public int getItemViewType(int position) {
        //在這裡進行判斷,如果我們的集合的長度為0時,我們就使用emptyView的佈局
        if (mDatas.size() == 0) {
            return VIEW_TYPE_EMPTY;
        }
        //如果有資料,則使用ITEM的佈局
        return VIEW_TYPE_ITEM;
    }
}
複製程式碼

這種做法,就是把我們的emptyView設定放進去Adapter,根據不同的情況引入不同的佈局,跟第一的區別就是顯示與否都交給系統去處理,通過引入不同佈局的做法達到了顯示emptyView的效果。

相信大家都會在想,哪一種方式更好用,這得看個人的需求,但我更傾向於用第二種,因為google這兩年提供了許多很好的資源給我們開發者使用,最熱的莫過於是support-design包裡的一些新控制元件,tabLayout、toolbar、CoordinatorLayout等等,但如果想要更好的使用它們很炫的一些效果,得好好了解一下NestedScrollingParent這個介面,google提供了這些介面很好的處理了事件分發的處理,而RecyclerView均實現了這些介面,能很好的配合support-design使用其特效。感興趣的朋友可以上去github下載demo,感受一下兩種方式的不同。

Paste_Image.png

Paste_Image.png

https://github.com/shenminjie/blog_demo
複製程式碼

相關文章