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

flutter,如何,优美,实现,一个,悬浮,navigationbar · 浏览次数 : 6

小编点评

# PNavigationBar 代码优化 **问题:** 如何创建一个悬浮的底部导航栏,并且在路由回来时重新绘制它? **解决方案:** 1. 使用 `WidgetsBinding` 监听 `postFrame` 事件,并在事件中调用 `PNavigationBar.show()` 方法。 2. 在 `didChangeDependencies` 方法中添加一个 `postFrame` 事件监听器,并在事件中调用 `PNavigationBar.show()` 方法。 3. 在 `show()` 方法中移除 `overlayEntry`,以便在路由回来时释放资源。 **代码:** ```dart 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 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); } } ``` **效果:** 悬浮的导航栏,在路由回来时重新绘制。

正文

【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<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);
  }
}

与【Flutter】如何优美地实现一个悬浮NavigationBar相似的内容:

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

【Flutter】如何优美地实现一个悬浮NavigationBar 最近写代码的时候遇到了一个如下的需求: 整体来说,底部的条是一个浮动的悬浮窗,有如下的三个按钮: 点击左边的要进入“主页” 点击中间的按钮要进行页面跳转,能够进入“创作页” 点击右边的按钮切换到“个人中心”页 使用Overlay来实

flutter系列之:在flutter中使用导航Navigator

简介 一个APP如果没有页面跳转那么是没有灵魂的,页面跳转的一个常用说法就是Navigator,flutter作为一个最为优秀的前端框架,Navigator肯定是必不可少的,那么在flutter中如何使用Navigator呢? 一起来看看吧。 flutter中的Navigator Navigator

Flutter调优--深入探究MediaQuery引起界面Rebuild的原因及解决办法

app界面逐渐复杂时,我们不得不考虑去优化界面性能。本文中介绍的例子在开发中是很常见的,如果不了解MediaQuery.of的机制,可能会引起大量使用此方法的界面发生重绘操作,造成页面卡顿、帧率下降。我们详细分析了背后的源码逻辑,介绍了解决办法,希望能给大家的调优工作提供些许帮助。

flutter系列之:如何自定义动画路由

简介 flutter中有默认的Route组件,叫做MaterialPageRoute,一般情况下我们在flutter中进行跳转的话,只需要向Navigator中传入一个MaterialPageRoute就可以了。 但是MaterialPageRoute太普通了,如果我们想要做点不同的跳转特效应该如何

flutter系列之:在flutter中使用相机拍摄照片

简介 在app中使用相机肯定是再平常不过的一项事情了,相机肯定涉及到了底层原生代码的调用,那么在flutter中如何快速简单的使用上相机的功能呢? 一起来看看吧。 使用相机前的准备工作 flutter中为使用camera提供了一个叫做camera的插件,我们首先需要安装这个插件。 安装插件的步骤很简

教你2种方法,将iOS设备通过MQTT协议连接到华为云物联网平台

本文讲述如何使用Flutter和Swift两种开发语言连接到华为云物联网平台。

flutter系列之:在flutter中使用媒体播放器

简介 现在的app功能越来越强大,除了基本的图文之外,还需要各种各样的其他的功能,比如视频,和直播。 直播可能会比较复杂,因为涉及到了拉流和推流,需要服务器端的支持,但是视频播放就比较简单了,那么如何在flutter中使用媒体播放器呢? 一起来看看吧。 使用前的准备工作 flutter本身是不支持媒

《吐血整理》高级系列教程-吃透Fiddler抓包教程(32)-Fiddler如何抓取IOS系统中Flutter应用程序的包

1.简介 上一篇讲解了安卓手机可以通过VPN代理来抓取Flutter应用程序的包,iOS(iphone)同样使用上一篇中VPN方法(原理与android是一致的),同样需要使用到VPN,在iOS也有许多与drony功能类似的软件,大家可以自己选择自己喜欢的使用,宏哥这里使用的是Shadowrocke

flutter系列之:做一个下载按钮的动画

[toc] # 简介 我们在app的开发过程中经常会用到一些表示进度类的动画效果,比如一个下载按钮,我们希望按钮能够动态显示下载的进度,这样可以给用户一些直观的印象,那么在flutter中一个下载按钮的动画应该如何制作呢? 一起来看看吧。 # 定义下载的状态 我们在真正开发下载按钮之前,首先定义几个

flutter系列之:如丝般顺滑的SliverAppBar

简介 对于一个APP来说,肯定会有一个AppBar,这个AppBar一般包含了APP的导航信息等。虽然我们可以用一个固定的组件来做为AppBar,但是这样就会丢失很多特效,比如将AppBar固定在顶部,AppBar可以在滑动的过程中进行大小变换等。 当然这一切都不需要自己来实现,flutter已经为