总篇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?[],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,//控制是否应该尝试暗示前导小部件为nullthis.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().primaryTextThemethis.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({Keykey,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;@overridedouble?get?minExtent?=>?minHeight;@overridedouble?get?maxExtent?=>?max(maxHeight,?minHeight);@overrideWidget?build(BuildContext?context,?double?shrinkOffset,?bool?overlapsContent)?{return?new?SizedBox.expand(child:?Container(child:?child,));}@overridebool?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章的需求。整体结构设计如下:图2页面总体架构代码如下:Widget?_buildMainWidget()?{return?CustomScrollView(key:?listViewKey,physics:?ClampingScrollPhysics(),controller:?_scrollController,slivers:?[_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项},)),);}4tab对应列表模块自适应高度计算每个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中,MapheightMap = new Map(),这样heightMap保存了列表所有item模块的高度,key就是该item模块在列表中的索引index,可以避免重复计算模块的高度,也很方便的获取每个item模块高度来定位对应的tab。5总结和展望我们使用Flutter跨端技术对沉浸式复杂交互页面进行了设计和实现,可以看到Flutter的能力和性能完全可以承载逻辑和交互复杂页面的实现,只是对比原生实现同等复杂的页面,性能稍差,但开发效率极大提高。以后,我们还需要进一步探索Flutter开发的能力,提升和优化Flutter页面性能。作者简介蒋雄锋2018年加入汽车之家,目前任职经销商技术部,主要涉及Android移动端、Flutter、React Native等大前端技术,负责汽车报价App业务的开发。阅读更多:面向推荐的汽车知识图谱构建基于Flink+Starrocks建设之家广告实时数据基于CodeSmith提高开发效率