前面几篇文章我们学习了Fragment+ViewPager+Bottom导航按钮实现的典型页面结构( Fragment+ViewPager+BottomNavigationView实现页面导航),从实践中对Fragment的应用有了更深的认识。本文我们再补充一种典型的,常见的又实用的页面结构:侧滑菜单式页面布局。同样是对Fragment的一种应用,结合控件DrawerLayout、NavigationView、ActionBarDrawerToggle实现典型的侧滑菜单式页面。
1. 目标效果

2. 案例教学
2.1 主界面布局
之前网上有各种自定义的控件方案来实现侧滑菜单,虽然可以实现,但五花八门复杂难懂。后来官方结合网上各种方案,统一出了专门的侧滑菜单控件:DrawerLayout,抽屉布局。顾名思义,就是像抽屉一样,从侧面抽出一个菜单页面。
我们要实现的效果是顶部带有ActionBar,侧滑菜单在ActionBar之下的,因为这种情况我们可以看到左上角有个菜单打开/关闭的图标。这个图标通常在关闭的时候是三条线,打开的时候自动变成箭头,并且这个过程有一个比较丝滑的动画转变效果。相信大家都见过这种效果,网上也有一些想自己手写动画实现的,特别费劲。其实系统已经帮我们实现了,我们只需要引用即可轻松实现。
activity_drawer.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/Theme.FragmentBottomTab1.AppBarOverlay">
<androidx.appcompat.widget.Toolbar
android:id="@+id/tool_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
app:popupTheme="@style/Theme.FragmentBottomTab1.PopupOverlay" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.drawerlayout.widget.DrawerLayout
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".fragment_test.drawerlayout.DrawerActivity"
tools:openDrawer="start">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fcv_drawer_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.google.android.material.navigation.NavigationView
android:id="@+id/nv"
android:layout_width="200dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:clickable="true"
app:headerLayout="@layout/drawer_header_layout"
app:menu="@menu/drawer_nav_menu" />
</androidx.drawerlayout.widget.DrawerLayout>
</LinearLayout>
整体还是上下结构,最上面是ActionBar,这里我们使用AppBarLayout+Toolbar来实现,当作我们的ActionBar。下面是重头戏侧滑菜单和主体内容,用DrawerLayout实现。
- AppBarLayout+Toolbar部分
需要注意的就是一些主题资源了。比如android:theme="@style/Theme.FragmentBottomTab1.AppBarOverlay"、android:background="?attr/colorPrimary"、app:popupTheme="@style/Theme.FragmentBottomTab1.PopupOverlay"。
在themes.xml里添加
<style name="Theme.FragmentBottomTab1.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
<style name="Theme.FragmentBottomTab1.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
- DrawerLayout部分
DrawerLayout是一种容器控件,我们只需要把菜单和主体页面放到里面即可。菜单和主体页面分别用NavigationView和FragmentContainerView承接。他们放置的先后顺序没有关系,决定他们摆放位置的是NavigationView的属性android:layout_gravity="start",表示NavigationView在抽屉的左侧,那么剩下的FragmentContainerView自然就是在右侧充当主体内容部分了。
再来看一下其他的一些细节。
-
tools:openDrawer=“start”
tools开头的这个命名空间表示的是只在代码编写阶段用来预览的属性,运行时不生效,因此这个属性只是为了让我们在预览界面里看到当前抽屉打开的效果。 -
app:menu="@menu/drawer_nav_menu"
侧滑菜单的菜单项部分,与上节文章里的底部导航菜单类似,只需要用menu文件就可以定义出菜单项了。drawer_nav_menu.xml 如下:
<?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:icon="@drawable/selector_home"
android:title="首页" />
<item
android:id="@+id/menu_find"
android:icon="@drawable/selector_find"
android:title="发现" />
<item
android:id="@+id/menu_mine"
android:icon="@drawable/selector_mine"
android:title="我的" />
</group>
</menu>
这里值得注意的是我们把三个菜单项用group标签包裹了起来,并且添加了属性android:checkableBehavior="single"。这表示这三个菜单项说一组,并且每次只会有一个被选中,自动帮我们完成了菜单项选中时的高亮效果。运行后你就会体会到它的方便,好用。
- app:headerLayout="@layout/drawer_header_layout"
这部分是侧滑菜单的头部部分,也就是常见的会有一个头像昵称之类的布局。这里就简单写一个,图片资源大家可以随便找一个就行了。
drawer_header_layout.xml
<?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:gravity="center"
android:orientation="vertical"
android:padding="20dp">
<ImageView
android:id="@+id/iv_avatar"
android:layout_width="100dp"
android:layout_height="100dp"
android:scaleType="centerCrop"
android:src="@drawable/test1" />
<TextView
android:id="@+id/tv_user_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="user"
android:textSize="20sp" />
</LinearLayout>
这样我们的布局就做好了。
2.2 准备Fragment
跟之前一样,还是用来演示的Fragment,直接贴代码吧。(实际应用中,这里你可以准备比较复杂的各种Fragment)
ExampleFragment.java:
public class ExampleFragment extends Fragment {
// 改为public,以便于外界能引用到
public static final String ARG_PARAM1 = "param1";
public static final String ARG_PARAM2 = "param2";
private String mParam1;
private String mParam2;
private TextView mTvContent;
public ExampleFragment() {
}
public static ExampleFragment newInstance(String param1, String param2) {
ExampleFragment fragment = new ExampleFragment();
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);
mTvContent = view.findViewById(R.id.tv_content);
// 如果传入的参数有值,则设置内容
if (!TextUtils.isEmpty(mParam1)) {
mTvContent.setText(mParam1);
}
}
}
另外,为了整个效果做的更完善些,我们把第一个页面,也就是首页也做成前文那种多栏目页面切换的效果。可直接复用前文的代码,详情见前文吧,不再赘述。
HomeFragment.java:
public class HomeFragment extends Fragment {
private static final String ARG_PARAM1 = "param1";
private static final String ARG_PARAM2 = "param2";
private String mParam1;
private String mParam2;
private ViewPager mViewPager;
private TabLayout mTabLayout;
private List<Fragment> mFragmentList;
private List<String> mTitleList;
private MyViewPagerAdapterForFragment mPagerAdapterForFragment;
public HomeFragment() {
}
public static HomeFragment newInstance(String param1, String param2) {
HomeFragment fragment = new HomeFragment();
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_home, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
initView();
initData();
initEvent();
}
private void initView() {
mViewPager = getView().findViewById(R.id.home_vp);
mTabLayout = getView().findViewById(R.id.home_tab_layout);
}
private void initData() {
mTitleList = new ArrayList<>();
mTitleList.add("热点");
mTitleList.add("娱乐");
mTitleList.add("历史");
mTitleList.add("科技");
mTitleList.add("体育");
mTitleList.add("地理");
mTitleList.add("新奇");
mTitleList.add("美图");
mFragmentList = new ArrayList<>();
for (int i = 0; i < mTitleList.size(); i++) {
SubFragment subFragment = SubFragment.newInstance("fragment" + i, "");
mFragmentList.add(subFragment);
}
}
private void initEvent() {
mPagerAdapterForFragment = new MyViewPagerAdapterForFragment(getChildFragmentManager(), mFragmentList, mTitleList);
mViewPager.setAdapter(mPagerAdapterForFragment);
mViewPager.setCurrentItem(0);
mTabLayout.setupWithViewPager(mViewPager);
}
}
fragment_home.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.tabs.TabLayout
android:id="@+id/home_tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabGravity="start"
app:tabMode="auto" />
<androidx.viewpager.widget.ViewPager
android:id="@+id/home_vp"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
用到的Adapter: MyViewPagerAdapterForFragment.java
public class MyViewPagerAdapterForFragment extends FragmentPagerAdapter {
private List<Fragment> mFragmentList;
private List<String> mTitleList;
public MyViewPagerAdapterForFragment(@NonNull FragmentManager fm, List<Fragment> fragmentList) {
super(fm);
mFragmentList = fragmentList;
}
public MyViewPagerAdapterForFragment(@NonNull FragmentManager fm, List<Fragment> fragmentList, List<String> titleList) {
super(fm);
mFragmentList = fragmentList;
mTitleList = titleList;
}
@NonNull
@Override
public Fragment getItem(int position) {
return mFragmentList == null ? null : mFragmentList.get(position);
}
@Override
public int getCount() {
return mFragmentList == null ? 0 : mFragmentList.size();
}
@Nullable
@Override
public CharSequence getPageTitle(int position) {
return mTitleList == null ? super.getPageTitle(position) : mTitleList.get(position);
}
}
2.3 主界面Activity
来看主体代码部分,首先还是控件声明、初始化。
public class DrawerActivity extends AppCompatActivity {
private NavigationView mNavigationView;
private DrawerLayout mDrawerLayout;
private ActionBarDrawerToggle mDrawerToggle;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_drawer);
initView();
}
private void initView() {
mDrawerLayout = findViewById(R.id.drawer_layout);
mNavigationView = findViewById(R.id.nv);
Toolbar toolbar = findViewById(R.id.tool_bar);
setSupportActionBar(toolbar);
mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
}
}
主要的三个控件:mDrawerLayout、mNavigationView、mDrawerToggle
前两个直接findViewById常规初始化即可,最后一个就是我们上面提到的,可以随着抽屉菜单打开/关闭而变换图标的一个按钮。它的初始化如代码中所示,传入了mDrawerLayout、toolbar,这样就把三者关联起来了。它本身是在toolbar上,点击后会与mDrawerLayout联动。最后两个参数就是两个描述打开/关闭的字符串,随便写两个即可,比如在strings.xml里添加:
<string name="navigation_drawer_open">Open navigation drawer</string>
<string name="navigation_drawer_close">Close navigation drawer</string>
然后,我们来对侧滑菜单的打开/关闭做一些处理。
在onCreate方法里继续添加initEvent方法
private void initEvent() {
mDrawerLayout.addDrawerListener(mDrawerToggle);
// 同步抽屉按钮与抽屉的状态,也就是当抽屉开的时候,及时更新按钮状态
mDrawerToggle.syncState();
mNavigationView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_home:
getSupportFragmentManager().beginTransaction().replace(R.id.fcv_drawer_container, HomeFragment.class, null).commit();
break;
case R.id.menu_find:
ExampleFragment findFragment = ExampleFragment.newInstance("这是FindFragment", "");
getSupportFragmentManager().beginTransaction().replace(R.id.fcv_drawer_container, findFragment).commit();
break;
case R.id.menu_mine:
ExampleFragment mineFragment = ExampleFragment.newInstance("这是MineFragment", "");
getSupportFragmentManager().beginTransaction().replace(R.id.fcv_drawer_container, mineFragment).commit();
break;
default:
break;
}
// 设置测滑菜单某项被选中
mNavigationView.setCheckedItem(item.getItemId());
// 选中某项后,抽屉要关闭
if (mDrawerLayout.isOpen()) {
mDrawerLayout.close();
}
return false;
}
});
// 默认设置菜单选中首页
setHomeFragment();
}
private void setHomeFragment() {
// 设置测滑菜单首页项被选中
mNavigationView.setCheckedItem(R.id.menu_home);
// 设置当前主体内容为首页
getSupportFragmentManager().beginTransaction().replace(R.id.fcv_drawer_container, HomeFragment.class, null).commit();
}
之前我们只是将左上角抽屉按钮mDrawerToggle创建了出来,但还没有真正建立与抽屉的绑定。通过mDrawerLayout.addDrawerListener(mDrawerToggle);,使得mDrawerToggle监听了抽屉的开关状态。再进一步调用mDrawerToggle.syncState();,使得抽屉按钮与抽屉的状态达到同步。
接着,我们还需要处理菜单项的事件,也就是点击某项需要替换主体页面为对应Fragment。为mNavigationView设置选中监听,在回调方法中,通过选中的ID,切换不同的Fragment。有了前文Fragment操作的基础,相信这些代码就是洒洒水啦~
注意到我们第一个页面复用了HomeFragment,其余两个为了演示还是用的ExampleFragment。
页面切换后,还需要处理两个小细节:
1.菜单项选中需要设置mNavigationView.setCheckedItem(item.getItemId());,这样再次打开菜单,选中的那一项才会高亮。
2.选中某项后,抽屉要关闭。这个需要手动调用mDrawerLayout.close();
最后,还是需要设置默认选中的页面。在setHomeFragment方法中,先设置测滑菜单首页项被选中,再将主体页面替换为首页。
3. 完整代码
最后附上主Activity代码: DrawerActivity.java
public class DrawerActivity extends AppCompatActivity {
private NavigationView mNavigationView;
private DrawerLayout mDrawerLayout;
private ActionBarDrawerToggle mDrawerToggle;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_drawer);
initView();
initEvent();
}
private void initEvent() {
mDrawerLayout.addDrawerListener(mDrawerToggle);
// 同步抽屉按钮与抽屉的状态,也就是当抽屉开的时候,及时更新按钮状态
mDrawerToggle.syncState();
mNavigationView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_home:
getSupportFragmentManager().beginTransaction().replace(R.id.fcv_drawer_container, HomeFragment.class, null).commit();
break;
case R.id.menu_find:
ExampleFragment findFragment = ExampleFragment.newInstance("这是FindFragment", "");
getSupportFragmentManager().beginTransaction().replace(R.id.fcv_drawer_container, findFragment).commit();
break;
case R.id.menu_mine:
ExampleFragment mineFragment = ExampleFragment.newInstance("这是MineFragment", "");
getSupportFragmentManager().beginTransaction().replace(R.id.fcv_drawer_container, mineFragment).commit();
break;
default:
break;
}
// 设置测滑菜单某项被选中
mNavigationView.setCheckedItem(item.getItemId());
// 选中某项后,抽屉要关闭
if (mDrawerLayout.isOpen()) {
mDrawerLayout.close();
}
return false;
}
});
// 默认设置菜单选中首页
setHomeFragment();
}
private void setHomeFragment() {
// 设置测滑菜单首页项被选中
mNavigationView.setCheckedItem(R.id.menu_home);
// 设置当前主体内容为首页
getSupportFragmentManager().beginTransaction().replace(R.id.fcv_drawer_container, HomeFragment.class, null).commit();
}
private void initView() {
mDrawerLayout = findViewById(R.id.drawer_layout);
mNavigationView = findViewById(R.id.nv);
Toolbar toolbar = findViewById(R.id.tool_bar);
setSupportActionBar(toolbar);
mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
// mDrawerLayout.post(new Runnable() {
// @Override
// public void run() {
// mDrawerToggle.syncState();
// }
// });
}
}
4. 总结
到此,整个Fragment+DrawerLayout+NavigationView实现侧滑菜单页面结构的案例就结束了。到本篇文章为止,我们对Android Fragment的基础内容+基础应用的系列文章也基本介绍完了。总结下,我们本系列文章介绍了以下知识点:
- Fragment的概念、认识
- Fragment的创建、移除、查找、替换、显示/隐藏等操作
- Fragment的数据传递
- Fragment的后台栈的认识
- 如何使用adb命令查看Fragment情况
- Fragment的应用:
- 实现底部导航页面结构
- Fragment+BottomTab实现页面导航
- Fragment+ViewPager+BottomTab实现页面导航
- Fragment+ViewPager+BottomNavigationView实现页面导航
- Fragment+ChildFragment+ViewPager+BottomNavigationView实现复杂页面导航
- 实现侧滑菜单页面结构
- Fragment+DrawerLayout+NavigationView实现侧滑菜单页面结构
- 实现底部导航页面结构
感谢各位支持,希望这些基础内容能为初学者提供一些指引和帮助!
本文通过Fragment、DrawerLayout和NavigationView详细讲解如何构建一个带有侧滑菜单的页面结构。主要内容包括:设置AppBarLayout+Toolbar,使用DrawerLayout管理菜单和内容区域,使用NavigationView创建菜单项,以及处理菜单项点击事件和页面切换。最后提供了完整代码示例。
579

被折叠的 条评论
为什么被折叠?



