【Flutter】如何優美地實現一個懸浮NavigationBar

来源:https://www.cnblogs.com/ZzTzZ/archive/2023/10/05/17743521.html
-Advertisement-
Play Games

【Flutter】如何優美地實現一個懸浮NavigationBar 最近寫代碼的時候遇到了一個如下的需求: 整體來說,底部的條是一個浮動的懸浮窗,有如下的三個按鈕: 點擊左邊的要進入“主頁” 點擊中間的按鈕要進行頁面跳轉,能夠進入“創作頁” 點擊右邊的按鈕切換到“個人中心”頁 使用Overlay來實 ...


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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 在每個Java新版本發佈的特性中,都會包含一些Preview(預覽)功能,這些功能主要用來給開發者體驗並收集建議。所以,Preview階段的功能並不是預設開啟的。 如果想體驗某個Java版本中的Preview功能,您還需要做一些設置才能把程式跑起來。 下麵以IDEA 2023.2為例,演示為Java ...
  • 當讀者需要獲取到特定進程內的寄存器信息時,則需要在上述代碼中進行完善,首先需要編寫`CREATE_PROCESS_DEBUG_EVENT`事件,程式被首次載入進入記憶體時會被觸發此事件,在該事件內首先我們通過`lpStartAddress`屬性獲取到當前程式的入口地址,並通過`SuspendThrea... ...
  • 1. 簡介 機緣巧合下寫的一個工程,本來是作為商家視覺識別上位機的替代品,但是最後沒用上,因此只開發了一半(廠家升級了攝像頭和軟體) 該工程基於WPF的.net6+mvvm 調用攝像頭進行識別 opencv開攝像頭(不想自己封裝win32api),yolov5對圖像進行檢測 2.引用庫 MVVM C ...
  • 經過版本更新,Mini API 的功能逐步完善,早期支持得不太好的 mini API 現在許多特性都可以用了,比如灰常重要的依賴註入。 咱們先來個相當簡單的註入測試。來,定義一個服務類,為了偷懶,老周這裡就不使用 介面 + 實現類 的方式了。 public class MyService : IDi ...
  • 眾所周知,在Cortex-M內核中,系統節拍由Systick時鐘提供,當配置好系統滴答時鐘後,每次時鐘中斷就會觸發中斷處理函數 xPortSysTickHandler(), void xPortSysTickHandler( void ) { /* The SysTick runs at the l ...
  • 搭建msf 官方有提供一鍵安裝腳本,如下: curl https://raw.githubusercontent.com/rapid7/metasploit-omnibus/master/config/templates/metasploit-framework-wrappers/msfupdate ...
  • MySQL 是世界上最流行的開源關係型資料庫管理系統之一,而其中的存儲引擎則是其關鍵組成部分之一。InnoDB 存儲引擎在 MySQL 中扮演了重要角色,提供了許多高級功能和性能優化,適用於各種應用程式和工作負載。本文將深入介紹 InnoDB 存儲引擎的各個方面,以幫助您更好地理解它的特性和優勢。 ...
  • 1. 為什麼要拆分資料庫? 單體項目在構建之初,資料庫的負載和數據量都不大,所以不需要對資料庫做拆分,小型財務系統、文書系統、ERP系統、OA系統,用一個MySQL資料庫實例基本就夠用了。 就像《淘寶技術這十年》裡面說到的,電商業務的數據量增長飛快,所以最開始的PHP+MySQL的架構已經不能滿足實 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...