侧滑菜单这种设计,在很多 APP 上面都有看到,例如大版本3.0之前的知乎、网易新闻、滴滴打车等。有些热衷于 Android Material Design 的开发者,甚至将一些 IOS 化的 Android 应用改头换面,将其 MD 化。这过程中,基本上都会给这些“改版”应用装上“抽屉”,例如 Xposed 上面的经典插件——“WechatUI ”、Github 上面的“Material Design 豆瓣客户端——豆芽”,以及一些第三方微博客户端。
既然侧滑菜单如此受大家欢迎,那我们就赶紧去了解一下它吧
侧滑菜单的实现方式
侧滑菜单主要有两种实现方式,一是使用开源库,如 SlidingMenu、MaterialDrawer 等,二是使用 Android 官方推荐的 DrawerLayout 实现。
我们就先以官方推荐的为基础来进行学习,如果能把官方推荐的知识掌握了,再使用开源库就更不是问题了。
DrawerLayout
DrawerLayout 是谷歌推出的一款用于实现侧滑菜单效果的控件,集成在 support library 里面,使用时需要加载 android-support-v4.jar 包。
DrawerLayout 分为侧边菜单和主内容区两部分,侧边菜单可以根据手势展开或隐藏,主内容区可以根据侧边菜单的变换而改变。
使用方法
先看代码
布局:
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.ginkwang.drawertest.MainActivity">
<FrameLayout
android:id="@+id/fl_content"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<!--左边抽屉菜单-->
<RelativeLayout
android:id="@+id/rl_left"
android:layout_width="200dp"
android:layout_height="match_parent"
android:layout_gravity="left"
>
<ListView
android:id="@+id/lv_left"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</RelativeLayout>
<!--右边抽屉菜单-->
<RelativeLayout
android:id="@+id/rl_right"
android:layout_width="200dp"
android:layout_height="match_parent"
android:layout_gravity="right"
>
<ListView
android:id="@+id/lv_right"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</RelativeLayout>
</android.support.v4.widget.DrawerLayout>
从布局文件可以看出,DrawerLayout 是根布局,然后紧跟着的是第一个子元素是默认内容(抽屉未打开的默认内容,即本例中的 FrameLayout),之后的是抽屉内容。
抽屉菜单的摆放通过抽屉布局的 android:layout_gravity="" 的属性来控制,可填入 "left"、"right"、"start"、"end"。此属性必须设置,要不然侧滑抽屉时会报错。
抽屉菜单宽度单位为 dp,大小一般不超过 320dp,这样打开抽屉后,依然可以看到部分内容布局。
代码
布局文件写好之后,侧滑效果按理说就已经实现了。但是此时你滑动菜单时,会发现左边拉出来什么内容都没有,这是因为我们没有给菜单布局注入数据。
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private Context mContext;
private DrawerLayout mDlMain;
private FrameLayout mFlContent;
private RelativeLayout mRlLeft, mRlRight;
private ListView mLvLeft;
private TextView mTvRight;
private String[] leftMenuNames = {"left_item1", "left_item2",
"left_item3", "left_item4"};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mContext = this;
initView();
}
private void initView() {
mDlMain = (DrawerLayout) findViewById(R.id.dl_main);
mFlContent = (FrameLayout) findViewById(R.id.fl_content);
mRlLeft = (RelativeLayout) findViewById(R.id.rl_left);
mRlRight = (RelativeLayout) findViewById(R.id.rl_right);
mLvLeft = (ListView) findViewById(R.id.lv_left);
mLvLeft.setAdapter(new ArrayAdapter<String>(mContext,
android.R.layout.simple_list_item_1, leftMenuNames));//给左边菜单写入数据
mTvRight = (TextView) findViewById(R.id.tv_right);
mTvRight.setText("right_content");//给右边菜单内容赋值
}
}
效果
左边菜单 右边菜单菜单添加点击事件
菜单做好之后,自然就会给菜单赋予点击事件,来控制主内容区显示的内容。给菜单添加点击事件,其实就是给菜单布局里面的 listview 添加点击事件。这个简单得很,就是 listview.setOnItemClickListener(this) 即可。点击之后,我们之前在主界面布局里定义的 FrameLayout 就会显示相应的 Fragment。代码如下:
mLvLeft.setOnItemClickListener(this);
...
//左侧菜单点击事件
@Override
public void onItemClick(AdapterView<?> pAdapterView, View pView, int pI, long pL) {
Fragment lFragment = new MenuFragment();
Bundle lBundle = new Bundle();
lBundle.putString("menu_str", "item_" + (pI + 1));
lFragment.setArguments(lBundle);
FragmentManager fragmentManager = getFragmentManager();
fragmentManager.beginTransaction().replace(R.id.fl_content, lFragment).commit();
mLvLeft.setItemChecked(pI, true);
mDlMain.closeDrawers();//关闭抽屉
}
MenuFragment 代码如下:
public class MenuFragment extends Fragment {
private View mView;
private Context mContext;
private TextView mTvMenuFragment;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
mView = inflater.inflate(R.layout.layout_fragment_menu, container, false);
mContext = getActivity();
Bundle lBundle = getArguments();
mTvMenuFragment = (TextView) mView.findViewById(R.id.tv_menu_fragment);
if (lBundle != null) {
mTvMenuFragment.setText(lBundle.getString("menu_str"));
}
return mView;
}
}
上面代码中,涉及到一个关闭抽屉的操作 DrawerLayout .closeDrawers(),那打开抽屉的操作就是 DrawerLayout .openDrawer(int gravity),gravity 为抽屉开启方向,应与布局文件中定义的方向一致,不然会报错。下面在代码中实现按钮控制抽屉的开启操作:
布局:
在默认显示布局的 FrameLayout 中添加操作按钮:
<FrameLayout
android:id="@+id/fl_content"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/btn_open_left_drawer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="开启左边抽屉"/>
<Button
android:id="@+id/btn_open_all_drawer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center|top"
android:text="开启全部抽屉"/>
<Button
android:id="@+id/btn_open_right_drawer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:text="开启右边抽屉"/>
</FrameLayout>
代码:
在代码中初始化按钮,并实现按钮点击事件:
private Button mBtnOpenLeftDrawer, mBtnOpenAllDrawer, mBtnOpenRightDrawer;
...
mBtnOpenLeftDrawer = (Button) findViewById(R.id.btn_open_left_drawer);
mBtnOpenLeftDrawer.setOnClickListener(this);
mBtnOpenAllDrawer = (Button) findViewById(R.id.btn_open_all_drawer);
mBtnOpenAllDrawer.setOnClickListener(this);
mBtnOpenRightDrawer = (Button) findViewById(R.id.btn_open_right_drawer);
mBtnOpenRightDrawer.setOnClickListener(this);
...
@Override
public void onClick(View pView) {
switch (pView.getId()) {
case R.id.btn_open_left_drawer:
mDlMain.openDrawer(Gravity.LEFT);
break;
case R.id.btn_open_all_drawer:
mDlMain.openDrawer(Gravity.LEFT);
mDlMain.openDrawer(Gravity.RIGHT);
break;
case R.id.btn_open_right_drawer:
mDlMain.openDrawer(Gravity.RIGHT);
break;
}
}
效果:
按钮控制侧滑菜单去除抽屉划出后主内容页背景的灰色
DrawerLayout.setScrimColor(Color.TRANSPARENT);
填充抽屉划出后与屏幕边缘之间的内容
DrawerLayout.setDrawerShadow();
常见问题
- 在点击DrawerLayout中的空白处的时候,底部的content会获得事件
由于Google的demo是一个ListView,所以ListView会获得焦点,事件就不会传递了,看不出来问题。但是如果用的include加载的布局,会出现这个情况,那么如何解决?
解决办法:在include进的那个布局里面,添加clickable=true - 除了抽屉的布局视图之外的视图究竟放哪里
左、右抽屉和中间内容视图默认是不显示的,其他布局视图都会直接显示出来,但是需要将其放在 DrawerLayout 内部才能正常使用(不要放在外面),否则要么是相互覆盖,或者就是触屏事件失效,滚动等效果全部失效。
使用 NavigationView 实现侧滑菜单的布局
前文中涉及到的侧滑菜单的布局都是我们自己写的,虽然逻辑并不复杂,各种样式也可定义无限制,但总是要耗费些许时间。而且各家 APP 最终实现的样式也五花八门,风格不统一。
终于,谷歌在 Android 大版本5.0时推出了 MD 风格的导航菜单 —— NavigationView 。NavigationView 整体分为两部分,上部分叫 HeaderLayout,下部分的点击项为 Menu。
我们就一起来看看 NavigationView 的使用方式吧。
NavigationView 使用方法
NavigationView 是 Design Support 中的控件,所以如果想要使用它,首先要先引入 Design 库。
引入 Design 库
在项目的 build.gradle 中添加如下代码:
dependencies {
...
compile 'com.android.support:design:26.0.0-alpha1'
}
然后我们还要准备好构成 NavigationView 的两个部分,Menu 和 HeaderLayout。
新建 Menu
在 xml 下新建 menu 文件夹,然后在 menu 文件夹下再新建 nav_manu.xml 文件,menu 文件代码为:
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<group android:checkableBehavior="single">
<item
android:id="@+id/item_nav_menu_news"
android:icon="@mipmap/ic_launcher"
android:title="@string/nav_menu_news"/>
<item
android:id="@+id/item_nav_menu_star"
android:icon="@mipmap/ic_launcher"
android:title="@string/nav_menu_star"/>
<item
android:id="@+id/item_nav_menu_share"
android:icon="@mipmap/ic_launcher"
android:title="@string/nav_menu_share"/>
<item
android:id="@+id/item_nav_menu_set"
android:icon="@mipmap/ic_launcher"
android:title="@string/nav_menu_set"/>
</group>
</menu>
可以看到,menu 文件夹下是一个 group 组,其中 android:checkableBehavior 属性设置为 single,代表 menu 项为单选。
然后每个菜单项是由一个个的 item 构成的,item 的几个属性的简单易懂,这里就不详述。
新建 HeaderLayout
在 xml 下的 layout 文件夹中新建 layout_nav_header.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="?attr/colorPrimary"
android:gravity="center"
android:orientation="vertical"
android:padding="15dp">
<ImageView
android:id="@+id/iv_nav_img"
android:layout_width="50dp"
android:layout_height="50dp"
android:src="@mipmap/ic_launcher"/>
<TextView
android:id="@+id/tv_nav_txt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:text="我知道你 的忧郁是为了什么"
android:textColor="@android:color/white"/>
</LinearLayout>
这段代码也很好懂,就是定义了一个 ImageView,下面是一行说明文字。
OK,我们正式进入主题,对 NavigationView 进行操作。
使用 Navigationview
此示例还是在前文介绍 DrawerLayout 的项目中运行的,为了不破坏之前的代码,我们新建一个活动,在另一个活动中使用 Navigationview。
新建一个跳转到新活动的按钮,并写好逻辑:
<Button
android:id="@+id/btn_goto_nav"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="NavigationView"/>
case R.id.btn_goto_nav:
startActivity(new Intent(MainActivity.this, NavActivity.class));
break;
NavActivity 就是我们新建的活动。
然后在这个活动中加入 Navigationview 控件,布局中其他还是和前文一样,只是将我们之前自定义的侧滑菜单换成 Navigationview。
<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/dl_nav"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.ginkwang.drawertest.NavActivity">
<FrameLayout
android:id="@+id/fl_nav_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="NavigationView 抽屉菜单"
android:textColor="@android:color/black"
android:textSize="25sp"/>
</FrameLayout>
<android.support.design.widget.NavigationView
android:id="@+id/nav_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="start"
app:menu="@menu/nav_menu"
app:headerLayout="@layout/layout_nav_header"/>
</android.support.v4.widget.DrawerLayout>
然后在代码中获取到 Navigationview,并设置其 menu 的点击事件:
private NavigationView mNavView;
...
mNavView = (NavigationView) findViewById(R.id.nav_view);
mNavView.setCheckedItem(R.id.item_nav_menu_news);
mNavView.setNavigationItemSelectedListener(this);
...
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
Toast.makeText(NavActivity.this, item.getTitle(), Toast.LENGTH_LONG).show();
mDlNav.closeDrawers();
return true;
}
逻辑也十分简单,就是打开抽屉之后,点击某一项 menu,会将抽屉关闭,然后提示你当前操作哪项的吐司。其中
mNavView.setCheckedItem(R.id.item_nav_menu_news);
表示设置 menu 的默认选中状态。
最后效果图:
NavigationView 抽屉菜单到这里,不知道大家看出一个问题没有,之前我们定义的 menu item 的 icon 是项目中自带的 ic_launcher。它的本身面目是这样的:
ic_launcher.png但是在效果图中,却显示成了灰色的。
对于这个问题,有两个解决办法:
在 Navigationview 的布局中添加 app:itemIconTint="@color/colorPrimary" 属性,颜色值自定义。
但是这样设置后,还只是能显示纯色,那如果非要显示原本的图片该怎么设置呢?让我们来看第二个解决方法;
在代码中获取到 Navigationview 后,设置其属性
mNavView.setItemIconTintList(null);
menu 颜色2
NavigationView 添加分割线
有时候,侧滑菜单的 item 太多,需要将其按类划分,设置分割线以及类标注以作明示。
这时,我们只需要将相应的 item 规划在一个 group 内即可。看代码:
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<group android:checkableBehavior="single">
<item
android:id="@+id/item_nav_menu_news"
android:icon="@mipmap/ic_launcher"
android:title="@string/nav_menu_news"/>
<item
android:id="@+id/item_nav_menu_star"
android:icon="@mipmap/ic_launcher"
android:title="@string/nav_menu_star"/>
</group>
<item android:title="@string/nav_menu_title_common">
<menu>
<group android:checkableBehavior="single">
<item
android:id="@+id/item_nav_menu_share"
android:icon="@mipmap/ic_launcher"
android:title="@string/nav_menu_share"/>
<item
android:id="@+id/item_nav_menu_set"
android:icon="@mipmap/ic_launcher"
android:title="@string/nav_menu_set"/>
</group>
</menu>
</item>
<item android:title="@string/nav_menu_title_other">
<menu>
<group android:checkableBehavior="single">
<item
android:id="@+id/item_nav_menu_send"
android:icon="@mipmap/ic_launcher"
android:title="@string/nav_menu_send"/>
</group>
</menu>
</item>
</menu>
如果只要分割线的话,只需要将 item 放在 group 内即可。如果想要副标题指示的话,就需要将其中的 item 规划在一个新的 item 中,并指定其 title 属性。看效果:
分割线NavigationView 头布局(HeaderLayout)点击事件
上面代码中已经介绍了 menu 的点击事件,只需要在代码中获取到 menu 菜单,然后重写 onNavigationItemSelected 即可。
头布局的点击事件的获取和 menu 的一样,也是在代码中先获取到头布局的 view,然后调用头布局 view 的 findViewById() 获取头布局里面的控件,然后获取到控件的点击事件,看代码:
private View mHeaderView;
private ImageView mIvHeaderImg;
...
mHeaderView = mNavView.getHeaderView(0);//获取头布局
mIvHeaderImg = (ImageView) mHeaderView.findViewById(R.id.iv_nav_img);//获取头布局内 imageview 控件
mIvHeaderImg.setOnClickListener(this);
...
@Override
public void onClick(View pView) {
Toast.makeText(NavActivity.this, "点击 ImageView", Toast.LENGTH_LONG).show();
}
整体逻辑也十分简单,就是其中获取头布局的代码 getHeaderView(0) ,可能会有人不理解。
其实 Navigationview 本质上是一个 RecyclerView,头布局通常是第一个元素,所以获取头布局的方式自然是 getHeaderView(0)。
注意
如果你的 buildToolsVersion 版本是 23.1.0,那获取头布局的代码为:
View headerLayout =
navigationView.inflateHeaderView(R.layout.navigation_header);
参考:
NavigationView获取Header View的问题
结合 ToolBar,一同构建侧滑菜单
写到这里,滑动菜单的实现的大部分知识点都已经涵盖在内了,但是还有一个问题,就是前文中的例子滑动菜单的出现都是我们用手滑动屏幕边缘才出来的(废话!所以叫滑动菜单啊!)。我的意思是,即诶岸上没有一个提示功能让人知道滑动菜单的存在。Material Design 的建议是在 Toolbar 的左边添加一个导航按钮,点击按钮抽屉就会出现。这相当于给用户两种打开抽屉的方式,防止一些用户不知道抽屉的存在。
接下来我们就来实现这个功能。
添加 Toolbar
我们还是在 NavigationView 的示例项目中操作,还不了解 Toolbar 的朋友请先看一下这篇文章 Android 笔记 —— Toolbar 使用总结,布局中添加 Toolbar 的代码就不贴了,直接看 Activity 中修改的逻辑。
mToolbar = (Toolbar) findViewById(R.id.tb_nav_bar);
setSupportActionBar(mToolbar);//添加 Toolbar
...
ActionBar lActionBar = getSupportActionBar();
if (lActionBar != null) {
lActionBar.setDisplayHomeAsUpEnabled(true);//显示返回按钮
lActionBar.setHomeAsUpIndicator(R.mipmap.ic_toolbar_menu);//替换返回按钮图标,改为导航按钮
}
...
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
mDlNav.openDrawer(Gravity.START);
break;
}
return true;
}
我们先是将 Toolbar 添加进来并获取,然后通过 getSupportActionBar() 获取到 ActionBar,这里 ActionBar 的具体实现是由 Toolbar 来完成的。
之后通过 ActionBar.setDisplayHomeAsUpEnabled(true) 显示 Toolbar 的返回按钮,再替换但会按钮的图标样式,改为导航图标。
这时,你点击导航按钮的话,菜单还是不会出现,而是返回到上一个活动,本质上它现在还是一个返回按钮。
接着我们再替换它的功能,通过重写 onOptionsItemSelected 方法,根据 item 的 id 获取到返回按钮并对其进行处理,HomeAsUp 的按钮 id 是 android.R.id.home。
到此,我们就完成了 Toolbar 添加导航按钮进行抽屉打开的操作了。看一下效果:
抽屉不遮挡 Toolbar
前面我们介绍与演示的都是抽屉拉出后遮挡住 Toolbar 的模式,那相信你也见到或使用过一些 APP ,就是抽屉拉出后,不遮挡 Toolbar,然后 Toolbar 上的导航按钮随着抽屉的拉出状态会变成返回按钮。类似下面这样:
抽屉不遮挡 Toolbar
接下来我们就一起来实现这样的效果吧!
首先,
布局:
<LinearLayout
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"
android:orientation="vertical">
<android.support.v7.widget.Toolbar
android:id="@+id/tb_nav_buttom"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:theme="@style/ToolbarTheme"/>
<android.support.v4.widget.DrawerLayout
android:id="@+id/dl_nav_buttom"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="抽屉不遮挡 Toolbar"
android:textColor="@android:color/black"
android:textSize="25sp"/>
</FrameLayout>
<android.support.design.widget.NavigationView
android:id="@+id/nv_buttom"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="start"
app:headerLayout="@layout/layout_nav_header"
app:menu="@menu/nav_menu"/>
</android.support.v4.widget.DrawerLayout>
</LinearLayout>
整个布局的实现逻辑也很简单,就是将 Toolbar 从 DrawerLayout 中隔离出去,放在 DrawerLayout 上面,最外层是一个相对布局,方向是从上至下的。
然后 Toolbar 中有一个设置布局的属性,我们来看一下里面定义了什么:
<style name="ToolbarTheme" parent="AppTheme">
<!--左边导航按钮颜色-->
<item name="android:textColorSecondary">@color/colorNav</item>
</style>
只是设置了一个导航按钮的颜色,然后将项目父布局的属性都继承下来。
代码:
来看一下代码中都有什么:
public class NavButtomActivity extends AppCompatActivity {
private DrawerLayout mDlNavButtom;
private Toolbar mToolbar;
private ActionBarDrawerToggle mActionBarDrawerToggle;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_nav_buttom_toolbar);
initView();
}
private void initView() {
mToolbar = (Toolbar) findViewById(R.id.tb_nav_buttom);
mToolbar.setTitle("抽屉不遮挡 Toolbar");//设置标题
mToolbar.setTitleTextColor(getResources().getColor(R.color.colorText));//标题颜色
setSupportActionBar(mToolbar);
mDlNavButtom = (DrawerLayout) findViewById(R.id.dl_nav_buttom);
mActionBarDrawerToggle = new ActionBarDrawerToggle(this, mDlNavButtom, mToolbar, 0, 0);
mDlNavButtom.setDrawerListener(mActionBarDrawerToggle);
mActionBarDrawerToggle.syncState();//ActionBarDrawerToggle与DrawerLayout的状态同步,
// 并将ActionBarDrawerToggle中的drawer图标,设置为ActionBar的Home-Button的icon
}
}
可以看到,我们没有按照前文示例中那样,将 Toolbar 的返回按钮替换为我们自己设置的 icon,而是使用 ActionBarDrawerToggle 的默认图标。
并调用 ActionBarDrawerToggle.syncState() 方法将 ActionBarDrawerToggle 与DrawerLayout 的状态同步,最终实现的效果是这样:
结束
至此,侧滑菜单(抽屉)的大部分用法已经介绍完了。当然,如果想要在项目中使用抽屉的话,远没有这样复杂,只需要构建工程时选择 Navigation Drawer Activity 即可。另外,Github 上面也有更多功能更强大的关于抽屉的开源库,可供大家选择。
网友评论