随笔博文

Jetpack之-Navigation

2022-12-12 13:21:17 michael007js 145

虽说是重学Jetpack,但其实也不尽是,因为有些Jetpack组件之前也没学过。最早开始使用Jetpack组件开发是2019年,当时公司开发的一款产品采用的MVVM架构,其中使用的Jetpack组件包括Paging、LiveData、ViewModel以及Room等,后面到了现在的公司就一直没有怎么使用过。但Jetpack组件作为谷歌的亲儿子项目,而且随着Compose 1.0稳定版的发布,精通Jetpack全组件开发在未来的趋势是不可逆的了,无论你是原生开发还是做跨平台。总而言之,未来还要做Android开发就得懂使用Jetpack组件协同开发构建应用。

Navigation的定义

Navigation就如它英文的字面意思是导航作用的,它是一个可简化Android导航的组件,多数是用在管理Fragment的切换,另外还支持Activity、导航图和子图、自定义目标,而且在studio中可通过可视化的方式,看见App的交互流程:

88.png

本文主要介绍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)
 }
}

演示效果:

QQ图片20220420155042.gif

使用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复用请参考: github.com/Flywith24/N… juejin.cn/post/684490…

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>标签配置enterAnimexitAnim属性:

<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)
}

效果展示: QQ图片20220420174223.gif

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)把它拿到。 效果如下:

QQ图片20220420215346.gif

如果要对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中通过navControllerNavigation的xml文件进行inflater,并通过navControllergraph设置进来:

//绑定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的一些基础用法,对于其他的诸如Toolbar、DrawerLayout的这些UI控件的绑定使用未曾提及,后续会继续学习探索。对于Jetpack组件的了解水平有限,如果有什么错误,麻烦指正,大家一起学习哈!


首页
关于博主
我的博客
搜索