再过两个月,我就30周岁了,开启人生的第四个十年。人生有几个十年,我已经度过了三个。我内心并未感觉自己三十岁,但每次听到同事们说出他们的年龄,心里暗暗比较比他们大了好几岁,才意识到他们真好,我曾经也是他们这样。自古三十而立,成家立业,我现在既没有成家,也不知道我这个写码的工作算不算立业,因为我还不知道过了30岁,还能写几年代码。但不管未来如何,当下好好写码,保持学习,未来也应该不会很差吧。
上面纯属扯淡,下面开始今天的正文。现在主流的app首页都是底部几个Tab,上面是Fragment展示内容,就像下图这样。实现这样的需求很简单,我想这应该是Android工程师必备的能力吧。虽然说很容易,没有什么技术难点,但我看到过一些人的实现并不是很完美,多多少少有点问题,在某些场景下会出现bug。
上面内容部分有两种方式实现:
- 使用FragmentContainerView,承载显示内容的Fragment;使用ViewPager2,和FragmentStateAdapter。
下面的Tab也有两种方式实现:
- 普通的控件实现,如:RadioButton;使用BottomNavigationView。
上面的两种方式可以与下面两种方式任一配合实现。
第一种 FragmentContainerView 搭配 RadioButton布局:
nav_1.xml(nav_2/3/4 类似)
nav_color.xml
HomeActivity 代码:
class MainActivity : FragmentActivity, CompoundButton.OnCheckedChangeListener {
//Fragment对应的Tag,用于在FragmentManager添加和寻找Fragment
companion object {
const val TAG_1 = "tag1"
const val TAG_2 = "tag2"
const val TAG_3 = "tag3"
const val TAG_4 = "tag4"
const val TAG_CHECKED = "checkedTag"
}
//当前选中的tab
private var checkedTag = TAG_1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (savedInstanceState == null) {//如果不是销毁重建,才添加fragment
val fragment1 = Fragment1.newInstance()
supportFragmentManager.beginTransaction()
.add(R.id.fragmentContainerView, fragment1 , TAG_1)
.commitNow()
} else {//重建时,恢复checkedTag
checkedTag = savedInstanceState.getString(TAG_CHECKED)!!
}
radio1.setOnCheckedChangeListener(this)
radio2.setOnCheckedChangeListener(this)
radio3.setOnCheckedChangeListener(this)
radio4.setOnCheckedChangeListener(this)
}
//保存checkedTag,待重建时恢复
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(TAG_CHECKED, checkedTag)
}
//Radio选中事件监听
override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) {
if (isChecked) {
changeRadioButton(buttonView.id)//将上一个选中的RadioButton置为非选中状态
when (buttonView.id) {//根据当前选中的tab,切换显示的Fragment
R.id.radio1 -> {
changeFragment(TAG_1) { Fragment1.newInstance() }
}
R.id.radio2 -> {
changeFragment(TAG_2) { Fragment2.newInstance() }
}
R.id.radio3 -> {
changeFragment(TAG_3) { Fragment3.newInstance() }
}
R.id.radio4 -> {
changeFragment(TAG_4) { Fragment4.newInstance() }
}
}
}
}
//将id不是checkedId的RadioButton置为不选中状态
private fun changeRadioButton(checkedId: Int) {
if (checkedId != R.id.radio1) radio1.isChecked = false
if (checkedId != R.id.radio2) radio2.isChecked = false
if (checkedId != R.id.radio3) radio3.isChecked = false
if (checkedId != R.id.radio4) radio4.isChecked = false
}
//切换显示的Fragment
private fun changeFragment(fragmentTag: String, createFragment: () -> Fragment) {
val fragmentManager = supportFragmentManager
val beginTransaction = fragmentManager.beginTransaction()
//先找找之前有没有添加过该Fragment
var fragment = fragmentManager.findFragmentByTag(fragmentTag)
if (fragment== null) {//没有添加过
fragment= createFragment()//创建Fragment,并添加,fragmentTag一定要传
beginTransaction.add(R.id.fragmentContainerView, fragment, fragmentTag)
}
//找到当前显示的Fragment,并隐藏
fragmentManager.findFragmentByTag(checkedTag)?.let {
beginTransaction.hide(it)
}
//显示选中的Fragment,注意这里提交事务使用的commitNow
beginTransaction.show(fragment).commitNow()
checkedTag = fragmentTag
}
}
需要注意几点:
- onCreate中,一定要判断savedInstanceState为空时才添加Fragment。如果不为空的话,说明Activity销毁重建了,FragmentManager会恢复之前添加的Fragment;显示隐藏Fragment提交事务时,要使用FragmentTransaction的 commitNow 方法,而不能使用commit方法。commitNow是同步提交,也就是立马就执行;commit是异步执行,也就是不会马上执行。 而我们的操作是需要马上生效的,所以当我们操作很快时,会发现使用commit会出现页面重叠的现象。不要建议用List或其他方式保存Fragment实例。当Activity销毁重建后,FragmentManager 恢复的Fragment 与 保存的 Fragment 不是同一对象。如果要获得Fragment,通过FragmentManager.findFragmentByTag 获取。所以在添加Fragment时一定要给它Tag。如果在Fragment 中添加 Fragment,要使用Fragment的childFragmentManager 来操作,一定不能使用Activity中的FragmentManager,不然会出现Fragment错乱重叠等问题。
以上几点在其他实现方式中同样生效。
第二种 FragmentContainerView 搭配 BottomNavigationView 的实现布局:
app:itemIconTint 设置图标选中和未选中的颜色。
app:itemTextColor 设置文字选中和未选中的颜色。
itemTextAppearanceActive、itemTextAppearanceInactive设置文本选中和未选中的大小。
app:labelVisibilityMode设置文本显示模式,labeled为总是显示。
nav_menu.xml
HomeActivity 代码
class MainActivity : FragmentActivity(), BottomNavigationView.OnNavigationItemSelectedListener {
//Fragment对应的Tag,用于在FragmentManager添加和寻找Fragment
companion object {
const val TAG_1 = "tag1"
const val TAG_2 = "tag2"
const val TAG_3 = "tag3"
const val TAG_4 = "tag4"
const val TAG_CHECKED = "checkedTag"
}
//当前选中的tab
private var checkedTag = TAG_1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (savedInstanceState == null) {
val fragment1 = Fragment1()
supportFragmentManager.beginTransaction().add(R.id.fragmentContainerView, fragment1, TAG_1).commitNow()
} else {
checkedTag = savedInstanceState.getString(TAG_CHECKED)!!
}
bottomNavigationView.setOnNavigationItemSelectedListener(this)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(TAG_CHECKED, checkedTag)
}
//BottomNavigationView选中切换事件
override fun onNavigationItemSelected(item: MenuItem): Boolean {
if (supportFragmentManager.isStateSaved) return false
when (item.itemId) {
R.id.item_1-> {
changeFragment(TAG_1) { Fragment1() }
}
R.id.item_2 -> {
changeFragment(TAG_2) { Fragment2() }
}
R.id.item_3 -> {
changeFragment(TAG_3) { Fragment3() }
}
R.id.item_4 -> {
changeFragment(TAG_4) { Fragment4() }
}
}
return true
}
//切换显示的Fragment
private fun changeFragment(fragmentTag: String, createFragment: () -> Fragment) {
if (fragmentTag == checkedTag) return //如果选中的tag是当前显示的tag不处理
val fragmentManager = supportFragmentManager
val beginTransaction = fragmentManager.beginTransaction()
var fragment = fragmentManager.findFragmentByTag(fragmentTag)
if (fragment == null) {
fragment = createFragment()
beginTransaction.add(R.id.fragmentContainerView, fragment, fragmentTag)
}
fragmentManager.findFragmentByTag(this.checkedTag)?.let {
beginTransaction.hide(it)
}
beginTransaction.show(fragment).commitNow()
this.checkedTag = fragmentTag
}
//设置badge数量
private fun setCartBadgeCount(count: Int) {
if (0 == count) {//移除
bottomNavigationView.removeBadge(R.id.nav2)
} else {//显示
bottomNavigationView.getOrCreateBadge(R.id.nav2).run {
backgroundColor = ContextCompat.getColor(this@MainActivity, R.color.red)
maxCharacterCount = 3//最多几位数,超过100,显示为99+
number = count
}
}
}
}
和第一种类似,只是监听事件不一样。另外BottomNavigationView 是支持显示和移除Badge的,显示时也可以不设置数量,这样就只显示一个小红点。
使用ViewPager2实现布局:
android:orientation="horizontal"设置ViewPager2为水平滑动。
HomeActivity 代码:
class HomeActivity : FragmentActivity(), BottomNavigationView.OnNavigationItemSelectedListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)
bottomNavigationView.setOnNavigationItemSelectedListener(this)
//页面切换监听
viewPager2.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
when (position) {
0 -> {
bottomNavigationView.selectedItemId = R.id.item_1
}
1 -> {
bottomNavigationView.selectedItemId = R.id.item_2
}
2 -> {
bottomNavigationView.selectedItemId = R.id.item_3
}
3 -> {
bottomNavigationView.selectedItemId = R.id.item_4
}
}
}
})
viewPager2.adapter = object : FragmentStateAdapter(this) {
override fun createFragment(position: Int): Fragment {
return when (position) {
0 -> {
Fragment1()
}
1 -> {
Fragment2()
}
2 -> {
Fragment3()
}
3 -> {
Fragment4()
}
else -> {
throw IllegalArgumentException()
}
}
}
override fun getItemCount(): Int {
return 4
}
}
}
//BottomNavigationView选中切换事件
override fun onNavigationItemSelected(item: MenuItem): Boolean {
if (supportFragmentManager.isStateSaved) return false
when (item.itemId) {
R.id.item_1-> {
viewPager2.currentItem = 0
}
R.id.item_2-> {
viewPager2.currentItem = 1
}
R.id.item_3-> {
viewPager2.currentItem = 2
}
R.id.item_4 -> {
viewPager2.currentItem = 3
}
}
return true
}
}
ViewPager2 是基于RecyclerView实现的,FragmentStateAdapter是RecyclerView.Apdater子类,有两个方法需要我们实现:createFragment 根据position返回一个Fragment;getItemCount 返回Fragment数量。Fragment相关的操作、选中状态保存等都不需要我们处理,FragmentStateAdapter已经处理好,并且当Fragment数量很多时,会回收不需要的Fragment。
这里还遗留了一些问题,比如Fragment里面也有水平滑动的View,会被ViewPager2拦截滑动不了。还有这里的写法似乎有些不妥。等我后面多用用这玩意再来补充。



