文章目录
上节我们介绍了引入BottomNavigationView实现页面导航的方法,大大简化了底部导航按钮的实现,详见: Fragment+ViewPager+BottomNavigationView实现页面导航本节我们将对Fragment+ViewPager+BottomNavigationView这种页面结构进一步丰富,引入 子Fragment,实现外层页面导航的前提下,首页内部又有 多个栏目页面的结构。
1. 目标效果
2. 案例教学
本节内容依然是在上节Fragment+ViewPager+BottomNavigationView实现页面导航基础上的进一步完善,细节参考上节。
2.1 主界面布局
与上节一致,直接贴代码。
activity_fragment_view_pager_bottom_navigation2.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">
<androidx.viewpager.widget.ViewPager
android:id="@+id/vp"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bnv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:windowBackground"
app:itemRippleColor="@color/green_700"
app:itemTextAppearanceInactive="@color/green_500"
app:labelVisibilityMode="labeled"
app:menu="@menu/bottom_nav_menu" />
</LinearLayout>
bottom_nav_menu.xml:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<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="我的" />
</menu>
2.2 主界面Fragment
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);
}
}
}
2.3 主界面Activity
FragmentViewPagerBottomNavigationActivity2.java
public class FragmentViewPagerBottomNavigationActivity2 extends AppCompatActivity {
private ViewPager mViewPager;
private List<Fragment> mFragments;
private MyViewPagerAdapterForFragment mMyViewPagerAdapter;
private BottomNavigationView mBottomNavigationView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_fragment_view_pager_bottom_navigation2);
initView();
initData();
initEvent();
}
private void initView() {
mViewPager = findViewById(R.id.vp);
mBottomNavigationView = findViewById(R.id.bnv);
}
private void initData() {
mFragments = new ArrayList<>();
// 承载多个栏目的主Fragment,把它单独用一个Fragment类来写,下文会对HomeFragment详述
mFragments.add(new HomeFragment());
// 这里是剩余两页:发现页面、我的页面
for (int i = 1; i < 3; i++) {
ExampleFragment fragment = ExampleFragment.newInstance("fragment" + i, "");
mFragments.add(fragment);
}
}
private void initEvent() {
mMyViewPagerAdapter = new MyViewPagerAdapterForFragment(getSupportFragmentManager(), mFragments);
mViewPager.setAdapter(mMyViewPagerAdapter);
mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
onPagerSelected(position);
Toast.makeText(FragmentViewPagerBottomNavigationActivity2.this, "当前第" + position + "页", Toast.LENGTH_SHORT).show();
}
@Override
public void onPageScrollStateChanged(int state) {
}
});
mBottomNavigationView.setOnItemSelectedListener(new NavigationBarView.OnItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_home:
mViewPager.setCurrentItem(0);
return true;
case R.id.menu_find:
mViewPager.setCurrentItem(1);
return true;
case R.id.menu_mine:
mViewPager.setCurrentItem(2);
return true;
}
return false;
}
});
mBottomNavigationView.setSelectedItemId(R.id.menu_home);
}
private void onPagerSelected(int position) {
switch (position) {
case 0:
mBottomNavigationView.setSelectedItemId(R.id.menu_home);
break;
case 1:
mBottomNavigationView.setSelectedItemId(R.id.menu_find);
break;
case 2:
mBottomNavigationView.setSelectedItemId(R.id.menu_mine);
break;
}
}
}
与上节不同的点就是添加Fragment数据的部分。本节我们想要对首页的Fragment里面再添加多个子Fragment,因此就不能用ExampleFragment这个仅展示一个文本的Fragment了。我们的首页Fragment里面需要有多个页面切换,以及标题,需要新的布局,新的代码。接下来我们为它新建一个HomeFragment来具体实现。而另外两个页面Find和Mine,我们还用ExampleFragment做个简单模拟显示下文本就行了。
2.4 多个子栏目的承载者HomeFragment
2.4.1 界面布局
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>
为了显示每个栏目的标题tab,我们引入了TabLayout这个控件。与上节的BottomNavigationView一样,它也是material库里面的,如果你引入不到,也需要添加implementation 'com.google.android.material:material:1.5.0'
的依赖。
这个控件有两个属性可以了解下:
-
app:tabMode
设置tab的模式的,有这几个取值:- fixed: 固定的,也就是标题不可滑动,有多少展示多少,平均分配长度
- scrollable:可滑动的,小于等于5个默认靠左固定,大于五个tab后就可以滑动
- auto:自动选择是否可滑动,小于等于5个默认居中固定,大于5个自动滑动
-
app:tabGravity
设置tab的位置- start: 居左
- fill:平均分配,铺满屏幕宽度
- center: 居中
2.4.2 代码部分
首先还是控件的初始化。
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;
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();
}
private void initView() {
mViewPager = getView().findViewById(R.id.home_vp);
mTabLayout = getView().findViewById(R.id.home_tab_layout);
}
}
然后是ViewPager部分
同样的,也涉及到ViewPager、Data、Adapter三个部分,与前文一样,这里快速过一下。
ViewPager我们已经声明并初始化,然后是数据部分。
- 数据部分
private List<Fragment> mFragmentList;
private List<String> mTitleList;
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
initView();
initData();
}
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);
}
}
这里的数据相比前文多了mTitleList,也就是那些tab标题。Fragment部分我们新建了一个SubFragment来承接。出于方便演示,SubFragment与ExampleFragment一样只有一个文本做显示,比较简单。
SubFragment.java:
public class SubFragment extends Fragment {
public static final String ARG_PARAM1 = "param1";
public static final String ARG_PARAM2 = "param2";
private String mParam1;
private String mParam2;
private TextView mTextView;
public SubFragment() {
}
public static SubFragment newInstance(String param1, String param2) {
SubFragment fragment = new SubFragment();
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_sub, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mTextView = view.findViewById(R.id.sub_fragment_content);
if (!TextUtils.isEmpty(mParam1)) {
mTextView.setText(mParam1);
}
}
}
- adapter部分
也是继承自FragmentPagerAdapter,只不过这里再多重写一个获取标题的方法getPageTitle
。也就是在前文基础上做一点改造:
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);
}
}
我们增加了一个重载的构造方法,用来传入titleList。在getPageTitle方法中返回对应的标题。
这样三个主要的部分都准备完毕,接下来这HomeFragment将其连接起来即可。
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() {
// 注意, 这里传入的是getChildFragmentManager()
mPagerAdapterForFragment = new MyViewPagerAdapterForFragment(getChildFragmentManager(), mFragmentList, mTitleList);
mViewPager.setAdapter(mPagerAdapterForFragment);
// 设置默认选中第一页
mViewPager.setCurrentItem(0);
// 关联tabLayout与ViewPager
mTabLayout.setupWithViewPager(mViewPager);
}
}
增加initEvent方法,在其中进行adapter的创建以及与ViewPager的绑定。注意, 在adapter的创建的时候需要传入FragmentManager,由于此时要创建的Fragment页面是子Fragment,也就是它的承载者本身就是一个Fragment,所以,这里调用的是getChildFragmentManager()
。
- 在一个Fragment里面创建子Fragment需要获取FragmentManager,调用的是
getChildFragmentManager()
。 - 作为对比,我们再回顾下在Activity里创建Fragment需要FragmentManager,调用的是
getSupportFragmentManager()
,不带Child
另外值得注意的一点是:tabLayout与ViewPager的联动。
我们滑动ViewPager,上面的标题Tab要跟着切换。反之,点击Tab,ViewPager也要跟着切换。这该怎么做到呢?很简单,有了TabLayout这个控件,只需要一行代码mTabLayout.setupWithViewPager(mViewPager);
即可。
这样我们就完成整个HomeFragment部分了,结合外层Activity,整个案例也完成了。
3. 总结
本文侧重的就是子Frament多栏目这部分,结合前文的底部导航+ViewPager,构成了最实用的、常见的Frament+ViewPager+BottomNavigationView页面结构。有了这套结构,你就可以以此为模板,快速构建App了。