企业网站 阿里云,新闻发布最新新闻,专业的vi设计企业,中华建设一、滚动布局
Flutter中可滚动布局基本都来自Sliver模型#xff0c;原理和安卓传统UI的ListView、RecyclerView类似#xff0c;滚动布局里面的每个子组件的样式往往是相同的#xff0c;由于组件占用内存较大#xff0c;所以在内存上我们可以缓存有限个组件#xff0c;滚动…一、滚动布局
Flutter中可滚动布局基本都来自Sliver模型原理和安卓传统UI的ListView、RecyclerView类似滚动布局里面的每个子组件的样式往往是相同的由于组件占用内存较大所以在内存上我们可以缓存有限个组件滚动布局时仅仅刷新组件的数据来达到滚动布局存放无限个子组件的目标。另一方面内容被渲染到了lazy widget里也就是SliverList and SliverGrid (以及它们的变种 SliverFixedExtentList 和 SliverAnimatedGrid。这些组件确保只有列表中可见部分会被布局和渲染。ListView 和 GridView只是CustomScrollView 和 SliverList 或 SliverGrid组合在一起方便我们使用的一个封装组件而已。它们允许我们直接使用box widget。而CustomScrollView的slivers属性明确要求我们使用sliver widget。但是sliver widget也只是box widget的包裹后者才是真正用于渲染的。按照列表内容的差异我们可以将scrolling widget分为以下三类 sliver widget是可滚动列表中的一部分它是面向viewport进行布局的。
二、ViewPort
ViewPort 是一个显示窗口它内部可包含多个 SliverViewPort 的宽高是确定的它内部 Slivers 的宽高之和是可以大于自身的宽高的ViewPort 为了提高性能采用懒加载机制它只会绘制可视区域内容 Widget。
ViewPort 有一些重要属性
class Viewport extends MultiChildRenderObjectWidget {/// 主轴方向final AxisDirection axisDirection;/// 纵轴方向final AxisDirection crossAxisDirection;/// center 决定 viewport 的 zero 基准线也就是 viewport 从哪个地方开始绘制默认是第一个 sliver/// center 必须是 viewport slivers 中的一员的 keyfinal Key center;/// 锚点取值[0,1]和 zero 的相对位置比如 0.5 代表 zero 被放到了 Viewport.height / 2 处final double anchor;/// 滚动的累计值确切的说是 viewport 从什么地方开始显示final ViewportOffset offset;/// 缓存区域也就是相对有头尾需要预加载的高度final double cacheExtent;/// children widgetListWidget slivers}
我们看到center是一个key表示从哪个sliver开始绘制绘制的起点是一条zero基准线。这条基准线相对于视口的位置叫anchor。视口相对于整个滚动列表的位置叫offset。如果子元素进入了视口上下cacheExtent的区域就会被预先加载。 以上图为例centersliver1anchor0.2此时sliver的top0.2 * viewport.height所以前面刚好能展示sliver0也就是浅蓝色区域会滚到整个列表的开始位置。再比如centersliver1anchor0.4此时sliver0上面会空出一条sliver大小的空间。
三、SliverConstraints
和 Box 布局使用 BoxConstraints 作为约束类似Sliver 布局采用 SliverConstraints 作为约束但相对于 Box 要复杂的多可以理解为 SliverConstraints 描述了 Viewport 和它内部的 Slivers 之间的布局信息
class SliverConstraints extends Constraints {//主轴方向AxisDirection? axisDirection;//Sliver 新数据沿主轴的哪个方向插入枚举类型正向或反向GrowthDirection? growthDirection;//用户滑动方向ScrollDirection? userScrollDirection;//当前Sliver理论上可能会固定在顶部已经滑出可视区域的总偏移double? scrollOffset;//当前Sliver之前的Sliver占据的总高度因为列表是懒加载如果不能预估时该值为double.infinitydouble? precedingScrollExtent;//上一个 sliver 覆盖当前 sliver 的大小通常在 sliver 是 pinned/floating//或者处于列表头尾时有效。double? overlap;//当前Sliver在Viewport中的最大可以绘制的区域。//绘制如果超过该区域会比较低效因为不会显示double? remainingPaintExtent;//纵轴的长度如果列表滚动方向是垂直方向则表示列表宽度。double? crossAxisExtent;//纵轴方向AxisDirection? crossAxisDirection;//Viewport在主轴方向的长度如果列表滚动方向是垂直方向则表示列表高度。double? viewportMainAxisExtent;//Viewport 预渲染区域的起点[-Viewport.cacheExtent, 0]double? cacheOrigin;//Viewport加载区域的长度范围://[viewportMainAxisExtent,viewportMainAxisExtent Viewport.cacheExtent*2]double? remainingCacheExtent;
}cacheOrigin
在Viewport 预渲染区域中的起点位于[-Viewport.cacheExtent, 0]之间
overlap
上图中的 sliver1 会被 SliverAppBarpinned true遮住遮住的大小就是 overlap此时 overlap 会一直大于 0如果设置像 iOS bouncing 那样的滑动效果那么当 list 滚动到顶部继续滑动的时候 overlap 会小于 0此刻并没有东西遮盖 sliver1而是 sliver1 的 top 和 viewport 的 top 有间距。
remainingPaintExtent
当前 Sliver 在 Viewport 中的最大可以绘制的区域。当viewport对sliver布局的时候会将remainingPaintExtent减去这个sliver的paintExtent后作为传入下一个sliver的remainingPaintExtent。remainingPaintExtent的初始值是viewportMainAxisExtent。
由此我们可以计算当前 sliver 距离顶部的距离
//如果大于0表示当前 sliver 距离顶部的高度为 topOffset。若已经到达顶部或超出顶部则该值始终等于 0。此时超出的距离可参考 scrollOffset。
topOffset viewportMainAxisExtent - remainingPaintExtent;当前sliver无论实际需要绘制多大的区域最终能绘制到viewport的大小不会超过remainingPaintExtent
// paintExtent 通常需要这样处理一下避免超出 remainingPaintExtent
paintExtent min(paintExtent, constraints.remainingPaintExtent);scrollOffset
scrollOffset 属性表示滚动滑出Viewport边界的距离类似于 web 的 scrollTop 属性。一般来说表示组件的上边界离开 viewport 顶部的长度未到达顶部之前都是 0。
SliverGeometry
Viewport 通过 SliverConstraints 告知它内部的 sliver 自己的约束信息比如还有多少空间可用、offset 等那么Sliver 则通过 SliverGeometry 反馈给 Viewport 需要占用多少空间量。 const SliverGeometry({//Sliver在主轴方向预估长度大多数情况是固定值用于计算sliverConstraints.scrollOffsetthis.scrollExtent 0.0, this.paintExtent 0.0, // 可视区域中的绘制长度this.paintOrigin 0.0, // 绘制的坐标原点相对于自身布局位置//在 Viewport中占用的长度如果列表滚动方向是垂直方向则表示列表高度。//范围[0,paintExtent]double? layoutExtent, this.maxPaintExtent 0.0,//最大绘制长度this.maxScrollObstructionExtent 0.0,double? hitTestExtent, // 点击测试的范围bool? visible,// 是否显示//是否会溢出Viewport如果为trueViewport便会裁剪this.hasVisualOverflow false,//scrollExtent的修正值layoutExtent变化后为了防止sliver突然跳动应用新的layoutExtent//可以先进行修正。this.scrollOffsetCorrection,double? cacheExtent, // 在预渲染区域中占据的长度
}) paintOrigin
当该值小于 0 时当前 sliver 的整体起始位置会向上偏移 paintOrigin.abs() 的长度。如果每次下拉 x 的长度paintOrigin 也向上移动 x 的距离则 sliver 相对静止由此可实现 pinned 效果。
layoutExtent
布局时占用的高度取值范围 [0, paintExtent]。即 layoutExtent 须小于等于 paintExtent 当前绘制的高度一般是等于。 当 layoutExtent 小于 paintExtent 时则一部分高度会被下一个 sliver 顶上。
示例 2 说明蓝色块高度是 100但是占据高度只有 30导致红色块向上顶了 70的高度。紫色部分是下方的红色与上方的蓝色重叠的区域 visible
当 visible 为 false 时会影响子节点的显示。在示例中只占据空间占据高度 layoutExtent而不显示界面。即不影响布局。
效果 说明中间 30 的高度为原蓝色块占据的空间 scrollExtent
可滚动的范围。一般来说对于 ListView在 sliver 上边界滚动到顶部之前 paintExtent 等于 layoutExtent 都等于 scrollExtent到达顶部后慢慢变小直到变为 0。而 scrollExtent 一直不变。 注如果 layoutExtent 不慢慢变小即保持不变并且大于 0则在当前 sliver 滚动到顶部后还可以继续滚动 scrollExtent 的长度除非 scrollExtent 也等于 0然后再执行下一个 sliver 的滚动。 示例 3 说明由于 layoutExtent 与 scrollExtent 都一直不变并且不等于 0。蓝色 sliver 向上滚动到 Viewport 顶部后还可以继续滚动 100 的高度当这 100 也滚完了下一个 sliver 才开始滚动。 Sliver 布局过程
RenderViewport 在 layout 它内部的 slivers 的过程如下 这个 layout 过程是一个自上而下的线性过程
给 sliver1 输入 SliverConstrains1 并且得到输出结果SliverGeometry1 根据 SliverGeometry1 重新生成一个新的 SliverConstrains2 输入给 sliver2 得到 SliverGeometry2…直至最后一个 sliver 具体的过程可以查看 RenderViewport 的 layoutChildSequence 方法。
Slivers
Flutter 提供了很多的 Sliver 组件下面我们主要说一下它们的作用是什么
SliverAppBar
类似于 android 中 CollapsingToolbarLayout可以根据滑动做伸缩布局并提供了 actionsbottom 等提高效率的属性。 SliverList / SliverGrid
用法和 ListView / GridView 基本一致。 此外ListView SliverList Scrollable也就是说 SliverList 不具备处理滑动事件的能力所以它必须配合 CustomScrollView 来使用。
SliverFixedExtentList
它比 SliverList 多了修饰词 FixedExtent意思是它的 item 在主轴方向上具有固定的高度/宽度。
设计它的原因是在 item 高度/宽度全都一样的场景下使用它的效率比 SliverList 高因为它不用通过 item 的 layout 过程就可以知道每个 item 的范围。
在使用的时候必须传入 itemExtent
SliverFixedExtentList(itemExtent: 50.0,delegate: SliverChildBuilderDelegate(...);},),
) SliverPersistentHeader SliverPersistentHeader 是一个可以固定/悬浮的 header它可以设置在列表的任意位置显示的内容需要设置 SliverPersistentHeaderDelegate。
SliverPersistentHeader(pinned: true,delegate: ...,
)SliverPersistentHeaderDelegate 是一个抽象类我们需要自己实现它它的实现很简单只有四个必须要实现的成员
class CustomDelegate extends SliverPersistentHeaderDelegate {/// 最大高度overridedouble get maxExtent 100;/// 最小高度overridedouble get minExtent 50;/// shrinkOffset: 当前 sliver 顶部越过屏幕顶部的距离/// overlapsContent: 下方是否还有 content 显示overrideWidget build(BuildContext context, double shrinkOffset, bool overlapsContent) {return your widget);}/// 是否需要刷新overridebool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) {return maxExtent ! oldDelegate.maxExtent ||minExtent ! oldDelegate.minExtent;}
}在实际运用中沉浸式的设计是很常见的使用 SliverPersistentHeaderDelegate 可以轻松的实现沉浸式的效果 它的实现原理就是根据 shrinkOffset 动态调整状态栏的样式和标题栏的颜色实现代码见下面的 沉浸式 Header。
SliverToBoxAdapter
将 BoxWidget 转变为 Sliver由于 CustomScrollView 只能接受 Sliver 类型的 child所以很多常用的 Widget 无法直接添加到 CustomScrollView 中此时只需要将 Widget 用 SliverToBoxAdapter 包裹一下就可以了。 最常见的使用就是 SliverList 不支持横向模式但是又无法直接将 ListView 直接添加到 CustomScrollView 中此时用 SliverToBoxAdapter 包裹一下 CustomScrollView(slivers: Widget[SliverToBoxAdapter(child: _buildHorizonScrollView(),),],));Widget _buildHorizonScrollView() {return Container(height: 50,child: ListView.builder(scrollDirection: Axis.horizontal,primary: false,shrinkWrap: true,itemCount: 15,itemBuilder: (context, index) {return Container(color: ColorUtils.randomColor(),width: 50,height: 50,);}),);} SliverPadding
可以用在 CustomScrollView 中的 Padding。 需要注意的是不要用它来包裹 SliverPersistentHeader 因为它会使 SliverPersistentHeader 的 pinned 失效如果 SliverPersistentHeader 非要使用 Padding 效果可以在 delegate 内部使用 Padding。
wrong code
SliverPadding(padding: EdgeInsets.symmetric(horizontal: 16),sliver: SliverPersistentHeader(pinned: true,floating: false,delegate: Delegate(),),)
correct code
class Delegate extends SliverPersistentHeaderDelegate {overrideWidget build(BuildContext context, double shrinkOffset, bool overlapsContent) Padding(padding: EdgeInsets.symmetric(horizontal: 16),child: Container(color: Colors.yellow,),);...
}SliverSafeArea
用法和 SafeArea 一致。
SliverFillRemaining
可以填充屏幕剩余控件的 Sliver。
参考文献
Flutter - 循序渐进 Sliver - 掘金flutter —— 布局原理与约束 Sliver 布局