列表组件在移动端上尤为重要,Sliver 作为 Flutter 列表组件中重要的一部分,开发者非常有必要了解 Sliver 的原理和用法。

两种类型的布局

Flutter 的布局可以分为两种:

  • Box ( RenderBox ): 2D 绘制布局
  • Sliver ( RenderSliver ):滚动布局

重要的概念

Sliver

Sliver 是 Flutter 中的一个概念,表示可滚动布局中的一部分,它的 child 可以是普通的 Box Widget。

ViewPort

  • ViewPort 是一个显示窗口,它内部可包含多个 Sliver;
  • ViewPort 的宽高是确定的,它内部 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 widgetList<Widget> slivers;}
复制代码

一图胜千言:

上图中假设每个 sliver 的 height 都相等且等于屏幕高度的 ⅕,这样设置 center = sliver1,屏幕的第一个显示的应该是 sliver 1,但是因为 anchor = 0.2,0.2 * viewport.height 正好等于 sliver1 的高度,所以屏幕上显示的第一个是 sliver 2。

ScrollPostion

ScrollPosition 决定了 Viewport 哪些区域是可见的,它包含了Viewport 的滚动信息,它的主要成员变量如下:


abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {// 滚动偏移量double _pixels;// 设置滚动响应效果,比如滑动停止后的动画final ScrollPhysics physics;// 保存当前的滚动偏移量到 PageStore 中,当 Scrollable 重建后可以恢复到当前偏移量final bool keepScrollOffset;// 最小滚动值double _minScrollExtent;// 最大滚动值double _maxScrollExtent;...
}
复制代码

ScrollPosition 的类继承关系如下:

|-- Listenable
|---- ChangeNotifier
|------ ScrollPosition
|-------- ScrollPositionWithSingleContext
复制代码

所以 ScrollPosition 可以作为被观察者,当数据改变的时候可以通知观察者。

Scrollable

Scrollable 是一个可滚动的 Widget,它主要负责:

  • 监听用户的手势,计算滚动状态发出 Notification
  • 计算 offset 通知 listeners

Scrollable 本身不具有绘制内容的能力,它通过构造注入的 viewportBuilder 来创建一个 Viewport 来显示内容,当滚动状态变化的时候,Scrollable 就会不断的更新 Viewport 的 offset ,Viewport 就会不断的更新显示内容。

Scrollable 主要结构如下:


Widget result = _ScrollableScope(scrollable: this,position: position,child: Listener(onPointerSignal: _receivedPointerSignal,child: RawGestureDetector(gestures: _gestureRecognizers,...,child: Semantics(...child: IgnorePointer(...child: widget.viewportBuilder(context, position),),),),),);
复制代码
  • _ScrollableScope 继承自 InheritedWidget,这样它的 children 可以方便的获取 scrollable 和 position;
  • RawGestureDetector 负责手势监听,手势变化时会回调 _gestureRecognizers;
  • viewportBuilder 会生成 viewport;

SliverConstraints

和 Box 布局使用 BoxConstraints 作为约束类似,Sliver 布局采用 SliverConstraints 作为约束,但相对于 Box 要复杂的多,可以理解为 SliverConstraints 描述了 Viewport 和它内部的 Slivers 之间的布局信息:

class SliverConstraints extends Constraints {// 主轴方向final AxisDirection axisDirection;// 窗口增长方向final GrowthDirection growthDirection;// 如果 Direction 是 AxisDirection.down,scrollOffset 代表 sliver 的 top 滑过 viewport 的 top 的值,没滑过 viewport 的 top 时 scrollOffset 为 0。final double scrollOffset;// 上一个 sliver 覆盖下一个 sliver 的大小(只有上一个 sliver 是 pinned/floating 才有效)final double overlap;// 轮到当前 sliver 开始绘制了,需要 viewport 告诉 sliver 当前还剩下多少区域可以绘制,受 viewport 的 size 影响final double remainingPaintExtent;// viewport 主轴上的大小final double viewportMainAxisExtent;// 缓存区起点(相对于 scrolloffset),如果 cacheExtent 设置为 0,那么 cacheOrigin 一直为 0final double cacheOrigin;// 剩余的缓存区大小final double remainingCacheExtent;...
}
复制代码

上图中的 sliver1 会被 SliverAppBar(pinned = true)遮住,遮住的大小就是 overlap,此时 overlap 会一直大于 0,如果设置像 iOS bouncing 那样的滑动效果,那么当 list 滚动到顶部继续滑动的时候 overlap 会小于 0(此刻并没有东西遮盖 sliver1,而是 sliver1 的 top 和 viewport 的 top 有间距)。

SliverGeometry

Viewport 通过 SliverConstraints 告知它内部的 sliver 自己的约束信息,比如还有多少空间可用、offset 等,那么Sliver 则通过 SliverGeometry 反馈给 Viewport 需要占用多少空间量。


class SliverGeometry extends Diagnosticable {// sliver 可以滚动的范围,可以认为是 sliver 的高度(如果是 AxisDierction.Down) final double scrollExtent;// 绘制起点(默认是 0.0),是相对于 sliver 开始 layout 的起点而言的,不会影响下一个 sliver 的 layoutExtent,会影响下一个 sliver 的paintExtentfinal double paintOrigin;// 绘制范围final double paintExtent;// 布局范围,当前 sliver 的 top 到下一个 sliver 的 top 的距离,范围是[0,paintExtent],默认是 paintExtent,会影响下一个 sliver 的 layout 位置final double layoutExtent;// 最大绘制大小,必须 >= paintExtentfinal double maxPaintExtent;// 如果 sliver 被 pinned 在边界的时候,这个大小为 Sliver 的自身的高度,其他情况为0,比如 pinned app barfinal double maxScrollObstructionExtent;// 点击有效区域的大小,默认为paintExtentfinal double hitTestExtent;// 是否可见,visible = (paintExtent > 0)final bool visible;// 是否需要做clip,免得chidren溢出final bool hasVisualOverflow;// 当前 sliver 占用了 SliverConstraints.remainingCacheExtent 多少像素值final double cacheExtent;...
}
复制代码

Sliver 布局过程

RenderViewport 在 layout 它内部的 slivers 的过程如下:

这个 layout 过程是一个自上而下的线性过程:

  • 给 sliver1 输入 SliverConstrains1 并且得到输出结果(SliverGeometry1) ,
  • 根据 SliverGeometry1 重新生成一个新的 SliverConstrains2 输入给 sliver2 得到 SliverGeometry2
  • 直至最后一个 sliver 具体的过程可以查看 RenderViewport 的 layoutChildSequence 方法。

ScrollView

以 ScrollView 为例,我们串联上面介绍的几个 Widget 之间的关系。 先来看 ScrollView 的 build 方法:

@overrideWidget build(BuildContext context) {final List<Widget> slivers = buildSlivers(context);final AxisDirection axisDirection = getDirection(context);final ScrollController scrollController = primary? PrimaryScrollController.of(context): controller;final Scrollable scrollable = Scrollable(...controller: scrollController,viewportBuilder: (BuildContext context, ViewportOffset offset) {return buildViewport(context, offset, axisDirection, slivers);},);return primary && scrollController != null? PrimaryScrollController.none(child: scrollable): scrollable;}
复制代码

可以看到 ScrollView 创建了一个 Scrollable,并传入了构造 ViewPort 的 buildViewPort 方法。 上面讲过 Scrollable 负责手势监听,通过 buildViewPort 创建视图,在手势变化的时候不停的更新 ViewPort,大概流程如下:

自定义 Sliver

CustomPinnedHeader 光看一些概念会难以理解,最好的方式是 debug 一下,我们可以 copy 一下 SliverToBoxAdapter 的代码自定义一个 Sliver 调试一下各个参数加深理解。


class CustomSliverWidget extends SingleChildRenderObjectWidget {const CustomSliverWidget({Key key, Widget child}): super(key: key, child: child);@overrideRenderObject createRenderObject(BuildContext context) {return CustomSliver();}
}
/// 一个 StickPinWidget
/// 主要讲述 Sliveronstraints 和 SliverGeometry 参数的作用
class CustomSliver extends RenderSliverSingleBoxAdapter {@overridevoid performLayout() {...// 将 SliverConstraints 转化为 BoxConstraints 对 child 进行 layoutchild.layout(constraints.asBoxConstraints(), parentUsesSize: true);...// 计算绘制大小final double paintedChildSize =calculatePaintOffset(constraints, from: 0.0, to: childExtent);// 计算缓存大小final double cacheExtent =calculateCacheOffset(constraints, from: 0.0, to: childExtent);...// 输出 SliverGeometry geometry = SliverGeometry(scrollExtent: childExtent,paintExtent: paintedChildSize,cacheExtent: cacheExtent,maxPaintExtent: childExtent,paintOrigin: 0,hitTestExtent: paintedChildSize,hasVisualOverflow: childExtent > constraints.remainingPaintExtent || constraints.scrollOffset > 0.0,);setChildParentData(child, constraints, geometry);}
}复制代码

我们把它放到 CustomScrollView 中:

eturn Scaffold(body: CustomScrollView(slivers: <Widget>[CustomSliverWidget(child: Container(color: Colors.red,height: 100,child: Center(child: Text("CustomSliver"),),)),_buildListView(),],));复制代码

效果如下:

我们修改 paintOrigin 为 10 的话,发现 CustomSliverWidget 的 layout 位置没有变,但绘制的起始点下移了 10 px,并且它下一个的 Sliver - item0 的 layout 没有被影响,但是 paint 时被遮住了一部分:

再做一个简单的修改,将 sliver 的绘制起始位置改为滑动的偏移量:

 geometry = SliverGeometry(...paintOrigin: constraints.scrollOffset,visible: true,);
复制代码

此时你会发现 CustomSliver 可以固定在头部:

我们尝试修改 paintExtrent 如下:

geometry = SliverGeometry(//将绘制范围改为 sliver 的高度paintExtent: childExtent,...);
复制代码

在滑动的过程,CustomSliver 只是绘制变了,layout 没有变,导致下面 item0 没有被滑动,这是因为 layoutExtent 默认等于 paintExtent,我们将 paintExtent 赋值了常量,滑动过程中只有 paintOrigin 在改变,layout 的初始位置和高度并没有改变,它会一直占据着位置。

CustomRefreshWidget

接下来我们再做一个简单的下拉刷新 Widget,效果很简单,下拉的时候显示,释放的时候缩回:


class CustomRefreshWidget extends SingleChildRenderObjectWidget {const CustomRefreshWidget({Key key, Widget child}): super(key: key, child: child);@overrideRenderObject createRenderObject(BuildContext context) {return SimpleRefreshSliver();}
}/// 一个简单的下拉刷新 Widget
class SimpleRefreshSliver extends RenderSliverSingleBoxAdapter {@overridevoid performLayout() {...final bool active = constraints.overlap < 0.0;/// 头部滑动的距离final double overscrolledExtent =constraints.overlap < 0.0 ? constraints.overlap.abs() : 0.0;double layoutExtent = child.size.height;print("overscrolledExtent:${overscrolledExtent - layoutExtent}");child.layout(constraints.asBoxConstraints(maxExtent: layoutExtent + overscrolledExtent,),parentUsesSize: true,);if (active) {geometry = SliverGeometry(scrollExtent: layoutExtent,/// 绘制起始位置paintOrigin: min(overscrolledExtent - layoutExtent, 0),paintExtent: max(max(child.size.height, layoutExtent) ,0.0,),maxPaintExtent: max(max(child.size.height, layoutExtent) ,0.0,),/// 布局占位layoutExtent: min(overscrolledExtent, layoutExtent),);} else {/// 如果不想显示可以直接设置为 zerogeometry = SliverGeometry.zero;}setChildParentData(child, constraints, geometry);}
}
复制代码

可以看到有3个关键的参数

  • constraints.overlap:List 第一个 Sliver 的 top 距离屏幕 top 的距离
  • paintOrigin:RefreshWidget 的绘制起始位置
  • layoutExtent:RefreshWidget 的高度

items 的 top 与屏幕顶部的距离就是 constraints.overlap,它是一个小于等于 0 的值。

  • 未操作时,overlap == 0,直接返回一个空 Widget(SliverGeometry.zero)
  • 下拉时,overlap < 0, 这时候将 paintOrigin = min(overscrolledExtent - RefreshWidget.height, 0) 就可以让 RefreshWidget 慢慢的拉下来。
  • 处理完 Paint 后,不要忘记处理 layout,前面说过,SliverGeometry 的 layoutExtent 会影响下一个 Sliver 的布局位置,所以 layoutExtent 也需要随着滑动而逐渐变大 layoutExtent = min(-overlap, RefreshWidget.height)

Scrolling Widget

常用的 List 如下,我们按照它包裹的内容分成了 3 类:

ListView


ListView.builder(itemCount: 50,itemBuilder: (context,index) {return Container(color: ColorUtils.randomColor(),height: 50,);}
复制代码

CustomScrollView

CustomScrollView(slivers: <Widget>[SliverAppBar(...),SliverToBoxAdapter(child:ListView(...),),SliverList(...),SliverGrid(...),],)
复制代码

NestedScrollView

NestedScrollView 其实里面是一个CustomScrollView,它的 headers 是 Sliver 的数组,body是被包裹在 SliverFillRemaining 中的,body 可以接受 Box。


NestedScrollView(headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {return <Widget>[SliverAppBar(expandedHeight: 100,pinned: true,title: Text("Nest"),),SliverToBoxAdapter(child: Text("second bar"),)];},body: ListView.builder(itemCount: 20,itemBuilder: (BuildContext context, int index) {return Text("item: $index");}),);复制代码

设计 CustomScrollView 的原因

复杂列表嵌套

如果直接使用 ListView 嵌套 ListView 会报错:

Vertical viewport was given unbounded height.

大致意思是在 layout 阶段父 Listview 不能判断子 Listview 的高度,这个错误可以通过设置内部的 Listview 的 shrinkWrap = true 来修正(shrinkWrap = true 代表 ListView 的高度等于它 content 的高度)。

ListView.builder(itemCount: 20,itemBuilder: (BuildContext context, int index) {return ListView.builder(shrinkWrap: true,itemCount: 5,itemBuilder: (BuildContext context, int index) {return Text("item: $index");});});
复制代码

但是这样做的话性能会比较差,因为内部的列表每次都要计算出所有 content 的高度,这个时候使用 CustomScrollView 更为合适:

CustomScrollView(slivers: <Widget>[SliverList(delegate: SliverChildBuilderDelegate((context, index) => Container(...),childCount: 50)),SliverList(delegate: SliverChildBuilderDelegate((context, index) => Container(...),childCount: 50))],);
复制代码

滑动特效

CustomScrollView 可以让它内部的 Slivers 进行联动,比如做一个可伸缩的 TitleBar 、中间区域可以固定的 header、下拉刷新组件等等。

Slivers

Flutter 提供了很多的 Sliver 组件,下面我们主要说一下它们的作用是什么:

SliverAppBar

类似于 android 中 CollapsingToolbarLayout,可以根据滑动做伸缩布局,并提供了 actions,bottom 等提高效率的属性。

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。

部分实例代码:

沉浸式 Header


import 'package:flutter/material.dart';
import 'package:flutter/services.dart';class GradientSliverHeaderDelegate extends SliverPersistentHeaderDelegate {final double collapsedHeight;final double expandedHeight;final double paddingTop;final String coverImgUrl;final String title;GradientSliverHeaderDelegate({this.collapsedHeight,this.expandedHeight,this.paddingTop,this.coverImgUrl,this.title,});@overridedouble get minExtent => this.collapsedHeight + this.paddingTop;@overridedouble get maxExtent => this.expandedHeight;@overridebool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) {return true;}Color makeStickyHeaderBgColor(shrinkOffset) {final int alpha = (shrinkOffset / (this.maxExtent - this.minExtent) * 255).clamp(0, 255).toInt();return Color.fromARGB(alpha, 255, 255, 255);}Color makeStickyHeaderTextColor(shrinkOffset) {if (shrinkOffset <= 50) {return Colors.white;} else {final int alpha = (shrinkOffset / (this.maxExtent - this.minExtent) * 255).clamp(0, 255).toInt();return Color.fromARGB(alpha, 0, 0, 0);}}Brightness getStatusBarTheme(shrinkOffset) {return shrinkOffset <= 50 ? Brightness.light : Brightness.dark;}@overrideWidget build(BuildContext context, double shrinkOffset, bool overlapsContent) {SystemUiOverlayStyle systemUiOverlayStyle = SystemUiOverlayStyle(statusBarColor: Colors.transparent,statusBarIconBrightness: getStatusBarTheme(shrinkOffset));SystemChrome.setSystemUIOverlayStyle(systemUiOverlayStyle);return Container(height: this.maxExtent,width: MediaQuery.of(context).size.width,child: Stack(fit: StackFit.expand,children: <Widget>[// 背景图Container(child: Image.asset(coverImgUrl,fit: BoxFit.cover,)),// 收起头部Positioned(left: 0,right: 0,top: 0,child: Container(color: this.makeStickyHeaderBgColor(shrinkOffset), // 背景颜色child: SafeArea(bottom: false,child: Container(height: this.collapsedHeight,child: Center(child: Text(this.title,style: TextStyle(fontSize: 20,fontWeight: FontWeight.w500,color: this.makeStickyHeaderTextColor(shrinkOffset), // 标题颜色),),)),),),),],),);}
}
复制代码
class CustomRefreshWidget extends SingleChildRenderObjectWidget {const CustomRefreshWidget({Key key, Widget child}): super(key: key, child: child);@overrideRenderObject createRenderObject(BuildContext context) {return SimpleRefreshSliver();}
}/// 一个简单的下拉刷新 Widget
class SimpleRefreshSliver extends RenderSliverSingleBoxAdapter {@overridevoid performLayout() {if (child == null) {geometry = SliverGeometry.zero;return;}child.layout(constraints.asBoxConstraints(), parentUsesSize: true);double childExtent;switch (constraints.axis) {case Axis.horizontal:childExtent = child.size.width;break;case Axis.vertical:childExtent = child.size.height;break;}assert(childExtent != null);final double paintedChildSize =calculatePaintOffset(constraints, from: 0.0, to: childExtent);assert(paintedChildSize.isFinite);assert(paintedChildSize >= 0.0);final bool active = constraints.overlap < 0.0;final double overscrolledExtent =constraints.overlap < 0.0 ? constraints.overlap.abs() : 0.0;double layoutExtent = child.size.height;print("overscrolledExtent:${overscrolledExtent - layoutExtent}");child.layout(constraints.asBoxConstraints(maxExtent: layoutExtent// Plus only the overscrolled portion immediately preceding this// sliver.+overscrolledExtent,),parentUsesSize: true,);if (active) {geometry = SliverGeometry(scrollExtent: layoutExtent,/// 绘制起始位置paintOrigin: min(overscrolledExtent - layoutExtent, 0),paintExtent: max(max(child.size.height, layoutExtent) ,0.0,),maxPaintExtent: max(max(child.size.height, layoutExtent) ,0.0,),/// 布局占位layoutExtent: min(overscrolledExtent, layoutExtent),);} else {/// 如果不想显示可以直接设置为 zerogeometry = SliverGeometry.zero;}setChildParentData(child, constraints, geometry);}
}
复制代码

使用:

@overrideWidget build(BuildContext context) {return Scaffold(body: CustomScrollView(/// android 需要设置弹簧效果 overlap 才会起作用physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()),CustomRefreshWidget(child: Container(height: 100,color: Colors.purple,child: Row(mainAxisAlignment: MainAxisAlignment.center,crossAxisAlignment: CrossAxisAlignment.center,children: <Widget>[Text("RefreshWidget",style: TextStyle(color: Colors.white),),Padding(padding: EdgeInsets.only(left: 10.0),child: CupertinoActivityIndicator(),)],),),),..._buildListView(),],));}

作者:TravelingLight_
链接:https://juejin.im/post/5eba7bd8f265da7bf32d47e5
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Flutter - 循序渐进 Sliver相关推荐

  1. Flutter的滚动以及sliver约束

    Flutter框架中有很多滚动的Widget,ListView.GridView等,这些Widget都是使用Scrollable配合Viewport来完成滚动的.我们来分析一下这个滚动效果是怎样实现的 ...

  2. Flutter Sliver大家族之SliverList(),SliverFixedExtentList(),SliverGrid()组件②

    Flutter Sliver大家族之SliverList,SliverFixedExtentList,SliverGrid组件② SliverFixedExtentList() SliverList( ...

  3. Flutter Sliver大家族之SliverPersistentHeader()和SliverToBoxAdapter()组件(实现固定头布局)③

    Flutter Sliver大家庭之SliverPersistentHeade和SliverToBoxAdapter实现固定头布局③ SliverPersistentHeader SliverToBo ...

  4. Flutter 布局控件完结篇

    本文对Flutter的29种布局控件进行了总结分类,讲解一些布局上的优化策略,以及面对具体的布局时,如何去选择控件. 1. 系列文章 Flutter 布局详解 Flutter 布局(一)- Conta ...

  5. Flutter 28: 图解 ListView/GridView 混用时滑动冲突小尝试

    小菜在学习过程中会在一个 Page 页面同时用到 GridView 和 ListView 或多个 ListView,此时就会遇到常见的滑动冲突问题.小菜尝试了两种解决滑动冲突的方案,仅记录一下基本的使 ...

  6. Flutter开发之《网易新闻客户端Flutter混合开发实践》笔记(52)

    摘自:网易新闻客户端Flutter混合开发实践 引言 网易新闻项目本身很庞大,业务繁多,全部改为Flutter实现肯定是不现实的,在使用Flutter的前期阶段,我们挑选了相对独立的几个模块,在现有工 ...

  7. Flutter开发之ListView添加HeaderView和FooterView(38)

    参考文章:Flutter ListView如何添加HeaderView和FooterView flutter的ListView添加HeaderView和FooterView使用CustomScroll ...

  8. Flutter基础笔记

    目录 List里面常用的属性和方法: Set Map forEach,map, where,any,every extends抽象类 和 implements Flutter环境搭建 入口文件.入口方 ...

  9. Flutter 填坑之 表单数据哪里去了?

    为什么80%的码农都做不了架构师?>>>    最近遇到一个奇怪的问题,一个 flutter 表单,填写数据的时候,时常丢失数据,再填写一遍,就可以了. 通过 debug ,找到了原 ...

  10. Flutter瀑布流及通用列表解决方案

    简介:解决flutter复杂布局过程以及对基础能力进行扩充的列表视图解决方案 作者:闲鱼技术-夜澜 背景 目前闲鱼业务中无论是首页还是搜索页都有大量可以落地瀑布流的场景,而在Flutter原生中只提供 ...

最新文章

  1. itunes刷机一直正在恢复固件要多久_iPhone “已停用”,为什么刷机后仍是“已停用”的状态?...
  2. Eclipse 3.6 更新中文语言包的方法
  3. 数组子数组求最大值1
  4. spring aop 如何切面到mvc 的controller--转载
  5. spring servlet 扩展undertow
  6. springboot启动时An attempt was made to call a method that does not exist
  7. html鼠标点击有手势出来,用原生js+css3撸的一个下拉手势事件插件
  8. js 数组头部添加_javaScript 为对象型数组创建表格
  9. 职场中必需修炼的七项意识
  10. Go语言反射之反射调用
  11. ------表达式---数值表示/算术运算符
  12. 一个...买裤子的全过程
  13. CAD地形图等高线标高批量取整工具,解决等高线标高出现小数的问题,等高线高程批量取整,在指定限差内将等高线标高修改为最接近的整数
  14. 最新度盘高速下载神器,免登录不限速,非常牛批!
  15. (Java)L1-039 古风排版
  16. 4418GPIO口调用过程
  17. 麒麟Linux系统根目录与单目录扩容详解,适用于大多数的centeros系统
  18. C语言sprintf函数解析(实现数据类型转换到字符串)
  19. CCSP国际注册云安全专家在中国设置考场
  20. 【渗透测试笔记】之【内网渗透——Windows系统散列值获取与防范】

热门文章

  1. 【Unity】OnePieceFTG(五)游戏流程
  2. 细说匿名内部类引用方法局部变量时为什么需要声明为final
  3. centos安装phpstudy(小皮)
  4. Python Tic Tac Toe游戏
  5. android qq传文件夹,电脑传到手机QQ的资料在哪个文件夹里?
  6. 解决 go get获取package时候time out超时问题
  7. matlab中marker太密,markersize_想问下MATLAB里 ‘Markersize’ 设置的值是‘Marker_
  8. 家用路由器的相关知识和功能
  9. D. 3-Coloring(思维+构造)
  10. 服务器被攻击网站打不开解决方案