Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android ListView.setEmptyView

Android ListView.setEmptyView

編輯:關於Android編程

概述

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

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只是持有一個引用。
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved