Android 抽屉菜单

作者: 牙锅子 | 来源:发表于2016-11-20 17:32 被阅读1840次

    版权声明:本文为博主原创文章,未经博主允许不得转载。
    微博:厉圣杰
    源码:AndroidDemo/DrawerLayout
    文中如有纰漏,欢迎大家留言指出。

    Android 抽屉菜单实现方式主要有两种方式,一是使用 Google 官方推出的侧滑菜单实现:DrawerLayout ,这个类是在 android-support-v4 包里;二是使用开源库,如: SlidingMenu ,不过此开源库自 2014年5月10号起就没有更新过,更是留有二百多没关闭的 issue ,故直接放弃此库。所以只考虑使用 DrawerLayout 实现抽屉式菜单。

    DrawerLayout

    DrawerLayout 是窗口的顶级容器,它允许从窗口的左边缘或右边缘拉出抽屉式菜单。下图是网易新闻抽屉式菜单样式:


    网易新闻抽屉式菜单

    抽屉式菜单的定位和布局通过 android:layout_gravity 属性来控制,其可选值为 leftright 或者 startend 。但是,每一边缘只能设置一个抽屉式视图,否则会抛出运行时异常。

    DrawerLayout 的第一个子View 用于显示主要内容,即抽屉菜单没有打开时显示的布局,它可以是 LinearLayout 、 FrameLayout ...第一个元素宽高默认都是 match_parent 的而且不用设置 layout_gravity 属性。

    接下来紧跟的子元素是抽屉菜单,如 ListView 、 LinearLayout 等。抽屉式菜单一般高度都设为 match_parent ,而宽度不应该超过 320dp ,这样用户可以在打开抽屉菜单时看到部分内容界面。

    典型 DrawerLayout 布局如下:

    <?xml version="1.0" encoding="utf-8"?>
    <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/activity_main"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <!-- The main content view -->
        <!-- 一般宽高都是 match_parent 且不用设置 layout_gravity 属性 -->
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">
            <!-- 用于替换 ActionBar,如果不需要可以直接删除 -->
            <include
                android:id="@+id/tool_bar"
                layout="@layout/tool_bar" />
    
            <!-- 一般 DrawerLayout 是与 Fragment 结合使用的,当点击抽屉菜单中的 item 时,使用 FragmentManager 来实现切换,实现内容页的改变 -->
            <FrameLayout
                android:id="@+id/content_frame"
                android:layout_width="match_parent"
                android:layout_height="match_parent" />
    
        </LinearLayout>
    
        <!-- The navigation drawer -->
        <!-- layout_width 一般小于 320dp -->
        <!-- 通过设置 layout_gravity 对抽屉菜单进行定位 -->
        <LinearLayout
            android:layout_width="240dp"
            android:layout_height="match_parent"
            android:layout_gravity="left"
            android:background="@color/white"
            android:orientation="vertical">
    
            <!-- 实现自定义布局,不一定要是 ListView -->
        </LinearLayout>
        
        <LinearLayout
            android:layout_width="240dp"
            android:layout_height="match_parent"
            android:layout_gravity="right"
            android:background="@color/white"
            android:orientation="vertical">
    
            <!-- 实现自定义布局,不一定要是 ListView -->
        </LinearLayout>
    
    </android.support.v4.widget.DrawerLayout>
    

    DrawerLayout 初试身手

    前面讲了 DrawerLayout 的基本用途、注意事项及其典型的布局格式,下面我们就进行实战,实现如下效果:
    ![屏幕快照 2016-11-20 下午1.00.06](http://odsdowehg.bkt.clouddn.com/屏幕快照 2016-11-20 下午1.00.06.png)

    从效果图中可以看出来,抽屉菜单分为 header 和 list 两部分,那么我们就需要在抽屉菜单这部分布局中实现。

    activity_drawer_layout.xml:

    <?xml version="1.0" encoding="utf-8"?>
    <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 -->
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">
    
            <include
                android:id="@+id/tool_bar"
                layout="@layout/tool_bar" />
    
            <TextView
                android:id="@+id/tv_content"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:gravity="center" />
    
        </LinearLayout>
    
        <!-- The navigation drawer -->
        <LinearLayout
            android:layout_width="240dp"
            android:layout_height="match_parent"
            android:layout_gravity="left"
            android:background="@color/white"
            android:orientation="vertical">
    
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@color/skyblue"
                android:gravity="center"
                android:orientation="horizontal"
                android:paddingBottom="20dp"
                android:paddingLeft="20dp"
                android:paddingTop="20dp">
    
                <ImageView
                    android:id="@+id/iv_header"
                    android:layout_width="60dp"
                    android:layout_height="60dp"
                    android:layout_gravity="center"
                    android:src="@mipmap/ic_launcher" />
    
                <TextView
                    android:id="@+id/tv_name"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center_vertical"
                    android:layout_marginLeft="40dp"
                    android:text="厉圣杰"
                    android:textColor="@color/black"
                    android:textSize="25sp" />
            </LinearLayout>
    
            <ListView
                android:id="@+id/lv_item"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_gravity="start"
                android:divider="@android:color/transparent"
                android:dividerHeight="0dp"
                android:listSelector="@color/colorAccent" />
        </LinearLayout>
    
        <LinearLayout
            android:layout_width="240dp"
            android:layout_height="match_parent"
            android:layout_gravity="right"
            android:background="@color/colorAccent">
    
            <TextView
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:gravity="center"
                android:text="Right Drawer" />
        </LinearLayout>
    
    </android.support.v4.widget.DrawerLayout>
    

    主要代码:

    public class DrawerLayoutActivity extends AppCompatActivity {
    
        private int[] mIcons = new int[]{R.mipmap.ic_contacts_black_24dp, R.mipmap.ic_message_black_24dp
                , R.mipmap.ic_wifi_black_24dp, R.mipmap.ic_settings_black_24dp};
        private String[] mContents = new String[]{"Contacts", "Message", "Wifi", "Settings"};
    
        private DrawerLayout mDrawerLayout;
        private Toolbar mToolbar;
        private TextView mTvContent;
        private ListView mLvItem;
        private DrawerItemAdapter mAdapter;
    
        private ActionBarDrawerToggle mDrawerToggle;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_drawer_layout);
    
            mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
            mToolbar = (Toolbar) findViewById(R.id.tool_bar);
            mTvContent = (TextView) findViewById(R.id.tv_content);
            mLvItem = (ListView) findViewById(R.id.lv_item);
            mAdapter = new DrawerItemAdapter();
            mLvItem.setAdapter(mAdapter);
    
            mLvItem.setOnItemClickListener(new AdapterView.OnItemClickListener() {
                @Override
                public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                    mTvContent.setText("Current Selected Item : " + position);
                }
            });
    
            //设置 Logo
            //mToolbar.setLogo(R.mipmap.ic_launcher);
            //使用 Toolbar 取代 ActionBar
            setSupportActionBar(mToolbar);
            //注意,设置 Toolbar 及相关点击事件最好放在 setSupportActionBar 后,否则很可能无效
            //设置 Navigation 图标和点击事件必须放在 setSupportActionBar 后,否则无效
            mToolbar.setNavigationIcon(R.mipmap.ic_menu_black_24dp);
            mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout, mToolbar,
                    R.string.open_drawer, R.string.close_drawer);
            mDrawerLayout.addDrawerListener(mDrawerToggle);
        }
    
        class DrawerItemAdapter extends BaseAdapter {
    
            @Override
            public int getCount() {
                return mIcons.length;
            }
    
            @Override
            public Object getItem(int position) {
                return null;
            }
    
            @Override
            public long getItemId(int position) {
                return 0;
            }
    
            @Override
            public View getView(int position, View convertView, ViewGroup parent) {
                ViewHolder vh;
                if (convertView == null) {
                    vh = new ViewHolder();
                    convertView = LayoutInflater.from(DrawerLayoutActivity.this).inflate(R.layout.item_drawer, null);
                    vh.ivIcon = (ImageView) convertView.findViewById(R.id.iv_icon);
                    vh.tvContent = (TextView) convertView.findViewById(R.id.tv_content);
                    convertView.setTag(vh);
                }
                vh = (ViewHolder) convertView.getTag();
                vh.ivIcon.setImageResource(mIcons[position]);
                vh.tvContent.setText(mContents[position]);
                return convertView;
            }
    
            class ViewHolder {
                ImageView ivIcon;
                TextView tvContent;
            }
        }
    }
    

    这里有一点要注意的是,设置 Toolbar 及相关点击事件最好放在 setSupportActionBar 后,否则很可能无效,关于 Toolbar 可以参考这篇文章

    如果想要实现以下这种效果,则只需要对布局文件稍作修改即可。
    ![屏幕快照 2016-11-20 下午5.06.07](http://odsdowehg.bkt.clouddn.com/屏幕快照 2016-11-20 下午5.06.07.png)

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
    
        <include layout="@layout/tool_bar" />
    
        <android.support.v4.widget.DrawerLayout
            android:id="@+id/drawer_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
    
            <!-- The main content view -->
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:orientation="vertical">
    
                <TextView
                    android:id="@+id/tv_content"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:gravity="center" />
    
            </LinearLayout>
    
            <!-- The navigation drawer -->
            <LinearLayout
                android:layout_width="240dp"
                android:layout_height="match_parent"
                android:layout_gravity="left"
                android:background="@color/white"
                android:orientation="vertical">
    
                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:background="@color/skyblue"
                    android:gravity="center"
                    android:orientation="horizontal"
                    android:paddingBottom="20dp"
                    android:paddingLeft="20dp"
                    android:paddingTop="20dp">
    
                    <ImageView
                        android:id="@+id/iv_header"
                        android:layout_width="60dp"
                        android:layout_height="60dp"
                        android:layout_gravity="center"
                        android:src="@mipmap/ic_launcher" />
    
                    <TextView
                        android:id="@+id/tv_name"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:layout_gravity="center_vertical"
                        android:layout_marginLeft="40dp"
                        android:text="厉圣杰"
                        android:textColor="@color/black"
                        android:textSize="25sp" />
                </LinearLayout>
    
                <ListView
                    android:id="@+id/lv_item"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_gravity="start"
                    android:divider="@android:color/transparent"
                    android:dividerHeight="0dp"
                    android:listSelector="@color/colorAccent" />
            </LinearLayout>
    
            <LinearLayout
                android:layout_width="240dp"
                android:layout_height="match_parent"
                android:layout_gravity="right"
                android:background="@color/colorAccent">
    
                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:gravity="center"
                    android:text="Right Drawer" />
            </LinearLayout>
    
        </android.support.v4.widget.DrawerLayout>
    
    </LinearLayout>
    

    另外说明一点,关于如此抽屉菜单样式实现还有以下几种方式:

    1. 通过往 ListView 中添加 Header
    2. 使用 NavigationView 实现

    踩过的坑

    测试 DrawerLayout 中踩了几个坑,其中一个就是在设置 Toolbar 的 Navigation 图标点击事件无效,解决办法就是将相关设置放在 setSupportActionBar() 方法后,另外几个坑都是跟设置主题样式有关。特此记录 log 及相关的解决方法。

    使用 DrawerLayout 碰到关于 style 的坑

    FATAL EXCEPTION: main
    Process: com.littlejie.drawerlayout, PID: 3964
    java.lang.RuntimeException: Unable to start activity ComponentInfo{com.littlejie.drawerlayout/com.littlejie.drawerlayout.DrawerLayoutActivity}: java.lang.IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activity.
       at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2665)
       at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2726)
       at android.app.ActivityThread.-wrap12(ActivityThread.java)
       at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1477)
       at android.os.Handler.dispatchMessage(Handler.java:102)
       at android.os.Looper.loop(Looper.java:154)
       at android.app.ActivityThread.main(ActivityThread.java:6119)
       at java.lang.reflect.Method.invoke(Native Method)
       at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
       at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)
    Caused by: java.lang.IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activity.
       at android.support.v7.app.AppCompatDelegateImplV7.createSubDecor(AppCompatDelegateImplV7.java:343)
       at android.support.v7.app.AppCompatDelegateImplV7.ensureSubDecor(AppCompatDelegateImplV7.java:312)
       at android.support.v7.app.AppCompatDelegateImplV7.setContentView(AppCompatDelegateImplV7.java:277)
       at android.support.v7.app.AppCompatActivity.setContentView(AppCompatActivity.java:140)
       at com.littlejie.drawerlayout.DrawerLayoutActivity.onCreate(DrawerLayoutActivity.java:36)
       at android.app.Activity.performCreate(Activity.java:6679)
       at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1118)
       at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2618)
       at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2726) 
       at android.app.ActivityThread.-wrap12(ActivityThread.java) 
       at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1477) 
       at android.os.Handler.dispatchMessage(Handler.java:102) 
       at android.os.Looper.loop(Looper.java:154) 
       at android.app.ActivityThread.main(ActivityThread.java:6119) 
       at java.lang.reflect.Method.invoke(Native Method) 
       at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886) 
       at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776) 
    

    碰到上述问题是,使用 AppCompatActivity 的主题设置不对,将主题改为如下即可:

    <style name="AppCompat" parent="Theme.AppCompat"></style>
    
    FATAL EXCEPTION: main
    Process: com.littlejie.drawerlayout, PID: 5397
    java.lang.RuntimeException: Unable to start activity ComponentInfo{com.littlejie.drawerlayout/com.littlejie.drawerlayout.DrawerLayoutActivity}: java.lang.IllegalStateException: This Activity already has an action bar supplied by the window decor. Do not request Window.FEATURE_SUPPORT_ACTION_BAR and set windowActionBar to false in your theme to use a Toolbar instead.
       at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2665)
       at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2726)
       at android.app.ActivityThread.-wrap12(ActivityThread.java)
       at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1477)
       at android.os.Handler.dispatchMessage(Handler.java:102)
       at android.os.Looper.loop(Looper.java:154)
       at android.app.ActivityThread.main(ActivityThread.java:6119)
       at java.lang.reflect.Method.invoke(Native Method)
       at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
       at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)
    Caused by: java.lang.IllegalStateException: This Activity already has an action bar supplied by the window decor. Do not request Window.FEATURE_SUPPORT_ACTION_BAR and set windowActionBar to false in your theme to use a Toolbar instead.
       at android.support.v7.app.AppCompatDelegateImplV7.setSupportActionBar(AppCompatDelegateImplV7.java:198)
       at android.support.v7.app.AppCompatActivity.setSupportActionBar(AppCompatActivity.java:130)
       at com.littlejie.drawerlayout.DrawerLayoutActivity.onCreate(DrawerLayoutActivity.java:53)
       at android.app.Activity.performCreate(Activity.java:6679)
       at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1118)
       at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2618)
       at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2726) 
       at android.app.ActivityThread.-wrap12(ActivityThread.java) 
       at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1477) 
       at android.os.Handler.dispatchMessage(Handler.java:102) 
       at android.os.Looper.loop(Looper.java:154) 
       at android.app.ActivityThread.main(ActivityThread.java:6119) 
       at java.lang.reflect.Method.invoke(Native Method) 
       at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886) 
       at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776) 
    

    发现上诉问题是因为 ActionBar 已经存在引起,故将 style 改为:

    <style name="AppCompat" parent="Theme.AppCompat.Light.NoActionBar"></style>
    
    <!-- 若 windowNoTitle 为 false ,则会抛出下面的异常 -->
    <style name="AppCompat1" parent="Theme.AppCompat.Light.DarkActionBar">
       <item name="windowActionBar">false</item>
       <item name="windowNoTitle">true</item>
    </style>
    
    Process: com.littlejie.drawerlayout, PID: 10761
     java.lang.RuntimeException: Unable to start activity ComponentInfo{com.littlejie.drawerlayout/com.littlejie.drawerlayout.DrawerLayoutActivity}: java.lang.IllegalArgumentException: AppCompat does not support the current theme features: { windowActionBar: false, windowActionBarOverlay: false, android:windowIsFloating: false, windowActionModeOverlay: false, windowNoTitle: false }
         at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2665)
         at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2726)
         at android.app.ActivityThread.-wrap12(ActivityThread.java)
         at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1477)
         at android.os.Handler.dispatchMessage(Handler.java:102)
         at android.os.Looper.loop(Looper.java:154)
         at android.app.ActivityThread.main(ActivityThread.java:6119)
         at java.lang.reflect.Method.invoke(Native Method)
         at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
         at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)
      Caused by: java.lang.IllegalArgumentException: AppCompat does not support the current theme features: { windowActionBar: false, windowActionBarOverlay: false, android:windowIsFloating: false, windowActionModeOverlay: false, windowNoTitle: false }
         at android.support.v7.app.AppCompatDelegateImplV7.createSubDecor(AppCompatDelegateImplV7.java:458)
         at android.support.v7.app.AppCompatDelegateImplV7.ensureSubDecor(AppCompatDelegateImplV7.java:312)
         at android.support.v7.app.AppCompatDelegateImplV7.setContentView(AppCompatDelegateImplV7.java:277)
         at android.support.v7.app.AppCompatActivity.setContentView(AppCompatActivity.java:140)
         at com.littlejie.drawerlayout.DrawerLayoutActivity.onCreate(DrawerLayoutActivity.java:34)
         at android.app.Activity.performCreate(Activity.java:6679)
         at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1118)
         at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2618)
         at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2726) 
         at android.app.ActivityThread.-wrap12(ActivityThread.java) 
         at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1477) 
         at android.os.Handler.dispatchMessage(Handler.java:102) 
         at android.os.Looper.loop(Looper.java:154) 
         at android.app.ActivityThread.main(ActivityThread.java:6119) 
         at java.lang.reflect.Method.invoke(Native Method) 
         at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886) 
         at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776) 
    

    参考

    1. Implementing Effective Navigation
    2. Android开发之DrawerLayout实现抽屉效果
    3. Toolbar 详解

    相关文章

      网友评论

      本文标题:Android 抽屉菜单

      本文链接:https://www.haomeiwen.com/subject/yxdypttx.html