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


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

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

    image

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

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

    使用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,才可以声明

    image

    画框的部分是主要部分。


    自定义实现一个PNavigationBar

    image

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

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

    图标的切换

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

    image

    中间按钮的实现

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

    image

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

    当然整个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 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);
      }
    }
    
    
  • 相关阅读:
    PTA 甲级 1039 Course List for Student
    华为ensp搭建VRRP主备份配置
    用户管理【MySQL】
    真假难辨!AI人像生成再进化!HyperHuman:基于隐式结构扩散的超逼真人像生成...
    成都瀚网科技有限公司:抖音怎么绑定抖音小店才好?
    @ConfigurationProperties和@Value的区别
    py11_Python 类间关系 依赖/关联/继承
    基于STM32F103ZET6库函数独立看门狗(IWDG)实验
    开发了一个Java库的Google Bard API,可以自动化与AI对话了
    数据结构学习笔记(Ⅳ):串
  • 原文地址:https://www.cnblogs.com/ZzTzZ/p/17743521.html