使用 Android Navigation Drawer 製作側選單 (1)

Navigation Drawer

Navigation Drawer "導覽抽屜",啊~就是隱藏式側選單啦。官方說法是,一個從螢幕左邊轉換而來的面板,用來顯示 App 主要的導覽選項。
顯示 navigation drawer 有兩種方法,從螢幕左邊往右滑,或觸碰 ActionBar 上的 app icon。navigation drawer 會從 ActionBar 下方展開,蓋在原本畫面之上,如下圖: 圖片來源:
http://developer.android.com/design/media/navigation_drawer_overview.png

要關閉 navigation drawer,有以下幾種方式:
  • 觸碰 navigation drawer 之外的畫面
  • 在螢幕上往左滑
  • 觸碰 ActionBar 的 app icon
  • 按下 Back 按鈕

關於 Navigation Drawer

使用 Navigation Drawer 的時機

1. 超過 3 個頂層(top-level)的畫面
也就是你有超過 3 個主要功能,它們都處於同一階層的時候才用。
2. 在較低階層時橫跨導覽
例如:你從A頁面一路進入到A1 -> A2 -> A3,接著你想從 A3 直接跳到 D1。 圖片來源:
http://developer.android.com/design/media/navigation_drawer_cross_nav.png
3. 深度的導覽分支
例如:你從A頁面一路進入到A1 -> A2 -> A3,接著你想從 A3 直接跳回到最上層的 A。 圖片來源:
http://developer.android.com/design/media/navigation_drawer_quick_to_top.png

做為導覽樞紐

Navigation Drawer 直接反射了你 App 的結構,可以讓使用者更容易以 navigation drawer 為樞紐,在各階層的畫面中快速的切換。

Navigation Drawer 的內容

Navigation Drawer 以清單方式呈現,可以有 標題、圖示及計數器,也可以合併相關的項目,成為一個下拉式選單。

Navigation Drawer 和 ActionBar

為了避免使用者產生迷惑,原本在 ActionBar 上顯示的該頁標題及 Action 選單按鈕,應該在使用者開啟 navigation drawer 時改變為 App 名稱及 Action overflow,而 overflow 中只顯示「設定」或「幫助」等項目。

Style

官方文件中對 Navigation Drawer 的風格有些標準的設計規則。例如:Navigation Drawer 的寬度應該在 240 dp 到 320 dp 之間,每個項目的高度不小於 48 dp。

建立 Navigation Drawer

Layout

要建立 Navigation Drawer 必須使用 Support Library 中的 DrawerLayout,以 DrawerLayout 物件當作根畫面(root view)的 layout,在其中包含一個用來顯示主要內容的 view,而另外一個 view 則是包含 navigation drawer 的內容。所以 layout 會是這樣(drawer.xml):
<android.support.v4.widget.DrawerLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <!-- The main content view -->
    <FrameLayout
        android:id="@+id/content_frame"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
    <!-- The navigation drawer -->
    <ListView android:id="@+id/left_drawer"
        android:layout_width="240dp"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        android:choiceMode="singleChoice"
        android:divider="@android:color/transparent"
        android:dividerHeight="0dp"
        android:background="#111"/>
</android.support.v4.widget.DrawerLayout>
這裡有幾點必須注意的重點:
  • 用來顯示主要內容的 view (即 FrameLayout) ,必須排在第一個,這是因為 XML 排序的關係,如此一來,之後的 navigation drawer 才會顯示在其上,蓋住它。
  • 用來顯示主要內容的 view 的寬及高必須和父層的 view 一樣(即 match_parent)。
  • drawer view (即 ListView) 必須指定 horizontal gravity (即 android:layout_gravity屬性),其值為 "start",不要用 "left",這樣在由右到左(RTL)的語言中,選單就會從右邊出現。
  • drawer view 的寬度以 dp 為單位,高度則符合父層的 view。寬度不能超過 320 dp。

Activity

有點要注意的地方,這裡為了舉例方便,範例使用的 SDK 最小版本為 11,這是為了可以直接取用 ActionBar ,如果要在更舊的版本中執行,必須搭配 ActionBarCompat 來使用,使用方式請參考 [Android]使用 ActionBarCompat 製作導覽列(1)

首先讓 Drawer 能出現,程式碼如下:
MainActivity.java
public class MainActivity extends Activity ActionBarActivity {

    private DrawerLayout layDrawer;
    private ListView lstDrawer;

    private ActionBarDrawerToggle drawerToggle;
    private CharSequence mDrawerTitle;
    private CharSequence mTitle;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        initActionBar();
        initDrawer();
        initDrawerList();
    }

    private void initActionBar(){
        getActionBar().setDisplayHomeAsUpEnabled(true);
        getActionBar().setHomeButtonEnabled(true);
    }

    private void initDrawer(){
        setContentView(R.layout.drawer);

        layDrawer = (DrawerLayout) findViewById(R.id.drawer_layout);
        lstDrawer = (ListView) findViewById(R.id.left_drawer);

        layDrawer.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START);

        mTitle = mDrawerTitle = getTitle();
        drawerToggle = new ActionBarDrawerToggle(
                this, 
                layDrawer,
                R.drawable.ic_drawer, 
                R.string.drawer_open,
                R.string.drawer_close) {

            @Override
            public void onDrawerClosed(View view) {
                super.onDrawerClosed(view);
                getActionBar().setTitle(mTitle);
            }

            @Override
            public void onDrawerOpened(View drawerView) {
                super.onDrawerOpened(drawerView);
                getActionBar().setTitle(mDrawerTitle);
            }
        };
        drawerToggle.syncState();

        layDrawer.setDrawerListener(drawerToggle);
    }

    private void initDrawerList(){
        String[] drawer_menu = this.getResources().getStringArray(R.array.drawer_menu);
        ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, R.layout.drawer_list_item, drawer_menu);
        lstDrawer.setAdapter(adapter);
    }
}
執行畫面: 說明:
initActionBar() 中,讓 ActionBar 的返回箭號顯現,並且讓 App icon 可以被點選。

接著 initDrawer(),讓側選單能夠出現。先將 layout 指定為我們建立的 drawer.xml 。layDrawer.setDrawerShadow() 用來設定側選單被開啟時的陰影。

建立側選單觸發器, 其中會將 DrawerLayout(layDrawer) 及 R.drawable.ic_drawer (側選單的三條線圖示) 等等參數指定給 ActionBarDrawerToggle 。然後要覆寫兩個方法,分別是開啟側選單 onDrawerOpened 及關閉側選單 onDrawerClosed。通常會在其中更新 ActionBar 的標題,或是顯示相關的 Action button。

最後,記得呼叫 drawerToggle.syncState(); 讓 ActionBar 中的返回箭號置換成 Drawer 的三條線圖示。並且把這個觸發器指定給 layDrawer 。
到目前為止就能執行了,可以從左邊往右滑出選單,但選單是空的。

在 initDrawerList() 中,就只簡單的把字串陣列建立一個 adapter 送給 lstDrawer 來顯示。這裡要在資源檔中加入一些資料,打開 res/values/strings.xml ,然後輸入以下文字:
<string name="drawer_open">Open navigation drawer</string>
<string name="drawer_close">Close navigation drawer</string>

<string-array name="drawer_menu">
    <item>Apple</item>
    <item>Book</item>
    <item>Cat</item>
    <item>Dog</item>
    <item>Eagle</item>
    <item>Food</item>
    <item>God</item>
    <item>House</item>
    <item>Iron</item>
    <item>Jingle</item>
</string-array>
還要建立一個給 list item 用的 layout,drawer_list_item.xml:
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/txtItem"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_alignParentLeft="true"
    android:layout_alignParentTop="true"
    android:padding="10dp"
    android:text="Item"
    android:textAppearance="?android:attr/textAppearanceMedium"
    android:textColor="#cccccc" />
單單一個 TextView 而以。
如此,一個基本的側選單的就製完成了。

續:使用 Android Navigation Drawer 製作側選單(2)
本文網址:http://blog.tonycube.com/2014/02/android-navigation-drawer-1.html
Tony Blog 撰寫,請勿全文複製,轉載時請註明出處及連結,謝謝 😀

16 則留言

  1. (留這系統有問題,以下為手動還原)
    =================================================

    (Flying Nick)

    感謝版主的中文說明,很實用,謝謝!

    回覆刪除
  2. 版主你好,執行時getActionBar().setDisplayHomeAsUpEnabled(true);此行出現錯誤訊息,可以請問一下如何解決嗎?

    以下錯誤訊息:
    java.lang.NullPointerException: Attempt to invoke virtual method 'void android.app.ActionBar.setDisplayHomeAsUpEnabled(boolean)' on a null object reference

    回覆刪除
    回覆
    1. Android 最近這幾版的改版中對 ActionBar 做了一些調整,
      所以要先確定你是在哪個版本上使用 ActionBar,
      你可以參考這篇(http://blog.tonycube.com/2015/06/android-navigation-drawer-toolbar.html),
      因為 Android 5.x 之後已經改用 Toolbar 了。

      刪除
  3. 版主你好,我再匯入android-support-v7-appcompat 後
    我專案的r檔就會消失,請問該如何解決
    我專案是剛開起來,只會加了一個toolbar.xml
    其他程式碼都還沒加,就出錯了

    回覆刪除
    回覆
    1. 通常是版本不對或衝突,你的 v7 和 v4 版本對不上,
      你可以去 SDK 目錄下 extras -> android -> support,
      在v4目錄下只會有 v4 的 support 檔,不要用這個,
      去 v7 的 appcompat/libs 目錄下,應該會同時有 v4及v7 兩個檔,
      把專案中的換成這同一組檔案,應該能解決。

      刪除
  4. 作者已經移除這則留言。

    回覆刪除
    回覆
    1. 注意你使用的(import)是哪個版本的Activity,用到不同API版本的類別,會無法呼叫。
      layDrawer 問題也相同,注意 API 版本,是否有用到 support lirbary 的類別。

      刪除
  5. 這邊顯示紅字
    layDrawer.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START); 裡面的 drawer_shadow顯示紅字
    &&
    R.drawable.ic_drawer,
    裡面的 ic_drawer 顯示紅字
    我嘗試按ALT+Enter開啟新的ic_drawer.xml && drawer_shadow.xml
    都無濟與事
    請問該怎麼解決好
    我的Android Studio版本是 2.1.1

    回覆刪除
    回覆
    1. 看看你的 import 是不是你自己的 package名稱.R
      錯誤應該是 import 了 android.R

      刪除
    2. 在續篇裡面有範例下載 https://github.com/tony915/NavigationDrawerDemo
      在 res/drawable-xxxx 4個目錄裡面有 ic_drawer.png ,你可能少了這個圖檔,
      drawer_shadow 也是一樣,它是陰影。前面那位朋友應該也是一樣問題。

      刪除
  6. 我是遇到↓這個問題,想請問一下這是為什麼?


    java.lang.RuntimeException: Unable to start activity ComponentInfo{com.tonycube.demo.navigationdrawerdemo/com.tonycube.demo.navigationdrawerdemo.MainActivity}: java.lang.IllegalArgumentException: AppCompat does not support the current theme features
    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2693)
    at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2758)
    at android.app.ActivityThread.access$900(ActivityThread.java:177)
    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1448)
    at android.os.Handler.dispatchMessage(Handler.java:102)
    at android.os.Looper.loop(Looper.java:145)
    at android.app.ActivityThread.main(ActivityThread.java:5942)
    at java.lang.reflect.Method.invoke(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:372)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1400)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1195)
    Caused by: java.lang.IllegalArgumentException: AppCompat does not support the current theme features
    at android.support.v7.app.AppCompatDelegateImplV7.ensureSubDecor(AppCompatDelegateImplV7.java:363)
    at android.support.v7.app.AppCompatDelegateImplV7.setContentView(AppCompatDelegateImplV7.java:246)
    at android.support.v7.app.AppCompatActivity.setContentView(AppCompatActivity.java:106)
    at com.tonycube.demo.navigationdrawerdemo.MainActivity.onCreate(MainActivity.java:36)
    at android.app.Activity.performCreate(Activity.java:6289)
    at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1119)
    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2646)
    at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2758) 
    at android.app.ActivityThread.access$900(ActivityThread.java:177) 
    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1448) 
    at android.os.Handler.dispatchMessage(Handler.java:102) 
    at android.os.Looper.loop(Looper.java:145) 
    at android.app.ActivityThread.main(ActivityThread.java:5942) 
    at java.lang.reflect.Method.invoke(Native Method) 
    at java.lang.reflect.Method.invoke(Method.java:372) 
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1400) 
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1195) 

    回覆刪除
    回覆
    1. 看一下 build.gradle 中的 dependencies 裡面,
      com.android.support:appcompat-v7:
      com.android.support:design:
      com.android.support:support-v4:
      是不是都有使用最新版本,SDK 記得要先更新。

      或是調整 theme.xml 裡的設定,
      這個問題是 theme 用了這個 API 等級沒有的功能。

      刪除
  7. 版主你好,我是這邊drawerToggle = new ActionBarDrawerToggle(
    this,
    layDrawer,
    R.drawable.ic_drawer,
    R.string.drawer_open,
    R.string.drawer_close)
    參數那幾行出現紅字,然後下面跳出訊息說ActionBarDrawerToggle() in ActionBarDrawerToggle cannot be applied to:
    Expected Parameters:
    Actual Arguments:

    activity:
    Activity
    this  
    drawerLayout:
    DrawerLayout
    layDrawer  
    toolbar:
    android.support.v7.widget.Toolbar
    R.drawable.ic_drawer  (int)
    openDrawerContentDescRes:
    int
    R.string.drawer_open  
    closeDrawerContentDescRes:
    int
    R.string.drawer_close
    toobar那裡顯示的參數是紅色的,好像是第三個參數有問題,想請問該如何解決?

    回覆刪除
    回覆
    1. 是參數問題,但你貼的內容我看不出原因,
      可能要檢查其他地方的程式碼,例如你的 Activity 是不是 extends ActionBarActivity
      我貼的程式碼好像有問題,
      你可以直接下載前一篇的範例 https://github.com/tony915/NavigationDrawerDemo
      這是舊 Eclipse 專案,AS 要用匯入 Eclipse 專案的方式。

      刪除

留言小提醒:
1.回覆時間通常在晚上,如果太忙可能要等幾天。
2.請先瀏覽一下其他人的留言,也許有人問過同樣的問題。
3.程式碼請先將它編碼後再貼上。(線上編碼:http://bit.ly/1DL6yog)
4.文字請加上標點符號及斷行,難以閱讀者恕難回覆。
5.感謝您的留言,您的問題也可能幫助到其他有相同問題的人。