JetPack知识点实战系列二:使用Navigation架构一步步实现APP的主体框架以及Navigation相关知识介绍

本节教程我将带大家来一步步实现主页的框架,一个Bottom Navigation框架,然后介绍Navigation的相关知识。

效果图

本节教程您学习到如下主要内容:

  1. BottomNavigation的搭建和原理介绍
  2. Navigation的的传值
  3. Navigation跳转动画的实现
  4. Navigation文件的拆分
  5. Deeplink导航的实现

搭建 Bottom Navigation Activity

让我们开始吧,我们先搭建APP的主框架。界面如下所示:

APP框架

界面主要有三部分组成,顶部的标题栏,中间的显示内容的区域和底下的页面切换的一些按钮。用专业的术语描述就是分为顶部的ActionBar(ToolBar),中间部分的NavHostFragment,底部的 BottomNavigationView 三部分。

新建5个Fragment

这5个Fragment分别是 发现, 视频
, 我的, 云村, 账号 五个模块的入口页面。

创建方式如下:

  1. 使用New -> Activity -> Empty Activity 新建一个名为 MainActivity.ktActivity
  2. 通过New -> Fragment -> Fragment(Blank) 新建5个Fragment,分别命名为 DiscoveryMainFragment.kt, VideoMainFragment.kt, MineMainFragment.kt, CloudMainFragment.kt, AccountMainFragment.kt, 。
  3. 删掉Fragment中不必要的代码,只留下onCreateView方法,示例如下:
class DiscoveryMainFragment : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_discovery_main, container, false)
    }

}
  1. 修改Fragment的显示内容,将FrameLayout改为ConstraintLayout,将TextView居中显示,然后文字的text改为对应的界面的名字

发现页面

新建 Menu

新建一个Menu作为底部导航视图BottomNavigationViewMenu,用来切换5个模块的显示。

创建方式如下:

  1. res -> New -> Android Resources File, 在弹出框内的File Name 填入 tab_menu, Resource type 选择 Menu。这样如果没有Menu文件夹会创建一个文件夹,然后在Menu文件夹下生成一个 tab_menu.xml文件

创建Menu

  1. Menu 添加5个Menu Item, 设置id,文字和图片

Menu添加

组装 BottomNavigationView

  1. activity_main.xml中拖入一个BottomNavigationView, 约束设置为高度包裹内容宽度填充父视图。
  2. 设置itemBackground的值为BottomNavigationView添加背景颜色
  3. itemIconTint设置一个颜色选择器,这样选中后和未选中的图片的颜色可以切换
  4. itemTextColor设置一个颜色选择器,这样选中后和未选中的图片的颜色可以切换,和itemIconTint的值最好是一致的,这样更符合审美
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_checked="true" android:color="@color/colorAccent" />
    <item android:state_checked="false" android:color="#757575" />
</selector>
  1. 设置 labelVisibilityModelabeled,表示MenuItem文字总是显

默认的情况下超过三个,只有选中的那个Item的文字才显示,不符合我们的设计要求。

  1. 设置 Menu 为我们上面创立的tab_menu

设置和最后的展示效果如下图:

BottomNavigationView

创建和设置Navigation

Navigation文件中定义了destination(Fragment)action(跳转路径)界面间的参数(arguments)及deeplink等内容,接下来我们一步步的了解这些内容。

  1. res -> New -> Android Resources File, 在弹出框内的File Name 填入 main_navigation, Resource type 选择 Navigation。这样如果没有Navigation文件夹会创建一个文件夹,然后在Navigation文件夹下生成一个 main_navigation.xml文件

Navigation创建

  1. 将5个Fragment添加进main_navigation.xml文件来

由于5个Fragment是独立的,没有相互跳转的关系,所以直接添加进来就行。后面会介绍界面跳转的定义

示例

注意:这里设置的id不是随便设置的哦,是需要和Menu item的id是一一对应的哦。否则会导致无法实现页面切换。

添加完成后的结果如下

添加完的结果

添加ToolBar 和 NaviGraph
  1. 回到activity_main.xml文件中来,我们添加一个ToolBar做为ActionBar,放置在顶部。

说明:把ActionBar替换为ToolBar的目的是为了能更好的对标题栏进行高度定制化,且ToolBar在各种Android版本的样式也很统一。

  1. ToolBarBottomNavigationView 之间的剩余空间放入 NavHostFragmentNavHostFragmentNaviGraph 选择前面建好的main_navigation

说明:NavHostFragment就是其他布局文件引入Navigation文件的容器,Navigation中的Fragment等的内容能在这里面显示

ToolBar

进行Navigation功能装配

先运行下程序,看看效果。

初步效果

发现的页面的内容是能够正常显示了,但是点击底部的Menu没有切换效果,接下来我们就来解决这个问题。

  • 打开MainActivity,添加两个属性 navControllerappBarConfiguration
// 1
private val navController by lazy { findNavController(R.id.fragment) }
// 2
private val appBarConfiguration by lazy { AppBarConfiguration.Builder(bottomNavigationView.menu).build()  }
  1. navController的用来负责导航的对象,也就是切换Fragment和管理导航栈(Navigation Stack)R.id.fragment是布局文件中NavHostFragment设置的id
  2. appBarConfiguration是来定义哪些Fragment处于导航栈的顶层,这样navController就能正确的处理导航栈。bottomNavigationView.menu就是前面创建的tab_menu
  • 接下来添加ToolBar和组装Navigation
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    // 1
    setSupportActionBar(toolbar)
    // 2
    setupNavigation()
}

/* 导航控件的实现 */
private fun setupNavigation() {
    // 2.1
    setupActionBarWithNavController(navController, appBarConfiguration)
    // 2.2
    bottomNavigationView.setupWithNavController(navController)
}
  1. 把布局文件中的toolbar设置为ActionBar,切换Fragment对应的标题就能自动显示在toolbar
  2. setupNavigation方法中的代码设置后 5个Fragment能导航到其他页面 和 底部Menu点击后5个Fragment之间的切换

再次运行下程序,发现切换的功能正常了。

正常切换

知识点拾遗

细心的读者可能发现了,由于我们的主题色是白色,造成了状态栏的文字和图片都无法识别。

状态栏

解决这个问题可以在style文件中添加如下的设置,然后状态栏的文字和图片就变成了深色。

<item name="android:windowLightStatusBar">true</item>

导航界面跳转

目前为止,实现了5个主页面之间的切换,接下来我们介绍如何实现从主界面跳转到二级界面,三级界面等其他界面。。。

  • 按照前面新建Fragment的步骤,新建一个名为DiscoverySecondaryFragment的页面作为二级页面,新建一个名为DiscoveryThirdFragment的页面作为三级页面
  • 将这两个文件加入到main_navigation文件中
  • id分别命名为 discovery_secondary_fragment, discovery_third_fragment
  • label分别命名为发现二级界面发现三级界面

发现二级界面

  • 发现页面发现二级界面之间连接起来,发现二级页面发现三级界面之间连接起来,他们之间的连线会有一个action id
    连线

  • 代码设置跳转

  1. 直接通过**Fragment id **进行跳转

DiscoveryMainFragment添加如下代码

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    go_to_discovery_second.setOnClickListener {
        // 1 
        findNavController().navigate(R.id.discovery_secondary_fragment)
    }
}

提示:R.id.discovery_secondary_fragment就是发现二级界面id

  1. 通过刚才**Action id **进行跳转

将刚才标记1处的代码替换成

    findNavController().navigate(R.id.action_discovery_main_fragment_to_discovery_secondary_fragment)

提示:R.id.action_discovery_main_fragment_to_discovery_secondary_fragment就是刚才连线界面的时候自动生成的Action id

二级和三级界面的逻辑同理,不再做介绍. 效果如下

跳转效果

  • 解决发现的两个问题

首先,我们虽然跳过去了,但是点击ToolBar左侧的返回按钮没法返回到上个页面,我们需要覆写MainActivityonSupportNavigateUp方法

override fun onSupportNavigateUp(): Boolean {
    return super.onSupportNavigateUp() || navController.navigateUp()
}

还有一个问题就是,底部的BottomNavigationView应该只有在显示5个主页面的时候才会显示,其他的页面应该是不能看到和选择的。我们可以在setupNavigation添加导航的监听器

/* 导航控件的实现 */
private fun setupNavigation() {
    // ...
    // 导航监听
    navController.addOnDestinationChangedListener { _, destination, _ ->
        if (destination.id in arrayOf(
                R.id.discovery_main_fragment,
                R.id.video_main_fragment,
                R.id.mine_main_fragment,
                R.id.cloud_main_fragment,
                R.id.account_main_fragment
            )) {
            bottomNavigationView.visibility = View.VISIBLE
        } else {
            bottomNavigationView.visibility = View.GONE
        }
    }
}

修改后的结果如下

修改后跳转结果

Navigation传值

我们接下来介绍下导航的时候如何传参。我们先修改下界面,发现页面添加一个TextView, 在这个页面输入的内容跳转到第二个页面的时候被带过去。需求如下图所示:

传参需求示例

普通传值方式

开始吧,流程如下:

  1. 选中发现二级界面,在Attributes -> Arguments 添加 “name” 字段的参数名,内容可空

传参

  1. 第一个界面代码中实现跳转带上参数

修改DiscoveryMainFragment中点击事件的方法

go_to_discovery_second.setOnClickListener {
    val etTitle = editTextTextPersonName.text.toString()
    if (TextUtils.isEmpty(etTitle)) {
        // 1
        findNavController().navigate(R.id.action_discovery_main_fragment_to_discovery_secondary_fragment)
    } else {
        // 2
        Bundle().also {
            it.putString("name", etTitle)
            findNavController().navigate(R.id.action_discovery_main_fragment_to_discovery_secondary_fragment, it)
        }
    }
}

上面代码的意思是如果没有输入则不需要传参,如果有输入,则把参数封装在Bundle中传递过去。

  1. 第二个界面代码中实现接收参数

DiscoveryScondaryFragment中添加如下代码

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    // ...
    arguments?.getString("name")?.let {
        name_tv.text = "您输入的名字为:$it"
    } ?: let {
        name_tv.text = "您未输入名字"
    }
}

代码逻辑也很明白,从arguments中去取字段为name的值。

最后效果如下:

传参结果

使用Safe Args传值

Safe Args是一个Gradle插件,可以强制页面跳转的时候传递正确的参数格式。

  • 开始引入 Safe Args

使用Safe Args 需要在appbuild.gradle中引入插件

apply plugin: 'kotlin-android-extensions'

由于需要Java8的支持,所以需要在appbuild.gradleandroid 函数下添加如下代码

compileOptions {
    sourceCompatibility = JavaVersion.VERSION_1_8
    targetCompatibility = JavaVersion.VERSION_1_8
}

kotlinOptions {
    jvmTarget = JavaVersion.VERSION_1_8
}

此外需要的 projectdependncies 中指定路径

def nav_version = "2.3.0"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
  • 修改代码
  1. 第一个界面代码中实现跳转带上参数
var etTitle = editTextTextPersonName.text.toString()
if (TextUtils.isEmpty(etTitle)) {
    etTitle = ""
} 
val toDirection = DiscoveryMainFragmentDirections.actionDiscoveryMainFragmentToDiscoverySecondaryFragment(etTitle)
findNavController().navigate(toDirection)

Safe Args如果没有值需要设置给一个默认值,否则会崩溃

说明:DiscoveryMainFragmentDirections 和 actionDiscoveryMainFragmentToDiscoverySecondaryFragment 都是插件自动生成,不需要手动创建

  1. DiscoveryMainFragmentDirections = DiscoveryMainFragment + Directions,代表从 DiscoveryMainFragment 开始跳转的导航

  2. actionDiscoveryMainFragmentToDiscoverySecondaryFragment 是 action id 的驼峰命名法,代表的导航到哪个Fragment

  3. 方法的传参就是 name, 如果需要多个参数,则是生成的需要传递多个参数的方法。

  1. 第二个界面代码中实现接收参数

在第二个页面定义一个args属性

private val args: DiscoverySecondaryFragmentArgs by navArgs()

说明:DiscoverySecondaryFragmentArgs 也是插件自动生成,不需要手动创建

  1. DiscoverySecondaryFragmentArgs = DiscoverySecondaryFragment + Args, 代表DiscoverySecondaryFragment 得到的上个页面传递过来参数的类

  2. args 里面有 name 的值

通过如下代码获取相应值。

val (picture) = args
if (picture === "") {
    name_tv.text = "您未输入名字"
} else {
    name_tv.text = "您输入的名字为:$picture"
}

Navigation跳转动画

为了不和前面的内容混淆,我们新建一个VideoSecondaryFragment, 然后连接一个ActionVideoMainFragmentVideoSecondaryFragment。然后修改下界面内ring,界面如下图所示:

视频界面

给 Action 设置 animation

我们首先来实现平移动画,动画方式是进入下个页面时,两个页面内容从右往左移,;返回上个页面的时候两个页面内容从左往右移。

  • 首先我们定义四个动画
// left_to_right_enter_anim
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate android:fromXDelta="-100%"
        android:toXDelta="0%"
        android:duration="200"
        />
</set>

// left_to_right_exit_anim
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate android:fromXDelta="0%"
        android:toXDelta="100%"
        android:duration="200"
        />
</set>

// right_to_left_enter_anim
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate android:fromXDelta="100%"
        android:toXDelta="0"
        android:duration="200"
        />
</set>

// right_to_left_exit_anim
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate android:fromXDelta="0%"
        android:toXDelta="-100%"
        android:duration="200"
        />
</set>
  • 将这四个动画设置给 action_video_main_fragment_to_video_secondary_fragment

选中action, 然后给enterAnim,exitAnim,popEnterAnimpopExitAnim选择相应的动画文件。

设置动画

最后的效果如下 :

动画效果

共享元素变换(Shared Element Transition)

共享元素变换给人某个UI元素能在两个Fragment/Activity之间共用的假象,能实现一个比较流畅的转场效果,给用户的感觉比较酷炫。

共享元素变换的概念我这里不做过多介绍,可以参考相关的开发文档。

我这里来实现一个图片从小到全屏的转场过程。

  • 定义一个Transition 文件

res -> New -> Android Resources File, 在弹出框内的File Name 填入 shared_element_transition, Resource type 选择 Transition。这样如果没有Transition文件夹会创建一个文件夹,然后在Transition文件夹下生成一个 shared_element_transition.xml文件

<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
    <changeBounds />
</transitionSet>

我们这儿定义的是一个changeBounds转场。

  • VideoMainFragment中对按钮添加点击事件
share_btn.setOnClickListener {
    // 1
    ViewCompat.setTransitionName(cat_iv, "cat")
    // 2
    val extra = FragmentNavigatorExtras(
        cat_iv to "cat"
    )
    // 3
    val mainDirection = VideoMainFragmentDirections.actionVideoMainFragmentToVideoSecondaryFragment()
    // 4
    findNavController().navigate(mainDirection, extra)
}

这段代码的作用是:

  1. cat_iv这个ImageView设置一个TranSitionName
  2. 构造一个FragmentNavigatorExtras对象,这个对象从名称来看就知道是给Navigator提供的,而不是下个页面。因为共享元素变换是交给Navigator来处理,它需要知道哪个View需要动画,以及动画的名称
  3. 这个和前面传参章节介绍的一样,因为不需要传参,所以不需要传值
  4. 开始导航
  • VideoSecondaryFragment中设置共享元素和动画

VideoSecondaryFragment添加如下代码

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    // 1
    sharedElementEnterTransition = TransitionInflater.from(context)
            .inflateTransition(R.transition.shared_element_transition)
    // 2
    ViewCompat.setTransitionName(cat_iv, "cat")
}

这段代码的作用是:

  1. 设置sharedElementEnterTransition
  2. cat_iv这个ImageView设置同样的TranSitionName

所有的工作就完成了,动画就开始了。

提示:有些需求图片是从网络中回去的,所以会涉及到延迟动画 postponeEnterTransition(), 等待网络请求完成再调用开始动画startPostponedEnterTransition() 这一过程。

共享元素变换

导航文件拆分

细心的你可能会发现一个问题,如果项目越来越大,页面越来越多,所有的都放在一个文件中会非常的臃肿,当Action非常复杂的时候会非常的混乱。

所以你肯定会提出一个问题,是否能把页面拆分到多个导航文件中去。Google已经给了肯定的答案。

  • 新建 Navigation文件

我们先创建一个ad_navigation.xml,里面加入一个页面AdFragment页面,界面展示如下:

广告导航

  • 导入到 主 Navigation 文件

导航拆分

导入后就可以直接使用了。使用方式和前面介绍的类似。

findNavController().navigate(R.id.ad_navigation)

Deeplink导航

Navigation也能通过Deeplink进行跳转。接下来我们介绍下如何实现Deeplink跳转。

  • 首先为广告页面添加一个deeplink

为广告页面添加一个JJMusic://ad.fragmentdeeplinkDeepLink]

  • 使用
findNavController().navigate(Uri.parse("JJMusic://ad.fragment"))

结尾

由于本节内容稍微有点多,提取下一节专门介绍ToolBar的功能,介绍Navigation跳转时切换时ToolBar的右上角按钮的切换和使用。

猜你喜欢

转载自blog.csdn.net/lcl130/article/details/108422026