Flutter - 循序渐进 Sliver

2023-12-18 14:48
文章标签 flutter 循序渐进 sliver

本文主要是介绍Flutter - 循序渐进 Sliver,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

列表组件在移动端上尤为重要,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的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/508799

相关文章

Flutter 进阶:绘制加载动画

绘制加载动画:由小圆组成的大圆 1. 定义 LoadingScreen 类2. 实现 _LoadingScreenState 类3. 定义 LoadingPainter 类4. 总结 实现加载动画 我们需要定义两个类:LoadingScreen 和 LoadingPainter。LoadingScreen 负责控制动画的状态,而 LoadingPainter 则负责绘制动画。

Flutter Button使用

Material 组件库中有多种按钮组件如ElevatedButton、TextButton、OutlineButton等,它们的父类是于ButtonStyleButton。         基本的按钮特点:         1.按下时都会有“水波文动画”。         2.onPressed属性设置点击回调,如果不提供该回调则按钮会处于禁用状态,禁用状态不响应用户点击。

flutter开发实战-flutter build web微信无法识别二维码及小程序码问题

flutter开发实战-flutter build web微信无法识别二维码及小程序码问题 GitHub Pages是一个直接从GitHub存储库托管的静态站点服务,‌它允许用户通过简单的配置,‌将个人的代码项目转化为一个可以在线访问的网站。‌这里使用flutter build web来构建web发布到GitHub Pages。 最近通过flutter build web,通过发布到GitHu

Flutter 中的低功耗蓝牙概述

随着智能设备数量的增加,控制这些设备的需求也在增加。对于多种使用情况,期望设备在需要进行控制的同时连接到互联网会受到很大限制,因此是不可行的。在这些情况下,使用低功耗蓝牙(也称为 Bluetooth LE 或 BLE)似乎是最佳选择,因为它功耗低,在我们的手机中无处不在,而且无需连接到更广泛的网络。因此,蓝牙应用程序的需求也在不断增长。 通过阅读本文,您将了解如何开始在 Flutter 中开

flutter开发多端平台应用的探索 下 (跨模块、跨语言通信之平台通道)

前文 Flutter 是一个跨平台的开发框架,它允许开发者使用相同的代码库来构建 iOS、Android、Web 和桌面应用程序。 上文flutter开发多端平台应用的探索 上(基本操作)-CSDN博客列举了一些特定平台的case(桌面端菜单,鼠标快捷键)的使用方法,有些是flutter提供了对应能力,只需要学习如何调API,有些事三方库支持,本文要探讨的平台通道是更为强大的工具,很多三方插件

Flutter-使用dio插件请求网络(get ,post,下载文件)

引入库:dio: ^2.1.13可直接运行的代码:包含了post,get 下载文件import 'package:flutter/material.dart';import 'package:dio/dio.dart';void main() {runApp(new MaterialApp(title: 'Container demo',home: new visitNetPage(),)

Flutter-选择附件,图片,视频。file_picker

仅供参考: 引入插件: file_picker: ^1.3.8 按照返回值,分了三组: // Single file path String filePath;第一组:返回文件地址 //选择任何文件 filePath = await FilePicker.getFilePath(type: FileType.ANY); // will let you pick one file path, fr

Flutter-图表显示charts_flutter

引入插件: charts_flutter: ^0.4.0 ChartFlutterBean import 'package:charts_flutter/flutter.dart';import 'package:myself_project/OrdinalSales%20.dart';class ChartFlutterBean {static List<Series<TimeSer

Flutter-常用插件汇总

audio_recorder: any #录音、播放flutter_sound: ^1.1.5#录音dropdown_menu: ^1.1.0#下拉菜单simple_permissions:#权限获取easy_alert:#弹框amap_location: any #高德地图location: any #gogle位置获取barcode_scan 0.0.8#二维码识别qr_mobile_vi