# 自定义滚动组件

# CustomScrollView

CustomScrollView 是可以使用 Sliver 来自定义滚动模型(效果)的组件。它可以包含多种滚动模型,举个例子,假设有一个页面,顶部需要一个 GridView,底部需要一个 ListView,而要求整个页面的滑动效果是统一的,即它们看起来是一个整体。如果使用 GridView+ListView 来实现的话,就不能保证一致的滑动效果,因为它们的滚动效果是分离的,所以这时就需要一个"胶水",把这些彼此独立的可滚动组件"粘"起来,而 CustomScrollView 的功能就相当于“胶水”。

# Slivers

Flutter中的 Slivers 大家族基本都是配合CustomScrollView来实现的,除了上面提到的滑动布局嵌套,你还可以使用Slivers来实现页面头部展开/收起、 AppBar随手势变换等等功能。

# SliverAppBar

如果你是一名Android开发者,一定使用过CollapsingToolbarLayout这个布局来实现AppBar展开/收起的功能,在Flutter里面则对应SliverAppBar控件。给 SliverAppBar 设置flexibleSpace和expandedHeight属性,就可以轻松完成AppBar展开/收起的功能:

CustomScrollView(
  slivers: <Widget>[
    SliverAppBar(
      actions: <Widget>[
        FlatButton(
          child: Text('click me', style: TextStyle(
                  fontSize: 20,
                  color: Colors.white
                ),),
        ),
      ],
      title: Text('SliverAppBar'),
      backgroundColor: Theme.of(context).accentColor,
      expandedHeight: 200.0,
      flexibleSpace: FlexibleSpaceBar(
        background: Image.asset('assets/imgs/bg.jpg', fit: BoxFit.cover),
      ),
      // floating: true,
      // snap: true,
      // pinned: true,
    ),
    SliverFixedExtentList(
      itemExtent: 120.0,
      delegate: SliverChildListDelegate(
        [1,2,3,4,5,6,7].map((product) {
          return Container(
            alignment: Alignment.center,
            child: new Text('list item $product'),
          );
        }).toList(),
      ),
    ),
  ],
)

如果设置floating属性为true,那么AppBar会在你做出下拉手势时就立即展开(即使ListView并没有到达顶部),该展开状态不显示flexibleSpace:

如果同时设置floating和snap属性为true,那么AppBar会在你做出下拉手势时就立即全部展开(即使ListView并没有到达顶部),该展开状态显示flexibleSpace:

如果不想AppBar消失,则设置pinned属性为true即可:

# SliverList

SliverList 的使用非常简单,只需设置delegate属性即可,我们一般使用SliverChildBuilderDelegate,注意记得设置childCount,否则Flutter没法知道怎么绘制:

CustomScrollView(
  slivers: <Widget>[
    SliverList(
      delegate: SliverChildBuilderDelegate(
            (BuildContext context, int index) {
          return _buildItem(context, products[index]);
        },
        childCount: 3,
      ),
    )
  ],
);

也可以通过下面的方式来设置childCount,如果不设置childCount,Flutter一旦发现delegate的某个index返回了null,就会认为childCount就是这个index。

SliverChildBuilderDelegate(
    (BuildContext context, int index) {
      if(index > products.length){
        return null; // 返回null则将此index设置为childCount
      }
  return _buildItem(context, products[index]);
},

也可以使用SliverChildListDelegate来构建delegate:

SliverChildListDelegate([
  _buildItem(),
  _buildItem(),
  _buildItem(),
]),

# SliverFixedExtentList

SliverFixedExtentList 可以为列表的每一项指定高度(itemExtent):

new SliverFixedExtentList(
  itemExtent: 50.0,
  delegate: new SliverChildBuilderDelegate(
          (BuildContext context, int index) {
        return new Container(
          alignment: Alignment.center,
          color: Colors.lightBlue[100 * (index % 9)],
          child: new Text('list item $index'),
        );
      },
      childCount: 50
  ),
),

提示

SliverFixedExtentList 跟 SliverList 不同的一点是, SliverFixedExtentList 的高度固定为其 itemExtent 属性值, 而 SliverList 的高度是自动的, 取决于其 delegate 中子元素的高度

# SliverPrototypeExtentList

SliverPrototypeExtentList 将其子项排列在沿着主轴的一条线上,从零偏移开始,没有间隙。每个子项的约束程度与沿主轴的prototypeItem和沿横轴的SliverConstraints.crossAxisExtent的程度相同。

# SliverChildListDelegate 和 SliverChildBuilderDelegate 的区别

  • SliverChildListDelegate 一般用来构item建数量明确的列表,会提前build好所有的子item,所以在效率上会有问题,适合item数量不多的情况(不超过一屏)。
  • SliverChildBuilderDelegate 构建的列表理论上是可以无限长的,因为使用来lazily construct优化。 (两者的区别有些类似于ListView和ListView.builder()的区别。)

# SliverGrid

SliverGrid 有三个构造函数:SliverGrid.count()SliverGrid.extentSliverGrid()

SliverGrid.count()指定了一行展示多少个item,下面的例子表示一行展示4个:

SliverGrid.count(children: scrollItems, crossAxisCount: 4)

SliverGrid.extent可以指定item的最大宽度,然后让Flutter自己决定一行展示多少个item:

SliverGrid.extent(children: scrollItems, maxCrossAxisExtent: 90.0)

SliverGrid()则是需要指定一个gridDelegate,它提供给了程序员一个自定义Delegate的入口,你可以自己决定每一个item怎么排列:

new SliverGrid(
  gridDelegate: new SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 2, //Grid按两列显示
    mainAxisSpacing: 10.0,
    crossAxisSpacing: 10.0,
    childAspectRatio: 4.0,
  ),
  delegate: new SliverChildBuilderDelegate(
        (BuildContext context, int index) {
      return new Container(
        alignment: Alignment.center,
        color: Colors.cyan[100 * (index % 9)],
        child: new Text('grid item $index'),
      );
    },
    childCount: 20,
  ),
),

# SliverPersistentHeader

SliverPersistentHeader 顾名思义,就是给一个可滑动的视图添加一个头(实际上,在CustomScrollView的slivers列表中,header可以出现在视图的任意位置,不一定要是在顶部)。这个Header会随着滑动而展开/收起,使用pinned和floating属性来控制收起时Header是否展示(pinned和floating属性不可以同时为true),pinned和floating属性的具体意义和SliverAppBar中相同。

SliverPersistentHeader(
  pinned: true,
  delegate: _SliverAppBarDelegate(
    minHeight: 60.0,
    maxHeight: 180.0,
    child: Container(
      child: Image.asset(
        "assets/imgs/bg.jpg", fit: BoxFit.fitWidth,),
    ),
  ),
),

构建一个 SliverPersistentHeader 需要传入一个delegate,这个delegate是 SliverPersistentHeaderDelegate 类型的,而 SliverPersistentHeaderDelegate 是一个abstract类,我们不能直接new一个 SliverPersistentHeaderDelegate 出来,因此,我们需要自定义一个 delegate 来实现 SliverPersistentHeaderDelegate 类:

class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
  _SliverAppBarDelegate({
     this.minHeight,
     this.maxHeight,
     this.child,
  });
  final double minHeight;
  final double maxHeight;
  final Widget child;
  
  double get minExtent => minHeight;
  
  double get maxExtent => maxHeight;
  
  Widget build(
      BuildContext context, double shrinkOffset, bool overlapsContent) {
    return new SizedBox.expand(child: child);
  }
  
  bool shouldRebuild(_SliverAppBarDelegate oldDelegate) {
    return maxHeight != oldDelegate.maxHeight ||
        minHeight != oldDelegate.minHeight ||
        child != oldDelegate.child;
  }
}

写一个自定义SliverPersistentHeaderDelegate很简单,只需重写build()、get maxExtent、get minExtent和shouldRebuild()这四个方法,上面就是一个最简单的SliverPersistentHeaderDelegate的实现。其中,maxExtent表示header完全展开时的高度,minExtent表示header在收起时的最小高度。因此,对于我们上面的那个自定义Delegate,如果将minHeight和maxHeight的值设置为相同时,header就不会收缩了,这样的Header跟我们平常理解的Header更像。

# SliverPadding

SliverPadding 可以为 sliver 添加间距

SliverPadding(
  padding: const EdgeInsets.all(8.0),
  sliver: new SliverGrid(
    // ...
  ),
),

# SliverToBoxAdapter

那么如果想要在滚动视图中添加一个普通的控件,那么就可以使用 SliverToBoxAdapter 来将各种视图组合在一起,放在 CustomListView 中。

结合SliverToBoxAdapter,滚动视图可以任意组合:

CustomScrollView(
  physics: ScrollPhysics(),
  slivers: <Widget>[
    SliverToBoxAdapter(
      child: Placeholder(fallbackHeight: 100,),
    ),
    SliverGrid.count(
      crossAxisCount: 3,
      children: products.map((product) {
        return _buildItemGrid(product);
      }).toList(),
    ),
    SliverToBoxAdapter(
      child: Placeholder(fallbackHeight: 100,),
    ),
    SliverFixedExtentList(
      itemExtent: 100.0,
      delegate: SliverChildListDelegate(
        products.map((product) {
          return _buildItemList(product);
        }).toList(),
      ),
    ),
  ],
);

# SliverFillViewport

SliverFillViewport 占满一屏或者比一屏更多的布局,

new SliverFillViewport(
    delegate: new SliverChildBuilderDelegate(
            (BuildContext context, int index) {
          //创建列表项
          return new Container(
            alignment: Alignment.center,
            color: Colors.lightBlue,
            child: new Text('SliverFillViewport'),
          );
        },
        childCount: 1
    ),
    viewportFraction: 1.0, // 占屏幕的比例
),

# SliverFillRemaining

SliverFillRemaining 用于填充完剩余视图里面的全部空间, 详见 示例2

# SliverSafeArea

SliverSafeArea 通过足够的填充来插入另一条条子以防止操作系统入侵的条子。例如,这将使条子缩进足以避开屏幕顶部的状态栏。为了防止各种边界的越界,比如说越过顶部的状态栏

# 示例1: 基础用法

import 'package:flutter/material.dart';
void main() {
  runApp(new StartApp());
}
class StartApp extends StatefulWidget {
  
  State<StatefulWidget> createState() {
    return new _StartAppState();
  }
}
class _StartAppState extends State<StartApp> {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '首页',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: CustomScrollViewTestRoute(),
    );
  }
}
class CustomScrollViewTestRoute extends StatelessWidget {
  
  Widget build(BuildContext context) {
    // 因为本路由没有使用Scaffold,为了让子级Widget(如Text)使用
    // Material Design 默认的样式风格,我们使用Material作为本路由的根。
    return Material(
        child: CustomScrollView(
          slivers: <Widget>[
            SliverAppBar(
              actions: <Widget>[
                FlatButton(
                  child: Text('click me', style: TextStyle(
                      fontSize: 20,
                      color: Colors.white
                  ),),
                ),
              ],
              backgroundColor: Theme.of(context).accentColor,
              expandedHeight: 200.0,
              flexibleSpace: FlexibleSpaceBar(
                background: Image.asset('assets/imgs/bg.jpg', fit: BoxFit.cover),
                title: Text('Demo'),
              ),
              pinned: true,
            ),
            SliverFixedExtentList(
              itemExtent: 120.0,
              delegate: SliverChildListDelegate(
                [1,2,3,4,5,6,7].map((product) {
                  return Container(
                    alignment: Alignment.center,
                    child: new Text('list item $product'),
                  );
                }).toList(),
              ),
            ),
            SliverPersistentHeader(
              pinned: true,
              delegate: _SliverAppBarDelegate(
                minHeight: 60.0,
                maxHeight: 180.0,
                child: Container(
                  child: Image.asset(
                    "assets/imgs/bg.jpg", fit: BoxFit.fitWidth,),
                ),
              ),
            ),
            SliverPadding(
              padding: const EdgeInsets.all(8.0),
              sliver: new SliverGrid( //Grid
                gridDelegate: new SliverGridDelegateWithFixedCrossAxisCount(
                  crossAxisCount: 2, //Grid按两列显示
                  mainAxisSpacing: 10.0,
                  crossAxisSpacing: 10.0,
                  childAspectRatio: 4.0,
                ),
                delegate: new SliverChildBuilderDelegate(
                      (BuildContext context, int index) {
                    //创建子widget
                    return new Container(
                      alignment: Alignment.center,
                      color: Colors.cyan[100 * (index % 9)],
                      child: new Text('grid item $index'),
                    );
                  },
                  childCount: 20,
                ),
              ),
            ),
            SliverToBoxAdapter(
              child: Placeholder(fallbackHeight: 100,),
            ),
            new SliverFixedExtentList(
              itemExtent: 50.0,
              delegate: new SliverChildBuilderDelegate(
                      (BuildContext context, int index) {
                    //创建列表项
                    return new Container(
                      alignment: Alignment.center,
                      color: Colors.lightBlue[100 * (index % 9)],
                      child: new Text('list item $index'),
                    );
                  },
                  childCount: 50 //50个列表项
              ),
            ),
          ],
        )
    );
  }
}
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
  _SliverAppBarDelegate({
     this.minHeight,
     this.maxHeight,
     this.child,
  });
  final double minHeight;
  final double maxHeight;
  final Widget child;
  
  double get minExtent => minHeight;
  
  double get maxExtent => maxHeight;
  
  Widget build(
      BuildContext context, double shrinkOffset, bool overlapsContent) {
    return new SizedBox.expand(child: child);
  }
  
  bool shouldRebuild(_SliverAppBarDelegate oldDelegate) {
    return maxHeight != oldDelegate.maxHeight ||
        minHeight != oldDelegate.minHeight ||
        child != oldDelegate.child;
  }
}

# 示例2: Sliver-sticky效果


class _StickyDemoState extends State<StickyDemo>
    with SingleTickerProviderStateMixin {
  TabController tabController;
  
  void initState() {
    super.initState();
    this.tabController = TabController(length: 2, vsync: this);
  }
  
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        slivers: <Widget>[
          SliverAppBar(
            pinned: true,
            elevation: 0,
            expandedHeight: 250,
            flexibleSpace: FlexibleSpaceBar(
              title: Text('Sliver-sticky效果'),
              background: Image.network(
                'http://img1.mukewang.com/5c18cf540001ac8206000338.jpg',
                fit: BoxFit.cover,
              ),
            ),
          ),
          SliverPersistentHeader(
            pinned: true,
            delegate: StickyTabBarDelegate(
              child: TabBar(
                labelColor: Colors.black,
                controller: this.tabController,
                tabs: <Widget>[
                  Tab(text: 'Home'),
                  Tab(text: 'Profile'),
                ],
              ),
            ),
          ),
          SliverFillRemaining(
            child: TabBarView(
              controller: this.tabController,
              children: <Widget>[
                Center(child: Text('Content of Home')),
                Center(child: Text('Content of Profile')),
              ],
            ),
          ),
        ],
      ),
    );
  }
}
class StickyTabBarDelegate extends SliverPersistentHeaderDelegate {
  final TabBar child;
  StickyTabBarDelegate({ this.child});
  
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
    return this.child;
  }
  
  double get maxExtent => this.child.preferredSize.height;
  
  double get minExtent => this.child.preferredSize.height;
  
  bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) {
    return true;
  }
}

# 参考

MIT Licensed | Copyright © 2018-present 滇ICP备16006294号

Design by Quanzaiyu | Power by VuePress