前文我们说过,Fragment的作用之一就是拓展页面,使得在一块有限的屏幕上展示更多的内容,也就是多个Fragment页面。那么如何在多个页面之间快速切换呢?
本文将介绍最简单、常见的底Tab+Fragment多页面结构的实现案例,也是对前文Fragment知识的实践应用。
1. 目标效果
就是这样一个简单的,常见的页面结构。底部是三个导航按钮,点击不同的按钮,中间主要内容区域切换不同的Fragment页面。
2. 案例教学
2.1 主界面布局
首先,新建工程FragmentBottomTab1,实现基本的页面布局。
主界面MainActivity的布局,activity_main.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.fragment.app.FragmentContainerView
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<!--分割线View-->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:background="@color/gray" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<RelativeLayout
android:id="@+id/rl_home"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1">
<ImageView
android:id="@+id/iv_home"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_centerHorizontal="true"
android:src="@drawable/selector_home" />
<TextView
android:id="@+id/tv_home"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/iv_home"
android:layout_centerHorizontal="true"
android:text="首页" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/rl_find"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1">
<ImageView
android:id="@+id/iv_find"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_centerHorizontal="true"
android:src="@drawable/selector_find" />
<TextView
android:id="@+id/tv_find"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/iv_find"
android:layout_centerHorizontal="true"
android:text="发现" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/rl_mine"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1">
<ImageView
android:id="@+id/iv_mine"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_centerHorizontal="true"
android:src="@drawable/selector_mine" />
<TextView
android:id="@+id/tv_mine"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/iv_mine"
android:layout_centerHorizontal="true"
android:text="我" />
</RelativeLayout>
</LinearLayout>
</LinearLayout>
整体分为上下两部分(中间有个View是分割线,可写可不写),因此采用线性布局,上面FragmentContainerView承载Fragment,下面的三个按钮用LinearLayout等包装而成。
看起来很简单的按钮却写了很长,导致整个文件内容很长,看起来不好看。我们做一点优化,把这部分抽出来作为一个单独的文件,再用include标签引入。如下:
底部菜单抽出到文件bottom_tab_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:orientation="horizontal">
<RelativeLayout
android:id="@+id/rl_home"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1">
<ImageView
android:id="@+id/iv_home"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_centerHorizontal="true"
android:src="@drawable/selector_home" />
<TextView
android:id="@+id/tv_home"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/iv_home"
android:layout_centerHorizontal="true"
android:text="首页" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/rl_find"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1">
<ImageView
android:id="@+id/iv_find"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_centerHorizontal="true"
android:src="@drawable/selector_find" />
<TextView
android:id="@+id/tv_find"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/iv_find"
android:layout_centerHorizontal="true"
android:text="发现" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/rl_mine"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1">
<ImageView
android:id="@+id/iv_mine"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_centerHorizontal="true"
android:src="@drawable/selector_mine" />
<TextView
android:id="@+id/tv_mine"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/iv_mine"
android:layout_centerHorizontal="true"
android:text="我" />
</RelativeLayout>
</LinearLayout>
主界面布局activity_main.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.fragment.app.FragmentContainerView
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<!--分割线View-->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:background="@color/gray" />
<include layout="@layout/bottom_tab_layout" />
</LinearLayout>
是不是很简洁,很好看了呢?
如果你原样复制粘贴上面代码,会发现很多报错的地方。那是因为用到了一些你没有的资源,比如:图片、颜色。
这里提供下。首先是颜色:
color.xml里补充:
<color name="gray">#716F6F</color>
<color name="green_200">#C5E1A5</color>
<color name="green_500">#8BC34A</color>
<color name="green_700">#689F38</color>
三个按钮图片drawable:
selector_home.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true" android:drawable="@drawable/ic_green_home_24" />
<item android:drawable="@drawable/ic_gray_home_24" />
</selector>
selector_find.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true" android:drawable="@drawable/ic_green_find_24" />
<item android:drawable="@drawable/ic_gray_find_24" />
</selector>
selector_mine.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true" android:drawable="@drawable/ic_green_mine_24" />
<item android:drawable="@drawable/ic_gray_mine_24" />
</selector>
这里使用的是可以根据不同状态切换不同图片的selector,以实现那种选中的时候绿色,非选中的时候灰色的效果。
到这里,你会发现还缺那些ic_xxx的文件。没错,这些才是真正的图片,你需要为每个图标准备灰色、绿色两种。你可以去网上找,如阿里图库,也可以利用Android studio的New Vector Asset功能新建矢量图(选中res目录,右键就能找到)。
这里就不贴这些图片的代码了,太多了。。。
2.2 准备Fragment
这里的Fragment就是在承载三个按钮对应内容的页面。简单起见,这里我们就用一个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);
}
}
}
ExampleFragment对应的布局fragment_example.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ExampleFragment">
<TextView
android:id="@+id/tv_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textSize="25sp"
android:text="你好" />
</FrameLayout>
上述ExampleFragment比较简单,就是接收参数,在布局中把接收的数据显示出来。我们后面会传入“这是HomeFragment”、“这是FineFragment”、“这是MineFragment”,从而展示不同的页面。
2.3 主界面MainActivity
首先做一些控件的基础工作。将布局中的控件声明并初始化(findViewById)。为底部导航按钮按钮设置点击事件监听。做完之后,MainActivity.java如下:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private FragmentContainerView mFragmentContainerView;
private RelativeLayout mRlHome,mRlFind,mRlMine;
private TextView mTvHome,mTvFind,mTvMine;
private ImageView mIvHome,mIvFind,mIvMine;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
initEvent();
}
private void initView() {
mFragmentContainerView = findViewById(R.id.fragment_container);
mRlHome = findViewById(R.id.rl_home);
mRlFind = findViewById(R.id.rl_find);
mRlMine = findViewById(R.id.rl_mine);
mTvMine = findViewById(R.id.tv_mine);
mTvFind = findViewById(R.id.tv_find);
mTvHome = findViewById(R.id.tv_home);
mIvMine = findViewById(R.id.iv_mine);
mIvFind = findViewById(R.id.iv_find);
mIvHome = findViewById(R.id.iv_home);
}
private void initEvent() {
// 为底部导航按钮设置点击监听
mRlHome.setOnClickListener(this);
mRlFind.setOnClickListener(this);
mRlMine.setOnClickListener(this);
}
/**
* 点击了底部导航按钮
* @param v
*/
@Override
public void onClick(View v) {
}
}
接下来,我们就要处理点击 底部导航按钮 后的响应行为了。
这里的逻辑也很简单:根据点击按钮的ID,添加不同的Fragment页面,设置对应按钮位选中状态(按钮背景和文字颜色变绿)。
在onClick方法中加入代码:
/**
* 点击了底部导航按钮
* @param v
*/
@Override
public void onClick(View v) {
// 根据点击按钮的ID,添加不同的Fragment页面
switch (v.getId()) {
case R.id.rl_home:
mFragmentManager = getSupportFragmentManager();
mFragmentTransaction = mFragmentManager.beginTransaction();
Bundle bundle = new Bundle();
bundle.putString(ExampleFragment.ARG_PARAM1, "这是HomeFragment");
mFragmentTransaction.replace(R.id.fragment_container, ExampleFragment.class, bundle, "home")
.commit();
break;
case R.id.rl_find:
mFragmentManager = getSupportFragmentManager();
mFragmentTransaction = mFragmentManager.beginTransaction();
Bundle bundleFind = new Bundle();
bundleFind.putString(ExampleFragment.ARG_PARAM1,"这是FindFragment");
mFragmentTransaction.replace(R.id.fragment_container, ExampleFragment.class, bundleSetting, "find")
.commit();
break;
case R.id.rl_mine:
mFragmentManager = getSupportFragmentManager();
mFragmentTransaction = mFragmentManager.beginTransaction();
Bundle bundleMine = new Bundle();
bundleMine.putString(ExampleFragment.ARG_PARAM1,"这是MineFragment");
mFragmentTransaction.replace(R.id.fragment_container, ExampleFragment.class, bundleMine, "mine")
.commit();
break;
default:
break;
}
}
注意到 mFragmentManager 和 mFragmentTransaction会重复使用到,将其添加到全局变量,即:
private FragmentManager mFragmentManager;
private FragmentTransaction mFragmentTransaction;
接下来是按钮状态的切换,我们将这个过程封装到一个单独的方法 setBottomTabSelected 来做。
/**
* 点击了底部导航按钮
* @param v
*/
@Override
public void onClick(View v) {
setBottomTabSelected(v.getId());
// 省略下面的switch case...
}
/**
* 设置底部导航按钮选中
* @param tabId
*/
private void setBottomTabSelected(int tabId) {
// 先重置所有按钮状态为 全不选
resetBottomTab();
// 再设置某一个按钮为 选中
switch (tabId) {
case R.id.rl_home:
mIvHome.setSelected(true);
mTvHome.setTextColor(getResources().getColor(R.color.green_500));
break;
case R.id.rl_find:
mIvFind.setSelected(true);
mTvFind.setTextColor(getResources().getColor(R.color.green_500));
break;
case R.id.rl_mine:
mIvMine.setSelected(true);
mTvMine.setTextColor(getResources().getColor(R.color.green_500));
break;
default:
break;
}
}
/**
* 重置所有按钮状态为 全不选
*/
private void resetBottomTab() {
mTvHome.setTextColor(getResources().getColor(R.color.black));
mTvFind.setTextColor(getResources().getColor(R.color.black));
mTvMine.setTextColor(getResources().getColor(R.color.black));
mIvHome.setSelected(false);
mIvFind.setSelected(false);
mIvMine.setSelected(false);
}
到此,我们已经完成了99%的内容。已经可以点击底部导航按钮实现Fragment页面切换了。还差最后一点,那就是最开始进入Activity时,用户并没有点击任何一个底部菜单,我们需要设置一个默认进入的页面。也很简单,只需要初始化的最后,触发一下某个底部导航按钮的点击行为就行了。
比如,我们想让用户默认进入HomeFragment,在initEvent方法最后添加:
// 默认点击下home按钮,进入HomeFragment
mRlHome.performClick();
2.4 完整MainActivity.java代码
最后,附上完整的MainActivity.java代码:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private FragmentContainerView mFragmentContainerView;
private RelativeLayout mRlHome,mRlFind,mRlMine;
private TextView mTvHome,mTvFind,mTvMine;
private ImageView mIvHome,mIvFind,mIvMine;
private FragmentManager mFragmentManager;
private FragmentTransaction mFragmentTransaction;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
initEvent();
}
private void initView() {
mFragmentContainerView = findViewById(R.id.fragment_container);
mRlHome = findViewById(R.id.rl_home);
mRlFind = findViewById(R.id.rl_find);
mRlMine = findViewById(R.id.rl_mine);
mTvMine = findViewById(R.id.tv_mine);
mTvFind = findViewById(R.id.tv_find);
mTvHome = findViewById(R.id.tv_home);
mIvMine = findViewById(R.id.iv_mine);
mIvFind = findViewById(R.id.iv_find);
mIvHome = findViewById(R.id.iv_home);
}
private void initEvent() {
// 为底部导航按钮设置点击监听
mRlHome.setOnClickListener(this);
mRlFind.setOnClickListener(this);
mRlMine.setOnClickListener(this);
// 默认点击下home按钮,进入HomeFragment
mRlHome.performClick();
}
/**
* 点击了底部导航按钮
* @param v
*/
@Override
public void onClick(View v) {
setBottomTabSelected(v.getId());
switch (v.getId()) {
case R.id.rl_home:
mFragmentManager = getSupportFragmentManager();
mFragmentTransaction = mFragmentManager.beginTransaction();
Bundle bundle = new Bundle();
bundle.putString(ExampleFragment.ARG_PARAM1, "这是HomeFragment");
mFragmentTransaction.replace(R.id.fragment_container, ExampleFragment.class, bundle, "home")
.commit();
break;
case R.id.rl_find:
mFragmentManager = getSupportFragmentManager();
mFragmentTransaction = mFragmentManager.beginTransaction();
Bundle bundleSetting = new Bundle();
bundleSetting.putString(ExampleFragment.ARG_PARAM1,"这是FindFragment");
mFragmentTransaction.replace(R.id.fragment_container, ExampleFragment.class, bundleSetting, "setting")
.commit();
break;
case R.id.rl_mine:
mFragmentManager = getSupportFragmentManager();
mFragmentTransaction = mFragmentManager.beginTransaction();
Bundle bundleMine = new Bundle();
bundleMine.putString(ExampleFragment.ARG_PARAM1,"这是MineFragment");
mFragmentTransaction.replace(R.id.fragment_container, ExampleFragment.class, bundleMine, "mine")
.commit();
break;
default:
break;
}
}
/**
* 重置所有按钮状态为 全不选
*/
private void resetBottomTab() {
mTvHome.setTextColor(getResources().getColor(R.color.black));
mTvFind.setTextColor(getResources().getColor(R.color.black));
mTvMine.setTextColor(getResources().getColor(R.color.black));
mIvHome.setSelected(false);
mIvFind.setSelected(false);
mIvMine.setSelected(false);
}
/**
* 设置底部导航按钮选中
* @param tabId
*/
private void setBottomTabSelected(int tabId) {
// 先重置所有按钮状态为 全不选
resetBottomTab();
// 再设置某一个按钮为 选中
switch (tabId) {
case R.id.rl_home:
mIvHome.setSelected(true);
mTvHome.setTextColor(getResources().getColor(R.color.green_500));
break;
case R.id.rl_find:
mIvFind.setSelected(true);
mTvFind.setTextColor(getResources().getColor(R.color.green_500));
break;
case R.id.rl_mine:
mIvMine.setSelected(true);
mTvMine.setTextColor(getResources().getColor(R.color.green_500));
break;
default:
break;
}
}
}