All in FlutterFlutterFlutter

Flutter之可滑动Widget

2019-05-17  本文已影响29人  TitanCoder
可滚动Widget

Scrollbar

const Scrollbar({
    Key key,
    @required this.child,
})

const CupertinoScrollbar({
    Key key,
    @required this.child,
})

主轴和纵轴

SingleChildScrollView

SingleChildScrollView类似于开发中常用的ScrollView, 不再详细介绍了, 下面看一下具体使用介绍吧

const SingleChildScrollView({
    Key key,
    // 设置滚动的方向, 默认垂直方向
    this.scrollDirection = Axis.vertical,
    // 设置显示方式
    this.reverse = false,
    // 内边距
    this.padding,
    // 是否使用默认的controller
    bool primary,
    // 设置可滚动Widget如何响应用户操作
    this.physics,
    this.controller,
    this.child,
})

scrollDirection

设置视图的滚动方向(默认垂直方向), 需要对应的设置其子WidgetColumn或者Row, 否则会报Overflow错误

scrollDirection: Axis.vertical,

// 枚举值
enum Axis {
  /// 水平滚动
  horizontal,
  /// 垂直滚动
  vertical,
}

reverse

physics

controller

代码示例

class ScrollView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    String str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    return Scrollbar(
      child: SingleChildScrollView(
        scrollDirection: Axis.vertical,
        reverse: true,
        padding: EdgeInsets.all(0.0),
        physics: BouncingScrollPhysics(),
        child: Center(
          child: Column( 
            //动态创建一个List<Widget>  
            children: str.split("") 
                //每一个字母都用一个Text显示,字体为原来的两倍
                .map((c) => Text(c, textScaleFactor: 2.0)) 
                .toList(),
          ),
        ),
      ),
    );
  }
}

ListView

ListView({
    // 公共参数上面都介绍过了
    Key key,
    Axis scrollDirection = Axis.vertical,
    bool reverse = false,
    ScrollController controller,
    bool primary,
    ScrollPhysics physics,
    EdgeInsetsGeometry padding,
    
    // 是否根据子widget的总长度来设置ListView的长度,默认值为false
    bool shrinkWrap = false,
    // cell高度
    this.itemExtent,
    // 子widget是否包裹在AutomaticKeepAlive中
    bool addAutomaticKeepAlives = true,
    // 子widget是否包裹在RepaintBoundary中
    bool addRepaintBoundaries = true,
    bool addSemanticIndexes = true,
    // 设置预加载的区域, moren 0.0
    double cacheExtent,
    //子widget列表
    List<Widget> children = const <Widget>[],
    // 子widget的个数
    int semanticChildCount,
})

属性介绍

shrinkWrap

itemExtent

addAutomaticKeepAlives

addRepaintBoundaries

使用示例

class ScrollView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ListView(
      itemExtent: 60,
      cacheExtent: 100,
      addAutomaticKeepAlives: false,
      children: renderCell(),
    );
  }

  List<Widget> renderCell() {
    String str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    return str.split("")
    .map((item) => ListTile(
      title: Text('字母--$item'),
      subtitle: Text('这是字母列表'),
      leading: Icon(Icons.wifi),
    )).toList();
  }
}

ListTile

const ListTile({
    Key key,
    // 前置(左侧)图标, Widget类型
    this.leading,
    // 标题, Widget类型
    this.title,
    // 副标题, Widget类型
    this.subtitle,
    // 后置(右侧)图标, Widget类型
    this.trailing,
    // 是否三行显示, subtitle不为空时才能使用
    this.isThreeLine = false,
    // 设置为true后字体变小
    this.dense,
    // 内容的内边距
    this.contentPadding,
    // 是否可被点击
    this.enabled = true,
    // 点击事件
    this.onTap,
    // 长按操作事件
    this.onLongPress,
    // 是否是选中状态
    this.selected = false,
})

// 使用示例
return ListTile(
  title: Text('index--$index'),
  subtitle: Text('我是一只小鸭子, 咿呀咿呀哟; 我是一只小鸭子, 咿呀咿呀哟; 我是一只小鸭子, 咿呀咿呀哟;'),
  leading: Icon(Icons.wifi),
  trailing: Icon(Icons.keyboard_arrow_right),
  isThreeLine: true,
  dense: false,
  contentPadding: EdgeInsets.all(10),
  enabled: index % 3 != 0,
  onTap: () => print('index = $index'),
  onLongPress: () => print('long-Index = $index'),
  selected: index % 2 == 0,
);

ListView.builder

ListView.builder({
    Key key,
    Axis scrollDirection = Axis.vertical,
    bool reverse = false,
    ScrollController controller,
    bool primary,
    ScrollPhysics physics,
    bool shrinkWrap = false,
    EdgeInsetsGeometry padding,
    this.itemExtent,
    // 
    @required IndexedWidgetBuilder itemBuilder,
    // 列表项的数量,如果为null,则为无限列表
    int itemCount,
    bool addAutomaticKeepAlives = true,
    bool addRepaintBoundaries = true,
    bool addSemanticIndexes = true,
    double cacheExtent,
    int semanticChildCount,
})

itemCount

列表项的数量,如果为null,则为无限列表

itemBuilder

代码示例

class ListBuild extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
      return ListView.builder(
        itemCount: 30,
        itemBuilder: (content, index) {
          return ListTile(
            title: Text('index--$index'),
            subtitle: Text('数字列表'),
            leading: Icon(Icons.wifi),
          );
        },
      );
    }
}

ListView.separated

ListView.separated可以生成列表项之间的分割器,它除了比ListView.builder多了一个separatorBuilder参数外, 其他参数都一样

ListView.separated({
    Key key,
    Axis scrollDirection = Axis.vertical,
    bool reverse = false,
    ScrollController controller,
    bool primary,
    ScrollPhysics physics,
    bool shrinkWrap = false,
    EdgeInsetsGeometry padding,
    @required IndexedWidgetBuilder itemBuilder,
    // 一个分割生成器
    @required IndexedWidgetBuilder separatorBuilder,
    @required int itemCount,
    bool addAutomaticKeepAlives = true,
    bool addRepaintBoundaries = true,
    bool addSemanticIndexes = true,
    double cacheExtent,
})

separatorBuilder

该参数是一个分割生成器, 同样是一个IndexedWidgetBuilder类型的参数

typedef IndexedWidgetBuilder = Widget Function(BuildContext context, int index);

代码示例

奇数行添加一条红色下划线,偶数行添加一条蓝色下划线。

lass SeparatedList extends StatelessWidget {
  //下划线widget预定义以供复用。  
  Widget lineView1 = Divider(color: Colors.red, height: 2, indent: 10,);
  Widget lineView2 = Divider(color: Colors.blue, height: 5, indent: 30);

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return ListView.separated(
      itemCount: 30,
        itemBuilder: (content, index) {
          return ListTile(
            title: Text('index--$index'),
            subtitle: Text('数字列表'),
            leading: Icon(Icons.wifi),
          );
        },
        separatorBuilder: (context, index) {
          return index % 2 == 0 ? lineView1 : lineView2;
        },
    );
  }
}

Divider

设置每一个子WIdget的分割线

const Divider({
    Key key,
    // 分割线所在的SizedBox的高度, 除内边距之外的距离上面的间距
    this.height = 16.0,
    // 分割线左侧间距
    this.indent = 0.0,
    // 分割线颜色
    this.color
})

ListView.custom

const ListView.custom({
    Key key,
    Axis scrollDirection = Axis.vertical,
    bool reverse = false,
    ScrollController controller,
    bool primary,
    ScrollPhysics physics,
    bool shrinkWrap = false,
    EdgeInsetsGeometry padding,
    this.itemExtent,
    // 动态初始化子元素
    @required this.childrenDelegate,
    double cacheExtent,
    int semanticChildCount,
})

childrenDelegate

其实在ListView的前面几种构造函数中, 都默认设置了childrenDelegate这个属性, 更多可参考官方文档

// ListView
ListView({
    // ...
  }) : childrenDelegate = SliverChildListDelegate(
         children,
         addAutomaticKeepAlives: addAutomaticKeepAlives,
         addRepaintBoundaries: addRepaintBoundaries,
         addSemanticIndexes: addSemanticIndexes,
       ), super();

// ListView.builder
ListView.builder({
    // ...
  }) : childrenDelegate = SliverChildBuilderDelegate(
         itemBuilder,
         childCount: itemCount,
         addAutomaticKeepAlives: addAutomaticKeepAlives,
         addRepaintBoundaries: addRepaintBoundaries,
         addSemanticIndexes: addSemanticIndexes,
       ), super();

// ListView.separated
ListView.separated({
    // ...
  }) : childrenDelegate = SliverChildBuilderDelegate(
         // ...
       ), super();
Widget build(BuildContext context, int index) {
    assert(builder != null);
    if (index < 0 || (childCount != null && index >= childCount))
      return null;
    Widget child;
    try {
      child = builder(context, index);
    } catch (exception, stackTrace) {
      child = _createErrorWidget(exception, stackTrace);
    }
    if (child == null)
      return null;
    if (addRepaintBoundaries)
      child = RepaintBoundary.wrap(child, index);
    if (addSemanticIndexes) {
      final int semanticIndex = semanticIndexCallback(child, index);
      if (semanticIndex != null)
        child = IndexedSemantics(index: semanticIndex + semanticIndexOffset, child: child);
    }
    if (addAutomaticKeepAlives)
      child = AutomaticKeepAlive(child: child);
    return child;
}
void didFinishLayout() {
    assert(debugAssertChildListLocked());
    final int firstIndex = _childElements.firstKey() ?? 0;
    final int lastIndex = _childElements.lastKey() ?? 0;
    widget.delegate.didFinishLayout(firstIndex, lastIndex);
}
class MySliverBuilderDelegate extends SliverChildBuilderDelegate {
  MySliverBuilderDelegate(
    Widget Function(BuildContext, int) builder, {
    int childCount,
    bool addAutomaticKeepAlives = true,
    bool addRepaintBoundaries = true,
  }) : super(builder,
            childCount: childCount,
            addAutomaticKeepAlives: addAutomaticKeepAlives,
            addRepaintBoundaries: addRepaintBoundaries);

  @override
  void didFinishLayout(int firstIndex, int lastIndex) {
    print('firstIndex: $firstIndex, lastIndex: $lastIndex');
  }
}

然后我们创建一个ListView.custom的列表视图

class CustomList extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return ListView.custom(
      childrenDelegate: MySliverBuilderDelegate(
        (BuildContext context, int index) {
          return ListTile(
            title: Text('index--$index'),
            subtitle: Text('数字列表'),
            leading: Icon(Icons.wifi),
          );
        }, childCount: 30,
      ),
    );
  }
}

GridView

GridView可以构建二维网格列表, 系统给出了五中构造函数

// 默认构造函数
GridView({
    Key key,
    Axis scrollDirection = Axis.vertical,
    bool reverse = false,
    ScrollController controller,
    bool primary,
    ScrollPhysics physics,
    bool shrinkWrap = false,
    EdgeInsetsGeometry padding,
    @required this.gridDelegate,
    bool addAutomaticKeepAlives = true,
    bool addRepaintBoundaries = true,
    bool addSemanticIndexes = true,
    double cacheExtent,
    List<Widget> children = const <Widget>[],
    int semanticChildCount,
})

SliverGridDelegateWithFixedCrossAxisCount

该子类实现了一个横轴为固定数量子元素的排列算法,其构造函数为:

const SliverGridDelegateWithFixedCrossAxisCount({
    // 横轴子元素的数量,此属性值确定后子元素在横轴的长度就确定了,即ViewPort横轴长度/crossAxisCount。
    @required this.crossAxisCount,
    // 主轴方向的间距
    this.mainAxisSpacing = 0.0,
    // 侧轴方向子元素的间距
    this.crossAxisSpacing = 0.0,
    // 子元素在侧轴长度和主轴长度的比例, 由于crossAxisCount指定后子元素横轴长度就确定了,然后通过此参数值就可以确定子元素在主轴的长度
    this.childAspectRatio = 1.0,
})

从上面的个属性可以发现,子元素的大小是通过crossAxisCountchildAspectRatio两个参数共同决定的。注意,这里的子元素指的是子widget的最大显示空间,注意确保子widget的实际大小不要超出子元素的空间, 代码示例如下

class ScrollView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GridView(
      padding: EdgeInsets.all(10),
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 3,
        childAspectRatio: 1,
        mainAxisSpacing: 10,
        crossAxisSpacing: 10
      ),
      children: <Widget>[
        Container(color: Colors.orange),
        Container(color: Colors.blue),
        Container(color: Colors.orange),
        Container(color: Colors.yellow),
        Container(color: Colors.pink)
      ],
    );
  }
}

GridView.count

GridView.count构造函数内部使用了SliverGridDelegateWithFixedCrossAxisCount,我们通过它可以快速的创建横轴固定数量子元素的GridView

GridView.count({
    Key key,
    Axis scrollDirection = Axis.vertical,
    bool reverse = false,
    ScrollController controller,
    bool primary,
    ScrollPhysics physics,
    bool shrinkWrap = false,
    EdgeInsetsGeometry padding,
    @required int crossAxisCount,
    double mainAxisSpacing = 0.0,
    double crossAxisSpacing = 0.0,
    double childAspectRatio = 1.0,
    bool addAutomaticKeepAlives = true,
    bool addRepaintBoundaries = true,
    bool addSemanticIndexes = true,
    double cacheExtent,
    List<Widget> children = const <Widget>[],
    int semanticChildCount,
})

上面SliverGridDelegateWithFixedCrossAxisCount中给出的示例代码等价于:

class CountGridView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return GridView.count(
      padding: EdgeInsets.all(10),
      crossAxisCount: 3,
      mainAxisSpacing: 10,
      crossAxisSpacing: 10,
      childAspectRatio: 1,
      children: <Widget>[
        Container(color: Colors.orange),
        Container(color: Colors.blue),
        Container(color: Colors.orange),
        Container(color: Colors.yellow),
        Container(color: Colors.pink)
      ],
    );
  }
}

SliverGridDelegateWithMaxCrossAxisExtent

该子类实现了一个侧轴子元素为固定最大长度的排列算法,其构造函数为:

const SliverGridDelegateWithMaxCrossAxisExtent({
    @required this.maxCrossAxisExtent,
    this.mainAxisSpacing = 0.0,
    this.crossAxisSpacing = 0.0,
    this.childAspectRatio = 1.0,
})
class ExtentScrollView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GridView(
      padding: EdgeInsets.all(10),
      gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
        maxCrossAxisExtent: 100,
        childAspectRatio: 1,
        mainAxisSpacing: 10,
        crossAxisSpacing: 10
      ),
      children: <Widget>[
        Container(color: Colors.orange),
        Container(color: Colors.blue),
        Container(color: Colors.orange),
        Container(color: Colors.yellow),
        Container(color: Colors.pink)
      ],
    );
  }
}

GridView.extent

同样GridView.extent构造函数内部使用了SliverGridDelegateWithMaxCrossAxisExtent,我们通过它可以快速的创建侧轴子元素为固定最大长度的的GridView

GridView.extent({
    Key key,
    Axis scrollDirection = Axis.vertical,
    bool reverse = false,
    ScrollController controller,
    bool primary,
    ScrollPhysics physics,
    bool shrinkWrap = false,
    EdgeInsetsGeometry padding,
    @required double maxCrossAxisExtent,
    double mainAxisSpacing = 0.0,
    double crossAxisSpacing = 0.0,
    double childAspectRatio = 1.0,
    bool addAutomaticKeepAlives = true,
    bool addRepaintBoundaries = true,
    bool addSemanticIndexes = true,
    List<Widget> children = const <Widget>[],
    int semanticChildCount,
})

上面SliverGridDelegateWithMaxCrossAxisExtent中给出的示例代码等价于:

class ExtentScrollView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GridView.extent(
      padding: EdgeInsets.all(10),
      maxCrossAxisExtent: 100,
      childAspectRatio: 1,
      mainAxisSpacing: 10,
      crossAxisSpacing: 10,
      children: <Widget>[
        Container(color: Colors.orange),
        Container(color: Colors.blue),
        Container(color: Colors.orange),
        Container(color: Colors.yellow),
        Container(color: Colors.pink)
      ],
    );
  }
}

GridView.builder

GridView.builder({
    Key key,
    Axis scrollDirection = Axis.vertical,
    bool reverse = false,
    ScrollController controller,
    bool primary,
    ScrollPhysics physics,
    bool shrinkWrap = false,
    EdgeInsetsGeometry padding,
    @required this.gridDelegate,
    @required IndexedWidgetBuilder itemBuilder,
    int itemCount,
    bool addAutomaticKeepAlives = true,
    bool addRepaintBoundaries = true,
    bool addSemanticIndexes = true,
    double cacheExtent,
    int semanticChildCount,
})
class BuilderGridView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GridView.builder(
      itemCount: 50,
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 4,
        mainAxisSpacing: 10,
        crossAxisSpacing: 10
      ),
      itemBuilder: (content, index) {
        return Container(
          color: Colors.orange,
          child: Center(
            child: Text('$index'),
          ),
        );
      },
    );
  }
}

GridView.custom

ListView.custom一样, 用于构建自定义子Widget, 有两个必须指定的参数, 这里就不在赘述了

const GridView.custom({
    Key key,
    Axis scrollDirection = Axis.vertical,
    bool reverse = false,
    ScrollController controller,
    bool primary,
    ScrollPhysics physics,
    bool shrinkWrap = false,
    EdgeInsetsGeometry padding,
    @required this.gridDelegate,
    @required this.childrenDelegate,
    double cacheExtent,
    int semanticChildCount,
})

CustomScrollView

const CustomScrollView({
    Key key,
    // 滑动方向
    Axis scrollDirection = Axis.vertical,
    bool reverse = false,
    ScrollController controller,
    bool primary,
    ScrollPhysics physics,
    bool shrinkWrap = false,
    double cacheExtent,
    this.slivers = const <Widget>[],
    int semanticChildCount,
})

上述属性除了slivers之外, 前面都有提到过, 接受一个Widget数组, 但是这里的Widget必须是Sliver类型的, 至于原因, 下面会详解

<div class="note warning"><p>什么是Sliver ??</p></div>

SliverAppBar

const SliverAppBar({
    Key key,
    // 导航栏左侧weidget
    this.leading,
    // 如果leading为null,是否自动实现默认的leading按钮
    this.automaticallyImplyLeading = true,
    // 导航栏标题
    this.title,
    // 导航栏右侧按钮, 接受一个数组
    this.actions,
    // 一个显示在AppBar下方的控件,高度和AppBar高度一样,可以实现一些特殊的效果,该属性通常在SliverAppBar中使用
    this.flexibleSpace,
    // 一个AppBarBottomWidget对象, 设置TabBar
    this.bottom,
    //中控件的z坐标顺序,默认值为4,对于可滚动的SliverAppBar,当 SliverAppBar和内容同级的时候,该值为0,当内容滚动 SliverAppBar 变为 Toolbar 的时候,修改elevation的值
    this.elevation = 4.0,
    // 背景颜色,默认值为 ThemeData.primaryColor。改值通常和下面的三个属性一起使用
    this.backgroundColor,
    // 状态栏的颜色, 黑白两种, 取值: Brightness.dark
    this.brightness,
    // 设置导航栏上图标的颜色、透明度、和尺寸信息
    this.iconTheme,
    // 设置导航栏上文字样式
    this.textTheme,
    // 导航栏的内容是否显示在顶部, 状态栏的下面
    this.primary = true,
    // 标题是否居中显示,默认值根据不同的操作系统,显示方式不一样
    this.centerTitle,
    // 标题间距,如果希望title占用所有可用空间,请将此值设置为0.0
    this.titleSpacing = NavigationToolbar.kMiddleSpacing,
    // 展开的最大高度
    this.expandedHeight,
    // 是否随着华东隐藏标题
    this.floating = false,
    // 是否固定在顶部
    this.pinned = false,
    // 只跟floating相对应,如果为true,floating必须为true,也就是向下滑动一点儿,整个大背景就会动画显示全部,网上滑动整个导航栏的内容就会消失
    this.snap = false,
})

使用示例

class CustomScrollViewTestRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //因为本路由没有使用Scaffold,为了让子级Widget(如Text)使用
    //Material Design 默认的样式风格,我们使用Material作为本路由的根。
    return Material(
      child: CustomScrollView(
        slivers: <Widget>[
          //AppBar,包含一个导航栏
          SliverAppBar(
            pinned: true,
            expandedHeight: 250.0,
            flexibleSpace: FlexibleSpaceBar(
              title: const Text('Demo'),
              background: Image.asset(
                "./images/avatar.png", fit: BoxFit.cover,),
            ),
          ),

          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,
              ),
            ),
          ),
          //List
          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个列表项
            ),
          ),
        ],
      ),
    );
  }
}

ScrollController

ScrollController({
    // 初始滚动位置
    double initialScrollOffset = 0.0,
    // 是否保存滚动位置
    this.keepScrollOffset = true,
    // 调试使用的输出标签
    this.debugLabel,
})

相关属性和方法

offset

可滚动Widget当前滚动的位置

jumpTo()

跳转到指定的位置, 没有动画效果

void jumpTo(double value) {
    assert(_positions.isNotEmpty, 'ScrollController not attached to any scroll views.');
    for (ScrollPosition position in List<ScrollPosition>.from(_positions))
      position.jumpTo(value);
}

animateTo()

跳转到指定的位置, 跳转时会有一个动画效果

Future<void> animateTo(double offset, {
    @required Duration duration,
    @required Curve curve,
  }) {
    assert(_positions.isNotEmpty, 'ScrollController not attached to any scroll views.');
    final List<Future<void>> animations = List<Future<void>>(_positions.length);
    for (int i = 0; i < _positions.length; i += 1)
      animations[i] = _positions[i].animateTo(offset, duration: duration, curve: curve);
    return Future.wait<void>(animations).then<void>((List<void> _) => null);
}

positions

// controller的offset属性
double get offset => position.pixels;

// 读取相关的滚动位置
controller.positions.elementAt(0).pixels
controller.positions.elementAt(1).pixels

滚动监听

ScrollController间接继承自Listenable,我们可以根据ScrollController来监听滚动事件。如:

controller.addListener(()=>print(controller.offset))

ScrollController控制原理

先看一下ScrollController另外几个方法的实现

// 创建一个存储位置信息的ScrollPosition
 ScrollPosition createScrollPosition(
    ScrollPhysics physics,
    ScrollContext context,
    ScrollPosition oldPosition,
  ) {
    return ScrollPositionWithSingleContext(
      physics: physics,
      context: context,
      initialPixels: initialScrollOffset,
      keepScrollOffset: keepScrollOffset,
      oldPosition: oldPosition,
      debugLabel: debugLabel,
    );
 }

 // 注册位置信息
 void attach(ScrollPosition position) {
    assert(!_positions.contains(position));
    _positions.add(position);
    position.addListener(notifyListeners);
  }

  // 注销位置信息
  void detach(ScrollPosition position) {
    assert(_positions.contains(position));
    position.removeListener(notifyListeners);
    _positions.remove(position);
  }

  // 销毁ScrollController
  @override
  void dispose() {
    for (ScrollPosition position in _positions)
      position.removeListener(notifyListeners);
    super.dispose();
  }

代码示例

创建一个ListView,当滚动位置发生变化时,我们先打印出当前滚动位置,然后判断当前位置是否超过1000像素,如果超过则在屏幕右下角显示一个“返回顶部”的按钮,该按钮点击后可以使ListView恢复到初始位置;如果没有超过1000像素,则隐藏“返回顶部”按钮。代码如下

class ScrollControllerTestRoute extends StatefulWidget {
  @override
  ScrollControllerTestRouteState createState() {
    return new ScrollControllerTestRouteState();
  }
}

class ScrollControllerTestRouteState extends State<ScrollControllerTestRoute> {
  ScrollController _controller = new ScrollController();
  bool showToTopBtn = false; //是否显示“返回到顶部”按钮

  @override
  void initState() {
    //监听滚动事件,打印滚动位置
    _controller.addListener(() {
      print(_controller.offset); //打印滚动位置
      if (_controller.offset < 1000 && showToTopBtn) {
        setState(() {
          showToTopBtn = false;
        });
      } else if (_controller.offset >= 1000 && showToTopBtn == false) {
        setState(() {
          showToTopBtn = true;
        });
      }
    });
  }

  @override
  void dispose() {
    //为了避免内存泄露,需要调用_controller.dispose
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("滚动控制")),
      body: Scrollbar(
        child: ListView.builder(
            itemCount: 100,
            itemExtent: 50.0, //列表项高度固定时,显式指定高度是一个好习惯(性能消耗小)
            controller: _controller,
            itemBuilder: (context, index) {
              return ListTile(title: Text("$index"),);
            }
        ),
      ),
      floatingActionButton: !showToTopBtn ? null : FloatingActionButton(
          child: Icon(Icons.arrow_upward),
          onPressed: () {
            //返回到顶部时执行动画
            _controller.animateTo(.0,
                duration: Duration(milliseconds: 200),
                curve: Curves.ease
            );
          }
      ),
    );
  }
}

参考文献


欢迎关注我的微信公众号,订阅我的博客!
上一篇下一篇

猜你喜欢

热点阅读