背景
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
相關
背景
PR #182 / #178 落地了
HomePageScaffold的 tab 模式(IndexedStack+ per-tabNavigator),目前各 tab 的 FAB(例如 CoursePage 的搜尋按鈕)仍位於內層 Scaffold 的floatingActionButtonslot:雖然視覺上已透過 SafeArea +
kFloatingActionButtonMargin對齊到同一條基線(接近 iOS 26 Liquid Glass 的「左 nav pill / 右 trailing pill」雙膠囊版型),但兩者分屬不同 layer,跨 tab 切換時 FAB 是「直接消失再出現」,無法做平滑轉場。packages/ap_common_liquid_glass/的GlassHomePageScaffold目前還沒有 tab 模式(停留在selectedIndex+onTabTappedcallback),未來補 tab 時應該採用同一個模型,避免 M3 / Glass 兩邊各長一份分歧的實作。動機(為什麼把 FAB 拉到同層)
_navBarVisible參考:Apple — Adopting Liquid Glass: Navigation。iOS 26 SwiftUI
TabView的.tabBarMinimizeBehavior+ 搜尋 role 都建立在「tab bar 與 trailing action 共用一個 chrome 層」的前提上。提案
1. 漸進式 API:
HomeTab.trailingAction(opt-in,不破壞現有 code)2. Scaffold 端佈局
3. 兩種機制併存,文件指引
HomeTab.trailingActionScaffold.floatingActionButton4. Liquid Glass 同步
GlassHomePageScaffold之後補 tab 模式時,直接採用同一個HomeTabmodelHomeTab.trailingAction欄位,Glass 端用 glass-styled trailing pill 渲染TabShellController共用核心邏輯(per-tab Navigator、observer、scroll minimize、tab-tap pop/scroll-to-top),M3 與 Glass 各自只負責 chrome(bar 樣式)後續延伸(不在這個 issue 範圍)
Tasks
HomeTab加trailingAction欄位HomePageScaffold在 tab bar Row 旁加 AnimatedSwitcher 渲染 trailingAction_navBarVisible(scroll minimize 一起淡出)GlassHomePageScaffold補 tab 模式時複用同一個HomeTabmodel(另 issue 追蹤)TabShellControllermixin 給 M3 + Glass 共用相關