標籤:
概述
ListView:一個可以垂直滑動的列表視圖。
setEmptyView()介面繼承至ListView的父類AdapterView。可想而知,ListView為空白時,才會顯示EmptyView,這與ListView的資料配接器有間接的聯絡。
使用情境
List使用非常廣泛,用於具有相同資料類型的資料模型顯示,也可以自訂List以符合實際的需求。
本文主要介紹List.setEmptyView()介面。使用情境為,當用戶端當前顯示視窗中顯示一個ListView,ListView需要通過Adapter將資料關聯到列表上顯示出來。這就會出現一個情境,就是當未設定Adapter或Adapter裡面的資料為空白時,如果給使用者一個友好的提示,提升使用者體驗。
Android官方源碼中已經提供了這樣的一個介面,通過這個介面,可以在使用ListView的過程中,利用內在的邏輯幫我們實現這個功能,減少代碼,讓並且是代碼更加的清晰,易懂。
實現方式:
EmptyView的添加包括兩種方式,一種是在建立布局的時候將EmptyView直接寫進去,這種方式的缺點在於缺乏靈活性。另一種是通過代碼將建立EmptyView,這種方式相對布局來添加更加具有靈活性,可以自訂EmptyView,動態修改。當然即便通過布局的方式添加了EmptyView,也可以再次通過代碼添加。
1.布局檔案實現EmptyView
<ViewGroup…<TextView android:id="@+id/emptyView" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:text="@string/tip_of_empty" android:visibility="gone" />…</ViewGroup>
2.代碼中實現
TextView emptyView = new TextView(context); emptyView.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT)); emptyView.setText(R.string.tip_of_empty); emptyView.setVisibility(View.GONE); ((ViewGroup)list.getParent()).addView(emptyView); list.setEmptyView(emptyView);
註:添加EmptyView有一個前提條件,所添加的EmptyView必須存在於ListView的父級容器中,或者說同一個視圖樹中,才會生效,這涉及到List.setEmptyView()的原理。
時間點
- ListView初始化
當列表初始化的時候,將EmptyView建立出來,添加進ListView中,該時間點會有一個不好的使用者體驗,就是在給ListView添加資料前,或者網路請求資料未返回前,ListView會隱藏,顯示EmptyView,當資料到達並更新ListView後,EmptyView會隱藏,ListView顯示,會有一種閃現的效果。不建議在初始化的時間點添加EmptyView.
- 設定Adapter
該時間點設定EmptyView在部分時候會有相同的顯示效果,當Adapter為空白就關聯ListView會和ListView初始化是一樣的,當在資料請求完,設定Adapter時,就可以剛好顯示當前的資料情況,這就是第三種時間點。
- 監聽網路請求返回資料
在擷取資料之後添加EmptyView可以恰當的反應當前的狀態,也是最佳的使用方式。
本次將主要介紹其中的一點即ListView的初始化,其他請大家去看源碼,最後會發現,原理都是一樣的。
實現原理
EmptyView顯示的原理如所示,當Adapter為null,或Adapter的資料為空白時,即ListView沒有資料進行顯示時,ListVewi會被設定為View.GONE,而EmptyView被設定為View.VISABLE;當資料不為空白時,邏輯相反。只有當EmptyView在當前的布局層級中,才能有這樣的效果,如所示。
ListView初始化:
ListActivity.java中的源碼實現:作為官網的例子,可以看出其具體的使用方法與邏輯,包括在何時進行EmptyView的添加,以及EmptyView添加的流程關係。
ListActivity建立的布局層級:ListActivity,顧名思義,該Activity為使用者維護了一個ListView,但當建立ListActivity時,並沒有在布局層級中出現。
因此,去查看ListActivity的源碼,源碼中介紹,在使用ListActivity時,需調用setContentView()/setListAdapter ()進行初始化。
從源碼中可以看出,在setListAdapter方法調用了ensureList()方法。
在ensureList()方法中會對ListActivity維護的ListView進行判斷,如果為null,會調用setContentView,否則返回繼續執行。接著看setContentView。
setContentView()方法調用完後,發現並沒有我們想要的結果,也沒有對listView進行初始化等等,陷入僵局。但是我們仔細看源碼,發現setContentView()中調用了Window. setContentView()。通過各種方式,最後發現,Activity這個類實現了Window.Callback介面,當Activity調用setContentView()後,會回調onContentChanged()方法。
在onContentChanged()中,回去初始化ListView,EmptyView。
通過ListActivity,EmptyView添加的具體流程為:
接下來看ListView.setEmptyView()的實現源碼。
源碼分析:
/** * Sets the view to show if the adapter is empty */ @android.view.RemotableViewMethod public void setEmptyView(View 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(); final boolean empty = ((adapter == null) || adapter.isEmpty()); updateEmptyStatus(empty); }
在setEmptyView中,可以看到兩點,即ListView中的mEmptyView對傳入的View對象是一個參考關聯性,第二點就是:對empty的定義,當adapter==null,或者adapter.isEmpty(),然後傳入updateEmptyStatus()。
/** * 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) { mEmptyView.setVisibility(View.VISIBLE); setVisibility(View.GONE); } else { // 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); } }
updateEmptyStatus()方法的原理就相當於前面繪製的原理圖,通過設定View的visibility屬性,實現EmptyView的邏輯。然而,setEmpty只是在添加的時候進行一個介面更新,當有資料之後,Adapter必須通知ListView,再去更新當前的visibility屬性,所以去看下和Adapter相關的兩個資料更新方法。
/** * Sets the data behind this ListView. * * The adapter passed to this method may be wrapped by a {@link WrapperListAdapter}, * depending on the ListView features currently in use. For instance, adding * headers and/or footers will cause the adapter to be wrapped. * * @param adapter The ListAdapter which is responsible for maintaining the * data backing this list and for producing a view to represent an * item in that data set. * * @see #getAdapter() */ @Override public void setAdapter(ListAdapter adapter) { if (mAdapter != null && mDataSetObserver != null) { mAdapter.unregisterDataSetObserver(mDataSetObserver); } resetList(); mRecycler.clear(); if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) { mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, adapter); } else { mAdapter = adapter; } mOldSelectedPosition = INVALID_POSITION; mOldSelectedRowId = INVALID_ROW_ID; // AbsListView#setAdapter will update choice mode states. super.setAdapter(adapter); if (mAdapter != null) { mAreAllItemsSelectable = mAdapter.areAllItemsEnabled(); mOldItemCount = mItemCount; mItemCount = mAdapter.getCount(); checkFocus(); mDataSetObserver = new AdapterDataSetObserver(); mAdapter.registerDataSetObserver(mDataSetObserver); mRecycler.setViewTypeCount(mAdapter.getViewTypeCount()); int position; if (mStackFromBottom) { position = lookForSelectablePosition(mItemCount - 1, false); } else { position = lookForSelectablePosition(0, true); } setSelectedPositionInt(position); setNextSelectedPositionInt(position); if (mItemCount == 0) { // Nothing selected checkSelectionChanged(); } } else { mAreAllItemsSelectable = true; checkFocus(); // Nothing selected checkSelectionChanged(); } requestLayout(); }
SetAdapter()方法中,有兩點,一個是checkFocus,另一個是為Adapter註冊了一個資料觀察者,後面源碼會介紹到,當adapter資料發送變化時,會回調觀察者的onChanged()方法。
checkFocus()源碼:
void checkFocus() { final T adapter = getAdapter(); final boolean empty = adapter == null || adapter.getCount() == 0; final boolean focusable = !empty || isInFilterMode(); // The order in which we set focusable in touch mode/focusable may matter // for the client, see View.setFocusableInTouchMode() comments for more // details super.setFocusableInTouchMode(focusable && mDesiredFocusableInTouchModeState); super.setFocusable(focusable && mDesiredFocusableState); if (mEmptyView != null) { updateEmptyStatus((adapter == null) || adapter.isEmpty()); } }
可以看出,checkFocus()方法中,調用了updateEmptyStatus(),即在設定資料配接器的時候,會對EmptyView進行更新。
接下來看註冊registerDataSetObserver資料觀察者源碼:
public void registerDataSetObserver(DataSetObserver observer) { mDataSetObservable.registerObserver(observer); }
到此,setAdapter()方法的邏輯就結束了,然後setEmptyView()和setAdapter()方法只會在資料初始化的時候調用一次,當資料發送變化的時候,需要手動去更新Adapter調用notifyDataSetChanged()。
notifyDataSetChanged()源碼:
/** * Notifies the attached observers that the underlying data has been changed * and any View reflecting the data set should refresh itself. */ public void notifyDataSetChanged() { mDataSetObservable.notifyChanged(); }Adapter調用notifyDataSetChanged()方法,實質上是調用mDataSetObservable. notifyChanged()方法。繼續跟蹤下去。/** * Invokes {@link DataSetObserver#onChanged} on each observer. * Called when the contents of the data set have changed. The recipient * will obtain the new contents the next time it queries the data set. */ public void notifyChanged() { synchronized(mObservers) { // since onChanged() is implemented by the app, it could do anything, including // removing itself from {@link mObservers} - and that could cause problems if // an iterator is used on the ArrayList {@link mObservers}. // to avoid such problems, just march thru the list in the reverse order. for (int i = mObservers.size() - 1; i >= 0; i--) { mObservers.get(i).onChanged(); } }}
在notifyChanged()方法中,會遍曆所有註冊的資料觀察者,並回調觀察者的onChanged()方法,通過源碼可以看到,在Adapter源碼中,建立了一個內部類AdapterDataSetObserver,並重寫了onChanged()方法。
class AdapterDataSetObserver extends DataSetObserver { private Parcelable mInstanceState = null; @Override public void onChanged() { mDataChanged = true; mOldItemCount = mItemCount; mItemCount = getAdapter().getCount(); // Detect the case where a cursor that was previously invalidated has // been repopulated with new data. if (AdapterView.this.getAdapter().hasStableIds() && mInstanceState != null && mOldItemCount == 0 && mItemCount > 0) { AdapterView.this.onRestoreInstanceState(mInstanceState); mInstanceState = null; } else { rememberSyncState(); } checkFocus(); requestLayout(); }}
onChanged()中有兩點,一點是mDataChanged,在updateEmptyStatus()方法中會去判斷該變數的狀態,當為true時,會更新ListView布局大小,而在onChanged()方法中會將mDataChanged置為true,通知布局更新,另一點是調用了checkFocus()方法,間接調用updateEmptyStatus()進行EmptyView的更新。
至此,EmptyView的設定基本的邏輯已經很清晰了,總結下。EmptyView的更新主要在updateEmptyStatus()中進行,在初始化ListView的Adapter以及資料更新後回調Adapter.notifyDataSetChanged()方法,其實質也是回調notifyDataSetChanged()方法。
總結
- SetEmpty原理:即動態更新ListView與EmptyView的Visibility屬性。
- 使用條件:EmptyView需在ListView的布局層級中。
- 注意事項:在使用代碼添加EmptyView的時候,需要注意不可以迴圈添加EmptyView,因為EmptyView會被添加進布局層級中,ListView只是持有一個引用。
純屬個人學習總結,有不正確的地方,肯定高人指正,謝謝!
Android ListView.setEmptyView