Material Design Patterns 教學 (1) - Navigation Drawer

想有漂亮的 Material Design,其實 Google 已提供 Android Design Support Library 可供使用。它支援 Android 2.1 或以上,提供不少好用的 UI element,可方便做到 Material Design Pattern 的效果。我們在此逐一介紹 (可以一段時間不用再煩惱寫什麼,Yeah! 用 code 代替一部份內容,寫少很多,Yeah Yeah!)。

安裝

Android Design Support Library 可通過 Gradle 來安裝:

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:22.2.1'
    compile 'com.android.support:design:22.2.1'
}

Navigation Drawer,通常是從左邊邊緣拉出來的一個導覽選單,是一個很常用到的 UI 元件。

![Navigation Drawer](/content/images/2015/08/patterns_navdrawer_selection2.png)
Navigation Drawer (source: Material Design Pattern)

很多 open source library 都在做這東西,不過既然 Google 提供,我們暫時又沒有等別的要求,便用它的吧。用 Design Library 來做到此效果,需用到 DrawerLayoutNavgationViewDrawerLayout 用來做從左到右拉出來的抽屜效果,NavigationView 用來在拉出來的畫面上顯示用戶資料和導覽選單。

準備

在 Android Studio 新增一個 project,為它加入 MainActivity。開啟 AndroidManifest.xml 確認使用 android:theme@style/AppTheme。再開啟 res/values/style.xml,將 AppTheme 的 parent 改為 Theme.AppCompat.Light.NoActionBar,並加入以下 items。這是因為我們之後會用 ToolBar 來做 ActionBar

<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
    <item name="colorPrimary">#212121</item>
    <item name="colorPrimaryDark">#187817</item>
    <item name="android:windowActionBar">false</item>
    <item name="android:windowActionModeOverlay">true</item>
    <item name="android:windowDrawsSystemBarBackgrounds">true</item>
    <item name="android:statusBarColor">@android:color/transparent</item>
</style>

設定 layout

然後設定 layout。DrawerLayout 的用法很簡單,它裏面只能包著兩個 view ,第一個是主要內容的 view ,第二個是拉出來的導覽菜單 view。最基本的設定如下:

<android.support.v4.widget.DrawerLayout
            xmlns:android="http://schemas.android.com/apk/res/android"
            xmlns:app="http://schemas.android.com/apk/res-auto"
            android:layout_width="match_parent"
            android:layout_height="match_parent" >

    <!-- the content layout -->
    <LinearLayout
            android:id="main_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            >
    </LinearLayout>
   
    <!-- the drawer layout -->
    <LinearLayout
            android:id="drawer_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            >
    </LinearLayout>
</android.support.v4.widget.DrawerLayout>

我們會以 LinearLayout 加上 ToolbarTextView 來作為主要內容。至於導覽菜單,你可以用 LinearLayout 來自行設計一個漂亮的外觀,也可以只用一個 ListView 來顯示。我們這裏自然用 NavigationView

NavigationView 分兩上下兩部份,上面的是 headerLayout,可以自設任何內容。下面的為 menu 部份,會載入 res/menu 下的 menu。經它點選了的 Menu item 會自動被 highlight,不用自己寫 code 記著,比起用 ListView 等無異更方便。

最後的 activity_main.xml 如下:

<android.support.v4.widget.DrawerLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">

    <!-- your content layout -->
    <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            >
            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="@dimen/abc_action_bar_default_height_material"
                android:background="?attr/colorPrimary"
                android:minHeight="?attr/actionBarSize"
                android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" />

            <TextView
                android:id="@+id/content_view"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Hello World" />
    </LinearLayout>

    <android.support.design.widget.NavigationView
            android:id="@+id/navigation_view"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_gravity="start"
            app:headerLayout="@layout/drawer_header"
            app:menu="@menu/drawer" />
</android.support.v4.widget.DrawerLayout>

NavigationView 中的 headerLayout 指向 res/layout/drawer_header.xmlapp:menu 是將會載入的 menu item。

headerLayout

你可自行為它加入背景圖片或個人頭像,弄得跟 Gmail 的一樣也可。為求簡單,我們只用 TextView 加上背景色。

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="190dp"
    android:background="#0097a7"
    >
    <TextView
            android:id="@+id/name"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="24dp"
            android:layout_marginStart="24dp"
            android:layout_alignParentBottom="true"
            android:text="Eddard Stark"
            android:textSize="14sp"
            android:textColor="#fff"
            android:textStyle="bold"
            android:paddingBottom="8dp"
            />
</RelativeLayout>
![Header Layout](/content/images/2015/08/headerLayout.jpg)
簡單就是美,加個背景圖的話會更好吧

也算不錯啦。

菜單是簡單的 menuItem,放在 /res/menu/drawer.xml:

<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <group android:checkableBehavior="single">
            <item
                android:id="@+id/navigation_item_1"
                android:checked="true"
                android:icon="@drawable/ic_launcher"
                android:title="Navigation Items 1"/>
            <item
                android:id="@+id/navigation_item_2"
                android:icon="@drawable/ic_launcher"
                android:title="Navigation Items 2"/>
            <item
                android:id="@+id/navigation_item_3"
                android:icon="@drawable/ic_launcher"
                android:title="Navigation Items 3"/>
    </group>
</menu>

groupcheckableBehavior 設為 single,代表只會 highlight 一個 menuItem

主菜

因為不是使用 onCreateOptionsMenu() 來載入導覽菜單,所以不能用 onOptionsItemSelected() 來設定反應,要加上 OnNavigationItemSelectedListener 來操作。在 onCreate() 中加入以下 coding:

final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);

contentView = (TextView) findViewById(R.id.content_view);
drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
NavigationView view = (NavigationView) findViewById(R.id.navigation_view);
view.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() {
    @Override public boolean onNavigationItemSelected(MenuItem menuItem) {
        Toast.makeText(MainActivity.this, menuItem.getTitle() + " pressed", Toast.LENGTH_LONG).show();
        contentView.setText(menuItem.getTitle());
      
        menuItem.setChecked(true);
        drawerLayout.closeDrawers();
        return true;
    }
});

我們將 toolbar 設成 ActionBar。並為 NavigationView 加上 OnNavigationItemSelectedListener,這樣按 menuItem 的話,便會顯示一個 toast,和將 contentView 的文字設為 menuItem 的 title。

執行來試一試,可看到 drawerLayout 可拉出來,按 menu 也會有反應。

但平時拉出 drawerLayout 時看到三條線變箭嘴 icon 的效果要怎麼做呢?

加上「三」圖示

這個便要用 actionBarDrawerToggle 來做了。先到 onCreate() 中將 ToolBar設為 ActionBar

      final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
      setSupportActionBar(toolbar);

然後加上 actionBarDrawerToggle :

      ActionBarDrawerToggle actionBarDrawerToggle = new ActionBarDrawerToggle( this, drawerLayout, toolbar, R.string.openDrawer , R.string.closeDrawer){
           @Override
           public void onDrawerClosed(View drawerView) {
                super .onDrawerClosed(drawerView);
           }

           @Override
           public void onDrawerOpened(View drawerView) {
                super .onDrawerOpened(drawerView);
           }
      };

      drawerLayout.setDrawerListener(actionBarDrawerToggle);
      actionBarDrawerToggle.syncState();

這樣便完成了。

Bonus - Configuration Change

有沒有發覺旋轉螢幕後 menu 和 contentView 會被重設?這是因為旋轉螢幕等於 Configuration Change,Configuration Change 後 Activity 會被消滅然後重生,等於執行了 onDestory() 後再 onCreate()。除非有特別處理,否則所有 variable 都會被 reset。

要如何記著我剛才按了的第三個 menu ?這便要用 onSaveInstanceState()

首先在 MainActivity 加一個 member variable 用來儲存點擊了的 menuItem id,

private int navItemId;

然後將之前 OnNavigationItemSelectedListener 中更新畫面的工作搬到一個新 method 中。因為除了 OnNavigationItemSelectedListener 會執行它外,之後 onCreate() 也會執行,所以將此 code 放在一個 method 裏會方便一點。

private void navigateTo(MenuItem menuItem){
    contentView.setText(menuItem.getTitle());

    navItemId = menuItem.getItemId();
    menuItem.setChecked(true);
}

注意這裏會用 navItemId 記著當前的 menuItem

之後再用 onSaveInstanceState() 來記著當前的 navItemId

private static final String NAV_ITEM_ID = "nav_index";

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putInt(NAV_ITEM_ID, navItemId);
}

最後在 onCreate() 讀取便可

if(null != savedInstanceState){
    navItemId = savedInstanceState.getInt(NAV_ITEM_ID, R.id.navigation_item_1);
}
else{
    navItemId = R.id.navigation_item_1;
}

navigateTo(view.getMenu().findItem(navItemId));

這樣就算旋轉螢幕多少次也沒有問題。


結語

以下的程式碼已放上 Github:

https://github.com/goofyz/android-material-design-tutorial/tree/part1_navigation_view

Navigation Drawer 通常是用作 top level navigation 的,別只為漂亮而加進你的 app 中,要想清楚 User Experience 才好。

下次我們講講 Floating Action Button

相關連結