Flutter | Sliver 系列

简介: Flutter | Sliver 系列

概述


CustomScrollView:一个滚动的容器,改组件不接受任何 child,但是你可以直接提供 Slivers 已创建各种滚动效果,例如页面中有多个可滑动的列表,如 Appbar, 列表,网格,等这种就可以直接使用 SliverAppBar,SliverList 和 SliverGrid


Slivers 不是单独指一个组件,而是指的一个系列,所以以 Sliver 开头的组件都是这个系列的,但是他们都只能作用于 CustomScrollView 中。


常用到的 Sliver 有,SliverAppbar,SliverList,SliverGrid,SliverToBoxAdapter 等


由于 CustomScrollView 的子组件只能是 Sliver 系列,如果要将一个普通的组件放在里面,必须使用 SliverToBoxAdapter 进行适配才行


简单的使用


class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(widget.title)),
      drawer: Drawer(),
      body: CustomScrollView(
        slivers: [
          SliverAppBar(
            title: Text("SliverAppbar"),
          ),
          SliverGrid(
            gridDelegate:
                SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4),
            delegate: SliverChildBuilderDelegate((context, index) {
              return Container(
                  color: Colors.primaries[index % Colors.primaries.length]);
            }, childCount: 40),
          ),
          SliverList(
              delegate: SliverChildBuilderDelegate((context, index) {
            return Container(
                height: 100,
                color: Colors.primaries[index % Colors.primaries.length]);
          }, childCount: 20))
        ],
      ),
    );
  }
}


其实我们仔细一点就会发现,其实 ListView 和 GridView 等组件内部使用的都是 Slivers,


ListView.builder({
 //......
}) : assert(itemCount == null || itemCount >= 0),
     assert(semanticChildCount == null || semanticChildCount <= itemCount!),
     childrenDelegate = SliverChildBuilderDelegate(
       itemBuilder,
       childCount: itemCount,
       addAutomaticKeepAlives: addAutomaticKeepAlives,
       addRepaintBoundaries: addRepaintBoundaries,
       addSemanticIndexes: addSemanticIndexes,
     ),
     super(
    //....
     );


那为什么要使用 Slivers 呢?最主要的原因就是可以在 slives 中添加多个组件,如在列表的上面和下面添加更多的内容。


并且 slivers 中,如果存在多个列表的话也是支持动态加载的,而不是会一次性全部渲染完


各式各样的 Slivers 组件


SliverList


在上面的例子中 SliverList 使用的是 SliverChildBuilderDelegate 这个delegate,它可以实现动态加载,当然 SliverList 中也有和 ListView 中一样的非动态加载的delegate,就是SliverChildListDelegate


SliverList(
    delegate: SliverChildListDelegate(
  [
    FlutterLogo(size: 100),
    FlutterLogo(size: 100),
    FlutterLogo(size: 100),
  ],
))


一般在列表数量较小并且显示内容确定的情况下可以使用次 delegate 。


SliverFixedExtentList


面的子元素中的宽高是动态的,需要手动设置高度,并且这种也不利于性能,所以我们可以使用 SliverFixedExtentList 来控制限制子元素的大小:


SliverFixedExtentList(
    itemExtent: 100,
    delegate: SliverChildListDelegate(
      [
        FlutterLogo(),
        FlutterLogo(),
        FlutterLogo(),
      ],
    ))


未限制前:,限制后:


SliverPrototypeExtentList


一般情况下,只要固定了列表中元素的高度,就可以提升不小的性能,但是在实际的项目中,想要固定元素的高度是非常麻烦的,就算是列表中的元素只有一行文字,也有可能会出现问题,例如直接在系统层面修改字体的大小,这也会导致高度的固定导致渲染出来的效果不尽人意。但是有了 SliverPrototypeExtentList 就简单多了。


在 SliverPrototypeExtentList 中,可以通过 prototypeItem 来传入一个原型,这个原型并不会渲染到屏幕上,在运行的过程中,Flutter 会将原型的尺寸计算出来,之后就会把所有的元素尺寸设置成这个原型的尺寸。


body: DefaultTextStyle(
  style: TextStyle(fontSize: 60, color: Colors.red),
  child: CustomScrollView(
    slivers: [
      SliverPrototypeExtentList(
          prototypeItem: Text(""),
          delegate: SliverChildListDelegate(
            [
              Text("Hello Word"),
              Text("Hello Word"),
              Text("Hello Word"),
            ],
          )),
    ],
  ),
),


如上,子元素的大小都会和 prototypeItem 中元素的大小进行同步,我们和 SliverFixedExtentList 对比看一下效果


body: DefaultTextStyle(
  style: TextStyle(fontSize: 60, color: Colors.red),
  child: CustomScrollView(
    slivers: [
      SliverFixedExtentList(
          itemExtent: 40,
          delegate: SliverChildListDelegate(
            [
              Text("Hello Word"),
              Text("Hello Word"),
              Text("Hello Word"),
            ],
          )),
    ],
  ),
),


使用 prototype:,使用 fixed:


从图中可以看到,尽管高度固定到 40,但是由于 Text 的大小被修改了,所以渲染出来的还是有问题。


SliverFillViewport


它也接受一个 delegate,支持动态的加载,只不过内部的子元素会占满整个屏幕


SliverFillViewport(
    delegate: SliverChildListDelegate([
  Container(color: Colors.red),
  Container(color: Colors.yellow),
  Container(color: Colors.blue),
]))


SliverAppbar


在 slivers 系列中,SliverAppbar 可以说是使用频率比较高的组件了,SliverAppbar 为应用栏提供了自定义滚动行为,下面我们来看一下


class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      drawer: Drawer(),
      body: DefaultTextStyle(
        style: TextStyle(fontSize: 60, color: Colors.red),
        child: CustomScrollView(
          slivers: [
            SliverAppBar(
              title: Text("Sliver AppBar"),
            ),
            SliverToBoxAdapter(child: Placeholder()),
            SliverList(
              delegate: SliverChildListDelegate(
                [
                  FlutterLogo(size: 200),
                  FlutterLogo(size: 200),
                  FlutterLogo(size: 200),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}


上面是一个磨人的 SliverAppbar,并没有实现任何特殊效果,


可以看到在滑动的过程中,SliverAppbar 被顶上去了,这也是非常正常的。接着我们来看一下都有哪些特殊效果吧


特殊效果


floating
SliverAppBar(
  title: Text("Sliver AppBar"),
  floating: true,
)


在向下滑动的时候,会首先将 SliveAppbar 显示出来,


pinned :一直显示在顶部,无视滑动,这样就和普通的导航栏差不多了。区别就是在滑动的时候 SliveAppbar 的底部会有一点点影子


snap:在滑动停止之后,导航会自动全部显示出来,需要注意的是必须搭配 floating 一起使用,如下:


SliverAppBar(
  title: Text("Sliver AppBar"),
  snap: true,
  floating: true,
)


flexibleSpace:可展开拉伸的部分


SliverAppBar(
  // title: Text("Sliver AppBar"),
  expandedHeight: 300,
  stretch: true,
  flexibleSpace: FlexibleSpaceBar(
    background: FlutterLogo(),
    title: Text("FlexibleSpaceBar title"),
    collapseMode: CollapseMode.parallax,
    stretchModes: [
      StretchMode.blurBackground,
      StretchMode.zoomBackground,
      StretchMode.fadeTitle,
    ],
  ),
),



SliverOpacity


透明组件,内部接受的是一个 sliver,所以需要用 SliverToAdapter 转一下


SliverOpacity(
  opacity: 0.5,
  sliver: SliverToBoxAdapter(
    child: FlutterLogo(
      size: 100,
    ),
  ),
)


SliverFillRemaining


该组件会填满当前页面的剩余空间


SliverFillRemaining(
  hasScrollBody: false,
  child: Center(
    child: CircularProgressIndicator(),
  ),
)

hasScrollBody :当前组件中是否有可滚动的组件


案例


首先看一下实现的效果(由于是 gif 图,所以看起来有一点卡):



准备数据


接口来源于网络,仅供学习使用


https://h5.48.cn/resource/jsonp/allmembers.php?gid=10


对应的数据类:


class Member {
  final String id;
  final String name;
  final String team;
  final String sid;
  final String gid;
  final String gname;
  final String sname;
  final String fname;
  final String tname;
  final String pid;
  final String pname;
  final String nickname;
  final String company;
  final String join_day;
  final String height;
  final String birth_day;
  final String star_sign_12;
  final String star_sign_48;
  final String speciality;
  final String hobby;
  final String experience;
  final String catch_phrase;
  final String status;
  final String ranking;
  final String tcolor;
  final String gcolor;
  String get avatarUrl => "https://www.snh48.com/images/member/zp_$id.jpg";
  Member(
    this.id,
    this.name,
   //.....自行添加
  );
  @override
  String toString() {
    return "$id  ---  $name";
  }
}


首页


class _DemoWidgetState extends State<DemoWidget> {
  List<Member> _member = [];
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("案例"),
      ),
      body: RefreshIndicator(
        onRefresh: () async {
          setState(() => _member.clear());
          final url = "https://h5.48.cn/resource/jsonp/allmembers.php?gid=10";
          final res = await http.get(Uri.parse(url));
          if (res.statusCode != 200) throw Error();
          final json = convert.jsonDecode(res.body);
          final members = (json["rows"] as List)
              .map((e) => Member(
                    e['sid'], e["sname"],e["tname"], e["sid"], e["gid"],e["gname"],e["sname"],e["fname"],e["tname"],
                    e["pid"],e["pname"], e["nickname"], e["company"], e["join_day"], e["height"],    e["birth_day"],
                    e["star_sign_12"], e["star_sign_48"], e["speciality"], e["hobby"], e["experience"],
                    e["catch_phrase"],  e["status"], e["ranking"], e["tcolor"],e["gcolor"],
                  ))
              .toList();
          setState(() => _member = members);
        },
        child: CustomScrollView(
          slivers: [
            SliverToBoxAdapter(),
            SliverPersistentHeader(
                delegate: _MyDelegate("SII", Color(0xffae86bb)), pinned: true),
            _buildTeamList("SII"),
            SliverPersistentHeader(
                delegate: _MyDelegate("NII", Color(0xff91cdeb)), pinned: true),
            _buildTeamList("NII"),
            SliverPersistentHeader(
                delegate: _MyDelegate("HII", Color(0xffa7b0ba)), pinned: true),
            _buildTeamList("HII"),
            SliverPersistentHeader(
                delegate: _MyDelegate("预备生", Color(0xff91cdeb)), pinned: true),
            _buildTeamList("预备生"),
            SliverPersistentHeader(
                delegate: _MyDelegate("荣誉毕业生", Color(0xff8ed2f5)),
                pinned: true),
            _buildTeamList("荣誉毕业生"),
            SliverPersistentHeader(
                delegate: _MyDelegate("S预备生", Color(0xff38b26d)), pinned: true),
            _buildTeamList("S预备生"),
            SliverPersistentHeader(
                delegate: _MyDelegate("X", Color(0xffa7b0ba)), pinned: true),
            _buildTeamList("X"),
          ],
        ),
      ),
    );
  }
  SliverGrid _buildTeamList(String teamName) {
    //进行筛选
    final teamMember =
        _member.where((element) => element.team == teamName).toList();
    return SliverGrid(
      delegate: SliverChildBuilderDelegate((context, index) {
        Member m = teamMember[index];
        return InkWell(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              //动画
              Hero(
                  tag: m.avatarUrl,
                  child: ClipOval(
                    child: CircleAvatar(
                      child: Image.network(m.avatarUrl),
                      backgroundColor: Colors.white,
                    ),
                  )),
              Text("${m.name}"),
            ],
          ),
          onTap: () => Navigator.of(context)
              .push(MaterialPageRoute(builder: (_) => DetailPage(m))),
        );
      }, childCount: teamMember.length),
      gridDelegate:
          SliverGridDelegateWithMaxCrossAxisExtent(maxCrossAxisExtent: 120),
    );
  }
}
class _MyDelegate extends SliverPersistentHeaderDelegate {
  final String title;
  final Color color;
  _MyDelegate(this.title, this.color);
  @override
  Widget build(
      BuildContext context, double shrinkOffset, bool overlapsContent) {
    return Container(
      height: 35,
      child: FittedBox(child: Text(title, style: TextStyle())),
      color: color,
    );
  }
  ///最高高度
  @override
  double get maxExtent => 35;
  ///最新高度
  @override
  double get minExtent => 35;
  ///重绘
  @override
  bool shouldRebuild(covariant _MyDelegate oldDelegate) {
    //如果 title 不相等,则重绘
    return oldDelegate.title != title;
  }
}


上面代码在 refresh 中进行了网络请求,然后进行解析数据,最后进行了刷新操作


上面代码都很简单,不太熟悉的可能就是 SliverPersistentHeader 了,这是一个可以置顶的 header,它可以出现在视图的任何一个位置, pinned 和 floating 属性用来控制收起是是否展示,具体意思和 SliverAppbar 中一样。


详情页面


class DetailPage extends StatelessWidget {
  final Member member;
  DetailPage(this.member);
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: CustomScrollView(
      slivers: [
        SliverAppBar(
            expandedHeight: 300,
            pinned: true,
            stretch: true,
            flexibleSpace: FlexibleSpaceBar(
              centerTitle: true,
              title: Text("${member.name}"),
              background: Center(
                child: Padding(
                  padding: const EdgeInsets.all(100),
                  //长宽比
                  child: AspectRatio(
                    aspectRatio: 1,
                     // 和上面那个页面的动画对应,tag 必须一致
                    child: Hero(
                      tag: member.avatarUrl,
                      child: Material(
                        elevation: 4.0,
                        shape: CircleBorder(),
                        child: ClipOval(
                          child: Image.network(
                            member.avatarUrl,
                            fit: BoxFit.cover,
                          ),
                        ),
                      ),
                    ),
                  ),
                ),
              ),
            )),
        SliverList(
            delegate: SliverChildListDelegate(
          [
            _buildInfo("战队:", member.team),
            _buildInfo("公司:", member.company),
            _buildInfo("时间:", member.join_day),
            _buildInfo("身高:", member.height),
            _buildInfo("生日:", member.birth_day),
            _buildInfo("星座:", member.star_sign_12),
            _buildInfo("运势:", member.star_sign_48),
            _buildInfo("爱好:", member.speciality),
            _buildInfo("签名:", member.catch_phrase),
          ],
        ))
      ],
    ));
  }
  _buildInfo(String label, String content) {
    return Card(
      child: Padding(
        padding: EdgeInsets.symmetric(vertical: 25),
        child: Row(
          children: [Text(label), Text(content)],
        ),
      ),
    );
  }
}


上面代码中有一个问题,本来使用了 stretch 属性之后,在下拉的时候应该会有一个放大的效果,但是运行代码的时候并没有,有知道原因的同学可以讲一下


相关文章
|
1月前
|
UED 开发者 容器
Flutter&鸿蒙next 的 Sliver 实现自定义滚动效果
Flutter 提供了强大的滚动组件,如 ListView 和 GridView,但当需要更复杂的滚动效果时,Sliver 组件是一个强大的工具。本文介绍了如何使用 Sliver 实现自定义滚动效果,包括 SliverAppBar、SliverList 等常用组件的使用方法,以及通过 CustomScrollView 组合多个 Sliver 组件实现复杂布局的示例。通过具体代码示例,展示了如何实现带有可伸缩 AppBar 和可滚动列表的页面。
118 1
|
2月前
|
Android开发 iOS开发 容器
鸿蒙harmonyos next flutter混合开发之开发FFI plugin
鸿蒙harmonyos next flutter混合开发之开发FFI plugin
|
1月前
|
开发框架 Dart 前端开发
Flutter 是谷歌推出的一款高效跨平台移动应用开发框架,使用 Dart 语言,具备快速开发、跨平台支持、高性能、热重载及美观界面等特点。
Flutter 是谷歌推出的一款高效跨平台移动应用开发框架,使用 Dart 语言,具备快速开发、跨平台支持、高性能、热重载及美观界面等特点。本文从 Flutter 简介、特点、开发环境搭建、应用架构、组件详解、路由管理、状态管理、与原生代码交互、性能优化、应用发布与部署及未来趋势等方面,全面解析 Flutter 技术,助你掌握这一前沿开发工具。
57 8
|
1月前
|
存储 JavaScript 前端开发
在Flutter开发中,状态管理至关重要。随着应用复杂度的提升,有效管理状态成为挑战
在Flutter开发中,状态管理至关重要。随着应用复杂度的提升,有效管理状态成为挑战。本文介绍了几种常用的状态管理框架,如Provider和Redux,分析了它们的基本原理、优缺点及适用场景,并提供了选择框架的建议和使用实例,旨在帮助开发者提高开发效率和应用性能。
36 4
|
1月前
|
传感器 前端开发 Android开发
在 Flutter 开发中,插件开发与集成至关重要,它能扩展应用功能,满足复杂业务需求
在 Flutter 开发中,插件开发与集成至关重要,它能扩展应用功能,满足复杂业务需求。本文深入探讨了插件开发的基本概念、流程、集成方法、常见类型及开发实例,如相机插件的开发步骤,同时强调了版本兼容性、性能优化等注意事项,并展望了插件开发的未来趋势。
42 2
|
2月前
|
开发者
鸿蒙Flutter实战:07-混合开发
鸿蒙Flutter混合开发支持两种模式:1) 基于har包,便于主项目开发者无需关心Flutter细节,但不支持热重载;2) 基于源码依赖,利于代码维护与热重载,需配置Flutter环境。项目结构包括AppScope、flutter_module等目录,适用于不同开发需求。
112 3
|
1月前
|
传感器 开发框架 物联网
鸿蒙next选择 Flutter 开发跨平台应用的原因
鸿蒙(HarmonyOS)是华为推出的一款旨在实现多设备无缝连接的操作系统。为了实现这一目标,鸿蒙选择了 Flutter 作为主要的跨平台应用开发框架。Flutter 的跨平台能力、高性能、丰富的生态支持和与鸿蒙系统的良好兼容性,使其成为理想的选择。通过 Flutter,开发者可以高效地构建和部署多平台应用,推动鸿蒙生态的快速发展。
243 0
|
1月前
|
Dart 安全 UED
Flutter&鸿蒙next中的表单封装:提升开发效率与用户体验
在移动应用开发中,表单是用户与应用交互的重要界面。本文介绍了如何在Flutter中封装表单,以提升开发效率和用户体验。通过代码复用、集中管理和一致性的优势,封装表单组件可以简化开发流程。文章详细讲解了Flutter表单的基础、封装方法和表单验证技巧,帮助开发者构建健壮且用户友好的应用。
82 0
|
2月前
|
开发框架 移动开发 Android开发
安卓与iOS开发中的跨平台解决方案:Flutter入门
【9月更文挑战第30天】在移动应用开发的广阔舞台上,安卓和iOS两大操作系统各自占据半壁江山。开发者们常常面临着选择:是专注于单一平台深耕细作,还是寻找一种能够横跨两大系统的开发方案?Flutter,作为一种新兴的跨平台UI工具包,正以其现代、响应式的特点赢得开发者的青睐。本文将带你一探究竟,从Flutter的基础概念到实战应用,深入浅出地介绍这一技术的魅力所在。
96 7
|
2月前
|
编解码 Dart API
鸿蒙Flutter实战:06-使用ArkTs开发Flutter鸿蒙插件
本文介绍了如何开发一个 Flutter 鸿蒙插件,实现 Flutter 与鸿蒙的混合开发及双端消息通信。通过定义 `MethodChannel` 实现 Flutter 侧的 token 存取方法,并在鸿蒙侧编写 `EntryAbility` 和 `ForestPlugin`,使用鸿蒙的首选项 API 完成数据的读写操作。文章还提供了注意事项和参考资料,帮助开发者更好地理解和实现这一过程。
120 0