【Android】DrawerLayout+NavigationView实现侧滑菜单页面
在 Android 开发中,侧滑菜单是一个非常常见的用户界面模式,它能够在屏幕的一侧显示一个导航菜单,允许用户通过滑动手势或点击按钮来访问不同的应用功能。本文将介绍如何使用 DrawerLayout
和 NavigationView
来实现这一功能。
效果展示
先来看看这两个组件的结合可以实现什么效果吧!
基本概念
1. DrawerLayout
DrawerLayout
是 Android 中一种强大的布局容器,它为开发者提供了在主屏幕一侧或两侧放置可滑动视图的能力,这种滑动视图通常被称为抽屉(Drawer)。抽屉通常用于放置导航菜单,但也可以用于其他类型的内容,如工具面板或设置菜单。
主要特性和功能
- 多层视图结构:
DrawerLayout
通常包含两个或多个子视图,其中一个是主要内容区域(通常是应用的主要 UI),另一个或多个是抽屉内容。抽屉视图可以位于屏幕的左侧、右侧或两侧。 - 抽屉操作: 抽屉可以通过手势(例如从屏幕边缘滑动)或编程方式打开和关闭。开发者可以通过监听抽屉状态的变化(如打开、关闭、拖动)来执行特定的操作。
- 可自定义的抽屉: 开发者可以完全自定义抽屉的内容和外观,包括其宽度、背景颜色、内容布局等。
- 兼容性和响应性:
DrawerLayout
设计时考虑到了不同的屏幕尺寸和方向,因此可以适应不同的设备和屏幕状态,如平板设备和横屏模式。
2. NavigationView
NavigationView
是一个专门用于在 DrawerLayout
中实现导航菜单的视图组件。它简化了侧边菜单的创建,并提供了丰富的功能和自定义选项。
主要特性和功能
- 菜单项配置:
NavigationView
使用菜单资源文件(XML 格式)来定义其内容。通过菜单资源文件,开发者可以轻松地配置菜单项的 ID、标题、图标等属性,并可以将菜单项分组,支持单选和多选行为。 - 菜单头部视图:
NavigationView
可以包含一个头部视图,通常用于显示用户信息或应用的标识。头部视图的布局可以完全自定义,支持包括头像、用户名等元素。 - 易于集成:
NavigationView
可以轻松地与DrawerLayout
结合使用。通过设置android:layout_gravity="start"
或android:layout_gravity="end"
属性,开发者可以将导航视图放置在抽屉的左侧或右侧。 - 样式和主题支持:
NavigationView
支持 Material Design 风格,并提供了多种自定义选项,如背景颜色、选中项的颜色等。开发者可以通过自定义样式和主题来改变其外观,使其与应用的整体风格保持一致。 - 事件处理:
NavigationView
提供了一个简单的回调接口NavigationView.OnNavigationItemSelectedListener
,用于处理菜单项的选择事件。开发者可以在这个接口中定义菜单项点击时的行为,如切换 Fragment、启动新 Activity 等。
实现步骤
1. 布局文件
首先,我们需要在布局文件中定义 DrawerLayout
和 NavigationView
的结构:
<?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">
<androidx.appcompat.widget.Toolbar
android:id="@+id/tool_bar"
android:layout_width="match_parent"
android:layout_height="50dp" />
<androidx.drawerlayout.widget.DrawerLayout
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:openDrawer="start"
tools:context=".MainActivity">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fg_container"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<com.google.android.material.navigation.NavigationView
android:id="@+id/nv_view"
android:layout_width="300dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:focusable="true"
app:headerLayout="@layout/drawer_header_layout"
app:menu="@menu/menu_botton"/>
</androidx.drawerlayout.widget.DrawerLayout>
</LinearLayout>
在这个布局文件中,DrawerLayout
包含一个 FragmentContainerView
作为主内容区域,以及一个 NavigationView
作为侧滑菜单。
NavigationView的这两项是需要我们自己去配置的,分别对应上下两部分:
有一点需要注意和强调一下Fragment的容器布局一定要放在上面,否则会被菜单栏盖过导致无法修改Fragment。
2. 配置菜单资源文件与头部资源文件
我们使用一个菜单资源文件来定义 NavigationView
中的菜单项:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<group android:checkableBehavior="single">
<item
android:id="@+id/menu_home"
android:title="Home"/>
<item
android:id="@+id/menu_find"
android:title="Find"/>
<item
android:id="@+id/menu_mine"
android:title="Mine"/>
</group>
</menu>
在这个菜单文件中,我们定义了三个菜单项:Home
、Find
和 Mine
。
头部资源文件如下:
<?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="wrap_content"
android:orientation="vertical"
android:gravity="center">
<ImageView
android:layout_width="100dp"
android:layout_height="100dp"
android:src="#CD1010"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="user"
android:textSize="30dp"/>
</LinearLayout>
大概长这样(如上)。
3. Fragment 实现
每个菜单项对应一个 Fragment
,这里是一个简单的 FragmentExample
的实现:
public class FragmentExample extends Fragment {
private TextView mTextView;
private static final String ARG_PARAM1 = "param1";
private static final String ARG_PARAM2 = "param2";
private String mParam1;
private String mParam2;
public FragmentExample() {
// Required empty public constructor
}
public static FragmentExample newInstance(String param1, String param2) {
FragmentExample fragment = new FragmentExample();
Bundle args = new Bundle();
args.putString(ARG_PARAM1, param1);
args.putString(ARG_PARAM2, param2);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
mParam1 = getArguments().getString(ARG_PARAM1);
mParam2 = getArguments().getString(ARG_PARAM2);
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_example, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mTextView = view.findViewById(R.id.tv_frag);
mTextView.setText(mParam1);
}
}
这个 Fragment
类通过 newInstance
方法接受参数,并在 onViewCreated
方法中设置文本视图的内容。
4. 主活动代码
在主活动中,我们需要设置 DrawerLayout
和 NavigationView
,并处理导航项的点击事件:
public class MainActivity extends AppCompatActivity {
private DrawerLayout mDrawerLayout;
private NavigationView mNavigationView;
private Toolbar mToolbar;
private ActionBarDrawerToggle mDrawerToggle;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mDrawerLayout = findViewById(R.id.drawer_layout);
mNavigationView = findViewById(R.id.nv_view);
mToolbar = findViewById(R.id.tool_bar);
setSupportActionBar(mToolbar);
mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout, mToolbar, R.string.drawer_open, R.string.drawer_close);
mDrawerLayout.addDrawerListener(mDrawerToggle);
mDrawerToggle.syncState();
setHomePage();
mNavigationView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
int itemId = menuItem.getItemId();
if (itemId == R.id.menu_home) {
FragmentExample homeFragment = FragmentExample.newInstance("home","");
getSupportFragmentManager().beginTransaction()
.replace(R.id.fg_container, homeFragment)
.commit();
} else if (itemId == R.id.menu_find) {
FragmentExample findFragment = FragmentExample.newInstance("find","");
getSupportFragmentManager().beginTransaction()
.replace(R.id.fg_container, findFragment)
.commit();
} else if (itemId == R.id.menu_mine) {
FragmentExample mineFragment = FragmentExample.newInstance("mine","");
getSupportFragmentManager().beginTransaction()
.replace(R.id.fg_container, mineFragment)
.commit();
}
if (mDrawerLayout.isDrawerOpen(GravityCompat.START)) {
mDrawerLayout.close();
}
return true;
}
});
}
private void setHomePage() {
FragmentExample homeFragment = FragmentExample.newInstance("home","");
getSupportFragmentManager().beginTransaction()
.replace(R.id.fg_container, homeFragment)
.commit();
mNavigationView.setCheckedItem(R.id.menu_home);
}
}
在这个代码中,我们初始化了 DrawerLayout
、NavigationView
和 Toolbar
,并设置了一个 ActionBarDrawerToggle
来处理菜单的开关。我们还设置了一个 NavigationItemSelectedListener
来处理菜单项的点击事件,根据选中的菜单项替换 FragmentContainerView
中的内容。
1. 类与成员变量
public class MainActivity extends AppCompatActivity {
private DrawerLayout mDrawerLayout;
private NavigationView mNavigationView;
private Toolbar mToolbar;
private ActionBarDrawerToggle mDrawerToggle;
}
- 成员变量
mDrawerLayout
、mNavigationView
、mToolbar
分别代表抽屉布局、导航视图和工具栏。 mDrawerToggle
是一个ActionBarDrawerToggle
对象,用于同步抽屉的开闭状态与工具栏的图标变化。
2. onCreate
方法
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mDrawerLayout = findViewById(R.id.drawer_layout);
mNavigationView = findViewById(R.id.nv_view);
mToolbar = findViewById(R.id.tool_bar);
setSupportActionBar(mToolbar);
mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout, mToolbar, R.string.drawer_open, R.string.drawer_close);
mDrawerLayout.addDrawerListener(mDrawerToggle);
mDrawerToggle.syncState();
setHomePage();
mNavigationView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
int itemId = menuItem.getItemId();
if (itemId == R.id.menu_home) {
FragmentExample homeFragment = FragmentExample.newInstance("home","");
getSupportFragmentManager().beginTransaction()
.replace(R.id.fg_container, homeFragment)
.commit();
} else if (itemId == R.id.menu_find) {
FragmentExample findFragment = FragmentExample.newInstance("find","");
getSupportFragmentManager().beginTransaction()
.replace(R.id.fg_container, findFragment)
.commit();
} else if (itemId == R.id.menu_mine) {
FragmentExample mineFragment = FragmentExample.newInstance("mine","");
getSupportFragmentManager().beginTransaction()
.replace(R.id.fg_container, mineFragment)
.commit();
}
if (mDrawerLayout.isDrawerOpen(GravityCompat.START)) {
mDrawerLayout.close();
}
return true;
}
});
}
主要功能和实现步骤:
- 视图绑定:
mDrawerLayout = findViewById(R.id.drawer_layout);
通过 ID 获取DrawerLayout
的引用。mNavigationView = findViewById(R.id.nv_view);
获取NavigationView
的引用。mToolbar = findViewById(R.id.tool_bar);
获取Toolbar
的引用。
- 设置工具栏:
setSupportActionBar(mToolbar);
设置Toolbar
作为活动的操作栏。
- ActionBarDrawerToggle:
mDrawerToggle = new ActionBarDrawerToggle(...)
创建ActionBarDrawerToggle
对象,用于控制DrawerLayout
的开闭以及工具栏图标的同步。mDrawerLayout.addDrawerListener(mDrawerToggle);
添加mDrawerToggle
作为DrawerLayout
的监听器。mDrawerToggle.syncState();
同步DrawerLayout
的状态与ActionBar
。
- 设置首页:
- 调用
setHomePage()
方法,将首页设置为默认页面。
- 调用
- 处理导航项选择事件:
- 使用
mNavigationView.setNavigationItemSelectedListener(...)
设置导航项的选择监听器。 - 根据选中的菜单项 ID (
itemId
),加载相应的 Fragment 到FragmentContainerView
(R.id.fg_container
)。 - 关闭抽屉:如果抽屉是打开状态,则在选择后关闭它。
- 使用
3. setHomePage
方法
private void setHomePage() {
FragmentExample homeFragment = FragmentExample.newInstance("home","");
getSupportFragmentManager().beginTransaction()
.replace(R.id.fg_container, homeFragment)
.commit();
mNavigationView.setCheckedItem(R.id.menu_home);
}
功能和实现:
- 创建一个
FragmentExample
实例作为首页的 Fragment,并传入参数 “home”。 - 使用
FragmentManager
进行 Fragment 的事务操作,replace
方法用新创建的 Fragment 替换R.id.fg_container
中的内容。 - 调用
commit()
提交事务。 - 设置
mNavigationView
的当前选中项为R.id.menu_home
,表示用户当前在首页。
注意事项
因为我们创建出的项目默认存在toolbar,我们需要取消默认的toolbar。
想实现这个也很简单,在theme中重新创建一个即可。
<style name="MyNoActionBar" parent="Base.Theme.DrawerLayoutTest">
<item name="android:windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
android:windowActionBar
: 这个属性决定了是否显示 ActionBar(即工具栏)。
false
: 设置为 false
表示在这个主题下,ActionBar 将被隐藏。这通常用于不需要显示 ActionBar 的界面,例如使用自定义工具栏或没有工具栏的界面。
windowNoTitle
: 这个属性决定了窗口是否显示标题。
true
: 设置为 true
表示窗口将没有标题。通常与 ActionBar
的显示状态结合使用,用于去掉默认的标题栏或 ActionBar。
结语
通过以上步骤,我们实现了一个完整的侧滑菜单界面。DrawerLayout
和 NavigationView
提供了强大的功能,允许我们轻松地实现复杂的导航布局。
本文参考:
DrawerLayout | Android Developers
不需要显示 ActionBar 的界面,例如使用自定义工具栏或没有工具栏的界面。
windowNoTitle
: 这个属性决定了窗口是否显示标题。
true
: 设置为 true
表示窗口将没有标题。通常与 ActionBar
的显示状态结合使用,用于去掉默认的标题栏或 ActionBar。
结语
通过以上步骤,我们实现了一个完整的侧滑菜单界面。DrawerLayout
和 NavigationView
提供了强大的功能,允许我们轻松地实现复杂的导航布局。
本文参考:
DrawerLayout | Android Developers
NavigationView | Android Developers