Android ListView.setEmptyView

來源:互聯網
上載者:User

標籤:

概述

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()的原理。

時間點
  1. ListView初始化
    當列表初始化的時候,將EmptyView建立出來,添加進ListView中,該時間點會有一個不好的使用者體驗,就是在給ListView添加資料前,或者網路請求資料未返回前,ListView會隱藏,顯示EmptyView,當資料到達並更新ListView後,EmptyView會隱藏,ListView顯示,會有一種閃現的效果。不建議在初始化的時間點添加EmptyView.
  2. 設定Adapter
    該時間點設定EmptyView在部分時候會有相同的顯示效果,當Adapter為空白就關聯ListView會和ListView初始化是一樣的,當在資料請求完,設定Adapter時,就可以剛好顯示當前的資料情況,這就是第三種時間點。
  3. 監聽網路請求返回資料
    在擷取資料之後添加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()方法。

總結
  1. SetEmpty原理:即動態更新ListView與EmptyView的Visibility屬性。
  2. 使用條件:EmptyView需在ListView的布局層級中。
  3. 注意事項:在使用代碼添加EmptyView的時候,需要注意不可以迴圈添加EmptyView,因為EmptyView會被添加進布局層級中,ListView只是持有一個引用。

純屬個人學習總結,有不正確的地方,肯定高人指正,謝謝!

Android ListView.setEmptyView

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.