使用导航架构组件在Android应用程序中实现导航

图片


来自翻译


你好,哈伯昌。 这是针对Android开发人员的新导航技术的文档文章的翻译。 该技术目前正在开发中,但已经可以使用,甚至可以在Android Studio 3.2及更高版本的预览版中使用。 我已经在行动中尝试过了,可以说她给我留下了深刻的印象。 最后,切换屏幕已不再是一件复杂的事情,特别是如果使用了从一个屏幕到另一个屏幕的数据传输。 实际上,我正在做翻译,以便让更多说俄语的开发人员关注该技术,并简化其研究。
如果发现重大错误或不正确,请在评论中报告。


建筑组成


导航体系结构组件使在应用程序中的目标之间轻松实现导航成为可能。 默认情况下,导航支持将片段和活动作为目标屏幕,但是您也可以添加对新型目标屏幕的支持 。 目标屏幕集称为应用程序的导航图


除了导航图上的目标屏幕外,它们之间还有称为action的连接。 图1展示了导航图的直观表示,该图简单地应用了由五个动作连接的六个目标屏幕。



图1.导航图


导航的体系结构组件是根据导航原理来实现的。


如果要在Android Studio中使用导航的体系结构组件,则需要Android Studio 3.2 Canary 14或更高版本。

使用导航支持设置项目


在创建导航图之前,您需要为项目配置导航。 为此,请按照下列步骤操作。


  1. 在build.gradle文件中添加导航支持(模块:app- Translator Note )。 有关更多信息,请参见将组件添加到您的项目
    注意事项 译者:最好不要偷懒并遵循上面的链接,因为该项目正在积极开发中,并且依赖关系肯定会发生变化:


    dependencies { def nav_version = "1.0.0-alpha05" // use -ktx for Kotlin implementation "android.arch.navigation:navigation-fragment:$nav_version" // use -ktx for Kotlin implementation "android.arch.navigation:navigation-ui:$nav_version" // optional - Test helpers // use -ktx for Kotlin androidTestImplementation "android.arch.navigation:navigation-testing:$nav_version" } 

  2. 在项目窗口中,右键单击res文件夹,然后选择“ 新建”>“ Android资源文件”
    将打开“ 新资源”对话框。
  3. 输入文件名,例如“ nav_graph ”。
  4. 资源类型下拉列表中选择导航
  5. 单击确定 。 将会发生以下情况:
    • 导航子目录出现在res目录中。
    • nav_graph.xml文件出现在导航目录中。
    • nav_graph.xml文件在导航编辑器中打开。 该文件包含您的导航图。
  6. 选择编辑模式文本 。 空的导航图的XML文件如下所示:
     <?xml version="1.0" encoding="utf-8"?> <navigation xmlns:android="http://schemas.android.com/apk/res/android"> </navigation> 
  7. 选择设计编辑模式以返回到导航编辑器。
    注意事项 转换器:如果导航编辑器未显示图形,请检查以黄色突出显示的下一个段落的开头。

导航编辑器概述


默认情况下,导航编辑器仅在Android Studio的Canary版本中可用。 要在Beta版本,候选版本或稳定版本中使用导航编辑器,请转到文件>设置 (对于Mac, Android Studio>首选项 ),选择实验类别,选中启用导航编辑器复选框,然后重新启动Android Studio。
注意事项 译者:我建议无论装配如何,都要检查此复选框是否值得。

在导航编辑器中,您可以快速创建导航图,而不必编写XML。 如图2所示,导航编辑器包含三个部分:



图2.导航编辑器


章节说明:


  1. 目标屏幕列表-显示添加到图形的所有目标屏幕
  2. 图形编辑器-包含图形的直观表示
  3. 属性编辑器-包含目标屏幕的属性和操作。

定义目标屏幕


创建导航图的第一步是为应用程序定义目标屏幕。 您可以创建一个空白的分配屏幕,也可以使用当前项目中的片段和活动。


Navigation的体系结构组件是为具有一个主要活动(Main Activity- Translator's Note )的应用程序设计的,该活动具有许多用作目标屏幕的片段。 主要活动是导航图的“主机”。 在具有许多活动的应用程序中,每个活动都是不同导航图的宿主。 在文档的后面部分将介绍如何将活动转换为导航图的主机

若要确定您的应用程序的目标屏幕,请按照下列步骤。


  1. 在图形编辑器中,单击“ 新建目标” 。 将打开“ 新目标”对话框。
  2. 单击创建空白目标或选择现有的片段或活动。 将打开“ Android组件”对话框。
  3. 在“ 片段名称”字段中输入名称 。 它必须与该片段的类名匹配。
  4. 在“ 片段布局名称”字段中输入名称 。 该名称将分配给布局片段文件。
  5. 点击完成 。 在目标屏幕列表中会出现一条带有新目标屏幕名称的行,并且目标屏幕本身会出现在图形编辑器中。 更详细地:
    • 图形编辑器显示目标屏幕的预览。 如果创建了空白目标屏幕,则新片段将包含题词“ Hello blank fragment”,并且在图形编辑器中将看到相同的题词。 如果选择现有片段或活动,则图形将显示其微型版本。
    • 如果创建了空白的目标屏幕,则会为其生成一个类。 类名将与步骤3中指定的名称匹配。
    • 如果创建了空白的目标屏幕,将为其生成一个布局文件。 文件名将与步骤4中指定的名称匹配。
      图3显示了空白的现有目标屏幕。

      图3.新的和现有的目标屏幕。
  6. 单击新创建的目标屏幕以将其选中。 属性面板包含以下属性:
    • 可以将“类型”字段设置为“片段”或“活动”,以显示如何在源代码中实现目标屏幕:作为片段还是作为活动。
    • 标签字段包含目标屏幕文件的XML名称。
    • ID字段包含目标屏幕的ID(作为独立实体- 译者注 ),用于在源代码中引用它(通过R.id-译者 )。
    • “类别”字段包含目标屏幕的类别名称(带有所有软件包的指示- 注意翻译器 )。
  7. 切换到文本编辑模式以切换到查看XML。 XML现在包含属性id,名称(类名),标签和布局,这些属性显示现有类和布局文件的名称:

 <?xml version="1.0" encoding="utf-8"?> <navigation xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" xmlns:android="http://schemas.android.com/apk/res/android" app:startDestination="@id/blankFragment"> <fragment android:id="@+id/blankFragment" android:name="com.example.cashdog.cashdog.BlankFragment" android:label="Blank" tools:layout="@layout/fragment_blank" /> </navigation> 

XML包含startDestination属性,该属性包含空目标屏幕的ID( 应用程序:startDestination =“ @ + id / fragment” )。 有关目标启动屏幕的更多信息,请参阅“目标启动屏幕”部分。

连接目标画面


您的应用程序必须具有多个目标屏幕才能连接它们。 下面介绍带有两个空白目标屏幕的导航图的XML:


 <?xml version="1.0" encoding="utf-8"?> <navigation xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" xmlns:android="http://schemas.android.com/apk/res/android" app:startDestination="@id/blankFragment"> <fragment android:id="@+id/blankFragment" android:name="com.example.cashdog.cashdog.BlankFragment" android:label="fragment_blank" tools:layout="@layout/fragment_blank" /> <fragment android:id="@+id/blankFragment2" android:name="com.example.cashdog.cashdog.BlankFragment2" android:label="Blank2" tools:layout="@layout/fragment_blank_fragment2" /> </navigation> 

使用操作连接目标屏幕。 要连接两个目标屏幕,您需要:


  1. 在图形编辑器中,将鼠标悬停在要从其进行过渡的目标屏幕上。 一个圆圈将出现在其上。

    图4.动作连接圈
  2. 单击圆圈并按住鼠标按钮。 将光标移动到要转换到的目标屏幕。 放手 两个目标屏幕之间会出现一个箭头,指示它们之间的转换。

    图5.连接的目标屏幕
  3. 单击箭头以突出显示操作。 在属性面板中显示以下内容:
    • 类型字段包含“操作”。
    • ID字段包含自动生成的操作ID。
    • Destination字段包含过渡到的目标屏幕的ID。
  4. 切换到文本编辑模式以查看XML。 动作标签已添加到父目标屏幕。 该操作具有生成的ID和包含下一个目标屏幕的ID的属性。

 <?xml version="1.0" encoding="utf-8"?> <navigation xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" xmlns:android="http://schemas.android.com/apk/res/android" app:startDestination="@id/blankFragment"> <fragment android:id="@+id/blankFragment" android:name="com.example.cashdog.cashdog.BlankFragment" android:label="fragment_blank" tools:layout="@layout/fragment_blank" > <action android:id="@+id/action_blankFragment_to_blankFragment2" app:destination="@id/blankFragment2" /> </fragment> <fragment android:id="@+id/blankFragment2" android:name="com.example.cashdog.cashdog.BlankFragment2" android:label="fragment_blank_fragment2" tools:layout="@layout/fragment_blank_fragment2" /> </navigation> 

将目标屏幕指定为开始屏幕


图形编辑器显示房屋图标 在第一个目标屏幕的名称之后。 此图标表示目标屏幕是导航图中的开始屏幕。 您可以按照以下步骤选择其他目标屏幕作为开始屏幕:


  • 在图形编辑器中突出显示所需的目标屏幕。
  • 单击属性栏上的“ 设置开始目标 ”。 现在,此目标屏幕是开始屏幕。 注意事项 转换器:您也可以在所需的目标屏幕上单击人民币,然后在打开的菜单中选择一个类似的项目。

将活动变成导航图的主机


由于将空NavHost元素添加到活动布局,因此活动成为导航图的主机。 NavHost是一个元素,该元素的存在允许您按应用程序用户所需的顺序更改目标屏幕。


导航中的NavHost默认情况下实现NavHostFragment


添加NavHost之后,必须使用app:navGraph属性将其映射到导航图。 这段代码演示了如何在布局中包括NavHostFragment并将其连接到导航图:


 ?xml version="1.0" encoding="utf-8"?> <android.support.constraint.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"> <fragment android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/my_nav_host_fragment" android:name="androidx.navigation.fragment.NavHostFragment" app:navGraph="@navigation/nav_graph" app:defaultNavHost="true" /> </android.support.constraint.ConstraintLayout> 

此示例包含app属性:defaultNavHost =“ true” 。 他负责拦截系统按钮“后退”( 注意翻译:系统按钮“后退”和应用程序顶部面板上的“向上”箭头的工作方式相同 )。 您还可以覆盖AppCompatActivity.onSupportNavigateUp()并调用NavController.navigateUp() ,如下所示:


爪哇
 @Override public boolean onSupportNavigateUp() { return Navigation.findNavController(this, R.id.nav_host_fragment).navigateUp(); } 

科特林
 override fun onSupportNavigateUp() = findNavController(R.id.nav_host_fragment).navigateUp() 

将动作绑定到小部件


使用NavController转到目标屏幕。 可以使用重载的静态findNavController()方法获得它:



收到NavController后,使用其navigation ()方法转到目标屏幕。 navigation()方法接受资源ID。 这可以是您要转到的目标屏幕的ID,也可以是操作ID。 使用操作ID代替目标屏幕ID,可以自定义屏幕之间的过渡动​​画。 有关更多信息,请参见在目标屏幕之间创建动画过渡


此代码演示了如何将操作绑定到按钮:


爪哇
 viewTransactionsButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Navigation.findNavController(view).navigate(R.id.viewTransactionsAction); } }); 

科特林
 viewTransactionsButton.setOnClickListener { view -> view.findNavController().navigate(R.id.viewTransactionsAction) } 

Android支持可存储最后打开的目标屏幕的后向堆栈 。 用户启动应用程序时,第一个目标屏幕被推入堆栈。 每次对navigation()方法的调用都会将一个新的目标屏幕推送到堆栈上。 按下Back或Up按钮将调用NavController.navigateUp()NavController.popBackStack()方法,以从堆栈中提取目标屏幕。


对于按钮,还可以使用便捷的Navigation.createNavigateOnClickListener()方法:


爪哇
 button.setOnClickListener(Navigation.createNavigateOnClickListener(R.id.next_fragment, null)); 

科特林
 button.setOnClickListener(Navigation.createNavigateOnClickListener(R.id.next_fragment, null)) 

将动作绑定到导航抽屉菜单


您可以使用目标屏幕的ID作为菜单项(项目)的ID将导航操作与“导航抽屉”相关联。 以下代码显示了目标屏幕的示例,其ID设置为details_page_fragment


 <fragment android:id="@+id/details_page_fragment" android:label="@string/details" android:name="com.example.android.myapp.DetailsFragment" /> 

使用与目标屏幕和菜单项相同的ID ,将自动链接菜单项和目标屏幕。 此代码演示如何将目标屏幕与菜单项关联(例如,这是menu_nav_drawer.xml文件):


 <item android:id="@id/details_page_fragment" android:icon="@drawable/ic_details" android:title="@string/details" /> 

或者这是带有类别(例如menu_overflow.xml )的菜单的示例:


 <item android:id="@id/details_page_fragment" android:icon="@drawable/ic_details" android:title="@string/details" android:menuCategory:"secondary" /> 

另外,导航架构组件包括NavigationUI类。 此类具有几种静态方法,可用于将菜单项与目标屏幕关联。 例如,此代码显示如何使用setupWithNavController()方法将菜单项连接到NavigationView


爪哇
 NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view); NavigationUI.setupWithNavController(navigationView, navController); 

科特林
 val navigationView = findViewById<NavigationView>(R.id.nav_view) navigationView.setupWithNavController(navController) 

您需要使用NavigationUI方法配置菜单的导航组件,以使它们的状态与NavController中的更改保持同步。


在目标屏幕之间传输数据


您可以通过两种方式在目标屏幕之间传输数据:使用Bundle对象或使用safeargs Gradle插件的类型安全方法。 请按照以下步骤使用Bundle对象传输数据。 如果使用的是Gradle,请参阅以类型安全的方式在目标屏幕之间传输数据一节。


  1. 在图形编辑器中,选择应接收数据的目标屏幕。
  2. 单击属性面板的“参数”部分中的“ 添加”+ )。 将出现一个带有空字段名称(名称),类型(类型)和默认值(默认值)的参数。
  3. 输入参数名称。
  4. 输入默认值。
  5. 突出显示指向目标屏幕的操作(箭头)。 参数默认值部分应包含刚创建的参数。
  6. 切换到文本编辑模式以查看XML。 参数标签已添加到目标屏幕,其中包含name和defaultValues属性。
  7. 在源代码中,创建一个捆绑包,然后使用navigation()方法将其传递到目标屏幕:

爪哇
 Bundle bundle = new Bundle(); bundle.putString("amount", amount); Navigation.findNavController(view).navigate(R.id.confirmationAction, bundle); 

科特林
 var bundle = bundleOf("amount" to amount) view.findNavController().navigate(R.id.confirmationAction, bundle) 

在数据传输到的目标屏幕中,使用getArguments()方法获取捆绑包并使用其内容:


爪哇
 TextView tv = view.findViewById(R.id.textViewAmount); tv.setText(getArguments().getString("amount")); 

科特林
 val tv = view.findViewById(R.id.textViewAmount) tv.text = arguments.getString("amount") 

以类型安全的方式在目标屏幕之间传输数据


导航体系结构组件具有一个称为safeargs的Gradle插件。 它生成最简单的类,以便对目标屏幕和操作屏幕的参数进行类型安全的访问。 safeargs方法是基于Bundle的使用,但是需要一些额外的代码以提高示例安全性。 要添加此插件,请在build.gradle中插入androidx.navigation.safeargs行(在Module:app- Translator comment中)。 例如,像这样:


 apply plugin: 'com.android.application' apply plugin: 'androidx.navigation.safeargs' android { //... } 

注意事项 转换器:另外,将依赖项类路径“ android.arch.navigation:navigation-safe-args-gradle-plugin:1.0.0-alpha02”添加到build.gradle(项目:ProjectName):


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

安装插件后,请按照以下步骤使用类型安全的数据传输:


  1. 在图形编辑器中,突出显示要将数据传输到的目标屏幕。
  2. 在属性面板的“参数”部分中单击+。 属性显示为空字段。
  3. 输入参数名称。
  4. 选择一个参数类型。
  5. 输入默认值。
  6. 突出显示目标屏幕之前的操作。 参数默认值部分应包含刚创建的参数。
  7. 单击文本进入XML编辑模式。

 <fragment android:id="@+id/confirmationFragment" android:name="com.example.buybuddy.buybuddy.ConfirmationFragment" android:label="fragment_confirmation" tools:layout="@layout/fragment_confirmation"> <argument android:name="amount" android:defaultValue="1" app:type="integer"/> </fragment> 

在safeargs插件生成代码之后( 即,在创建类型为-Translator 's Note的参数之后 ),还为发送屏幕和接收屏幕生成了类。
注意事项 转换器:创建自变量后,我必须单击“与Gradle文件同步项目”按钮,以便生成类。


  • 发送者屏幕类别与原始名称相同,在末尾添加了单词Directions。
    也就是说,如果您具有FirstFragment目标屏幕,则生成的类称为FirstFragmentDirections。 此类有一个方法(!),其名称与传递该参数的操作的ID匹配。 也就是说,如果您的FirstFragment屏幕将参数传递给SecondFragment屏幕,并且如果将连接它们的动作称为action_first_to_second,则这是所需的FirstFragmentDirections.action_first_to_second()方法。


  • sender类还包含一个子类,该子类是上述方法的返回类型。 也就是说,在我们的情况下,action_first_to_second()方法的返回类型将为Action_first_to_second。


  • 收件人类的名称与原始名称相同,并在末尾添加了Args一词。 对于我们的示例,SecondFragment是主机,因此将为其生成SecondFragmentArgs类。 此类具有fromBundle()方法来获取参数。



注意事项 译者:现在一切都会变得更加清晰。 与原始代码相比,我对代码进行了一些修改。 所做的更改仅涉及名称,每个名称可以是单独的。 这是为了易于理解。 在这里,名称myArgument用作参数名称,其类型为String。


这段代码演示了如何使用safeargs通过navigation()方法传递参数:


爪哇
 @Override public void onClick(View view) { FirstFragmentDirections.Action_first_to_second action = FirstFragmentDirections.action_first_to_second(); action.setMyArgument("My argument value"); Navigation.findNavController(view).navigate(action); } 

科特林
 override fun onClick(v: View?) { val action = FirstFragmentDirections.action_first_to_second action.myArgument = "My argument value" v.findNavController().navigate(action) } 

这段代码演示了如何使用safeargs提取参数:


爪哇
 @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { String myArgument = SecondFragmentArgs.fromBundle(getArguments()).getMyArgument(); } 

科特林
 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val myArgument = SecondFragmentArgs.fromBundle(arguments).myArgument } 

注意事项 译者:我个人真的很喜欢这种传递参数的方法。 不再有私有的静态最终字符串MY_ARGUMENT_KEY =“ MY_ARGUMENT”。 现在设置和检索参数,使用其单独的(!)Getter和setter方法非常方便。


将目标屏幕分组为嵌套图


可以将目标屏幕的序列分组为一个子图。 子图称为“嵌套图”(nested graph) ,而父图则称为“根图”(root graph)(root graph) 。 嵌套图可用于组织应用程序用户界面各部分的重用,例如授权分支(屏幕顺序)。


除了根,嵌套图还必须具有开始屏幕。 嵌套图封装了其目标屏幕。 嵌套图外部的屏幕(例如,根图的屏幕)只能访问嵌套图的开始屏幕。 图6显示了一个简单的汇款应用程序的导航图。 该图有两个分支:用于转移资金和查看余额。



图6.汇款应用程序的导航图


将目标屏幕分组为嵌套图:


  1. 在图形编辑器中,按住shift键 ,然后单击要包括在嵌套图形中的屏幕。 每个屏幕都应突出显示。
  2. 右键单击其中之一,然后从上下文菜单中选择“ 移至嵌套图”>“新建图 ”。 选定的屏幕变成一个嵌套图。 图7显示了图编辑器中的嵌套图。

    图7.图形编辑器中的嵌套图形
  3. 单击嵌套图以将其选中。 以下属性应出现在属性面板中:
    • 类型字段包含一个“嵌套图”。
    • ID字段包含为嵌套图生成的ID。
  4. 双击嵌套图。 出现嵌套图的屏幕。
  5. 在屏幕列表(左)中,单击“ 根”以返回到根图。
  6. Text XML. . navigation . ID sendMoneyGraph startDestination chooseRecipient :
     <?xml version="1.0" encoding="utf-8"?> <navigation xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" xmlns:android="http://schemas.android.com/apk/res/android" app:startDestination="@id/mainFragment"> <fragment android:id="@+id/mainFragment" android:name="com.example.cashdog.cashdog.MainFragment" android:label="fragment_main" tools:layout="@layout/fragment_main" > <action android:id="@+id/action_mainFragment_to_ sendMoneyGraph" app:destination="@id/sendMoneyGraph" /> <action android:id="@+id/action_mainFragment_to_viewBalanceFragment" app:destination="@id/viewBalanceFragment" /> </fragment> <fragment android:id="@+id/viewBalanceFragment" android:name="com.example.cashdog.cashdog.ViewBalanceFragment" android:label="fragment_view_balance" tools:layout="@layout/fragment_view_balance" /> <navigation android:id="@+id/sendMoneyGraph" app:startDestination="@id/chooseRecipient"> <fragment android:id="@+id/chooseRecipient" android:name="com.example.cashdog.cashdog.ChooseRecipient" android:label="fragment_choose_recipient" tools:layout="@layout/fragment_choose_recipient"> <action android:id="@+id/action_chooseRecipient_to_chooseAmountFragment" app:destination="@id/chooseAmountFragment" /> </fragment> <fragment android:id="@+id/chooseAmountFragment" android:name="com.example.cashdog.cashdog.ChooseAmountFragment" android:label="fragment_choose_amount" tools:layout="@layout/fragment_choose_amount" /> </navigation> </navigation> 
  7. ID , , navigate() :

Java
 Navigation.findNavController(view).navigate(R.id.action_mainFragment_to_sendMoneyGraph); 

Kotlin
 view.findNavController(view).navigate(R.id.action_mainFragment_to_sendMoneyGraph) 


Android, ( deep link ) URI (, – . ), - . URI - , .



  1. .
  2. + Depp Links . Add Deep Link .
  3. URI URI. "www.cashdog.com/sendmoney" ( www.cashdog.com , ).
    URI:
  4. Auto Verify Google , URI . Android App Links .
  5. Add . .
  6. Text . :
     <deepLink app:uri="https://cashdog.com/sendmoney"/> 

Back, , . , , .



manifest.xml :


  • Android Studio 3.0 3.1, intent-filter. , .
  • Android Studio 3.2+, nav-graph :
     <activity name=".MainActivity"> <nav-graph android:value="@navigation/main_nav" /> </activity> 

, .



Navigation , " " " ".
:


  1. . Navigation Property Animation View Animation . Animation Resources .
  2. , .
  3. Transitions , Enter. .
  4. , .
  5. Transitions , Exit. .
  6. , .
  7. Text . XML action. , . specifyAmountFragment , , :
     <fragment android:id="@+id/specifyAmountFragment" android:name="com.example.buybuddy.buybuddy.SpecifyAmountFragment" android:label="fragment_specify_amount" tools:layout="@layout/fragment_specify_amount"> <action android:id="@+id/confirmationAction" app:destination="@id/confirmationFragment" app:enterAnim="@anim/slide_in_right" app:exitAnim="@anim/slide_out_left" app:popEnterAnim="@anim/slide_in_left" app:popExitAnim="@anim/slide_out_right" /> </fragment> 

, ( enterAnim exitAnim ) ( popEnterAnim popExitAnim ).



- Navigation Editor, . , Report a bug .


另请阅读


Navigation . :




, , , . - . , . ! , - ( Android) – .


UPD: : https://github.com/PrincessYork/android_navigation

Source: https://habr.com/ru/post/zh-CN416025/


All Articles