咱们都知道 BottomNavigationBar 是一个移动端十分常用的底部导航栏组件,能够用于点击处理激活菜单,并经过回调来处理界面的切换。

Flutter 组件集录 | 桌面导航 NavigationRail
Flutter 组件集录 | 桌面导航 NavigationRail

但是在桌面端,因为一般是宽大于高,所以 BottomNavigationBar 并不适用。而是侧边的导航栏较为常见,比方下面飞书的客户端界面布局。

Flutter 组件集录 | 桌面导航 NavigationRail

为了满足桌面端的导航栏适用需求,官方新增了 NavigationRail 组件,而非对 BottomNavigationBar 组件进行适配。之前我也说过,关于差异较大的结构,并没有必要让一个组件经过适配来完结两头需求。分离开来也不是坏事,让一件衣服一起适配 蚂蚁燕子 是很困难的,这时做两件衣服,各司其职显然是更好地方式。

BottomNavigationBarNavigationRail 两个导航便是如此,从语义上来看 Bottom 便是用于底部的导航, Rail扶手铁轨 的意思,作为侧栏导航的语义,仍是很生动有趣的。两者分别处理特定的结构,这也很符合 单一责任 的准则。

该组件已录入 【FlutterUnit】 ,能够在 App 中体验。别的,本文中的代码可在对应文件夹中查看:

Flutter 组件集录 | 桌面导航 NavigationRail


1. NavigationRail 组件的根本运用

下面是 NavigationRail 组件的结构办法,其间必须传入的有两个参数:

  • destinations : 表明导航栏的信息,是 NavigationRailDestination 列表。
  • selectedIndex: 表明激活索引,int 类型。

Flutter 组件集录 | 桌面导航 NavigationRail


咱们先来完结如下最简略的运用场景,左边导航栏,在点击时切换右侧内容页:

Flutter 组件集录 | 桌面导航 NavigationRail

假如导航栏的数据是固定的,能够提前定义如下的 destinations 常量。如下的 _buildLeftNavigation 办法担任构建左边导航栏,NavigationRail 在结构中能够经过 onDestinationSelected 回调办法,来监听用户和导航栏的交互事情,传递用点击的索引方位。

final List<NavigationRailDestination> destinations = const [
  NavigationRailDestination(icon: Icon(Icons.message_outlined),label: Text("音讯")),
  NavigationRailDestination(icon: Icon(Icons.video_camera_back_outlined),label: Text("视频会议")),
  NavigationRailDestination(icon: Icon(Icons.book_outlined),label: Text("通讯录")),
  NavigationRailDestination(icon: Icon(Icons.cloud_upload_outlined),label: Text("云文档")),
  NavigationRailDestination(icon: Icon(Icons.games_sharp),label: Text("工作台")),
  NavigationRailDestination(icon: Icon(Icons.calendar_month),label: Text("日历"))
];
Widget _buildLeftNavigation(int index){
  return NavigationRail(
    onDestinationSelected: _onDestinationSelected,
    destinations: destinations,
    selectedIndex: index,
  );
}
void _onDestinationSelected(int value) {
  //TODO 更新索引 + 切换界面
}

NavigationRail 的文档注释中说道:该组件一般在 Row 中,运用于 Scaffold.body 特点下。这也很简单了解,这是一个左右结构,在 Row 中能够经过 Expanded 能够自动延伸主体内容。如下,主体内容界面经过 PageView 进行构建,其间的 TestContent 组件在实际运用中换成你的需求界面。

@override
Widget build(BuildContext context) {
  return Scaffold(
    body: Row(
      children: [
        _buildLeftNavigation(index),
        Expanded(child: PageView(
          children:const [
            TestContent(content: '音讯',),
            TestContent(content: '视频会议',),
            TestContent(content: '通讯录',),
            TestContent(content: '云文档',),
            TestContent(content: '工作台',),
            TestContent(content: '日历',),
          ],
        ))
      ],
    ),
  );
}

最后是要害的一点:点击时,怎么完结导航索引的切换和主体内容的切页。思路其实很简略,咱们现已知道用户点击导航菜单的回调事情。关于 PageView 来说,能够经过 PageController 切换界面,NavigationRail 能够经过 selectedIndex 确认激活索引,所以只需用新索引重新构建 NavigationRail即可。 如下代码所示,在 _onDestinationSelected 在处理这两件重要的事。如下 tag1 处,经过 PageControllerjumpToPage 办法进行界面跳转。

这儿经过 ValueListenableBuilder 来监听 _selectIndex 完结部分更新构建,如下 tag2 处,只需更新 _selectIndex 的值,就能够告诉 ValueListenableBuilder 触发 builder 办法,运用新索引,构建 NavigationRail 。这样能够避免直接触发 _MyHomePageState 的更新办法,对 Scaffold 全体进行更新。

class _MyHomePageState extends State<MyHomePage> {
 final PageController _controller = PageController();
 final  ValueNotifier<int> _selectIndex = ValueNotifier(0);
  // 略同...
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Row(
        children: [
          ValueListenableBuilder<int>(
            valueListenable: _selectIndex,
            builder: (_,index,__)=>_buildLeftNavigation(index),
          ),
          Expanded(child: PageView(
            controller: _controller,
           // 略同...
  }
  void _onDestinationSelected(int value) {
    _controller.jumpToPage(value); // tag1
    _selectIndex.value = value; //tag2
  }
  @override
  void dispose(){
    _controller.dispose();
    _selectIndex.dispose();
    super.dispose();
  }
}

这样就完结了 NavigationRail 最根本的运用,完结了左边导航结构以及点击时的切换逻辑。NavigationRail 在结构办法中还有很多其他的装备参数用于款式调整,这些不是中心,但能够如虎添翼,下面一同来看一下。


2.首尾组件与折叠

leadingtrailing 特点相当于两个插槽,如下所示,表明导航菜单外的首尾组件。

Flutter 组件集录 | 桌面导航 NavigationRail

Widget _buildLeftNavigation(int index){
  return NavigationRail(
    leading: const Icon(Icons.menu_open,color: Colors.grey,),
    trailing: FlutterLogo(),
    onDestinationSelected: _onDestinationSelected,
    destinations: destinations,
    selectedIndex: index,
  );
}

这儿有个小细节,trailing 紧随最后一个菜单,怎么让它像飞书的导航那样,在最尾部呢?偷瞄一些源码能够看出 trailing 是和导航菜单一同被放入 Column 中的。

Flutter 组件集录 | 桌面导航 NavigationRail

所以咱们能够经过 Expanded 来延伸剩余空间构成紧约束,经过 Align 使 FlutterLogo 排在下方:

Flutter 组件集录 | 桌面导航 NavigationRail

Widget _buildLeftNavigation(int index){
  return NavigationRail(
    leading: const Icon(Icons.menu_open,color: Colors.grey,),
    extended: false,
    trailing: const Expanded(
      child: Align(
        alignment: Alignment.bottomCenter,
        child: Padding(
          padding: EdgeInsets.only(bottom: 20.0),
          child: FlutterLogo(),
        ),
      ),
    ),
    onDestinationSelected: _onDestinationSelected,
    destinations: destinations,
    selectedIndex: index,
  );
}

别的,NavigationRail 中有个 extendedbool 参数,用于操控是否打开侧边栏,当该特点变化时,会进行动画打开和收起。如下所示,点击头部时,更新 NavigationRailextended 入参即可:

Flutter 组件集录 | 桌面导航 NavigationRail


3.影深 与 标签类型

elevation 表明暗影的深度,这是十分常见的一个特点,如下红框所示,设置 elevation 之后右侧会有暗影,该值越大,暗影越显着。

Flutter 组件集录 | 桌面导航 NavigationRail


labelType 参数表明标签类型,对应的特点是 NavigationRailLabelType 枚举。用于表明什么时候显现文字标签,默许是 none ,也便是只显现图标,没有文字。

enum NavigationRailLabelType {
  none,
  selected,
  all,
}

设置为 all 时,作用如下:导航菜单会一起显现 图标文字标签

Flutter 组件集录 | 桌面导航 NavigationRail


设置为 selected 时,作用如下:只要激活的导航菜单会一起显现 图标文字标签

Flutter 组件集录 | 桌面导航 NavigationRail

别的,有一点需求留意: 当 extended 特点为 true 时, labelType 必须为 NavigationRailLabelType.none 否则会报错。

Flutter 组件集录 | 桌面导航 NavigationRail

---->[NavigationRail结构断语]----
assert(!extended || (labelType == null || labelType == NavigationRailLabelType.none)),

4.布景、文字、图标款式
  • unselectedLabelTextStyle : 未选中签文字款式
  • selectedLabelTextStyle : 选中标签文字款式
  • unselectedIconTheme : 未选中图标款式
  • selectedIconTheme : 选中图标款式

这四个款式根本上是望文生义,下面经过一个深色布景版别来运用一下:

Flutter 组件集录 | 桌面导航 NavigationRail

@override
Widget build(BuildContext context) {
  const Color textColor =  Color(0xffcfd1d7);
  const  Color activeColor =  Colors.blue;
  const TextStyle labelStyle =  TextStyle(color: textColor,fontSize: 11);
  return NavigationRail(
    backgroundColor: const Color(0xff324465),
    unselectedIconTheme: const IconThemeData(color: textColor) ,
    selectedIconTheme: const IconThemeData(color: activeColor) ,
    unselectedLabelTextStyle: labelStyle,
    selectedLabelTextStyle: labelStyle,
    // 略同...
}

5.指示器与最小宽度
  • useIndicator : 是否增加指示器
  • indicatorColor : 指示器色彩

这两个特点用于操控图标后面的布景指示器,如下是在 NavigationRailLabelType.all 类型下指示器的款式,经过圆角矩形包裹图标:

Flutter 组件集录 | 桌面导航 NavigationRail


NavigationRailLabelType.none 类型下,指示器经过圆形包裹图标:

Flutter 组件集录 | 桌面导航 NavigationRail


  • minWidth : 默许 72 ,未打开时导航栏宽度

Flutter 组件集录 | 桌面导航 NavigationRail

  • indicatorColor :默许 256 ,打开时导航栏宽度

Flutter 组件集录 | 桌面导航 NavigationRail

NavigationRail 组件的特点介绍就到这儿,总的来看,悬浮和点击时,导航栏仍是一股 Material 的味。个人觉得这并不合适桌面端,导航栏的菜单可定制性也一般般,只能满足根本的需求。关于略微特别点的款式,无法支撑,比方飞书客户端的导航款式。别的像 拖动替换菜单方位 这样的交互,咱们也只经过自定义组件来完结。

Flutter 组件集录 | 桌面导航 NavigationRail


6.剖析 NavigationRail 组件,学习思路

就像世界上并没有什么包治百病的 ,咱们也并不能苛求一个组件能满足一切的布局需求。关于一个原生组件满足不了的需求,发挥创造才能去解决问题,这应是咱们的本职工作。学习官方关于组件完结的思路是十分重要的,它能够为你提供一个主方向。

Flutter 组件集录 | 桌面导航 NavigationRail

咱们能够发现 NavigationRailSwitchBottomNavigationBar 等组件相同,虽然本身是 StatefulWidget, 但关于激活状况的数据并不是在内部状况中维护,而是让 运用者自动提供,比方这儿在结构 NavigationRail 时必须传入 selectedIndex 。 该组件只提供回调事情来告诉运用者,这样的用意是让运用者更简单 操控 该状况,而不是完全封装在状况类内部。

别的,从 selectedIndex 特点在状况类中的运用中能够看出,每个菜单的条目组件经过 _RailDestination 进行构建。从这儿能够看出,_RailDestination 会经过 selected 特点来区分是否激活,而且会经过 onTap 回调点击事情。在此触发 widget.onDestinationSelected ,将当前索引 i 传递给用户。

Flutter 组件集录 | 桌面导航 NavigationRail

这儿 _RailDestinationStatelessWidget, 只说明并不需求维护内部状况的变化,组需求根据结构中的装备信息构建需求的组件即可。这就尽可能地简化了 _RailDestination 的构建逻辑,让其相对独立,专注地去做一件事。这便是组件分离的优点之一:既能够简化构建结构,增加可读性,又能够将相对独立的构建逻辑内聚在一同。咱们完全能够在日常开发中对这样的分离进行学习和发挥。


别的这儿比较值得学习的还有动画的处理,我看了一下现在桌面的一些应用,比方 微信飞书有道词典百度网盘AndroidStudio有道云笔记 ,这些导航栏在切换时都是没有动画的。如下所示,NavigationRail 对应的状况类中维护了两种动画操控器,这也是 NavigationRail 为什么需求是 StatefulWidget 的原因。

Flutter 组件集录 | 桌面导航 NavigationRail

其间 _destinationControllers 用于处理,菜单布景指示器在点击时激活/非激活的透明度渐变动画。能够追寻一下动画器的去向: 在 NavigationIndicator 中经过 FadeTransition运用动画器完结透明度渐变动画。

_RailDestination -->  _AddIndicator --> NavigationIndicator

Flutter 组件集录 | 桌面导航 NavigationRail


最后看一下 _extendedController 动画操控器,它对应的动画器也被传入 _RailDestination 中来完结动画功用。这个动画操控器在 extended 特点变化时,打开折叠导航栏的动画。如下源码所示,能够看出关于这个动画更多的细节。 动画过程中文字标签有个透明度渐变的动画,宽度约束经过对 ConstrainedBox 进行限制,并经过 AlignwidthFactor 操控文字标签区域的尺寸。

Flutter 组件集录 | 桌面导航 NavigationRail

这儿的 ClipRect 组件套的很迷,我试了一下去除后并不影响动画作用,一开始不知道为什么要加。之后将动画时长拉长,进行了一些测试发现端倪,假如不进行裁剪,就会呈现如下的不和谐状况。默许动画 200 ms 看不出太大差异。从这儿我又学到了一个小技巧:怎么动画打开一个区域。

Flutter 组件集录 | 桌面导航 NavigationRail

所以说源码是最好的老师,经过剖析源码的完结去考虑和学习,是生长的一条很好的途径。而不是什么东西都靠别人给你灌输,遇到不会的或优柔寡断时就到处问。Flutter 组件的源码相对独立,套路也比较简略,很合适去研究学习。《Flutter 组件集录》 专栏专门用于收录我对 Flutter 常用组件的运用介绍,其间一般也会有相关源码完结的一些剖析。对一些才能稍弱的朋友,也能够根据这些介绍去测验研究。那本文就到这儿,谢谢观看 ~


  • 我正在参加技能社区创作者签约方案招募活动,点击链接报名投稿。