介紹

Navigation Drawer通常是從左邊邊緣拉出來的一個選單,是現在很常看到的UI元件。Google有提供此功能,這裡將使用Google提供的功能,來做到此效果,需用到DrawerLayout和NavigationView。DrawerLayout的功能是用來做從左到右拉出來的效果,NavigationView則是用來在拉出來的畫面上顯示資料和選單。 TreeView是一種在Windows常見的多層清單表示方式,而在Android中只提供了ExpandableListView,最多只支援到兩層清單的表示,所以如果想實現三層或多層的清單,就需要我們自己客製了,TreeView已有人做出,我將他改成自己需要用的字合併到Navigation Drawer,達到實現側邊欄裡還要能顯示多層清單的功能。

開啟AndroidManifest.xml確認使用android:theme為@style/AppTheme。再開啟styles.xml,將AppTheme的parent改為Theme.AppCompat.Light.NoActionBar,並加入以下items。

<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">#212121</item>
        <item name="colorPrimaryDark">#187817</item>
        <item name="windowActionBar">false</item>
        <item name="windowNoTitle">true</item>
        <item name="android:windowFullscreen">true</item>
        <item name="android:windowActionModeOverlay">true</item>
        <item name="android:windowDrawsSystemBarBackgrounds">true</item>
        <item name="android:statusBarColor">@android:color/transparent</item>
</style>

再來設定layout,DrawerLayout主要是一個跟畫面(root view),裡面只能包含兩個view,一個是主要內容的view,另一個是拉出來的選單view,這裡以LinearLayout和TextView來作為主要內容。NavigationView分上下兩部分,上面的是headerLayout,下面的是menu,通常會設置res/menu,然後再載入。activity_main如下。

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

    <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/contentView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello World"/>
    </LinearLayout>

    <android.support.design.widget.NavigationView
        android:id="@+id/navigationView"
        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>

drawer_header,可自行加入圖片或個人頭像,我只用TextView加上背景色。

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="190dp"
    android:background="#FF0000">

    <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="Royshow"
        android:textSize="18sp"
        android:textColor="#fff"
        android:textStyle="bold"
        android:paddingBottom="8dp"/>
</RelativeLayout>

將menu,設置在res/menu/drawer,如下。

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

在onCreate中加入以下程式,

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。並為Navigation加上OnNavigationItemSelectedListener,這樣按menuItem的話,就會顯示toast。 執行試一試,drawerLayout可以拉出來。

NavigationDrawerImg

這個要利用actionBarDrawerToggle來做了,到onCreat()加上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();

這樣三就出來了。

NavigationDrawerMenuIconImg

TreeView-Element.java

public class Element {

    private String contentText;
    private int level;
    private int id;
    private int parendId;
    private boolean hasChildren;
    private boolean isExpanded;
    public static final int NO_PARENT = -1;
    public static final int TOP_LEVEL = 0;
    public Element(String contentText, int level, int id, int parendId, boolean hasChildren, boolean isExpanded) {
        super();
        this.contentText = contentText;
        this.level = level;
        this.id = id;
        this.parendId = parendId;
        this.hasChildren = hasChildren;
        this.isExpanded = isExpanded;
    }
    public boolean isExpanded() {
        return isExpanded;
    }

    public void setExpanded(boolean isExpanded) {
        this.isExpanded = isExpanded;
    }

    public String getContentText() {
        return contentText;
    }

    public void setContentText(String contentText) {
        this.contentText = contentText;
    }

    public int getLevel() {
        return level;
    }

    public void setLevel(int level) {
        this.level = level;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public int getParendId() {
        return parendId;
    }

    public void setParendId(int parendId) {
        this.parendId = parendId;
    }

    public boolean isHasChildren() {
        return hasChildren;
    }

    public void setHasChildren(boolean hasChildren) {
        this.hasChildren = hasChildren;
    }
}

TreeView-TreeViewAdapter.java

public class TreeViewAdapter extends BaseAdapter {
    private ArrayList<Element> elementsData;
    private ArrayList<Element> elements;
    private LayoutInflater inflater;
    private int indentionBase;

    public TreeViewAdapter(ArrayList<Element> elements, ArrayList<Element> elementsData, LayoutInflater inflater) {
        this.elements = elements;
        this.elementsData = elementsData;
        this.inflater = inflater;
        indentionBase = 50;
    }

    public ArrayList<Element> getElements() {
        return elements;
    }

    public ArrayList<Element> getElementsData() {
        return elementsData;
    }

    @Override
    public int getCount() {
        return elements.size();
    }

    @Override
    public Object getItem(int position) {
        return elements.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder holder = null;
        if (convertView == null) {
            holder = new ViewHolder();
            convertView = inflater.inflate(R.layout.treeview_item, null);
            holder.disclosureImg = (ImageView) convertView.findViewById(R.id.disclosureImg);
            holder.contentText = (TextView) convertView.findViewById(R.id.contentText);
            convertView.setTag(holder);
        } else {
            holder = (ViewHolder) convertView.getTag();
        }
        Element element = elements.get(position);
        int level = element.getLevel();
        holder.disclosureImg.setPadding(
                indentionBase * (level + 0),
                holder.disclosureImg.getPaddingTop(),
                holder.disclosureImg.getPaddingRight(),
                holder.disclosureImg.getPaddingBottom());
        holder.contentText.setText(element.getContentText());
        if (element.isHasChildren() && !element.isExpanded()) {
            holder.disclosureImg.setImageResource(R.drawable.close);
            //這裡要主動設置一下icon可見,因為convertView重用了,"設置了不可見"的view
            holder.disclosureImg.setVisibility(View.VISIBLE);
        } else if (element.isHasChildren() && element.isExpanded()) {
            holder.disclosureImg.setImageResource(R.drawable.open);
            holder.disclosureImg.setVisibility(View.VISIBLE);
        } else if (!element.isHasChildren()) {
            holder.disclosureImg.setImageResource(R.drawable.close);
            holder.disclosureImg.setVisibility(View.INVISIBLE);
        }
        return convertView;
    }

    static class ViewHolder{
        ImageView disclosureImg;
        TextView contentText;
    }
}

TreeView-TreeViewItemClickListener.java

public class TreeViewItemClickListener implements AdapterView.OnItemClickListener{

    private TreeViewAdapter treeViewAdapter;
    public TreeViewItemClickListener(TreeViewAdapter treeViewAdapter) {
        this.treeViewAdapter = treeViewAdapter;
    }

    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        Element element = (Element) treeViewAdapter.getItem(position);

        ArrayList<Element> elements = treeViewAdapter.getElements();

        ArrayList<Element> elementsData = treeViewAdapter.getElementsData();

        if (!element.isHasChildren()) {
            return;
        }

        if (element.isExpanded()) {
            element.setExpanded(false);

            ArrayList<Element> elementsToDel = new ArrayList<Element>();
            for (int i = position + 1; i < elements.size(); i++) {
                if (element.getLevel() >= elements.get(i).getLevel())
                    break;
                elementsToDel.add(elements.get(i));
            }
            elements.removeAll(elementsToDel);
            treeViewAdapter.notifyDataSetChanged();
        } else {
            element.setExpanded(true);

            int i = 1;
            for (Element e : elementsData) {
                if (e.getParendId() == element.getId()) {
                    e.setExpanded(false);
                    elements.add(position + i, e);
                    i ++;
                }
            }
            treeViewAdapter.notifyDataSetChanged();
        }
    }
}

TreeView-treeview_item.xml

這裡主要是自己製作多層的清單項目,使用ImageView和TextView,如下。

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/disclosureImg"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_alignParentLeft="true" />

    <TextView
        android:id="@+id/contentText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_toRightOf="@id/disclosureImg"
        android:textSize="20dp"
        android:textColor="@color/white"/>

</RelativeLayout>

修改activity_main.xml

在NavigationView裡,加入一個LinearLayout和ListView,如下。

<android.support.design.widget.NavigationView
        android:id="@+id/navigationView"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_gravity="start">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            android:background="@color/black">

            <ListView
                android:id="@+id/treeListView"
                android:layout_width="match_parent"
                android:layout_height="match_parent" />
        </LinearLayout>

    </android.support.design.widget.NavigationView>

MainActivity.java

public class MainActivity extends AppCompatActivity {

    private static final String NAV_ITEM_ID = "nav_index";

    DrawerLayout drawerLayout;
    TextView contentView;

    private int navItemId;

    private ArrayList<Element> elements;
    private ArrayList<Element> elementsData;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);

        init();

        ListView treeview = (ListView) findViewById(R.id.treeListView);
        TreeViewAdapter treeViewAdapter = new TreeViewAdapter(elements, elementsData, inflater);

        TreeViewItemClickListener treeViewItemClickListener = new TreeViewItemClickListener(treeViewAdapter);
        treeview.setAdapter(treeViewAdapter);
        treeview.setOnItemClickListener(treeViewItemClickListener);

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

        contentView = (TextView) findViewById(R.id.contentView);
        drawerLayout = (DrawerLayout) findViewById(R.id.drawerLayout);
        NavigationView view = (NavigationView) findViewById(R.id.navigationView);
        view.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() {
            @Override
            public boolean onNavigationItemSelected(MenuItem menuItem) {
                Toast.makeText(MainActivity.this, menuItem.getTitle() + " pressed", Toast.LENGTH_LONG).show();
                navigateTo(menuItem);

                drawerLayout.closeDrawers();
                return true;
            }
        });

        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();

//        if(null != savedInstanceState){
//            navItemId = savedInstanceState.getInt(NAV_ITEM_ID, R.id.navigationItem1);
//        }
//        else{
//            navItemId = R.id.navigationItem1;
//        }
//        navigateTo(view.getMenu().findItem(navItemId));
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

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

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

    private void init() {
        elements = new ArrayList<Element> ();
        elementsData = new ArrayList<Element> ();

        Element element1 = new Element("模組A第一層", Element.TOP_LEVEL, 0, Element.NO_PARENT, true, false);

        Element element2 = new Element("模組A第二層", Element.TOP_LEVEL + 1, 1, element1.getId(), true, false);
        Element element3 = new Element("模組A第三層", Element.TOP_LEVEL + 2, 2, element2.getId(), true, false);
        Element element4 = new Element("模組A第四層", Element.TOP_LEVEL + 3, 3, element3.getId(), false, false);

        Element element5 = new Element("模組A第二層-2", Element.TOP_LEVEL + 1, 4, element1.getId(), true, false);
        Element element6 = new Element("模組A第三層-2", Element.TOP_LEVEL + 2, 5, element5.getId(), true, false);
        Element element7 = new Element("模組A第四層-2", Element.TOP_LEVEL + 3, 6, element6.getId(), false, false);

        Element element8 = new Element("模組A第二層-3", Element.TOP_LEVEL + 1, 7, element1.getId(), false, false);

        Element element9 = new Element("模組B第一層", Element.TOP_LEVEL, 8, Element.NO_PARENT, true, false);
        Element element10 = new Element("模組B第二層", Element.TOP_LEVEL + 1, 9, element9.getId(), true, false);
        Element element11 = new Element("模組B第三層", Element.TOP_LEVEL + 2, 10, element10.getId(), true, false);
        Element element12 = new Element("模組B第四層", Element.TOP_LEVEL + 3, 11, element11.getId(), true, false);
        Element element13 = new Element("模組B第五層", Element.TOP_LEVEL + 4, 12, element12.getId(), false, false);

        elements.add(element1);
        elements.add(element9);

        elementsData.add(element1);
        elementsData.add(element2);
        elementsData.add(element3);
        elementsData.add(element4);
        elementsData.add(element5);
        elementsData.add(element6);
        elementsData.add(element7);
        elementsData.add(element8);
        elementsData.add(element9);
        elementsData.add(element10);
        elementsData.add(element11);
        elementsData.add(element12);
        elementsData.add(element13);
    }

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

程式碼已放上Github