关注“之家技术”,获取更多技术干货
笔者部门内部一款C端app 新能源车系页展示车系的所有相关信息,包括车系图,车系信息,车型,口碑,车主实拍,相关推荐车系,广告等众多模块信息,展示信息模块多,模块信息需要可动态配置,可扩展性强,交互复杂,是一个重要的车系详情展示页面。为了节省开发时间,整个页面采用跨端技术Flutter开发,是使用Flutter开发复杂页面的一次有益验证和实践。
页面设计稿如下图
图1
整体页面设计分析如下:
1.这个页面是一个典型的沉浸式的头部可伸缩折叠的页面设计。整体分为头部区域(包括顶部标题,车系背景,车系信息),可吸顶的tab区域(车型,口碑等),与tab对应的内容列表区域。
2.头部区域顶部标题栏和背景车系图延伸到状态栏,且背景车系图沉浸在标题栏之下。页面整体向上滑动时,头部区域的背景图和车系信息模块被推走,但头部的标题栏固定,滑动到顶部以后,tab模块和车型标签吸顶。
3. 与tab对应的内容区域是一个整体列表,列表滑动到不同的内容模块时,tab也自动切换到对应的模块,点击tab时,列表也可以定位到对应的模块。
此种设计,内容模块多,不同模块之间的交互也比较复杂,而且需求变动较多,需要很好的可扩展性,通常在需要展示复杂信息的详情页上使用。
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
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项
},
)),
);
}
每个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
我们使用Flutter跨端技术对沉浸式复杂交互页面进行了设计和实现,可以看到Flutter的能力和性能完全可以承载逻辑和交互复杂页面的实现,只是对比原生实现同等复杂的页面,性能稍差,但开发效率极大提高。以后,我们还需要进一步探索Flutter开发的能力,提升和优化Flutter页面性能。
蒋雄锋
2018年加入汽车之家,目前任职经销商技术部,主要涉及Android移动端、Flutter、React Native等大前端技术,负责汽车报价App业务的开发。