美文网首页
JetPack知识点实战系列二:Navigation实践之实现A

JetPack知识点实战系列二:Navigation实践之实现A

作者: chonglingliu | 来源:发表于2020-09-05 17:10 被阅读0次

    本节教程我将带大家来一步步实现主页的框架,一个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,文字和图片
    添加MenuItem

    组装 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是独立的,没有相互跳转的关系,所以直接添加进来就行。后面会介绍界面跳转的定义

    添加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等的内容能在这里面显示

    添加NavHostFragment

    进行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" 字段的参数名,内容可空
    添加Argument
    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.fragmentdeeplink

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

    结尾

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

    相关文章

      网友评论

          本文标题:JetPack知识点实战系列二:Navigation实践之实现A

          本文链接:https://www.haomeiwen.com/subject/fagzsktx.html