Skip to content

feat(ui): 把 trailing FAB 拉到 bottom bar 同層以支援 tab 切換動畫(M3 + Liquid Glass 共用模式) #184

Description

@abc873693

背景

PR #182 / #178 落地了 HomePageScaffold 的 tab 模式(IndexedStack + per-tab Navigator),目前各 tab 的 FAB(例如 CoursePage 的搜尋按鈕)仍位於內層 Scaffold 的 floatingActionButton slot:

HomePageScaffold (outer)
├── bottomNavigationBar: floating tab bar pill  (left, scaffold layer)
└── body: IndexedStack
    └── tab N's Navigator → CoursePage Scaffold
        └── floatingActionButton: search FAB    (right, INNER scaffold layer)

雖然視覺上已透過 SafeArea + kFloatingActionButtonMargin 對齊到同一條基線(接近 iOS 26 Liquid Glass 的「左 nav pill / 右 trailing pill」雙膠囊版型),但兩者分屬不同 layer,跨 tab 切換時 FAB 是「直接消失再出現」,無法做平滑轉場。

packages/ap_common_liquid_glass/ 的 GlassHomePageScaffold 目前還沒有 tab 模式(停留在 selectedIndex + onTabTapped callback),未來補 tab 時應該採用同一個模型,避免 M3 / Glass 兩邊各長一份分歧的實作。

動機(為什麼把 FAB 拉到同層)

動畫類型 目前架構 FAB 在同層
Tab 切換時 trailing icon 換位 / 變色 ❌ ✅ AnimatedSwitcher / Hero
Tab bar + trailing 合併成單一 pill(iOS 26 collapsed state) ❌ 跨 layer 不可能 ✅ 同 Stack 內 morph
FAB 隨 scroll 一起隱藏(與 tab bar minimize 同步) ❌ 各自 listener ✅ 共用 _navBarVisible
FAB 位置永遠固定,不因 page 不同而跳 ❌ 各 page 自決 ✅ 由 scaffold 統一
跨 tab 的 Hero animation(trailing → tab body 內) ❌ Hero tag 不易跨 Navigator ✅ 同層直接 Hero

參考:Apple — Adopting Liquid Glass: Navigation。iOS 26 SwiftUI TabView 的 .tabBarMinimizeBehavior + 搜尋 role 都建立在「tab bar 與 trailing action 共用一個 chrome 層」的前提上。

提案

1. 漸進式 API:HomeTab.trailingAction(opt-in,不破壞現有 code)

class HomeTab {
  ...
  /// Optional trailing pill / FAB rendered next to the tab bar at the
  /// same baseline. When provided, [HomePageScaffold] owns it and
  /// animates the transition when switching tabs. Inner Scaffolds may
  /// still use [Scaffold.floatingActionButton] independently — both
  /// coexist (use one or the other per tab as appropriate).
  final Widget? trailingAction;
}

2. Scaffold 端佈局

Row(
  tab pill (left, AlignmentDirectional.bottomStart),
  Spacer,
  AnimatedSwitcher(
    duration: 250ms,
    transitionBuilder: (child, anim) => FadeTransition + ScaleTransition,
    child: KeyedSubtree(
      key: ValueKey(_currentTabIndex),
      child: widget.tabs[_currentTabIndex].trailingAction ?? const SizedBox.shrink(),
    ),
  ),
)

3. 兩種機制併存,文件指引

  • 新 page / 想要動畫:用 HomeTab.trailingAction
  • 舊 page / 複雜 FAB(ExtendedFAB / SpeedDial / 多動作):照舊用 Scaffold.floatingActionButton
  • 文件註明:「兩者擇一以避免視覺衝突」

4. Liquid Glass 同步

  • GlassHomePageScaffold 之後補 tab 模式時,直接採用同一個 HomeTab model
  • 共用 HomeTab.trailingAction 欄位,Glass 端用 glass-styled trailing pill 渲染
  • 抽 TabShellController 共用核心邏輯(per-tab Navigator、observer、scroll minimize、tab-tap pop/scroll-to-top),M3 與 Glass 各自只負責 chrome(bar 樣式)

後續延伸(不在這個 issue 範圍)

  • iOS 26 collapsed state:scroll 後 tab bar + trailing 合併成單一 pill(morph 動畫)
  • Tab 內子頁面 push 時的 trailing action override(per-route 而非 per-tab)

Tasks

  • HomeTab 加 trailingAction 欄位
  • HomePageScaffold 在 tab bar Row 旁加 AnimatedSwitcher 渲染 trailingAction
  • FAB 與 tab bar 共用 _navBarVisible(scroll minimize 一起淡出)
  • 範例 app 示範一個 tab 用 trailingAction、另一個 tab 沿用 inner FAB
  • 文件 / CLAUDE.md 註明兩機制差異
  • GlassHomePageScaffold 補 tab 模式時複用同一個 HomeTab model(另 issue 追蹤)
  • 評估是否抽 TabShellController mixin 給 M3 + Glass 共用

相關

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions