Jetpack之-Navigation
MVVM
架构,其中使用的Jetpack组件包括Paging、LiveData、ViewModel以及Room等,后面到了现在的公司就一直没有怎么使用过。但Jetpack组件作为谷歌的亲儿子项目,而且随着Compose 1.0稳定版的发布,精通Jetpack全组件开发在未来的趋势是不可逆的了,无论你是原生开发还是做跨平台。总而言之,未来还要做Android开发就得懂使用Jetpack组件协同开发构建应用。
Navigation的定义
Navigation就如它英文的字面意思是导航作用的,它是一个可简化Android导航的组件,多数是用在管理Fragment的切换,另外还支持Activity、导航图和子图、自定义目标,而且在studio中可通过可视化的方式,看见App的交互流程:
本文主要介绍Navigation在Fragment管理方面的应用
Navigation的基本使用
对于一个新的知识点,我们还是通过简单的使用去了解它。
添加Navigation组件库的依赖
implementation "androidx.navigation:navigation-fragment-ktx:2.4.2"
implementation "androidx.navigation:navigation-ui-ktx:2.4.2"
创建三个Fragment以备用
新建三个Fragment,依次是FragmentA、FragmentB、FragmentC,:
class FragmentA : Fragment() {
val tv_a_b by bindView<TextView>(R.id.tv_a_b)
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_a,container,false)
}
}
class FragmentB : Fragment() {
val tv_b_c by bindView<TextView>(R.id.tv_b_c)
val tv_b_a by bindView<TextView>(R.id.tv_b_a)
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_b,container,false)
}
}
class FragmentC : Fragment() {
val tv_c_b by bindView<TextView>(R.id.tv_c_b)
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_c,container,false)
}
}
这里控件初始化使用的是bindView
方法,这是个自定义对控件进行延时初始化的方法,只有在第一次使用到控件才会初始化它,这里把封装的方法分享出来:
fun <V : View> Activity.bindView(id: Int): Lazy<V?> = lazy {
viewFindId(id)?.saveAs<V>()
}
val viewFindId: Activity.(Int) -> View?
get() = { findViewById(it) }
fun <V : View> Fragment.bindView(id: Int): Lazy<V?> = lazy {
frgViewFindId(id)?.saveAs<V>()
}
val frgViewFindId: Fragment.(Int) -> View?
get() = { view?.findViewById(it) }
fun <T> Any.saveAs():T?{
return this as? T
}
创建Navigation的xml文件
首先我们在res文件夹下创建一个navigation文件夹,这里要注意在新建文件夹的时要选择Android Resource Directory,不然新建出来的文件右键创建Navigation文件的时候没有Navigation Resource File一项.
在新建好的navigation文件夹右键new中选择Navigation Resource File创建一个名为
jetpack_navigation
的xml文件.在
jetpack_navigation.xml
中配置创建的3个Fragment:
<?xml version="1.0" encoding="utf-8"?>
<navigation 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:id="@+id/jetpack_navigation"
app:startDestination="@id/fragment_a">
<fragment
android:id="@+id/fragment_a"
android:name="com.transocks.myapplication.FragmentA"
android:label="fragment_a"
tools:layout="@layout/fragment_a" >
<action
android:id="@+id/action_fragment_a_to_b"
app:destination="@id/fragment_b" />
</fragment>
<fragment
android:id="@+id/fragment_b"
android:name="com.transocks.myapplication.FragmentB"
android:label="fragment_b"
tools:layout="@layout/fragment_b" >
<action
android:id="@+id/action_fragment_b_to_c"
app:destination="@id/fragment_c" />
<action
android:id="@+id/action_fragment_b_to_a"
app:destination="@id/fragment_a" />
</fragment>
<fragment
android:id="@+id/fragment_c"
android:name="com.transocks.myapplication.FragmentC"
android:label="fragment_c"
tools:layout="@layout/fragment_c" >
<action
android:id="@+id/action_fragment_c_to_b"
app:destination="@id/fragment_b" />
</fragment>
</navigation>
这里有一点要注意的是在<fragment>
标签中要有id,name,label、layout
这四个属性的:
id: 当前<
fragment>
标签中对应Fragment的标识,用来给其他Fragment跳转使用;name: 对应的自定义Fragment;
label: 内容包含目标Fragment的XML布局文件名称
layout:当前标签对应Fragment的布局文件
<action>
标签在<fragment>
标签中,其主要是给出一个id让外部操作的时候指定,destination
则表示的是当前id操作要往哪里跳转,其使用id指定跳转的fragment。
app:startDestination
属性指的是默认的起始位置,它是navigation
标签的属性:
<navigation 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:id="@+id/jetpack_navigation"
app:startDestination="@id/fragment_a">
在Activity文件中建立NavHostFragment
NavHostFragment
是一个导航界面的容器,是用来展示Navigation的一系列Fragment。看下面xml的代码:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
tools:context=".MainActivity">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/jetpack_navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>
这里请注意id是必须要添加的,不然运行起来会奔溃
name:这里指的是NavHost实现类的名称;
navGraph:存放的是建好在Navigation中的资源文件,也就是确定了Navigation Graph;
defaultNavHost:与系统的返回按钮相关联,true代表属性可以指定NavHostFragment会拦截系统返回按钮。
跳转实现
以下代码一次是FragmentA、B、C的onViewCreated
方法里面的点击事件:
//FragmentA
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
tv_a_b?.setOnClickListener {
Navigation.findNavController(it).navigate(R.id.action_fragment_a_to_b)
}
}
//FragmentB
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
tv_b_a?.setOnClickListener {
Navigation.findNavController(it).navigate(R.id.action_fragment_b_to_a)
}
tv_b_c?.setOnClickListener {
Navigation.findNavController(it).navigate(R.id.action_fragment_b_to_c)
}
}
//FragmentC
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
tv_c_b?.setOnClickListener {
Navigation.findNavController(it).navigate(R.id.action_fragment_c_to_b)
}
}
演示效果:
使用Bundle传值
使用Bundle传值非常简单,我们再FragmentA点击跳转把值传进来:
tv_a_b?.setOnClickListener {
val bundle = Bundle()
bundle.putString("Key","我在Fragment A过来的")
Navigation.findNavController(it).navigate(R.id.action_fragment_a_to_b,bundle)
}
然后在FragmentB的onViewCreated
中接收:
val value = arguments?.getString("Key") Toast.makeText(activity, value?.saveAs<String>(), Toast.LENGTH_SHORT).show()
这里比较有一点bug的就是目前navigation在每次切换后Fragment都会被销毁重建,谷歌那边说会在navigation-fragment-ktx
的2.4.0版本修复,但目前依旧还是存在重建的问题。如果要实现Fragment复用请参考:
Fragment切换动画效果
加上动画的过渡效果,让Fragment切换起来没有那么生硬,更像是activity是跳转,这样也可以提高一下用户体验。我们先定义两个动画,分别是:进场动画和退出动画。
frg_in:
<?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android"> <!-- 定义从右向左进入的动画 --> <translate android:duration="300" android:fromXDelta="100%" android:interpolator="@android:anim/accelerate_interpolator" android:toXDelta="0" /> </set>
frg_out:
<?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android"> <translate android:duration="300" android:fromXDelta="0" android:toXDelta="-100%" android:interpolator="@android:anim/accelerate_interpolator" /> </set>
Fragment之间跳转的过渡动画有两种实现方式,一种是在navigation文件中的<action>
标签配置enterAnim
和exitAnim
属性:
<fragment android:id="@+id/fragment_a" android:name="com.transocks.myapplication.FragmentA" android:label="fragment_a" tools:layout="@layout/fragment_a" > <action android:id="@+id/action_fragment_a_to_b" app:destination="@id/fragment_b" app:enterAnim="@anim/frg_in" app:exitAnim="@anim/frg_out" /> </fragment>
另外一种方式就是通过navOptions
代码实现:
val options = navOptions { anim { //这里换一个进入动画,容易看出区别 enter = R.anim.frg_in_bottomtoup exit = R.anim.frg_out } } tv_b_c?.setOnClickListener { //这个null是bundle,因为没有传值就给个null了 Navigation.findNavController(it).navigate(R.id.action_fragment_b_to_c,null,options) }
效果展示:
NavigationUI
上面介绍了Navigation管理Fragment的一般用法,这种交互的场景很适合用在诸如登录页面,可以调整注册页面、忘记密码页面,但对于我们现阶段使用的习惯,Fragment用在最多的场景无可厚非是在首页Tab切换不同页面的时候。在以往,我们可以使用Tablayout实现操作Fragment,而Navigation则可以绑定menus、drawers和bottom navigation,我们通过BottomNavigationView
结合Navigation
实现底部导航栏切换Fragment的功能。
首先,我们res下创建一个menu文件夹,并创建一个menu_tab的menu资源文件:
<?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@+id/fragment_a" android:icon="@drawable/tab_home_normal" android:title="首页" /> <item android:id="@+id/fragment_b" android:icon="@drawable/tab_account_normal" android:title="账号" /> <item android:id="@+id/fragment_c" android:icon="@drawable/tab_more_normal" android:title="更多" /> </menu>
这里的id就是jetpack_navigation
文件中各<fragment>
标签中的id,然后在MainActivity布局文件中添加BottomNavigationView
,并关联创建的menu_tab
文件,代码如下:
<com.google.android.material.bottomnavigation.BottomNavigationView android:id="@+id/tab_navi" android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="parent" app:menu="@menu/menu_tab" />
最后在MainActivity
中把NavController
取出来,并BottomNavigationView
绑定,看下面代码:
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) //绑定BottomNavigationView val navHost: NavHostFragment? = supportFragmentManager .findFragmentById(R.id.nav_host_fragment)?.saveAs<NavHostFragment>() val navController = navHost?.navController val bottomNaviView by bindView<BottomNavigationView>(R.id.tab_navi) navController?.let { bottomNaviView?.setupWithNavController(it) } } }
NavController
是通过NavHostFragment
获取的,所以通过supportFragmentManager.findFragmentById(R.id.nav_host_fragment)
把它拿到。 效果如下:
如果要对BottomNavigationView
切换进行监听可用:setOnNavigationItemSelectedListener
。 另外Navigation还支持和以下控件进行绑定使用:
Toolbar
CollapsingToolbarLayout
ActionBar
DrawerLayout
代码动态加载Navigation的xml资源文件
要代码动态加载navigation首先要把MainActivity中布局文件的app:navGraph="@navigation/jetpack_navigation"
去掉:
<androidx.fragment.app.FragmentContainerView android:id="@+id/nav_host_fragment" android:name="androidx.navigation.fragment.NavHostFragment" android:layout_width="0dp" android:layout_height="0dp" app:defaultNavHost="true" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" />
接下来要做到的就是在MainActivity中通过navController
把Navigation
的xml文件进行inflater
,并通过navController
的graph
设置进来:
//绑定BottomNavigationView val navHost: NavHostFragment? = supportFragmentManager.findFragmentById(R.id.nav_host_fragment)?.saveAs<NavHostFragment>() val navController = navHost?.navController //设置graph val navGraph = navController?.navInflater?.inflate(R.navigation.jetpack_navigation) navController?.graph = navGraph ?: return val bottomNaviView by bindView<BottomNavigationView>(R.id.tab_navi) navController?.let { bottomNaviView?.setupWithNavController(it) }
Navigation的返回栈
返回栈是Android维护的一个堆栈,里面包含了我们访问的每一个Destination
(指的是跳转的目的地),对于activity来说,一般返回后调用finish都会将当前Activity
从堆栈中退出并销毁。而对于Navigation
管理的Fragment,每一次navigate()
方法都会把对应的Destination
放到栈顶,所以每次调用导航action时都会往回退栈中添加一个Destination
。如此反复,回退栈中将会包含很多重复的Destination
。当我们通过NavController
调用 navigateUp()
和popBackStack()
方法来进行前进或向后的操作时,会移除栈顶的目的地。如果要对action
配置进行返回栈的清空栈的操作则可以通过添加app:launchSingleTop="true"
、app:popUpTo="@+id/jetpack_navigation"
和app:popUpToInclusive="true"
属性来设置:
<fragment
android:id="@+id/fragment_b"
android:name="com.transocks.myapplication.FragmentB"
android:label="fragment_b"
tools:layout="@layout/fragment_b" >
<action
android:id="@+id/action_fragment_b_to_c"
app:destination="@id/fragment_c" />
<action
android:id="@+id/action_fragment_b_to_a"
app:destination="@id/fragment_a"
app:launchSingleTop="true"
app:popUpTo="@+id/jetpack_navigation"
app:popUpToInclusive="true"/>
</fragment>
这里设置就是当我们返回第一个Fragment时,先清空返回栈再跳转。
在代码中我们可以这样实现:
tv_b_a?.setOnClickListener {
val navOption = NavOptions.Builder()
.setLaunchSingleTop(true)
.setPopUpTo(R.id.jetpack_navigation, true)
.build()
Navigation.findNavController(it).navigate(R.id.action_fragment_b_to_a,null,navOption)
}
如果要在activity中清空返回栈,则可以通过调用popBackStack
方法:
navController.popBackStack(R.id.jetpack_navigation,true)
小结
Navigation组件一个用来构建应用界面的框架,它解决了以往通过FragmentTransaction
管理Fragment的麻烦。当然,谷歌推出此组件最重要的是想推广一个Activity架构开发应用的想法。本文只是简单介绍了Navigation