【Flutter】如何优美地实现一个悬浮NavigationBar

【Flutter】如何优美地实现一个悬浮NavigationBar

最近写代码的时候遇到了一个如下的需求:

【Flutter】如何优美地实现一个悬浮NavigationBar

整体来说,底部的条是一个浮动的悬浮窗,有如下的三个按钮:

  • 点击左边的要进入“主页”
  • 点击中间的按钮要进行页面跳转,能够进入“创作页”
  • 点击右边的按钮切换到“个人中心”页

使用Overlay来实现悬浮效果

首先是这个窗口该如何创建的问题,显然需要Overlay悬浮在整个窗口顶部。

但是不能直接写在initState内,这样会触发“Build时重绘”的错误。所以我们可以利用WidgetsBinding,来监听Callback,这样可以保证在首页Build完成时能够立刻绘制这个悬浮的窗口。

/rootpage @override   void didChangeDependencies() {     print('root didChangeDependencies');     super.didChangeDependencies();     var widgetsBinding = WidgetsBinding.instance;     widgetsBinding.addPostFrameCallback((callback) {       print('addPostFrameCallback');       PNavigationBar.show(context, _tabController);     });   } 

我将这个放入到了didChangeDependencies内,主要是想通过混入TickerProviderStateMixin能够在路由回来时重新触发didChangeDependencies,不过理想很丰满。最后在实验的过程中反倒没有触发,没有找到原因,希望有感兴趣的大佬可以指点一下。

理论参考:Flutter 小而美系列|TickerProviderStateMixin 对生命周期的影响 - 掘金 (juejin.cn)


使用TabBar+TabView来实现NavigationBar的效果

首先说最简单的TabView部分

@override   Widget build(BuildContext context) {     return Scaffold(       body: TabBarView(         controller: _tabController,         children: [           HomePage(),           UserPage(),         ],       ),     );   } 

这里需要一个TabController,相信比较熟悉的朋友们也知道,需要混入TickerProviderStateMixin,才可以声明

【Flutter】如何优美地实现一个悬浮NavigationBar

画框的部分是主要部分。


自定义实现一个PNavigationBar

【Flutter】如何优美地实现一个悬浮NavigationBar

(具体的代码在本文最后)

整个PNavigationBar的实现非常简单,定义了一个show,一个remove,一个refresh方法,这样可以保证任何组件任何页面都可以随时控制PNavigationBar的出现和消失。

图标的切换

因为NavigationBar是存在切换图标的功能的,而我们通过Image.asset获取的图标却没办法更新,所以我们需要手动调用overlayEntry.markNeedsBuild方法,来对整个底部组件进行重绘

【Flutter】如何优美地实现一个悬浮NavigationBar

中间按钮的实现

相信大家也会有最初跟我一样的疑问,因为TabBar与TabView,还有TabController的数必须一致,而我们中间有一个自定义的加号按钮,我在这里的实现非常简单粗暴,当然如果有更好的方法欢迎大佬指教。

【Flutter】如何优美地实现一个悬浮NavigationBar

我这里只是通过简单的运算,来将两个组件分别控制在左边和右边,之后加号按钮在中间。

当然整个TabBar的渲染逻辑其实是有问题的,想要更深入地改TabBar的排列方式,必须需要自己手写一个TabBar。默认的排列方式就是放到Expanded内的,具体参考了以下这篇博客:

Flutter系列之设置TabBar的tab紧凑排列_flutter tabbar间隔-CSDN博客


关于页面路由的问题

最难的部分就是这里,主要在于如何控制路由到其他界面就可以消失,再pop回来就可以显示。

我们希望这些功能都可以在RootPage这一层实现,而不在各种子页面的push和pop中增添代码负担。

具体实现起来最初我的尝试是didChangeDependencies,但是最后实验下来并没有结果,我自己也并不知道原因。(小白是这样的)

而我最终决定采用原始的NavigationObserver方法,这里感谢这个组件替我实现了这个功能:

lifecycle_lite | Flutter Package (pub.dev)

于是可以通过简单的onShow和onHide就可以实现啦!


代码呈现

当然还有很多细节都没有提到,写这个功能时遇到的问题也有不少,本人技术有限,能力有限。等代码再优化的时候可以作为库开源给大家。现在就暂且以这种博客的形式分享组件和代码。

import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:picturebook/pages/test/test_page.dart';  import '../color_utils.dart';  class PNavigationBar {   static OverlayEntry? overlayEntry;    static show(BuildContext context, TabController tabController) {     var overlayState = Overlay.of(context);     overlayEntry = OverlayEntry(       maintainState: true,       builder: (BuildContext context) {         final size = MediaQuery.of(context).size;         final height = size.height;         final width = size.width;         final boxWidth = width * 0.46;         final boxHeight = 60.h;         final iconHeight = 45.h;         return Positioned(           bottom: height * 0.06,           left: (width - boxWidth) / 2,           right: (width - boxWidth) / 2,           child: Stack(             children: [               Container(                   decoration: BoxDecoration(                     color: ColorUtils.orange,                     borderRadius: BorderRadius.circular(boxHeight / 2),                   ),                   width: boxWidth,                   height: boxHeight,                   child: TabBar(                     controller: tabController,                     indicatorColor: Colors.transparent,                     padding: EdgeInsets.zero,                     onTap: (index) {                       tabController.animateTo(index);                       overlayEntry?.markNeedsBuild();                     },                     tabs: [                       Padding(                         padding: EdgeInsets.only(right: iconHeight / 3),                         child: Container(                           width: iconHeight,                           height: iconHeight,                           decoration: BoxDecoration(                             color: Colors.white30,                             borderRadius: BorderRadius.circular(iconHeight / 2),                           ),                           child: Center(                               child: Image.asset(                             tabController.index == 0                                 ? 'assets/home_1.png'                                 : 'assets/home_0.png',                             width: iconHeight * 0.5,                           )),                         ),                       ),                       Padding(                         padding: EdgeInsets.only(left: iconHeight / 3),                         child: Container(                           width: iconHeight,                           height: iconHeight,                           decoration: BoxDecoration(                             color: Colors.white30,                             borderRadius: BorderRadius.circular(iconHeight / 2),                           ),                           child: Center(                               child: Image.asset(                             tabController.index == 1                                 ? 'assets/user_1.png'                                 : 'assets/user_0.png',                             width: iconHeight * 0.5,                           )),                         ),                       ),                     ],                   )),               Align(                 alignment: Alignment.center,                 child: Padding(                   padding: EdgeInsets.only(top: (boxHeight - iconHeight) / 2),                   child: InkWell(                     onTap: () {                       print('push');                       Navigator.push(                         context,                         MaterialPageRoute(builder: (context) => TestPage()),                       );                     },                     child: Container(                       width: iconHeight,                       height: iconHeight,                       decoration: BoxDecoration(                         color: Colors.white,                         borderRadius: BorderRadius.circular(iconHeight / 2),                       ),                       child: Center(                           child: Image.asset(                         'assets/add.png',                         width: iconHeight * 0.5,                       )),                     ),                   ),                 ),               ),             ],           ),         );       },     );     overlayState.insert(overlayEntry!);   }    static remove() {     if (overlayEntry != null) {       overlayEntry!.remove();     }   }    static refresh(){     overlayEntry?.markNeedsBuild();   }  }  

下面是使用的实例,非常优美简洁:

import 'package:flutter/material.dart'; import 'package:lifecycle_lite/lifecycle_mixin.dart'; import 'package:picturebook/pages/home_page.dart'; import 'package:picturebook/pages/user_page.dart'; import 'package:picturebook/utils/navigation/navigation_util.dart';  class RootPage extends StatefulWidget {   const RootPage({super.key});    @override   State<RootPage> createState() => _RootPageState(); }  class _RootPageState extends State<RootPage>     with TickerProviderStateMixin, LifecycleStatefulMixin {   late TabController _tabController;    @override   void initState() {     super.initState();     _tabController = TabController(length: 2, vsync: this)..addListener(() {       PNavigationBar.refresh();     });   }    @override   void didChangeDependencies() {     super.didChangeDependencies();     var widgetsBinding = WidgetsBinding.instance;     widgetsBinding.addPostFrameCallback((callback) {       PNavigationBar.show(context, _tabController);     });   }    @override   Widget build(BuildContext context) {     return Scaffold(       body: TabBarView(         controller: _tabController,         children: [           HomePage(),           UserPage(),         ],       ),     );   }    @override   void whenHide() {     PNavigationBar.remove();   }    @override   void whenShow() {     PNavigationBar.show(context, _tabController);   } }  

发表评论

评论已关闭。

相关文章