欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Navigation组件的使用详解

程序员文章站 2022-05-28 23:25:28
...

 

 

 

Navigation 是 JetPack 中的一个组件,用于方便的实现页面的导航,所以抽象出了一个 destination 的概念,大部分情况一个 destination 就表示一个 Fragment,但是它同样可以指代 Activity、其它的导航图

最初要有个起始页面,叫 start destination,处于栈底,是启动时的第一个页面,当然也是返回可见的最后一个页面。多个 destination 连接起来就组成了一个导航图,类似于一种栈结构,页面先进后出。destination 之间的连接叫做 action

准备

1、在 Android Studio 3.2 Canary 14 以上的版本中,打开 Preferences -> Experimental -> Enable Navigation Editor,然后重启。

2、添加依赖:

       implementation 'androidx.navigation:navigation-fragment-ktx:2.0.0'

       implementation 'androidx.navigation:navigation-ui-ktx:2.1.0'

创建资源文件

3、在 res 目录右击,选择 New > Android Resource File,Resource type 选择 Navigation。如下图

Navigation组件的使用详解

创建 destination

先创建一个 Fragment

class HomeFragment : Fragment() {

    // TODO: Rename and change types of parameters
    private var mParam1: String? = null
    private var mParam2: String? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        if (arguments != null) {
            mParam1 = arguments!!.getString(ARG_PARAM1)
            mParam2 = arguments!!.getString(ARG_PARAM2)
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_home, container, false)
    }

    companion object {
  
        // TODO: Rename and change types and number of parameters
        fun newInstance(param1: String, param2: String): HomeFragment {
            val fragment = HomeFragment()
            val args = Bundle()
            args.putString(ARG_PARAM1, param1)
            args.putString(ARG_PARAM2, param2)
            fragment.arguments = args
            return fragment
        }
    }

}

然后配置 navigation 文件,打开 res/navigation/nav_home 文件,添加一个 fragment 节点

  • name 指定 Fragment 的路径
  • tools:layout 指定布局文件
  • app:startDestination 指定这个 Fragment 是 start destination

Navigation组件的使用详解

 

也可以在 nav_home 的 design 视图下,选择 Create blank destination 来创建一个 Fragment,而不用先创建好再选择。

Navigation组件的使用详解

Activity 中引用

第一种方式是在 xml 里写 fragment。如下:

<?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">

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/bottomNavigationView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#ffffff"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:menu="@menu/tab_menu" />

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/home_fragment_layout"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:visibility="visible"
        app:layout_constraintBottom_toBottomOf="@+id/second_fragment_layout"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <fragment
            android:id="@+id/home_fragment"
            android:name="androidx.navigation.fragment.NavHostFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:defaultNavHost="true"
            app:navGraph="@navigation/nav_home" />
    </androidx.constraintlayout.widget.ConstraintLayout>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/second_fragment_layout"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:visibility="gone"
        app:layout_constraintBottom_toTopOf="@+id/bottomNavigationView"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <fragment
            android:id="@+id/second_fragment"
            android:name="androidx.navigation.fragment.NavHostFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:defaultNavHost="true"
            app:navGraph="@navigation/nav_second"
            tools:layout_editor_absoluteX="146dp"
            tools:layout_editor_absoluteY="264dp" />

    </androidx.constraintlayout.widget.ConstraintLayout>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/person_fragment_layout"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:visibility="gone"
        app:layout_constraintBottom_toBottomOf="@+id/second_fragment_layout"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <fragment
            android:id="@+id/person_fragment"
            android:name="androidx.navigation.fragment.NavHostFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:defaultNavHost="true"
            app:navGraph="@navigation/nav_person" />
    </androidx.constraintlayout.widget.ConstraintLayout>


</androidx.constraintlayout.widget.ConstraintLayout>
  • android:name 是 HomeFragment,它实现了 NavHost,这是一个用于放置管理 destination 的空视图。
  • app:navGraph 用于将这个HomeFragment 和 nav_home.xml 关联起来。
  • app:defaultNavHost 表示 NavHostFragment 可以拦截处理返回键。

第二种方式是通过代码创建 NavHostFragment,先修改 Activity 的 xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 
    ... >

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/frame_layout" />
</android.support.constraint.ConstraintLayout>

然后在 Activity 中引入:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_navigation)
    
    val finalHost = HomeFragment.create(R.navigation.nav_home)
    supportFragmentManager.beginTransaction()
            .replace(R.id.frame_layout, finalHost)
            .setPrimaryNavigationFragment(finalHost) // 等价于 xml 中的 app:defaultNavHost="true"
            .commit()
}

多个Fragment切换控制:

底部Tab使用的是BottomNavigationView

tab_menu.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/homeFragment"
        android:icon="@drawable/ic_filter_1_black_24dp"
        android:title="首页" />
    <item
        android:id="@+id/secondFragment"
        android:icon="@drawable/ic_filter_2_black_24dp"
        android:title="第二" />
    <item
        android:id="@+id/personFragment"
        android:icon="@drawable/ic_filter_3_black_24dp"
        android:title="个人中心" />
</menu>

MainActivity类代码:

package com.king.navigationdemo

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.navigation.NavController
import androidx.navigation.Navigation
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {
    companion object {
        const val HOME_FRAGMENT = 0
        const val SECOND_FRAGMENT = 1
        const val PERSON_FRAGMENT = 2
    }

    private var homeFragment: NavController? = null
    private var secondFragment: NavController? = null
    private var personFragment: NavController? = null
    private var fragmentList = mutableListOf<ConstraintLayout>()
    private var showFragment = 0
    private var isBack = false
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        initNavigationController()
        initTab()
    }

    private fun initTab() {
        fragmentList.add(home_fragment_layout)
        fragmentList.add(second_fragment_layout)
        fragmentList.add(person_fragment_layout)
        //设置导航栏菜单项Item选中监听
        bottomNavigationView.setOnNavigationItemSelectedListener { item ->
            when (item.itemId) {
                R.id.homeFragment -> {
                    showFragmentNavigation(HOME_FRAGMENT)
                }
                R.id.secondFragment ->{
                    showFragmentNavigation(SECOND_FRAGMENT)
                }
                R.id.personFragment ->{
                    showFragmentNavigation(PERSON_FRAGMENT)
                }
            }
            true
        }
    }

    private fun showFragmentNavigation(index: Int) {
        showFragment = index
        fragmentList.forEachIndexed { i, layout ->
            layout.visibility = if (i == index) {
                View.VISIBLE
            } else {
                View.GONE
            }

        }
    }

    private fun initNavigationController() {
        homeFragment = Navigation.findNavController(this, R.id.home_fragment)
        secondFragment = Navigation.findNavController(this, R.id.second_fragment)
        personFragment = Navigation.findNavController(this, R.id.person_fragment)
    }
    override fun onBackPressed() {
        when (showFragment) {
            HOME_FRAGMENT -> {
                isBack = homeFragment?.popBackStack() ?: false
            }
            SECOND_FRAGMENT -> {
                isBack = secondFragment?.popBackStack() ?: false
            }
            PERSON_FRAGMENT -> {
                isBack = personFragment?.popBackStack() ?: false
            }
        }
        if (!isBack) {
            super.onBackPressed()
        }

    }
}

添加跳转动画

点击目标箭头,右侧添加动画:

Navigation组件的使用详解

代码自动变成:

<?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/nav_home_frament"
    app:startDestination="@+id/nav_home_frament"
    tools:ignore="UnusedNavigation">
    <fragment
        android:id="@+id/nav_home_frament"
        android:name="com.king.navigationdemo.fragments.HomeFragment"
        android:label="home"
        tools:layout="@layout/fragment_home"
        >

    </fragment>
    <action
        android:id="@+id/action_nav_home_frament_to_nav_second_fragment"
        app:destination="@+id/nav_home_frament"
        app:enterAnim="@anim/nav_default_enter_anim"
        app:exitAnim="@anim/nav_default_exit_anim"
        app:popEnterAnim="@anim/nav_default_pop_enter_anim"
        app:popExitAnim="@anim/nav_default_pop_exit_anim" >
    </action>
</navigation>

支持 View 动画和属性动画,enterAnim 和 exitAnim 是去往栈里添加一个 destination 时两个 destination 的动画,popEnterAnim 和 popExitAnim 是从栈里移除一个 destination 时的动画。

传递数据

要跳转到 SecondFragment,要往 SecondFragment 里带数据,在目的 Fragment 里添加 <argument>

<?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/nav_second"
    android:label="second"
    app:startDestination="@+id/nav_second_fragment"
    tools:ignore="UnusedNavigation">
<fragment
android:id="@+id/nav_second_fragment"
android:name="com.king.navigationdemo.fragments.SecondFragment"
android:label="second"
tools:layout="@layout/fragment_second">
<argument
    android:name="name"
    android:defaultValue="default_value" />
</fragment>
    </navigation>

FirstFragment 添加数据

button.onClick {
     val bundle = bundleOf("name" to "silas")
     Navigation.findNavController(getView()!!)
            .navigate(R.id.action_nav_graph_first_fragment_to_nav_graph_second_fragment, bundle)
}

 

SecondFragment 获取数据

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                          savedInstanceState: Bundle?): View? {
    arguments?.getString("name")?.let { toast("hello $it") }
    return inflater.inflate(R.layout.fragment_second, container, false)
}

类型安全方式传递数据

项目的 build.gradle 中添加

buildscript {
    repositories {
        google()
    }
    dependencies {
        classpath "android.arch.navigation:navigation-safe-args-gradle-plugin:1.0.0-alpha05"
    }
}

module 的 build.gradle 中应用:

apply plugin: "androidx.navigation.safeargs"

同步发现要升级 gradle 版本到 4.6,随之 gradle tools 必须到 3.2.0-rc02,然后要升级 kotlin 版本,然后又让下载 build tools 28.0.2,然后总是不能下载,看网上方法,关闭代理,把 Preferences -> HTTP ProxyNo proxy 改成 Auto-detect proxy settings

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

    <fragment
        android:id="@+id/nav_graph_second_fragment"
        android:name="pot.ner347.androiddemo.navigation.SecondFragment"
        android:label="second"
        tools:layout="@layout/fragment_second" >
        <argument android:name="name" android:defaultValue="Max" app:argType="string"/>
    </fragment>
</navigation>

和普通的区别就在于 <argument> 多了个 argType 指定了数据类型。

FirstFragment 修改

val action = FirstFragmentDirections.actionNavGraphFirstFragmentToNavGraphSecondFragment()
action.setName("Silas")
Navigation.findNavController(getView()!!).navigate(action)

SecondFragment 接收

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
    toast("hello ${SecondFragmentArgs.fromBundle(arguments).name}")
    return inflater.inflate(R.layout.fragment_second, container, false)
}

 

如果 FirstFragment 去掉 action.setName("Silas"),那么 SecondFragment 里得到的也是默认值 Max。

看生成的 FirstFragmentDirections 的 setName 和 SecondFragmentArgs 的 fromBundle:

@NonNull
public ActionNavGraphFirstFragmentToNavGraphSecondFragment setName(@NonNull String name) {
  if (name == null) {
    throw new IllegalArgumentException("Argument \"name\" is marked as non-null but was passed a null value.");
  }
  this.name = name;
  return this;
}
@NonNull
public static SecondFragmentArgs fromBundle(Bundle bundle) {
SecondFragmentArgs result = new SecondFragmentArgs();
bundle.setClassLoader(SecondFragmentArgs.class.getClassLoader());
if (bundle.containsKey("name")) {
  result.name = bundle.getString("name");
  if (result.name == null) {
    throw new IllegalArgumentException("Argument \"name\" is marked as non-null but was passed a null value.");
  }
}

加了一些判断,所谓安全也就是指这个吧。

Demo源码地址:https://github.com/wangzhuang/NavigationDemo.git

 

相关标签: Navigation