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

Navigation Drawer

本篇接續:使用 Android Navigation Drawer 製作側選單(1)
前面我們做到從螢幕左邊往右滑來開啟側選單,但是觸碰 ActionBar 的 App icon 時卻沒有任何反應,現在就來處理 App icon 的動作。很簡單,覆寫 onOptionsItemSelected 方法即可:
public boolean onOptionsItemSelected(MenuItem item) {

    //home
    if (drawerToggle.onOptionsItemSelected(item)) {
        return true;
    }

    return super.onOptionsItemSelected(item);
}
這樣當你觸碰 App icon 時就可以開關 drawer 了。

Action Buttons 事件

onOptionsItemSelected 這個方法同時也可以處理 Action button 的事件。首先,加入 action button 要用到的字串資源 strings.xml:
<string name="action_refresh">重新整理</string>
<string name="action_edit">編輯</string>
<string name="action_search">搜尋</string>
<string name="action_info">軟體資訊</string>
接著建立 res/menu/main.xml:
<menu xmlns:android="http://schemas.android.com/apk/res/android" >

    <item
        android:id="@+id/action_refresh"
        android:orderInCategory="1"
        android:showAsAction="ifRoom|withText"
        android:icon="@drawable/ic_launcher"
        android:title="@string/action_refresh"/>
    <item
        android:id="@+id/action_edit"
        android:orderInCategory="2"
        android:showAsAction="ifRoom|withText"
        android:title="@string/action_edit"/>
    <item
        android:id="@+id/action_info"
        android:orderInCategory="4"
        android:showAsAction="ifRoom|withText"
        android:title="@string/action_info"/>
    <item
        android:id="@+id/action_search"
        android:orderInCategory="3"
        android:showAsAction="ifRoom|withText"
        android:title="@string/action_search"/>

</menu>
參數說明:
  • android:id:在 Activity 中可以用來判斷是否被點選。
  • android:orderInCategory:選單的順序。若省略,會依 xml 中的順序呈現。
  • android:showAsAction:在另一篇中有選項的說明,這裡我們讓它"位置夠用才顯示 | 同時顯示文字"。
  • android:icon:圖示。
  • android:title:選單的文字。
這裡我故意用到 4 個選單那麼多,並且第 3 個及第 4 個故意順序對換,但以 orderInCategory 來設定順序;來看看直式(空間不夠),及橫式(空間足夠)時,Action Button 的不同顯示方式。

Portrait (直式)

Landscape (橫式)

空間不夠時,只會顯示 icon (若有),並且擠不下的會在 overflow (3個點)裡出現。空間夠用當然就全部顯示。

最後在 Activity 中覆寫 onCreateOptionsMenu:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.main, menu);
    return true;
}
就能讓 Action button 出現了。點選後的事件在 onOptionsItemSelected 中處理,在剛才的程式碼中加入 switch 的部份:
@Override
public boolean onOptionsItemSelected(MenuItem item) {

    //home
    if (drawerToggle.onOptionsItemSelected(item)) {
        return true;
    }

    //action buttons
    switch (item.getItemId()) {
    case R.id.action_edit:
        //....
        break;

    case R.id.action_search:
        //....
        break;

    default:
        break;
    }

    return super.onOptionsItemSelected(item);
}

Drawer item 點選事件

最後,我們要讓側選單的項目被點選時,載入對應的內容,即 Fragment。這邊示範 3 個 Fragment,程式碼大同小異,其中一個示範如何接受傳過來的值。

FragmentApple.java

public class FragmentApple extends Fragment {

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return initView(inflater, container);
    }

    private View initView(LayoutInflater inflater, ViewGroup container) {
        View view = inflater.inflate(R.layout.fragment_apple, container, false);

        return view;
    }

}

fragment_apple.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <TextView
        android:id="@+id/txtApple"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"
        android:text="Apple"
        android:textAppearance="?android:attr/textAppearanceLarge" />

</RelativeLayout>
簡單的顯示一個文字欄位。

和 FragmentBook.java 一樣,但 FragmentCat.java 多了接值的部份:
public class FragmentCat extends Fragment {

    public static final String CAT_COLOR = "cat_color";

    private String color = "";

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

        color = this.getArguments().getString(CAT_COLOR);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return initView(inflater, container);
    }

    private View initView(LayoutInflater inflater, ViewGroup container) {
        View view = inflater.inflate(R.layout.fragment_cat, container, false);

        TextView txtCat = (TextView) view.findViewById(R.id.txtCat);
        String colorCat = color + " " + txtCat.getText().toString();
        txtCat.setText(colorCat);

        return view;
    }

}
呼叫 getArguments() 來接值參數,然後 getXXX() 來取得該值。

MainActivity.java

回到 MainActivity 中,建立一個當側選單項目被點選時要做什麼的方法 selectItem():
private void selectItem(int position) {
    Fragment fragment = null;

    switch (position) {
    case 0:
        fragment = new FragmentApple();
        break;

    case 1:
        fragment = new FragmentBook();
        break;

    case 2:
        fragment = new FragmentCat();
        Bundle args = new Bundle();
        args.putString(FragmentCat.CAT_COLOR, "Brown");
        fragment.setArguments(args);
        break;

    default:
        //還沒製作的選項,fragment 是 null,直接返回
        return;
    }

    FragmentManager fragmentManager = getFragmentManager();
    fragmentManager.beginTransaction().replace(R.id.content_frame, fragment).commit();

    // 更新被選擇項目,換標題文字,關閉選單
    lstDrawer.setItemChecked(position, true);
    setTitle(drawer_menu[position]);
    layDrawer.closeDrawer(lstDrawer);
}

@Override
public void setTitle(CharSequence title) {
    mTitle = title;
    getActionBar().setTitle(mTitle);
}
參數 position 是被點選的那個項目的索引。依不同的索引,建立不同的 Fragment ,其中 FragmentCat 有傳值(一個字串)。最後就是把原本的 Fragment 換成被選擇的,同時更新 ActionBar 的標題並關閉側選單。

接著就是建立偵聽器,在接收到事件時呼叫 selectItem() 方法:
private class DrawerItemClickListener implements ListView.OnItemClickListener {
    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        selectItem(position);
    }
}
然後在 initDrawerList() 中指定給 lstDrawer:
//側選單點選偵聽器
lstDrawer.setOnItemClickListener(new DrawerItemClickListener());
大功告成!!

範例程式碼:https://github.com/tony915/NavigationDrawerDemo

-- (補充 2014/6/4) --

側選單加入圖示

建立新的 layout 給 ListView 用,drawer_list_item2.xml 如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <ImageView
        android:id="@+id/imgIcon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBottom="@+id/txtItem"
        android:layout_alignTop="@+id/txtItem"
        android:padding="3dp"
        android:scaleType="fitCenter" />
    
 <TextView
     android:id="@+id/txtItem"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:layout_alignParentTop="true"
     android:layout_toRightOf="@+id/imgIcon"
     android:padding="10dp"
     android:text="Item"
     android:textAppearance="?android:attr/textAppearanceMedium"
     android:textColor="#cccccc" />

</RelativeLayout>
只是把原本的 TextView 外面多加一個 Layout,這樣就可以多加一個 ImageView 進來。接著修改程式碼。原本使用的 ArrayAdapter,在這裡無法使用,必須改用 SimpleAdapter,或自己建立 BaseAdapter 來使用。修改後的 initDrawerList() 方法如下:
private void initDrawerList(){
  drawer_menu = this.getResources().getStringArray(R.array.drawer_menu);
//  ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, R.layout.drawer_list_item, drawer_menu);
  
  List<HashMap<String,String>> lstData = new ArrayList<HashMap<String,String>>();
  for (int i = 0; i < 10; i++) {
   HashMap<String, String> mapValue = new HashMap<String, String>();
   mapValue.put("icon", Integer.toString(R.drawable.ic_launcher));
   mapValue.put("title", drawer_menu[i]);
   lstData.add(mapValue);
  }
  SimpleAdapter adapter = new SimpleAdapter(this, lstData, R.layout.drawer_list_item2, new String[]{"icon", "title"}, new int[]{R.id.imgIcon, R.id.txtItem});
  lstDrawer.setAdapter(adapter);
  
  //側選單點選偵聽器
  lstDrawer.setOnItemClickListener(new DrawerItemClickListener());
 }
這裡的 icon 圖檔,我只示範一個同樣的圖示,你可以換成一個陣列,數量和 drawer_menu 陣列一樣,這樣每個項目就會有自己的圖示,記得要把圖示的資源碼轉成字串。

結果如下:

-- (補充 at 2014/8/11) --

如何讓實體按鈕的 "Back" 返回上一頁

fragmentManager.beginTransaction().replace(R.id.content_frame, fragment).commit();
改為
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
  fragmentTransaction.replace(R.id.content_frame, fragment);
  fragmentTransaction.addToBackStack("home");
  fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
  fragmentTransaction.commit();
這樣就會將前一個 Fragment 保留至堆疊,但是按 Back 鍵到最後一個的時候,因為是 content_frame,會顯示空空的內容,所以必須加寫 Back 事件,讓堆疊的數量等於 0 時,關閉 App。
@Override
 public void onBackPressed() {
  super.onBackPressed();
  FragmentManager fragmentManager = this.getFragmentManager();
  int stackCount = fragmentManager.getBackStackEntryCount();
  if (stackCount == 0) {
   this.finish();
  }
 }

範例程式放在 GitHub 中的 NavigationDrawerDemo

參考資料

本文網址:http://blog.tonycube.com/2014/02/android-navigation-drawer-2.html
Tony Blog 撰寫,請勿全文複製,轉載時請註明出處及連結,謝謝 😀

57 則留言

  1. 您好:
    不好意思我這邊的程式,我將FragmentApple(Book、Cat)換成我的檔名他卻顯示錯誤 該換成甚麼會比較好?

    switch (position) {
    case 0:
    fragment = new FragmentApple();
    break;

    case 1:
    fragment = new FragmentBook();
    break;

    case 2:
    fragment = new FragmentCat();
    Bundle args = new Bundle();
    args.putString(FragmentCat.CAT_COLOR, "Brown");
    fragment.setArguments(args);
    break;

    回覆刪除
    回覆
    1. 你的檔名是指有一個類別檔案是繼承 Fragment ,像這樣嗎
      public class XXX extends Fragment
      ...

      XXX 是你的類別檔案的名稱,且這個類別必須繼承 Fragment。

      刪除
    2. 還有要注意一下 import 的部份,因為 Fragment 有 V4 的支援版本及Android 4.x 之後的版本,所以要看一下是否有 import 對的版本。

      刪除
  2. 你好:
    請問Fragment是在MainActivity裡繼承還是在另外幾個裡面繼承?
    因為我MainActivity是繼承Activity(我看您上面是這樣打的)
    謝謝

    回覆刪除
    回覆
    1. 每一個繼承 Fragment 的 XXX 類別都是一個獨立的檔案,
      MainActivity也是一個獨立的檔案。
      文章下方有原始碼連結,你可以直接在上面看(src目錄)比較清楚,
      或是下載(在右下方有 Download ZIP)。

      刪除
  3. 很棒的教學喔 節省我很多的研究時間 3Q

    回覆刪除
  4. 先謝謝用心的教學文~

    我想請教一下,如果我要在選單文字左邊增加icon的話,我該如何操作 謝謝

    回覆刪除
    回覆
    1. 修改 layout 及 adpater 即可,我寫在文章後面的補充。

      刪除
    2. 再次感謝你的教學!!!

      刪除
  5. 支持!! 這些都是很實用的教學
    希望版主下次可以出有關Bitmap network的教學

    回覆刪除
  6. 請教版主 :
    在這個側選單 點選任一頁面後
    可以 使用 硬鍵Back 回去上一頁嗎?
    我有在MainActivity 的 FragmentTransaction 後面加.addToBackStack(null)
    但沒有用 , 板主要怎麼解決

    回覆刪除
    回覆
    1. 我在文章後面多寫了補充,範例程式碼也更新了。

      刪除
    2. 謝謝版主的解決方案
      可以back鍵返回了
      雖然我把原本的FragmentActivity 換成 Activity
      才可以動作。
      而且我進去 fragment 好像開啟後有邊框 殘影,
      是不是我的app寫得太亂之類的,
      有fragment子分頁太大的問題 ?

      刪除
    3. 案返回後
      是不是fragment會 重新執行
      因為我返回Home後,
      在 異步執行緒怪怪的 (好像會衝突)
      我2個fragment都有加 異步執行緒
      是哪裡有問題嗎?

      刪除
    4. 殘影?沒遇過,如果是用模擬器,可能是模擬器顯示的問題;如果實機也是如此,就要找原因。

      異步執行緒在返回時要手動結束掉,否則它會繼續執行到完。

      刪除
    5. 那應該是開啟的背景動畫吧?(猜想)
      我用的是黑色背景的layout , 所以才會這樣
      謝謝版主的回答

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

      刪除
  7. 請教Tony
    我想要在這 Navigation Drawer上再加上 Swipe Tabs
    可是我的Tab點選卻一直顯示不出下面的Fragment
    如果把Navigation Drawer的程式碼拿掉才會出現Tab裡的Fragment
    是Navigation Drawer覆蓋在Swipe Tab上的關係嗎?
    我看了很久程式碼還找不出哪裡需要修改
    拜託Tony指點一下

    回覆刪除
    回覆
    1. SwipeTab我也沒用過,幫你找到兩篇文章,參考一下
      http://stackoverflow.com/questions/22389933/swiping-tabs-inside-a-navigation-drawer-fragment

      這篇是教學
      http://androidgreeve.blogspot.tw/2014/01/android-actionbar-navigating-with-swipeable-tabs-and-views.html

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

      刪除
    3. 請問Tony~~是這樣的
      我主程式進入到MainActivity中
      如你的程式碼會先判斷
      if (savedInstanceState == null) {
      selectItem(0);
      }
      然後進到我的BrowserFragment中
      而這個BrowserFragment是有用viewPager和Action的結合
      第一次進入到這個BrowserFragment中都很正常
      但當我利用drawer切換至其他的Fragment再切換回來後
      這個BrowserFragment卻只剩下上面的ActionBar Tab
      下面原本利用viewpager切換的Fragment卻變成一片空白
      但滑動和切換TAB都很正常
      一定要重新開啟這個Activity才會恢復正常
      請問是哪邊少做了甚麼嗎?

      刪除
    4. 我的做法裡面,是用 replace 把目前的 fragment 置換成被選到的。
      也就是說,每次點選drawer切換項目時,fragment 都是新建立的,
      而前一個 fragment 就被釋放了。
      看看你切換的動作是怎麼做的,問題應該是出在切換上,
      fragment 本身應該是沒問題。

      刪除
  8. 請問Tony
    這一行 setTitle(drawer_menu[position]);
    一直失敗對照很多次,拜託指點一下

    回覆刪除
    回覆
    1. 要看DDMS告訴你什麼錯誤。

      刪除
    2. 感謝大大,我已經解決問題了,不過現在有一個問題想問Tony大大,就是因為就是我程式碼有加入try catch 去網路抓取資料,可是因為加入try catch的關係,會因為讀取速度的關係畫面經常卡住,所以我想問的是我要怎麼讓畫面先轉入過去,內容資料讀取完之後在呈現出來,例如臉書的切換到動態載入,畫面先轉入過去之後,內容等讀取完之後在呈現出來,請問我該怎麼做,麻煩大大給個方向。謝謝

      刪除
    3. 要用非同步的方式,在資料下載完成後再顯示,
      參考 AsyncTask
      http://blog.tonycube.com/2011/08/asynctask.html

      或 Handler
      http://developer.android.com/reference/android/os/Handler.html

      刪除
    4. Tony大大,上面那個問題我想實作在Fragment ,也是用一樣的辦法嗎?

      刪除
    5. 一樣,只要你不想被載入過久的動作顯響到畫面,兩個動作就必須分開在兩個執行緒上做。

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

    回覆刪除
  10. Tony大您好,我想請問一下,如何在最上方加入Search View,我嘗試了許多方法都無法成功

    回覆刪除
    回覆
    1. 和 Action button 作法一樣,請參考
      http://www.coderzheaven.com/2012/10/20/actionbar-search-option-options-android/

      刪除
  11. Tony大大你好,我把Swipe Tab與Navigation Drawer放在一起,然而,在run的時候點擊Navigation Drawer 的item所顯示的畫面與tab中的viewpage重疊到,請問有方法可以另外再Navigation Drawer的click 事件時建立新的畫面嗎? (因為是初學者有些概念不是很懂)

    回覆刪除
    回覆
    1. "Navigation Drawer 的item所顯示的畫面"應該會是一個 fragment,
      tab 本身屬於另一個 fragment,
      所以當你點選Navigation Drawer 的item時,
      應該換掉 tab 這個

      找範例裡面這一段,
      先建立想要的 fragment 然後把它換掉
      FragmentManager fragmentManager = getFragmentManager();
      fragmentManager.beginTransaction().replace(R.id.content_frame, fragment).commit();

      刪除
    2. Tony大大,按造你的說法,我嘗試了把tab放入
      Navigation Drawer item的 fragment ,解決了畫面衝突問題,
      在此感謝你的解答 : )

      刪除
  12. Hi,Tony
    我有一個問題想請教你,就是我的錯誤訊息是
    \appcompat_v7\res\values-v21\themes_base.xml:81: error: Error: No resource found that matches the given name: attr 'android:colorAccent'.
    在之前我用4.0.3版本開發的時候沒問題,可是顯在4.0.3編譯時不能成功,反而用到5.0.1才能編譯成功,請問這是為什麼?那有辦法降回4.0.3版本嗎?

    回覆刪除
    回覆
    1. 可能 AndroidMenifest.xml 中, targetSdkVersion 是設為21(Android 5.0),所以4.0.3無法編譯。
      這篇(http://code2care.org/pages/appcompat_v7-errors-after-updates-to-api-level-21-material-theme/)
      有說到解決方法,但是以 5.0 去編譯。

      你可以在"專案"(按右鍵)->Properties->Android->選你要的"Project Build Target"看看能不能編譯。

      刪除
    2. 目前嘗試下來發現 Android 5.0 可以編譯成功,但是進去之後會出現問題
      private void initActionBar(){
      getActionBar().setDisplayHomeAsUpEnabled(true);
      getActionBar().setHomeButtonEnabled(true);
      }
      這個方法無法使用,我有上網查過google有說Android 5.0 ActionBar 有做變動,請問大大知道這是什麼問題嗎?還有如果我開發平台用5.0.1 那麼我4.0.3版本 可否使用?

      刪除
    3. 我找到這篇(http://blog.csdn.net/xwhnew/article/details/40624715)是說在 5.0 之後,以ToolBar 取代ActionBar,所以可能程式碼要修改。

      你的 minSdkVersion 設為 15 (也就是 4.0.3)
      targetSdkVersion 設為 21
      這樣 4.0.3 之後的裝置都能跑。

      刪除
  13. 你好tony 我有幾個問題然後我算是新手所以問題如果有點怪還請見諒~~
    1.開啟程式只會在activity_main停留一會就會切換到第一個選項的頁面
    請問要怎麼讓一開始是停在activity_main?
    2.要怎麼讓title bar變成白色然後更改文字甚至或讓他消失 等於只有那個選單的圖案而已

    回覆刪除
    回覆
    1. 1.程式是因為你怎麼寫,它怎麼跑,所以看一下你寫了什麼程式造成了它的目前的行為。

      2.要修改 title bar (現在叫Actioin Bar),參考 https://developer.android.com/training/basics/actionbar/styling.html

      PS.文字不加標點符號是在告訴閱讀者,不要看!趕快離開!

      刪除
  14. Tony大您好
    我是android新手
    參考了您做Navigation Drawer的教學文
    請問您有研究過用v7去做嗎?
    android.support.v7.app.ActionBarDrawerToggle;
    v7版本好像就內建圖檔,比較方便些
    網路上找不到v7的教學文,好苦手QQ

    回覆刪除
    回覆
    1. 作者已經移除這則留言。

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

      刪除
    3. 沒用過 v7 的版本,這裡有教怎麼從 v4 轉換成 v7,
      看起是原本的 ActionBar 不能用了,要改用 Toolbar,
      整體的行為是沒多大更動

      刪除
  15. 感謝
    為我想寫的軟體提供範本
    感恩!!!

    回覆刪除
  16. 作者已經移除這則留言。

    回覆刪除
  17. Tony大你好,請問可以讓app本身預設開啟時不是顯示側選單的第一個選項而是顯示另一畫面嗎?

    回覆刪除
    回覆
    1. 把這範例中的這段程式碼
      FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
      fragmentTransaction.replace(R.id.content_frame, fragment);//<<<<<這裡
      fragmentTransaction.addToBackStack("home");
      fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
      fragmentTransaction.commit();

      我標示"這裡"的那個 fragment 用你想要當主畫面的 Fragment 取代,把這段程式碼放在 Activity 的 onCreate ,這樣它一開啟後,就會立即換成你所指定的 fragment

      刪除
    2. 可以了!謝謝^^ 可是還有個問題想問,就是雖然有顯示我指定的畫面,可是側選單的背景顏色就不見了...直接透明,可以請問是什麼原因嗎?

      刪除
    3. 在側選單的 layout 指定顏色看看

      刪除
  18. Tony大大,請問R.id.content_frame 這是指什麼呢?

    回覆刪除
    回覆
    1. Tony 大大,抱歉我看到了,沒從第一頁開始看起- -

      刪除
  19. 你好,請問setItemChecked沒有反應,應該是哪裡出了問題呢?
    我在xml中android:choiceMode不管設定multipleChoice或singleChoise都沒有用...

    回覆刪除
    回覆
    1. 先確認 check 的值是不是有改變,如果有那就是 view 的問題,有可能 view 有改變但是看不出來,
      看看這篇有沒有幫助 http://blog.csdn.net/zhangyingli/article/details/46356895

      刪除

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