Android開發指南:Action Bar

Action Bar是一個用來標明用戶在app中所處的位置,呈現用戶action和導航模式的窗口功能。Action bar可以讓系統優雅地在不同屏幕配置上進行適配,爲你的用戶提供跨應用的統一使用體驗。
Android開發指南:Action Bar - Pizzanicky - Pizzanicky的小窩
 圖1:一個包括【1】app圖標,【2】兩個action項,以及【3】overflow的Action bar

Action bar提供下面幾個主要功能:
提供一個用來展示app身份和用戶在app中所處位置的固定專用空間;
讓重要的action處於明顯的易操作的位置(如搜索);
支持持續的應用中導航及視圖切換(利用tab標籤和下拉菜單)。

要了解更多關於Action bar的互動模式和設計準則,參見Android設計指南:Action Bar

ActionBar API在Android3.0(API level 11)首次引入,同時爲兼容Android2.1(API level 7)以上,這些API也存在於Support Library中。

本指南主要介紹support library的action bar使用,不過如果你的app只支持Android3.0以上,那麼你可以直接使用framework提供的ActionBar API,多數API都是一樣的——只不過是存在於不同的包命名空間——除了一些方法名等,後面會提到。

注意:確保你從正確的包import了ActionBar類(和相關API):
如支持API level 11以下:

import android.support.v7.app.ActionBar

如只支持API level 11及以上:

import android.app.ActionBar

注意:如果想查找關於用來顯示情境相關action項的情境相關action bar,見Menu指南。

添加Action Bar

如上所述,本指南主要講述support library中的ActionBar,因此在添加action bar之前,你必須先在你的project中設置appcompat v7 support library,參照Support Library Setup

Project設置好support library之後,按下面的步驟添加action bar:
1. 繼承ActionBarActivity,創建你的activity;
2. 爲你的activity使用(或繼承)Theme.Appcompat中的主題。如下:
<activity android:theme="@style/Theme.AppCompat.Light" ... >
這樣你的activity運行在Android2.1(API level 7)及以上時就會有action bar了。

對於API level 11及以上
所有使用Theme.Holo主題(或其繼承者)的activity都會包含action bar,當targetSdkVersion minSdkVersion屬性設爲“11”或以上時,默認主題就是它。如果不需要某個activity顯示action bar,可以將 它的主題設置爲Theme.Holo.NoActionBar

移除Action Bar
在程序運行時,可以調用hide()隱藏action bar,如下:
ActionBar actionBar = getSupportActionBar();
actionBar.hide();
對於API level 11及以上
getActionBar()方法獲得ActionBar

Action Bar隱藏時,系統會自動調整頁面佈局來填滿空餘出來的屏幕空間,用show()可以重新顯示出action bar。

注意隱藏或者移除action bar會使activity重新對頁面進行佈局來補充action bar佔用的部分,如果你的activity經常顯示和隱藏action bar,那麼使用overlay模式會更好。Overlay模式會把action bar繪製在佈局之上,遮擋頂部的一部分,這樣,你的action bar隱藏或者重新出現時,activity的佈局就不會改變。要開啓overlay模式,須要爲你的activity創建一個自定義主題,並將其windowActionBarOverlay屬性設爲true。更多信息參見Styling the Action Bar

用logo替換圖標
系統會默認在action bar上使用manifest文件中applicationactivity元素裏icon屬性指定的圖標,不過如果你同時還指定了logo屬性,action bar則會使用logo圖片。

通常logo應該比圖標寬一些,但是不應包含不必要的文字。一般來說,應該在需要使用傳統的用戶認可熟悉的商標等標誌時使用logo,YouTube app的logo就是個很好的例子——logo是用戶期望中的標誌,而app圖標則是爲遵循方形圖標而進行過修改的。

添加Action項

Action bar爲用戶提供每個情境下最重要的action的操作,直接以圖標和/或文字形式出現在action bar上的項稱爲action按鈕,action bar無法容納或不那麼重要的action則放在overflow中。用戶可通過點按右側的overflow按鈕(或者設備上的Menu按鍵)來顯現overflow菜單。
Android開發指南:Action Bar - Pizzanicky - Pizzanicky的小窩
圖2:帶有3個action按鈕和overflow按鈕的action bar

Activity啓動時,系統通過調用activity的onCreateOptionsMenu()來生成action項,用這個方法來加載定義action項的菜單資源。下面是個菜單資源的例子:
res/menu/main_activity_actions.xml
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
    <item android:id="@+id/action_search"
          android:icon="@drawable/ic_action_search"
          android:title="@string/action_search"/>
    <item android:id="@+id/action_compose"
          android:icon="@drawable/ic_action_compose"
          android:title="@string/action_compose" />
</menu>
然後在activity的onCreateOptionsMenu()方法中,把菜單資源加載到參數傳入的Menu對象,這樣每一項就都被添加到了action bar:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
    // Inflate the menu items for use in the action bar
    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.menu.main_activity_actions, menu);
    return super.onCreateOptionsMenu(menu);
}
要讓action項直接顯示爲action按鈕,須要在<item>標籤中加入showAsAction="ifRoom",如下:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:yourapp="http://schemas.android.com/apk/res-auto" >
    <item android:id="@+id/action_search"
          android:icon="@drawable/ic_action_search"
          android:title="@string/action_search"
          yourapp:showAsAction="ifRoom"  />
    ...
</menu>
如果action bar上的空間不夠,該項就會被放進overflow。
使用support library中的XML屬性
要注意到上例中的showAsAction屬性用到了<menu>標籤中的一個自定義名字空間,使用support library中定義的任何 XML屬性都必須這樣做,因爲這些屬性在老設備的Android framework中並不存在,所以必須用自己的名字空間來作爲 support library的所有屬性的前綴。

如果你的菜單項既有標題又有圖標——通過title和icon屬性指定——那麼默認會只顯示圖標,如果要顯示文字標題,就須要在showAsAction屬性裏添加"withText",如下:
<item yourapp:showAsAction="ifRoom|withText" ... />
注意:"withText"只是提示action bar應該顯示文字標題,action bar會在可能的情況下顯示文字,但如果該項有圖標而 且action bar沒有空間,那麼文字還是有可能不顯示。

但對於每個菜單項都應該定義title,哪怕你不顯示它,原因如下:
如果action bar沒有空間顯示action項,那麼這些項就會出現在overflow中,而在overflow中只會顯示文字標題;
視力障礙用戶使用的屏幕閱讀功能會讀出菜單項的文字標題;
如果action項只顯示爲圖標,用戶可以長按圖標來顯示標題作爲提示。

icon屬性是可選的,但建議對其進行指定。對於圖標設計方面的建議,參見Iconography設計指南。你還可以從下載頁面下載整套的標準action bar圖標(比如搜索和放棄修改)。

可以用"always"來聲明action項永遠顯示爲action按鈕,然而,你不應該用這種方式讓一個action項始終顯示爲action按鈕,否則在較窄的屏幕上可能造成佈局問題。最好還是用"ifRoom"請求讓它顯示爲action按鈕,同時也允許系統在空間不足時把它放入overflow。需要用到這個值的情況是當這個項包含一個無法摺疊,並且要永遠可見,提供關鍵功能的action view

處理action項的點擊事件
當用戶點按一個action時,系統調用activity的onOptionsItemSelected()方法,用傳入這個方法的MenuItem對象調用getItemId()可以辨別是哪個action,方法返回<item>標籤的id屬性設置的唯一ID,然後就可以根據ID執行相應的action。如下:
@Override
public boolean onOptionsItemSelected(MenuItem item) {
    // Handle presses on the action bar items
    switch (item.getItemId()) {
        case R.id.action_search:
            openSearch();
            return true;
        case R.id.action_compose:
            composeMessage();
            return true;
        default:
            return super.onOptionsItemSelected(item);
    }
}
注:如果你通過Fragment類的onCreateOptionsMenu()回調來從fragment加載菜單項,用戶選擇action項時系 統會調用該fragment的onOptionsItemSelected()。不過,activity會先得到一個處理這個事件的機會,系統會在 調用fragment的同方法之前先調用activity的onOptionsItemSelected()。爲確保activity的所有fragment都有處 理事件的機會,在自己處理完事件後不要直接返回false,而應該總是默認把調用傳遞給父類。

使用分離式action bar
分離式action bar可以在屏幕比較窄的情況下(比如豎屏的設備)提供一個位於屏幕底部的action bar來放置action項。

這種把action item分離出來的方式確保了在窄屏上能有足夠的空間來顯示action項,同時還在頂部留出了用於頁面導航和標題顯示的空間。
Android開發指南:Action Bar - Pizzanicky - Pizzanicky的小窩
 圖3. 模擬圖,帶有標籤的action bar(左);分離式action bar(中);不顯示圖標和標題(右)

在support library中啓用分離式action bar,需要兩個步驟:
1. 在manifest文件中爲每個activity或者爲application添加uiOptions="splitActionBarWhenNarrow",這個屬性只有API level 14及以上才能解析(老版本上會直接忽略)。
2. 要支持老版本,須要爲每個activity元素添加一個meta-data子元素,名稱設爲"android.support.UI_OPTIONS"
例子:
<manifest ...>
    <activity uiOptions="splitActionBarWhenNarrow" ... >
        <meta-data android:name="android.support.UI_OPTIONS"
                   android:value="splitActionBarWhenNarrow" />
    </activity>
</manifest>
分離式action bar還允許導航標籤收縮到主action bar,前提是不顯示圖標和標題(如圖3中右側所示),要達到這個效果,必須用setDisplayShowHomeEnabled(false)setDisplayShowTitleEnabled(false)來禁用圖標和標題。

用App圖標返回上層界面

把app圖標用作Up按鈕使用戶能夠在層級關係的界面中進行導航,比如,A界面顯示一個list,點擊其中一項進入界面B,這時界面B就應該包含一個能夠返回A界面的Up按鈕。
注:Up導航和系統的back導航有區別,back按鈕用來按用戶最近訪問的事件順序進行逆向回退,基本上基於界面間 的臨時關係,而非app的層級結構(up導航的基礎)。
Android開發指南:Action Bar - Pizzanicky - Pizzanicky的小窩
 圖4. Gmail中的Up按鈕

要把app圖標用作Up按鈕,須調用setDisplayHomeAsUpEnabled(),例子:
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_details);

    ActionBar actionBar = getSupportActionBar();
    actionBar.setDisplayHomeAsUpEnabled(true);
    ...
}
這樣action bar的圖標旁就多了一個Up括號(如圖4),不過,它默認是不會做任何事情的,要指定用戶點擊時打開的activity,你有兩個選擇:
在manifest文件中指定父activity
如果父activity永遠是同一個時這是最好的選擇。通過在manifest中聲明哪個是父activity,用戶點擊Up按鈕時, action bar會自動執行正確的action。
從Android4.1(API level 16)開始,可以通過activity元素的parentActivityName屬性來聲明父activity。
如要通過support library來支持較老的設備,加入一個meta-data元素,將android.support.PARENT_ACTIVITY的值 指定爲父activity。例子:
<application ... >
    ...
    <!-- The main/home activity (has no parent activity) -->
    <activity
        android:name="com.example.myfirstapp.MainActivity" ...>
        ...
    </activity>
    <!-- A child of the main activity -->
    <activity
        android:name="com.example.myfirstapp.DisplayMessageActivity"
        android:label="@string/title_activity_display_message"
        android:parentActivityName="com.example.myfirstapp.MainActivity" >
        <!-- Parent activity meta-data to support API level 7+ -->
        <meta-data
            android:name="android.support.PARENT_ACTIVITY"
            android:value="com.example.myfirstapp.MainActivity" />
    </activity>
</application>
這樣指定後,再用setDisplayHomeAsUpEnabled()來啓用Up按鈕,action bar就已經能實現返回上級的功能了。

如果由於用戶到達目前界面方式不同而造成父activity不一定相同,那麼這種方法會比較合適。也就是說,如果用戶有多種途徑可以到達當前界面,那麼Up按鈕應該沿着該途徑返回上層。
當用戶在你的app中導航(在你app自己的task中)時按了Up按鈕,系統會調用getSupportParentActivityIntent()。如果Up應該打開的activity由於用戶到達當前界面的途徑不同而不同,那麼就應該override此方法並返回啓動相應activity的Intent
當你的activity運行在屬於你的app的task中時,如果用戶點按Up按鈕,系統會爲你的activity調用onCreateSupportNavigateUpTaskStack(),這樣,你必須用傳入此方法的TaskStackBuilder對象來創建相應的back stack。
即使你通過override getSupportParentActivityIntent()指定了上層activity,你還是可以在manifest文件中聲明“默認”父activity,這樣可以不用重新實現onCreateSupportNavigateUpTaskStack(),它的默認實現即會基於這個聲明的父activity來構建back stack。
注:如果你用一系列fragment來構建你app的層級結構,而非用多個activity,那麼上面兩個選擇就都不行了,這 時,要在fragment間進行up操作,就需要override onSupportNavigateUp()來進行相應的fragment切換——通 常是調用popBackStack()把fragment從back stack中彈出。

更多關於實現Up導航,見Providing Up Navigation

添加Action View

Action View是一個出現在action bar上用來替代action按鈕的部件,它能在不改變activity或者fragment,並且不替換action bar的情況下提供快捷豐富的action。比如,你有一個搜索action,那麼你可以在action bar上添加一個嵌有SearchView的action view,如圖5。
Android開發指南:Action Bar - Pizzanicky - Pizzanicky的小窩
 圖5. 帶有可收起的SearchView的action bar

要聲明一個action view,使用actionLayout或者actionViewClass屬性分別來指定一個佈局資源或要使用的widget類。下面是添加SearchView widget的例子:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:yourapp="http://schemas.android.com/apk/res-auto" >
    <item android:id="@+id/action_search"
          android:title="@string/action_search"
          android:icon="@drawable/ic_action_search"
          yourapp:showAsAction="ifRoom|collapseActionView"
          yourapp:actionViewClass="android.support.v7.widget.SearchView" />
</menu>
注意showAsAction屬性中還多了"collapseActionView"這樣一個值,這是可選的,聲明瞭該action view可以收起爲一個按鈕。(這個行爲將在下一節處理可收起的action view中詳細解釋)

如需配置action view(如增加事件監聽),可在onCreateOptionsMenu()回調中進行。你可以調用static方法MenuItemCompat.getActionView()來獲得action view對象,並傳入相應的MenuItem。下面是獲取這個搜索widget的例子:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.main_activity_actions, menu);
    MenuItem searchItem = menu.findItem(R.id.action_search);
    SearchView searchView = (SearchView) MenuItemCompat.getActionView(searchItem);
    // Configure the search info and add any event listeners
    ...
    return super.onCreateOptionsMenu(menu);
}
對於API level 11及以上
對相應的MenuItem調用getActionView()來獲得action view:
menu.findItem(R.id.action_search).getActionView()
要了解更多搜索widget的信息,見Creating a Search Interface

處理可收起的action view
爲節省action bar空間,可以把你的action view收起成一個action按鈕,收起後,系統可能會把它放進overflow,但是用戶選中它時,action view還是會照樣出現在action bar上。你可以通過在showAsAction屬性中添加"collapseActionView"來使你的action view能夠摺疊起來,如前面的XML所示。

因爲系統會在用戶選擇action時展開action view,所以你無需響應onOptionsItemSelected()回調。系統仍然會調用onOptionsItemSelected(),但如果你返回true(表明你已經處理了事件),action view就不會展開了。

系統也會在用戶按Up或Back按鈕時收起你的action view。

如需根據action view的可見性來更新你的activity,可以通過定義一個OnActionExpandListener並傳給setOnActionExpandListener()來接收展開收起時的回調。例子:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.options, menu);
    MenuItem menuItem = menu.findItem(R.id.actionItem);
    ...

    // When using the support library, the setOnActionExpandListener() method is
    // static and accepts the MenuItem object as an argument
    MenuItemCompat.setOnActionExpandListener(menuItem, new OnActionExpandListener() {
        @Override
        public boolean onMenuItemActionCollapse(MenuItem item) {
            // Do something when collapsed
            return true;  // Return true to collapse action view
        }

        @Override
        public boolean onMenuItemActionExpand(MenuItem item) {
            // Do something when expanded
            return true;  // Return true to expand action view
        }
    });
}

添加Action Provider

類似於action viewaction provider也是用自定義的佈局來替換action按鈕,和action view不同的是,action provider接管了該action的所有行爲,而且點擊時可以顯示一個子菜單。

要聲明action provider,給菜單<item>標籤的actionViewClass屬性中爲ActionProvider添加一個完整的類名。

你可以繼承ActionProvider類來創建自己的action provider,不過Android提供了一些預先建好的action provider,比如ShareActionProvider,它簡化了“分享”action,它會在action bar上展示一個所有可供直接分享的app列表(如圖6)。
Android開發指南:Action Bar - Pizzanicky - Pizzanicky的小窩
圖6. 一個帶有ShareActionProvider的action bar,顯示了分享目標

因爲每個ActionProvider都定義了自己的action行爲,所以無需在onOptionsItemSelected()方法中監聽action。當然,如果需要,你還是可以在該方法中監聽點擊事件,以便你可以同時執行別的action,但要一定要返回false,確保action provider還能接收到onPerformDefaultAction()回調來執行既定的action。

不過,如果action provider提供自己的action子菜單,用戶打開子菜單或選中子菜單項時你的activity是不會收到onOptionsItemSelected()調用的。

使用ShareActionProvider
要使用ShareActionProvider添加“分享”action,須用ShareActionProvider<item>標籤定義一個actionProviderClass屬性,例子:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:yourapp="http://schemas.android.com/apk/res-auto" >
    <item android:id="@+id/action_share"
          android:title="@string/share"
          yourapp:showAsAction="ifRoom"
          yourapp:actionProviderClass="android.support.v7.widget.ShareActionProvider"
          />
    ...
</menu>
這樣這個action provider就接管了這個action項,包括它的行爲和外觀,但是標題仍然是必須的,以供它在overflow中時使用。

然後唯一需要做的事就是定義用來分享的Intent,在你的onCreateOptionsMenu()中調用MenuItemCompat.getActionProvider(),傳入擁有action provider的MenuItem,然後在返回的ShareActionProvider對象上調用setShareIntent(),傳入一個附有相應內容的ACTION_SEND intent。

你應該在onCreateOptionsMenu()時調用setShareIntent()來初始化分享action,但由於用戶情境可能會改變,你必須在可分享內容變化時再次調用setShareIntent()來更新intent。

例子:
private ShareActionProvider mShareActionProvider;

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.main_activity_actions, menu);

    // Set up ShareActionProvider's default share intent
    MenuItem shareItem = menu.findItem(R.id.action_share);
    mShareActionProvider = (ShareActionProvider)
            MenuItemCompat.getActionProvider(shareItem);
    mShareActionProvider.setShareIntent(getDefaultIntent());

    return super.onCreateOptionsMenu(menu);
}

/** Defines a default (dummy) share intent to initialize the action provider.
  * However, as soon as the actual content to be used in the intent
  * is known or changes, you must update the share intent by again calling
  * mShareActionProvider.setShareIntent()
  */
private Intent getDefaultIntent() {
    Intent intent = new Intent(Intent.ACTION_SEND);
    intent.setType("image/*");
    return intent;
}
這樣ShareActionProvider就會處理該action項的所有用戶行爲,你也無需調用onOptionsItemSelected()處理點擊事件。

默認設置下,ShareActionProvider會根據用戶對分享目標的選擇頻度對各分享目標進行排序,最常使用的目標會出現在下拉菜單的頂端,而且會作爲默認分享目標直接顯示在action bar上。這個排序信息默認會存儲在一個私有文件中,文件名由DEFAULT_SHARE_HISTORY_FILE_NAME指定,如果你只爲一種action使用ShareActionProvider或它的一個子類,那你應該繼續使用默認的歷史文件,不用做任何處理。但如果要爲多個不同含義的action使用,那麼每個ShareActionProvider都應該指定自己的歷史文件,來維護自己的歷史記錄。要爲ShareActionProvider指定不同的歷史文件,要調用setShareHistoryFileName()並提供一個XML文件名(如,"custom_share_history.xml")。

注:儘管ShareActionProvider根據使用頻率來給分享目標排名,但這個行爲是可擴展的,而 ShareActionProvider的子類可以有不同的行爲和對歷史文件的排名方式。

創建自定義Action Provider
創建你自己的action provider允許你以獨立模塊的方式重用和管理動態action項的行爲,而不用在你的fragment或activity代碼中來處理action行爲。如前所述,Android已經爲分享action提供了一個ActionProvider的實現:ShareActionProvider

要爲其它的action創建你自己的action provider,只需繼承ActionProvider類並實現相應的回調方法,其中最主要的幾個:
此構造方法要傳入application的Context,你應該把它保存在成員field中,以便在其它回調方法中使用。
在這裏爲該action項定義action view,用構造方法中獲得的Context來實例化一個LayoutInflater,並從XML資源中加載你的action view佈局,然後再設置事件監聽器。例子:
public View onCreateActionView(MenuItem forItem) {
    // Inflate the action view to be shown on the action bar.
    LayoutInflater layoutInflater = LayoutInflater.from(mContext);
    View view = layoutInflater.inflate(R.layout.action_provider, null);
    ImageButton button = (ImageButton) view.findViewById(R.id.button);
    button.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            // Do something...
        }
    });
    return view;
}
onPerformDefaultAction()
當action overflow中的菜單項被選中時系統會調用此方法,action provider應該爲該菜單項執行一個默認action。
但是,如果你的action provider通過onPrepareSubMenu()提供子菜單,那麼即使action provider放置在overflow中,子菜單也一樣會彈出。這樣一來,有子菜單時onPerformDefaultAction()就不會調用。
注:實現onOptionsItemSelected()Activity或fragment可以通過處理選中事件(並返回true)來override  action provider的默認行爲(除非它使用了子菜單),這種情況下,系統不會調用onPerformDefaultAction()


添加導航tab

Android開發指南:Action Bar - Pizzanicky - Pizzanicky的小窩
 圖7. 寬屏上的action bar tab

Action bar上的tab使用戶更方便地在app中的不同視圖間瀏覽切換,ActionBar提供的tab可以理想地適應不同屏幕尺寸。比如,當屏幕足夠寬時,tab和action按鈕在action bar上一字排開(例如在平板上,如圖7),而當屏幕比較窄時,這些tab就會出現在一個單獨的bar(稱爲stacked action bar,如圖8)上。某些情況下,Android系統爲保證action bar的顯示效果,會把tab顯示爲一個下拉菜單。
Android開發指南:Action Bar - Pizzanicky - Pizzanicky的小窩
圖8. 窄屏上的tab

要使用tab,你的佈局必須有一個ViewGroup,用來放置和tab相關的Fragment。這個ViewGroup一定要有資源ID,這樣你才能在代碼中引用它來切換tab。如果tab的內容會充滿activity,那麼你的activity就完全不需要佈局了(你甚至不用調用setContentView()),你可以把fragment都放在默認的根視圖裏,根視圖可以用android.R.id.content這個ID來引用。

在確定了fragment都出現在佈局的哪些地方後,添加tab的其他基本步驟如下:
1. 實現ActionBar.TabListener接口,該接口提供tab事件的回調,比如用戶點擊其中一個時你可以實現切換。
2. 爲你要添加的每個tab,實例化一個ActionBar.Tab,並調用setTabListener()來設置ActionBar.TabListener,還須用setText()給tab設置標題(可選的,還可以用setIcon()設置圖標)。
3. 調用addTab()把tab添加到action bar。

注意,ActionBar.TabListener的回調方法不指定哪個fragment是和tab相關聯的,而只是哪個ActionBar.Tab被選中了。你必須自己定義ActionBar.Tab和其代表的Fragment之間的關係,根據你的設計不同,有好幾種方法可以定義這個關係。

例如,下面時一種可能的ActionBar.TabListener實現,每個tab都要其自己的listener實例:
public static class TabListener<T extends Fragment> implements ActionBar.TabListener {
    private Fragment mFragment;
    private final Activity mActivity;
    private final String mTag;
    private final Class<T> mClass;

    /** Constructor used each time a new tab is created.
      * @param activity  The host Activity, used to instantiate the fragment
      * @param tag  The identifier tag for the fragment
      * @param clz  The fragment's Class, used to instantiate the fragment
      */
    public TabListener(Activity activity, String tag, Class<T> clz) {
        mActivity = activity;
        mTag = tag;
        mClass = clz;
    }

    /* The following are each of the ActionBar.TabListener callbacks */

    public void onTabSelected(Tab tab, FragmentTransaction ft) {
        // Check if the fragment is already initialized
        if (mFragment == null) {
            // If not, instantiate and add it to the activity
            mFragment = Fragment.instantiate(mActivity, mClass.getName());
            ft.add(android.R.id.content, mFragment, mTag);
        } else {
            // If it exists, simply attach it in order to show it
            ft.attach(mFragment);
        }
    }

    public void onTabUnselected(Tab tab, FragmentTransaction ft) {
        if (mFragment != null) {
            // Detach the fragment, because another one is being attached
            ft.detach(mFragment);
        }
    }

    public void onTabReselected(Tab tab, FragmentTransaction ft) {
        // User selected the already selected tab. Usually do nothing.
    }
}
警告:一定不能在這三個回調中爲fragment轉換調用commit()——系統會自動爲你調用,如果你自己再調可能會拋 出異常,另外,也不能將這些fragment轉換加入back stack。

在上例中,當相應的tab被選中時,listener只是把fragment附到(attach())activity佈局中——或者如果fragment還沒有實例化,就創建一個fragment並添加(add())到佈局中(作爲android.R.id.content的子視圖)。當tab不被選擇時,則將fragment分離(detach())。

剩下的就是創建各ActionBar.Tab並將其加入ActionBar,另外,你必須調用setNavigationMode(NAVIGATION_MODE_TABS)使tab可見。

下面的代碼添加了兩個tab,它們都使用了上面定義的listener:
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // Notice that setContentView() is not used, because we use the root
    // android.R.id.content as the container for each fragment

    // setup action bar for tabs
    ActionBar actionBar = getSupportActionBar();
    actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
    actionBar.setDisplayShowTitleEnabled(false);

    Tab tab = actionBar.newTab()
                       .setText(R.string.artist)
                       .setTabListener(new TabListener<ArtistFragment>(
                               this, "artist", ArtistFragment.class));
    actionBar.addTab(tab);

    tab = actionBar.newTab()
                   .setText(R.string.album)
                   .setTabListener(new TabListener<AlbumFragment>(
                           this, "album", AlbumFragment.class));
    actionBar.addTab(tab);
}
如果activity進入了stop狀態,你應該用saved instance state來保持當前選中的tab,這樣用戶回來時就能恢復打開相應的tab。該保存狀態時,可以用getSelectedNavigationIndex()來獲知當前tab,該方法返回選中tab的索引。

警告:保存每個fragment的狀態至關重要,這樣用戶用tab切換fragment並返回到之前的fragment時,能看上去 和離開時一樣。有些狀態是默認會保存的,但是對於自定義view你可能就得手動保存。要了解關於保存fragment狀 態的信息,見Fragment API指南。

注:上述ActionBar.TabListener的實現只是若干種技術中的一種,另一個廣泛使用的方法是用ViewPager來管理 fragment,這樣用戶就可以用滑動手勢來切換tab,這種情況下,你只要在onTabSelected()回調中告訴 ViewPager當前的tab位置。要了解更多,見Creating Swipe Views with Tabs

添加下拉導航

作爲activity導航(或條件過濾)的另一種模式,action bar提供了內建的下拉菜單(也稱爲spinner),例如,下拉菜單能爲activity內容的排序提供不同的方式選擇。

下拉菜單適用於內容的改變比較重要但不需很頻繁的情況,對於內容切換比較頻繁的情況,則應該使用導航tab。
Android開發指南:Action Bar - Pizzanicky - Pizzanicky的小窩
 圖9. Action bar上的下拉導航菜單

啓用下拉導航的基本步驟:
1. 創建一個提供下拉菜單選項內容的SpinnerAdapter,和用來繪製每個菜單項的佈局文件。
2. 實現ActionBar.OnNavigationListener來定義用戶選中菜單項時的行爲。
3. 在activity的onCreate()方法中,調用setNavigationMode(NAVIGATION_MODE_LIST)來啓用action bar的下拉菜單。
4. 用setListNavigationCallbacks()指定下拉菜單的回調方法,例如:
actionBar.setListNavigationCallbacks(mSpinnerAdapter, mNavigationCallback);
這個方法需要SpinnerAdapterActionBar.OnNavigationListener作爲參數。

步驟雖不多,但實現SpinnerAdapterActionBar.OnNavigationListener是其中最花功夫的。有許多爲你的下拉導航實現功能的方法,而且實現各種SpinnerAdapter的方法超出了本文的範疇(參考SpinnerAdapter類瞭解更多信息),不過爲讓你儘快上手,這裏還是提供了一個SpinnerAdapterActionBar.OnNavigationListener的例子。

SpinnerAdapter和OnNavigationListener的例子
SpinnerAdapter是爲類似action bar中的下拉菜單這樣的spinner widget提供數據的adapter,它是個接口,你可以直接實現它,但Android已經提供了一些現成的有用實現供你繼承使用,比如ArrayAdapterSimpleCursorAdapter。下例是用ArrayAdapter的實現簡單地創建一個SpinnerAdapter,數據源是個字符串數組:
SpinnerAdapter mSpinnerAdapter = ArrayAdapter.createFromResource(this, R.array.action_list,
          android.R.layout.simple_spinner_dropdown_item);
createFromResource()方法需要3個參數:應用Context,字符串數組的資源ID,以及列表項的佈局文件。

資源文件中定義的字符串數組如下:
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string-array name="action_list">
        <item>Mercury</item>
        <item>Venus</item>
        <item>Earth</item>
    </string-array>
</pre>
createFromResource()返回的ArrayAdapter是可以傳入setListNavigationCallbacks()直接使用的(上面的第4步),儘管如此,在這之前你還是要先創建OnNavigationListener

你的ActionBar.OnNavigationListener實現是在用戶選中下拉列表項時用來處理fragment切換或其它針對activity的操作的,該listener中只有一個回調方法需要實現:onNavigationItemSelected()

onNavigationItemSelected()會收到list項在list中的位置,還有SpinnerAdapter提供的唯一ID。

下例實例化了一個OnNavigationListener的匿名實現,在一個id爲R.id.fragment_container的container中插入一個Fragment
mOnNavigationListener = new OnNavigationListener() {
  // Get the same strings provided for the drop-down's ArrayAdapter
  String[] strings = getResources().getStringArray(R.array.action_list);

  @Override
  public boolean onNavigationItemSelected(int position, long itemId) {
    // Create new fragment from our own Fragment class
    ListContentFragment newFragment = new ListContentFragment();
    FragmentTransaction ft = openFragmentTransaction();
    // Replace whatever is in the fragment container with this fragment
    //  and give the fragment a tag name equal to the string at the position selected
    ft.replace(R.id.fragment_container, newFragment, strings[position]);
    // Apply changes
    ft.commit();
    return true;
  }
};
這就成了個完整的OnNavigationListener,你可以調用setListNavigationCallbacks()(步驟4),傳入ArrayAdapter和這個OnNavigationListener

本例中,用戶選中下拉菜單項時,佈局中就會加入一個fragment(替換R.id.fragment_container視圖中的當前fragment)。加入時fragment會帶有一個tag字符串,也就是對應的下拉菜單項的字符串。

下面是本例中定義各fragment的ListContentFragment類:
public class ListContentFragment extends Fragment {
    private String mText;

    @Override
    public void onAttach(Activity activity) {
      // This is the first callback received; here we can set the text for
      // the fragment as defined by the tag specified during the fragment transaction
      super.onAttach(activity);
      mText = getTag();
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        // This is called to define the layout for the fragment;
        // we just create a TextView and set its text to be the fragment tag
        TextView text = new TextView(getActivity());
        text.setText(mText);
        return text;
    }
}

Action Bar的風格化
如果想實現能代表品牌風格的視覺設計,action bar允許你對其每個細節進行定製,包括action bar顏色,文字顏色,按鈕風格等等。你須使用Android的style and theme框架來定義自己的風格屬性。
注意:提供背景圖資源時,確保使用Nine-Patch drawables來保證拉伸時的效果,nine-patch圖片應小於40dp 高,30dp寬。

總體外觀
指定爲action bar定義各種風格的風格資源文件
默認爲Widget.AppCompat.ActionBar,你的自定義風格應該以它爲父風格
支持的各種風格如下:
定義action bar背景的圖片資源
定義stacked action bar(tab標籤)的圖片資源
定義分離式action bar的圖片資源
爲action按鈕定義風格資源
默認爲Widget.AppCompat.ActionButton你的自定義風格應該以它爲父風格
定義overflow action項的風格資源
默認爲Widget.AppCompat.ActionButton.Overflow你的自定義風格應該以它爲父風格
定義一個或多個action bar顯示選項,比如是否使用app logo,是否顯示activity標題,以及是否使用Up  action。displayOptions中列出了所有可以使用的值。
定義action項直接的分隔圖片資源
爲action bar標題定義風格資源
默認爲TextAppearance.AppCompat.Widget.ActionBar.Title你的自定義風格應該以它爲父風格

聲明action bar是覆蓋activity佈局還是將activity佈局下移(例如,Gallery app使用了覆蓋模式)。默認值 false
通常,action bar需要有自己的屏幕空間,activity的佈局則填滿剩餘的空間。Action bar爲覆蓋模式時,activity 佈局會使用所有的屏幕空間,系統會把action bar繪製在其上,如果你想讓自己的內容在action bar顯示和隱藏時保 持同樣的大小和位置,那麼覆蓋模式會很有用。你也可能單純爲其視覺效果而使用它,因爲你可以給action bar一個 半透明的背景,這樣用戶透過action bar也可以看到下面的內容。
注:Holo類主題默認使用半透明背景的action bar,不過也可以修改爲你自己的風格,另外不同設備上的 DeviceDefault主題也可能會默認使用不透明的背景。

開啓覆蓋模式時,你的activity佈局並不知道action bar位於其上,因此在action bar覆蓋區域不能放置重要信息或 UI組件,要想合理適配,你可以引用actionBarSize的系統值來決定視圖上方應該留出多少空間,例如:
<SomeView
    ...
    android:layout_marginTop="?android:attr/actionBarSize" />
也可以在運行時用getHeight()獲取action bar的高度,這個方法返回調用時的action bar高度,在activity生命周 期的早期方法中調用時返回的高度可能不包括stacked action bar(由於navigation tabs)。要得到運行時action  bar包括stacked action bar的總高度,見例子app Honeycomb Gallery中的TitlesFragment

Action項

定義action按鈕的風格資源
默認爲Widget.AppCompat.ActionButton你的自定義風格應該以它爲父風格
定義每個action項的背景資源drawable,應爲一個可以指示不同選擇狀態的state-list drawable
定義每個overflow項的背景,應爲一個可以指示不同選擇狀態的state-list drawable
定義action項之間分隔的drawable資源
定義action項文字的顏色
定義action項文字的外觀風格資源
定義action view widgets的主題資源

Navigation tabs

定義action bar的tab標籤的風格資源
默認爲Widget.AppCompat.ActionBar.TabView你的自定義風格應該以它爲父風格
定義navigation tab下方細條的風格資源
默認爲Widget.AppCompat.ActionBar.TabBar你的自定義風格應該以它爲父風格
定義navigation tab中的文字風格資源
默認爲Widget.AppCompat.ActionBar.TabText你的自定義風格應該以它爲父風格

下拉列表

定義下拉導航的風格(如背景和文字風格
默認爲Widget.AppCompat.Spinner.DropDown.ActionBar你的自定義風格應該以它爲父風格

Theme例子

這是個爲activity定義自定義主題的例子,CustomActivityTheme,包含了幾個用來自定義action bar的風格。

注意,每個action bar風格有兩個版本,第一個屬性名前包含了android:前綴,以支持framework中已包含這些屬性的API level 11及以上版本;第二個版本沒有android:前綴,針對使用support library中風格屬性的老版本,效果是一樣的。
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- the theme applied to the application or activity -->
    <style name="CustomActionBarTheme"
           parent="@style/Theme.AppCompat.Light">
        <item name="android:actionBarStyle">@style/MyActionBar</item>
        <item name="android:actionBarTabTextStyle">@style/TabTextStyle</item>
        <item name="android:actionMenuTextColor">@color/actionbar_text</item>

        <!-- Support library compatibility -->
        <item name="actionBarStyle">@style/MyActionBar</item>
        <item name="actionBarTabTextStyle">@style/TabTextStyle</item>
        <item name="actionMenuTextColor">@color/actionbar_text</item>
    </style>

    <!-- general styles for the action bar -->
    <style name="MyActionBar"
           parent="@style/Widget.AppCompat.ActionBar">
        <item name="android:titleTextStyle">@style/TitleTextStyle</item>
        <item name="android:background">@drawable/actionbar_background</item>
        <item name="android:backgroundStacked">@drawable/actionbar_background</item>
        <item name="android:backgroundSplit">@drawable/actionbar_background</item>

        <!-- Support library compatibility -->
        <item name="titleTextStyle">@style/TitleTextStyle</item>
        <item name="background">@drawable/actionbar_background</item>
        <item name="backgroundStacked">@drawable/actionbar_background</item>
        <item name="backgroundSplit">@drawable/actionbar_background</item>
    </style>

    <!-- action bar title text -->
    <style name="TitleTextStyle"
           parent="@style/TextAppearance.AppCompat.Widget.ActionBar.Title">
        <item name="android:textColor">@color/actionbar_text</item>
    </style>

    <!-- action bar tab text -->
    <style name="TabTextStyle"
           parent="@style/Widget.AppCompat.ActionBar.TabText">
        <item name="android:textColor">@color/actionbar_text</item>
    </style>
</resources>
在manifest文件中,可以爲整個app指定theme:
<application android:theme="@style/CustomActionBarTheme" ... />
或者爲單個activity指定:
<activity android:theme="@style/CustomActionBarTheme" ... />
注意:確保每個theme和風格在 <style> 標籤中聲明其父theme,這樣它們會從父theme中繼承所有未顯式聲明風格。修改action bar時,使用父theme很重要,這樣你可以不用重新實現所有的風格屬性(比如文字大小或action項的間距),只需override你要改變的風格。

更多關於app中風格和theme資源的信息,請閱讀Styles and Themes

原文:Action Bar
發佈了19 篇原創文章 · 獲贊 3 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章