λ:
2021.9.9更新: 之前版本把頁面切換直接放進(jìn)了Activity, 跳轉(zhuǎn)其他頁是靠啟動(dòng)新Activity厂镇。但實(shí)踐中都是單Activity模式,所以重新整理一下诗箍。
# 倉庫地址: https://github.com/lzyprime/android_demos
# branch: bottom_navigation
git clone -b bottom_navigation https://github.com/lzyprime/android_demos
底部導(dǎo)航配合多頁面切換是常見邏輯政恍,微信敬辣,qq,抖音蟆豫,淘寶等等议忽,常見app里幾乎都有這種設(shè)計(jì)。
底部導(dǎo)航欄:BottomNavgationView + menu
, 涉及到圖標(biāo)和標(biāo)題在點(diǎn)擊時(shí)的變化(顏色十减,大小栈幸,選中與未選中圖標(biāo)變化)
頁面切換有兩個(gè)方案:
參考 使用 NavigationUI 更新界面組件。 會(huì)發(fā)現(xiàn)用navigation組件
實(shí)現(xiàn)頁面切換可以一行代碼搞定帮辟。但是單Activity模式下速址,會(huì)有NavHost嵌套,需要處理層級問題由驹。并且默認(rèn)情況下芍锚,頁面是不保存狀態(tài)的,切頁面每次都重建蔓榄,2.4.0
會(huì)加saveState處理并炮。
ViewPager2
,底層是RecyclerView
, 相比Navgation
會(huì)有頁面緩存甥郑,支持滑動(dòng)切頁手勢逃魄,也不會(huì)有NavHost嵌套問題。
Activity
Activity
|- MainNavHost
|- MainNavFragment(home)
| |- HomePageNavHost
| | |- Item1Fragment(home)
| | |- Item2Fragment
| | |- Item3Fragment
| |
| |- BottomNavgationView
|
|
|- SecondaryFragment
|- ViewPager
| |- Item3Fragment(home)
| |- Item2Fragment
| |- Item1Fragment
|- BottomNavgationView
項(xiàng)目總體層次圖澜搅。問題主要出在MainNavHost
,HomePageNavHost
嵌套問題上伍俘。
<!-- activity_main.xml -->
<androidx.coordinatorlayout.widget.CoordinatorLayout ...>
<androidx.fragment.app.FragmentContainerView
...
android:id="@+id/mainNavHost"
android:name="androidx.navigation.fragment.NavHostFragment"
app:defaultNavHost="true"
app:navGraph="@navigation/main_graph" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
Activity中只放一個(gè)NavHostFragment
組織全局導(dǎo)航。 defaultNavHost=true
攔截系統(tǒng)返回按鈕, 執(zhí)行navigateUp
勉躺。
main_graph
:
Navigation 實(shí)現(xiàn)頁面切換
<!-- main_nav_fragment.xml -->
<LinearLayout ...>
<androidx.fragment.app.FragmentContainerView
...
android:id="@+id/homePageNavHost"
android:name="androidx.navigation.fragment.NavHostFragment"
app:navGraph="@navigation/main_nav_graph" />
<com.google.android.material.bottomnavigation.BottomNavigationView
...
android:id="@+id/mainBtmNavView"
app:menu="@menu/main_nav_menu" />
</LinearLayout>
main_nav_graph
:
main_nav_menu
:
class MainNavFragment : Fragment(R.layout.main_nav_fragment) {
private val binding by viewBinding<MainNavFragmentBinding>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val navController =
(childFragmentManager.findFragmentById(R.id.homePageNavHost) as NavHostFragment).navController
binding.mainBtmNavView.setupWithNavController(navController)
}
}
通過setupWithNavController
將NavHost與BottomNavgationView綁定。內(nèi)部就是給BottomNavgationView設(shè)置Listener, 監(jiān)聽onNavigationItemSelected(MenuItem item)
事件饵溅。然后通過item.itemId
導(dǎo)航到同id目的地柳弄。
所以,menu
中id要與 導(dǎo)航圖
中一致
但是如果想在子頁面Item1, Item2, Item3
操作全局導(dǎo)航概说,就要想辦法拿到MainNavHost
的navController:
class Item1Fragment : Fragment(R.layout.item1_fragment) {
private val binding: Item1FragmentBinding by viewBinding()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val globalNavHostController = requireActivity().findNavController(R.id.mainNavHost)
...
}
}
按照層級關(guān)系,在子頁面調(diào)用findNavController()
會(huì)返回 HomePageNavHost
的 controller嚣伐。所以要額外操作糖赔,去拿Activity中MainNavHost
。 凡是HomePageNavHost
嵌套圖中的View都會(huì)有這種問題轩端。
ViewPager2
ViewPager2就不會(huì)有這些問題放典,因?yàn)樵谕粋€(gè)NavHost下。
<!-- secondary_nav_fragment.xml -->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout ...>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/secondaryViewPager"
... />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/secondaryBtmNavView"
app:menu="@menu/secondary_nav_menu"
... />
</LinearLayout>
class SecondaryNavFragment : Fragment(R.layout.secondary_nav_fragment) {
private val binding by viewBinding<SecondaryNavFragmentBinding>()
private val fragments = arrayOf(Item3Fragment(), Item2Fragment(), Item1Fragment())
private val itemIds = arrayOf(R.id.item3, R.id.item2, R.id.item1)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.secondaryViewPager.adapter = object : FragmentStateAdapter(this) {
override fun getItemCount(): Int = fragments.size
override fun createFragment(position: Int): Fragment = fragments[position]
}
binding.secondaryViewPager.offscreenPageLimit = fragments.size
//binding.secondaryViewPager.isUserInputEnabled = false // 禁用滑動(dòng)
binding.secondaryViewPager.registerOnPageChangeCallback(object :
ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
binding.secondaryBtmNavView.selectedItemId = itemIds[position]
}
})
binding.secondaryBtmNavView.setOnItemSelectedListener {
binding.secondaryViewPager.currentItem = itemIds.indexOf(it.itemId)
true
}
}
}
ViewPager2設(shè)置:
- 設(shè)置
FragmentStateAdapter
。由于ViewPager2底層是RecyclerView, 所以這肯定是個(gè)RecyclerView.Adapter
子類奋构。 -
offscreenPageLimit
壳影, 緩存頁面數(shù) - 注冊
OnPageChangeCallback
,監(jiān)聽頁面滑動(dòng)弥臼,頁面切換完成時(shí)宴咧,設(shè)置底部導(dǎo)航欄選中項(xiàng)。 - 如果不想滑動(dòng)切換頁面,
isUserInputEnabled = false
BottomNavigationView設(shè)置:
設(shè)置點(diǎn)擊事件監(jiān)聽径缅,ViewPager2
跳轉(zhuǎn)對應(yīng)頁面掺栅。