Flutter沉浸式複雜吸頂互動頁面結構設計與實踐

語言: CN / TW / HK

關注“之家技術”,獲取更多技術乾貨

總篇169篇 2022年第44篇

1

專案背景

筆者部門內部一款C端app 新能源車系 頁展示車系的所有相關資訊,包括車系圖,車系資訊,車型,口碑,車主實拍,相關推薦車系,廣告等眾多模組資訊,展示資訊模組多,模組資訊需要可動態配置,可擴充套件性強,互動複雜,是一個重要的車系詳情展示頁面。為了節省開發時間,整個頁面採用跨端技術Flutter開發,是使用Flutter開發複雜頁面的一次有益驗證和實踐。

2

頁面設計分析

頁面設計稿如下圖

圖1

整體頁面設計分析如下:

1.這個頁面是一個典型的沉浸式的頭部可伸縮摺疊的頁面設計。整體分為頭部區域(包括頂部標題,車系背景,車系資訊),可吸頂的tab區域(車型,口碑等),與tab對應的內容列表區域。

2.頭部區域頂部標題欄和背景車系圖延伸到狀態列,且背景車系圖沉浸在標題欄之下。頁面整體向上滑動時,頭部區域的背景圖和車系資訊模組被推走,但頭部的標題欄固定,滑動到頂部以後,tab模組和車型標籤吸頂。

3. 與tab對應的內容區域是一個整體列表,列表滑動到不同的內容模組時,tab也自動切換到對應的模組,點選tab時,列表也可以定位到對應的模組。

此種設計,內容模組多,不同模組之間的互動也比較複雜,而且需求變動較多,需要很好的可擴充套件性,通常在需要展示覆雜資訊的詳情頁上使用。

3

頁面架構設計

3.1 整體滑動元件

從上面的設計分析可以看出,頭部,tab區域和內容列表的滑動效果是統一的,它們看起來像是一個整體,所以需要一個”膠水”元件將這些彼此獨立可滾動的widget “粘”起來,使得這些widget滑動協調一致。Flutter中充當協調滑動粘合劑的元件主要是CustomScrollView和NestedScrollView。

CustomScrollView是可以使用sliver來自定義滾動模型(效果)的可無限滾動型別的widget。它可以包含多種滾動模型,例如,如果一個頁面頂部需要一個GridView,底部需要一個ListView,而要求整個頁面的滑動效果是統一的,即它們看起來是一個整體,如果使用GridView+ListView來實現的話,就不能保證一致的滑動效果,因為它們的滾動效果是分離的,CustomScrollView讓你可以直接提供 slivers來建立不同的滾動效果,比如SliverList,SliverGrids 以及其他Sliver家族的元件。如 SliverAppBar,可以在CustomScrollView的頂部佈局一個appBar導航欄;SliverAdapter可以將一個普通的子Widget變成一個Sliver元件插入到CustomScrollView中一起協調滑動,這就帶來了很大的可擴充套件性,可以擴充套件很多的普通元件和SliverList結合在一起滑動。

建構函式如下:

const CustomScrollView({
Key key,
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController controller,
bool primary,
ScrollPhysics physics,
bool shrinkWrap = false,
Key center,
double anchor = 0.0,
double cacheExtent,
this.slivers = const <Widget>[],
int semanticChildCount,
DragStartBehavior dragStartBehavior = DragStartBehavior.start,
})

Slivers屬性是一個Widget陣列,可以新增不同的sliver家族的widget,如SliverList,GridView,SliverAdapter等。

NestedScrollView就是一個支援巢狀滑動的ScrollView,其作用就是作為控制元件父佈局,從而具備(巢狀)滑動功能。其建構函式如下:

const NestedScrollView({
Key key,
this.controller,
this.scrollDirection = Axis.vertical,
this.reverse = false,
this.physics,
@required this.headerSliverBuilder,
@required this.body,
this.dragStartBehavior = DragStartBehavior.start,
})

headerSliverBuilder 屬性可以build 一個sliver家族的List 多個Sliver Widget作為NestedScrollView的頭部區域,所以可以結合SliverAppBar一起使用。Body屬性可以是一個普通的列表Widget,如ListView。NestedScrollView + SliverAppBar + ListView就組成一套頭部加列表的巢狀滑動組合。

3.2 頭部沉浸式滑動效果

如圖2的頭部沉浸式的可伸縮摺疊效果,可使用SliverAppBar來實現,通常結合 CustomScrollView 、 NestedScrollView 來使用它,其建構函式如下:

const SliverAppBar({
Key key,
this.leading, //在標題左側顯示的一個控制元件,在首頁通常顯示應用的 logo;在其他介面通常顯示為返回按鈕
this.automaticallyImplyLeading = true,//? 控制是否應該嘗試暗示前導小部件為null
this.title, //當前介面的標題文字
this.actions, //一個 Widget 列表,代表 Toolbar 中所顯示的選單,對於常用的選單,通常使用 IconButton 來表示;對於不常用的選單通常使用 PopupMenuButton 來顯示為三個點,點選後彈出二級選單
this.flexibleSpace, //一個顯示在 AppBar 下方的控制元件,高度和 AppBar 高度一樣, // 可以實現一些特殊的效果,該屬性通常在 SliverAppBar 中使用
this.bottom, //一個 AppBarBottomWidget 物件,通常是 TabBar。用來在 Toolbar 標題下面顯示一個 Tab 導航欄
this.elevation, //陰影
this.forceElevated = false,
this.backgroundColor, //APP bar 的顏色,預設值為 ThemeData.primaryColor。改值通常和下面的三個屬性一起使用
this.brightness, //App bar 的亮度,有白色和黑色兩種主題,預設值為 ThemeData.primaryColorBrightness
this.iconTheme, //App bar 上圖示的顏色、透明度、和尺寸資訊。預設值為 ThemeData().primaryIconTheme
this.textTheme, //App bar 上的文字主題。預設值為 ThemeData().primaryTextTheme
this.primary = true, //此應用欄是否顯示在螢幕頂部
this.centerTitle, //標題是否居中顯示,預設值根據不同的作業系統,顯示方式不一樣,true居中 false居左
this.titleSpacing = NavigationToolbar.kMiddleSpacing,//橫軸上標題內容 周圍的間距
this.expandedHeight, //展開高度
this.floating = false, //是否隨著滑動隱藏標題
this.pinned = false, //是否固定在頂部
this.snap = false, //與floating結合使用
})

SliverAppBar是Sliver家族的AppBar,是AppBar的增強升級版。AppBar位置是固定在應用最上面的,而SliverAppBar是可以隨內容滾動的,可以實現沉浸式的頭部伸縮摺疊效果。

(1)title屬性可以建立跟AppBar一樣的標題導航欄;

(2)flexibleSpace屬性還可以擴充套件AppBar的內容,可以將整個頭部區域Widget融合在裡面,實現頭部區域的跟隨滑動。

(3) pinned: 為true,則appBar會固定在頂部;false,則SliverPersistentHeader吸頂時,appBar會滑出螢幕

(4)primary: true,則appBar不會置頂到狀態列;false,則appbar會覆蓋在狀態列上

(5)floating: 為true時,snap一定為true,則吸頂時,頭部先滑動,頭部完全展示後,列表才滑動,這個屬性需要結合snap屬性一起使用,來產生頭部的各種滑動摺疊效果。

(6) expandedHeight:預設高度是狀態列和導航欄的高度,如果flexibleSpace中包含了頭部區域widget,要大於前兩者的高度,是整個頭部加上狀態列和導航欄的高度。

3.3 滑動吸頂的tab

滑動吸頂的tab可以使用SliverPersistentHeader來實現。 SliverPersistentHeader是可以根據滾動而變大變小的元件,SliverAppBar就是基於這個實現的,其建構函式如下:

  const SliverPersistentHeader({
Key? key,
required this.delegate,
this.pinned = false,
this.floating = false,
})

(1 ) delegate: SliverPersistentHeaderDelegate。 需要自定義實現SliverPersistentHeaderDelegate,SliverAppBar也是基於這個實現的,只是邏輯更復雜。 一個自定義伸縮高度的SliverPersistentHeaderDelegate實現如下:

  class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
_SliverAppBarDelegate({
@required this.minHeight,
@required this.maxHeight,
@required this.child,
});

double minHeight;
double maxHeight;
Widget child;

@override
double get minExtent => minHeight;

@override
double get maxExtent => max(maxHeight, minHeight);

@override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
return new SizedBox.expand(child: Container(
child: child,
));
}

@override
bool shouldRebuild(_SliverAppBarDelegate oldDelegate) {
return maxHeight != oldDelegate.maxHeight ||
minHeight != oldDelegate.minHeight ||
child != oldDelegate.child;
}
}

(2) pinned:true,SliverPersistentHeader會以摺疊高度固定顯示在頭部,false:縮小到摺疊高度後滑出頁面。

(3) floating:true 的時候下滑先展示SliverPersistentHeader,展示完成後才展示其他滑動元件內容

通過以上分析,要實現第2章中的需求,可以使用CustomScrollView 或者NestedScrollView,再搭配SliverAppBar + SliverPersistentHeader來實現。相比於NestedScrollView,CustomScrollView的slivers屬性可以建立多個sliver家族的widget,這些widget可以是SliverList、SliverGrid、SliverPersistentHeader或是SliverAdapter包裹的普通widget,這些slivers widget彼此獨立,又可以協調一致滑動,可以隨意組合,有更高的可擴充套件性。因此,筆者選擇CustomScrollView + SliverAppBar + SliverPersistentHeader的組合來實現第2章的需求。整體結構設計如下:

頁面總體架構程式碼如下:

    Widget _buildMainWidget() {
return CustomScrollView(
key: listViewKey,
physics: ClampingScrollPhysics(),
controller: _scrollController,
slivers: <Widget>[
_buildSliverBar(), //建立整個頭部的SliverAppBar
_buildStickyBar(), //建立吸頂的tab
_buildSpecBar(), //建立跟隨吸頂的車型選擇的tab
_buildList() //sliver 列表
],
);
}

Widget _buildSliverBar(){
return SliverAppBar(
brightness: Brightness.light,
backgroundColor: Colors.white,
title: _buildNavWidget(), //頂部導航標題欄
pinned: true,
floating: false,
snap: false,
primary: false,
expandedHeight: headHeight //指定整個頭部sliverAppBar的高度
elevation: 0,
flexibleSpace: new FlexibleSpaceBar(
collapseMode: CollapseMode.pin,
background: _buildHeadModuelWidget()),
);
}

Widget _buildStickyBar() {
return SliverPersistentHeader(
pinned: true, //是否固定在頂部
delegate: _SliverAppBarDelegate(
minHeight: tabHeight , //收起的高度
maxHeight: tabHeight , //展開的最大高度
child: _buildTabBar()
),

);
}

Widget _buildList() {
return SliverPadding(
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return _buildBodyListItem(); //列表的item項
},
)),
);
}

4

tab對應列表模組 自適應

高度計算

每個tab都對應著SliverList中的一個item 模組,列表上下滑動過程中,tab要和SliverList中相應的模組匹配,就需要計算item 模組的高度,而每個item 模組是根據不同的資料來渲染的,高度都是動態的。

我們在item 模組的資料Model中定義一個屬性GlobalKey itemKey = new GlobalKey(); 這樣每個item 模組就可以從自己的資料model中獲取到唯一的itemKey,這個itemKey就是該item 模組Widget的唯一標識,通過這個GlobalKey,我們就可以獲取到該item 模組Widget渲染的基本資訊,包括Widget的高度資訊,模組高度計算如下:

double cardHeight = _newEnergyList[index]
.adsorptionKey
.currentContext
.findRenderObject()
.paintBounds
.size
.height;

然後 將每個item 模組的高度放入一個Map中,Map heightMap = new Map(),這樣heightMap儲存了列表所有item模組的高度,key就是該item模組在列表中的索引index,可以避免重複計算模組的高度,也很方便的獲取每個item模組高度來定位對應的tab。

5

總結和展望

我們使用Flutter跨端技術對沉浸式複雜互動頁面進行了設計和實現,可以看到Flutter的能力和效能完全可以承載邏輯和互動複雜頁面的實現,只是對比原生實現同等複雜的頁面,效能稍差,但開發效率極大提高。以後,我們還需要進一步探索Flutter開發的能力,提升和優化Flutter頁面效能。

作者簡介

蔣雄鋒

2018年加入汽車之間,目前任職經銷商技術部,主要涉及Android移動端、Flutter、React Native等大前端技術,負責汽車報價App業務的開發。