美文网首页跨平台
Flutter了解之入门篇10-1(可滚动组件)

Flutter了解之入门篇10-1(可滚动组件)

作者: 平安喜乐698 | 来源:发表于2022-09-20 19:26 被阅读0次
    目录
      1. ListView
      2. SingleChildScrollView
      3. GridView(二维网格列表)
    

    Flutter官方并没有对Widget进行分类,对其分类主要是为了对Widget进行功能区分。

    当组件超过显示窗口时,Flutter会提示Overflow错误。为此,Flutter提供了多种可滚动组件用于显示列表和长布局。

    /*
    Flutter有两种布局模型:
        1. 基于RenderBox的盒模型布局。
        2. 基于RenderSliver (Sliver) 按需加载列表布局。
    */
    主纵轴
      滚动方向称为主轴,非滚动方向称为纵轴。
    
    可滚动组件的组成部分
      1. Scrollable (继承自StatefulWidget) 
        处理滑动手势,确定滑动偏移,滑动偏移变化时构建Viewport 。
      2. Viewport
        渲染当前视口中需要显示的Sliver。
        父组件为Scrollable。
      3. Sliver
        对子组件进行构建和布局。
        父组件为Viewport。
    
    具体过程
      1. Scrollable监听到用户滑动行为后,根据最新的滑动偏移构建Viewport 。
      2. Viewport将当前视口信息和配置信息通过SliverConstraints传递给Sliver。
      3. Sliver中对子组件(RenderBox)按需进行构建和布局,然后确认自身的位置、绘制等信息,保存在geometry(SliverGeometry类型的对象)中。
    
    
    基于Sliver的延迟构建模型
      通常可滚动组件的子组件非常多、占用高度非常大,如果一次性将子组件全部构建将会非常昂贵。为此,Flutter提出Sliver(薄片)概念,如果一个可滚动组件支持Sliver模型,则可以分成许多Sliver,且只有当Sliver出现在视口中时才去构建它。
      支持:ListView、GridView。不支持:SingleChildScrollView。
    
    
    公共属性(最终会透传给Scrollable和Viewport)
      1. scrollDirection
        滚动方向。
        Axis.vertica垂直方向(默认),Axis.horizontal水平方向。
      2. reverse
        是否按照阅读方向相反的方向滑动。
        决定可滚动组件的初始滚动位置是在“头”还是“尾”,取false时初始滚动位置在“头”,反之则在“尾”。
      3. primary
        是否使用widget树中默认的PrimaryScrollController;
        当滑动方向为垂直方向且没有指定controller时,primary默认为true。
      4. padding
        内边距
      5. controller(ScrollController类型)
        控制滚动位置和监听滚动事件。
        当子树中的可滚动组件没有显式指定controller且primary属性值为true时(默认就为true),可滚动组件会使用Widget树中默认的PrimaryScrollController。这种机制的好处是父组件可以控制子树中可滚动组件的滚动行为,例如,Scaffold正是使用这种机制在iOS中实现了点击导航栏回到顶部的功能。
      6. physics(ScrollPhysics类型)
        决定可滚动组件如何响应用户操作。比如用户滑动完抬起手指后,继续执行动画;或者滑动到边界时,如何显示。NeverScrollableScrollPhysics():禁止滚动。
        Flutter默认会根据各平台分别使用不同的ScrollPhysics对象,应用不同的显示效果,如当滑动到边界时,继续拖动的话,在iOS上会出现弹性效果,而在Android上会出现微光效果。如果想在所有平台下使用同一种效果,可以显式指定:
          1. ClampingScrollPhysics:Android下微光效果。
          2. BouncingScrollPhysics:iOS下弹性效果。
    
    
    cacheExtent
        预渲染的高度(下图中顶部和底部灰色的区域)。
        如果RenderBox进入这个区域,即使它未显示在屏幕上,也要先进行构建,预渲染是为了后面进入Viewport时更流畅。
        默认值是250,在构建可滚动列表时可以指定这个值(最终会传给 Viewport)。
    
    ListView
    1. Scrollable、Viewport、Sliver
    Scrollable({
      this.axisDirection = AxisDirection.down,  // 滚动方向
      this.controller,
      this.physics,
      // 滑动时Scrollable会调用此回调构建新的Viewport,同时传递一个ViewportOffset类型的offset参数(描述Viewport该显示哪一部分)。
      // 重新构建Viewport(本身也是Widget,只是配置信息)不是一个昂贵的操作,Viewport变化时对应的RenderViewport会更新信息,并不会随着Widget进行重新构建。
      @required this.viewportBuilder, 
    })
    
    Viewport({
      Key? key,
      this.axisDirection = AxisDirection.down,
      this.crossAxisDirection,
      this.anchor = 0.0,
      // 滚动偏移。Scrollabel构建Viewport 时传入(描述了Viewport该显示哪一部分)。
      required ViewportOffset offset, 
      // 类型为Key,表示从什么地方开始绘制,默认是第一个元素
      this.center,
      this.cacheExtent, // 预渲染区域
      // pixel:cacheExtent值为预渲染区域的具体像素长度
      // viewport:cacheExtent值是一个乘数,预渲染区域的像素长度=cacheExtent*viewport。
      this.cacheExtentStyle = CacheExtentStyle.pixel, 
      this.clipBehavior = Clip.hardEdge,
      List<Widget> slivers = const <Widget>[], // 需要显示的 Sliver 列表
    })
    
    Sliver对应的渲染对象类型是RenderSliver。
    RenderSliver和RenderBox的相同点是都继承自RenderObject类,不同点是在布局时约束信息不同。RenderBox在布局时父组件传递给它的约束信息是BoxConstraints(只包含最大宽高的约束);而 RenderSliver在布局时父组件传递给它的约束是SliverConstraints。
    
    1. Scrollbar (Material风格的滚动条)

    使用:作为可滚动组件的任意一个父组件。

    Scrollbar(
      child: SingleChildScrollView(
        ...
      ),
    );
    Scrollbar在iOS平台会自动切换为CupertinoScrollbar(iOS风格)。
    Scrollbar和CupertinoScrollbar都是通过监听滚动通知来确定滚动条位置的。
    
    1. ScrollController(间接继承自Listenable)

    可滚动组件都有一个controller属性(控制和监听滚动)

    ScrollController({
      double initialScrollOffset = 0.0, // 初始滚动位置
      this.keepScrollOffset = true,// 是否保存滚动位置
      ...
    })
    监听滚动事件
      controller.addListener(()=>print(controller.offset))
    
    
    常用的属性和方法:
    1. offset
      可滚动组件当前的滚动位置。
    2. jumpTo(double offset)、animateTo(double offset,...)
      用于跳转到指定的位置,不同之处在于,后者在跳转时会执行一个动画。
    

    示例

    创建一个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() {
        super.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
                );
              }
          ),
        );
      }
    }
    

    滚动位置恢复

    PageStorage是一个用于保存页面(路由)相关数据的功能型组件,它拥有一个存储桶,子树中的Widget可以通过指定不同的PageStorageKey来存储各自的数据或状态。
    
    每次滚动结束,可滚动组件都会将滚动位置offset存储到PageStorage中,当可滚动组件重新创建时再恢复。
      ScrollController.keepScrollOffset为false,则滚动位置将不会被存储,可滚动组件重新创建时会使用ScrollController.initialScrollOffset;
      ScrollController.keepScrollOffset为true时,可滚动组件在第一次创建时,会滚动到initialScrollOffset处,因为这时还没有存储过滚动位置。在接下来的滚动中就会存储、恢复滚动位置,忽略initialScrollOffset。
    
    当一个路由中包含多个可滚动组件时,如果发现在进行一些跳转或切换操作后,滚动位置不能正确恢复,这时可以通过显式指定不同的PageStorageKey来分别跟踪不同的可滚动组件的位置,如:
      ListView(key: PageStorageKey(1), ... );
      ListView(key: PageStorageKey(2), ... );
    
    注意:一个路由中包含多个可滚动组件时,如果要分别跟踪它们的滚动位置,并非一定就得给他们分别提供PageStorageKey。这是因为Scrollable本身是一个StatefulWidget,它的状态中也会保存当前滚动位置,所以,只要可滚动组件本身没有被从树上detach掉,那么其State就不会销毁,滚动位置就不会丢失。只有当Widget发生结构变化,导致可滚动组件的State销毁或重新构建时才会丢失状态,这种情况就需要显式指定PageStorageKey,通过PageStorage来存储滚动位置,一个典型的场景是在使用TabBarView时,在Tab发生切换时,Tab页中的可滚动组件的State就会销毁,这时如果想恢复滚动位置就需要指定PageStorageKey。
    

    ScrollPosition

    真正保存滑动位置信息的对象。
      offset只是一个便捷属性:double get offset => position.pixels;
    
    一个ScrollController对象可以同时被多个可滚动组件使用,ScrollController会为每一个可滚动组件创建一个ScrollPosition对象,并保存在positions属性中(List<ScrollPosition>)。
      controller.positions.elementAt(0).pixels
      controller.positions.elementAt(1).pixels
      controller.positions.length 被几个可滚动组件使用
    
    ScrollController的animateTo() 和 jumpTo(),内部最终都会调用ScrollPosition的同名方法(真正来控制跳转滚动位置)。
    

    ScrollController控制原理

    ScrollPosition createScrollPosition(
        ScrollPhysics physics,
        ScrollContext context,
        ScrollPosition oldPosition);
    void attach(ScrollPosition position) ;
    void detach(ScrollPosition position) ;
    
    当ScrollController和可滚动组件关联时,可滚动组件首先会调用ScrollController的createScrollPosition()方法来创建一个ScrollPosition来存储滚动位置信息,接着,可滚动组件会调用attach()方法,将创建的ScrollPosition添加到ScrollController的positions属性中,这一步称为“注册位置”,只有注册后animateTo() 和 jumpTo()才可以被调用。
    
    当可滚动组件销毁时,会调用ScrollController的detach()方法,将其ScrollPosition对象从ScrollController的positions属性中移除,这一步称为“注销位置”,注销后animateTo() 和 jumpTo() 将不能再被调用。
    
    注意:ScrollController的animateTo() 和 jumpTo()内部会调用【所有】ScrollPosition的同名方法。
    

    滚动监听

    Flutter Widget树中子Widget可以通过发送通知(Notification)与父(包括祖先)Widget通信。父级组件可以通过NotificationListener组件来监听自己关注的通知,这种通信方式类似于Web开发中浏览器的事件冒泡。
    
    可滚动组件在滚动时会发送ScrollNotification类型的通知,ScrollBar正是通过监听滚动通知来实现的。通过NotificationListener监听滚动事件和通过ScrollController有两个主要的不同:
        1. 通过NotificationListener可以在从可滚动组件到widget树根之间任意位置都能监听。而ScrollController只能和具体的可滚动组件关联后才可以。
        2. 收到滚动事件后获得的信息不同;NotificationListener在收到滚动事件时,通知中会携带当前滚动位置和ViewPort的一些信息,而ScrollController只能获取当前滚动位置。
    
    在接收到滚动事件时,参数类型为ScrollNotification,它包括一个metrics属性,它的类型是ScrollMetrics,该属性包含当前ViewPort及滚动位置等信息:
        1. pixels:当前滚动位置。
        2. maxScrollExtent:最大可滚动长度。
        3. extentBefore:滑出ViewPort顶部的长度;此示例中相当于顶部滑出屏幕上方的列表长度。
        4. extentInside:ViewPort内部长度;此示例中屏幕显示的列表部分的长度。
        5. extentAfter:列表中未滑入ViewPort部分的长度;此示例中列表底部未显示到屏幕范围部分的长度。
        6. atEdge:是否滑到了可滚动组件的边界。
    

    示例

    import 'package:flutter/material.dart';
    class ScrollNotificationTestRoute extends StatefulWidget {
      @override
      _ScrollNotificationTestRouteState createState() =>
          new _ScrollNotificationTestRouteState();
    }
    class _ScrollNotificationTestRouteState
        extends State<ScrollNotificationTestRoute> {
      String _progress = "0%"; // 保存进度百分比
      @override
      Widget build(BuildContext context) {
        return Scrollbar( // 进度条
          // 监听滚动通知
          child: NotificationListener<ScrollNotification>(
            onNotification: (ScrollNotification notification) {
              double progress = notification.metrics.pixels /
                  notification.metrics.maxScrollExtent;
              // 重新构建
              setState(() {
                _progress = "${(progress * 100).toInt()}%";
              });
              print("BottomEdge: ${notification.metrics.extentAfter == 0}");
              //return true; // 放开此行注释后,进度条将失效
            },
            child: Stack(
              alignment: Alignment.center,
              children: <Widget>[
                ListView.builder(
                    itemCount: 100,
                    itemExtent: 50.0,
                    itemBuilder: (context, index) {
                      return ListTile(title: Text("$index"));
                    }
                ),
                CircleAvatar(  //显示进度百分比
                  radius: 30.0,
                  child: Text(_progress),
                  backgroundColor: Colors.black54,
                )
              ],
            ),
          ),
        );
      }
    }
    

    1. ListView

    沿一个方向线性排列所有子组件。支持基于Sliver的延迟构建模型。

    /*
      1. ListView中的列表项组件是RenderBox,并不是Sliver。
      2. 一个ListView中只有一个Sliver(对列表项进行按需加载),默认是SliverList,如果指定了itemExtent,则为SliverFixedExtentList;如果prototypeItem属性不为空,则为SliverPrototypeExtentList。
      3. 可以通过ListView.custom自定义列表项生成模型,它需要实现一个SliverChildDelegate用来给ListView生成列表项组件。
      4. 可滚动组件的构造函数如果需要一个列表项Builder则支持基于Sliver的懒加载模型的,反之则不支持。
      5. ListView高度边界无法确定时会异常
    */
    ListView({
      // 可滚动widget公共参数
      Axis scrollDirection = Axis.vertical,    
      bool reverse = false,
      ScrollController controller,
      bool primary,
      ScrollPhysics physics,
      EdgeInsetsGeometry padding,    
    
      // ListView各个构造函数的共同参数  
      double itemExtent,
      Widget? prototypeItem, // 列表项原型
      bool shrinkWrap = false,
      bool addAutomaticKeepAlives = true,
      bool addRepaintBoundaries = true,
      double cacheExtent,
    
      // 子widget列表
      // 这种方式适合只有少量的子组件数量已知且比较少的情况,反之则应该使用ListView.builder 按需动态构建列表项。
      List<Widget> children = const <Widget>[],
    })
    
    说明:(强烈建议指定itemExtent或prototypeItem)
    1. itemExtent
      如果不为null,则表示滚动方向上子组件的长度。
      指定后滚动系统可以提前知道列表的长度,而无需每次构建子组件时都去再计算,会更加高效。
    2. prototypeItem(列表项原型)
      如果所有列表项长度相同但不知道具体多少,可以指定一个列表项prototypeItem,可滚动组件会在layout时计算一次它延主轴方向的长度,和指定itemExtent一样。
      注意:itemExtent和prototypeItem互斥,不能同时指定。
    3. shrinkWrap
      是否根据子组件的总长度来设置ListView的长度。
      默认false ,ListView的会在滚动方向尽可能多的占用空间。当ListView在一个无边界(滚动方向上)的容器中时,shrinkWrap必须为true。
    4. addAutomaticKeepAlives
      是否将列表项(子组件)包裹在AutomaticKeepAlive 组件中;
      如果设置为true(默认为true,在懒加载列表中会为每一个列表项添加AutomaticKeepAlive父组件),在列表项滑出视口时不会被GC(垃圾回收),它会使用KeepAliveNotification来保存其状态。
      如果列表项自己维护其KeepAlive状态,那么此参数必须置为false。
    5. addRepaintBoundaries
      是否将列表项(子组件)包裹在RepaintBoundary组件中。默认为true。
      将列表项包裹在RepaintBoundary中可以在滚动时避免列表项重绘,但是当列表项重绘的开销非常小时,不添加RepaintBoundary反而会更高效。
      如果列表项自己维护其KeepAlive状态,那么此参数必须置为false
    

    示例

    ListView(
      children: [
        imgSection,
        titleSection,
        buttonSection,
        textSection,
      ],
    ),
    ListView(
      padding: EdgeInsets.all(10),
      children: [
        ListTitle(
          title:Text('hello'),
          subTitle:('world'),
        ),
        ListTitle(
          leading:Icon(Icons.settings,color:Colors.yellow,size:30),
          trailing:Image.network("http://.../1.png"),
          title:Text(
              'hello',
               style: TextStyle(
                  fontSize: 24,
               ),
          ),
          subTitle:('world'),
        ),
      ],
    ),
    
    1. 默认构造函数

    有一个接受Widget列表的children参数。
    适合只有少量的子组件的情况。不支持基于Sliver的懒加载模型。
    通过此方式创建的ListView和使用SingleChildScrollView+Column的方式没有本质的区别。

    示例

    ListView(
      shrinkWrap: true, 
      padding: const EdgeInsets.all(20.0),
      children: <Widget>[
        const Text('I\'m dedicating every day to you'),
        const Text('Domestic life was never quite my style'),
        const Text('When you smile, you knock me out, I fall apart'),
        const Text('And I thought I was so smart'),
      ],
    );
    

    示例2

    import 'package:flutter/material.dart';
    void main() => runApp(new MyApp());
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        final title = 'Basic List';
        return new MaterialApp(
          title: title,
          home: new Scaffold(
            appBar: new AppBar(
              title: new Text(title),
            ),
            body: new ListView(
              children: <Widget>[
                new ListTile(
                  leading: new Icon(Icons.map),
                  title: new Text('Map'),
                ),
                new ListTile(
                  leading: new Icon(Icons.photo),
                  title: new Text('Album'),
                ),
                new ListTile(
                  leading: new Icon(Icons.phone),
                  title: new Text('Phone'),
                ),
              ],
            ),
          ),
        );
      }
    }
    

    示例3(水平滚动)

    import 'package:flutter/material.dart';
    void main() => runApp(new MyApp());
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        final title = 'Horizontal List';
        return new MaterialApp(
          title: title,
          home: new Scaffold(
            appBar: new AppBar(
              title: new Text(title),
            ),
            body: new Container(
              margin: new EdgeInsets.symmetric(vertical: 20.0),
              height: 200.0,
              child: new ListView(
                scrollDirection: Axis.horizontal,    // 水平滚动
                children: <Widget>[
                  new Container(
                    width: 260.0,
                    color: Colors.red,
                  ),
                  new Container(
                    width: 260.0,
                    color: Colors.blue,
                  ),
                ],
              ),
            ),
          ),
        );
      }
    }
    
    1. ListView.builder 构造函数

    适合列表项比较多(或不确定)的情况。支持基于Sliver的懒加载模型的。

    ListView.builder({
      ...
      // 列表项的构建器,返回值为一个widget。当滚动到对应index位置时会调用。
      @required IndexedWidgetBuilder itemBuilder,
      // 列表项的数量,如果为null,则为无限列表。
      int itemCount,
    })
    

    示例

    ListView.builder(
        itemCount: 100,
        itemExtent: 50.0, // 高度为50.0
        itemBuilder: (BuildContext context, int index) {
          return ListTile(title: Text("$index"));
        }
    );
    

    示例2

    import 'package:flutter/foundation.dart';
    import 'package:flutter/material.dart';
    void main() {
      runApp(new MyApp(
        items: new List<String>.generate(10000, (i) => "Item $i"),
      ));
    }
    class MyApp extends StatelessWidget {
      final List<String> items;  // 数据源
      MyApp({Key key, @required this.items}) : super(key: key);
      @override
      Widget build(BuildContext context) {
        final title = 'Long List';
        return new MaterialApp(
          title: title,
          home: new Scaffold(
            appBar: new AppBar(
              title: new Text(title),
            ),
            body: new ListView.builder(
              itemCount: items.length,
              itemBuilder: (context, index) {
                return new ListTile(
                  title: new Text('${items[index]}'),
                );
              },
            ),
          ),
        );
      }
    }
    

    示例3(不同类型的item)

    import 'package:flutter/foundation.dart';
    import 'package:flutter/material.dart';
    void main() {
      runApp(new MyApp(
        items: new List<ListItem>.generate(
          1000,
          (i) => i % 6 == 0
              ? new HeadingItem("Heading $i")
              : new MessageItem("Sender $i", "Message body $i"),
        ),
      ));
    }
    class MyApp extends StatelessWidget {
      final List<ListItem> items;
      MyApp({Key key, @required this.items}) : super(key: key);
      @override
      Widget build(BuildContext context) {
        final title = 'Mixed List';
        return new MaterialApp(
          title: title,
          home: new Scaffold(
            appBar: new AppBar(
              title: new Text(title),
            ),
            body: new ListView.builder(
              itemCount: items.length,
              itemBuilder: (context, index) {
                final item = items[index];
                if (item is HeadingItem) {
                  return new ListTile(
                    title: new Text(
                      item.heading,
                      style: Theme.of(context).textTheme.headline,
                    ),
                  );
                } else if (item is MessageItem) {
                  return new ListTile(
                    title: new Text(item.sender),
                    subtitle: new Text(item.body),
                  );
                }
              },
            ),
          ),
        );
      }
    }
    abstract class ListItem {}
    class HeadingItem implements ListItem {
      final String heading;
      HeadingItem(this.heading);
    }
    class MessageItem implements ListItem {
      final String sender;
      final String body;
      MessageItem(this.sender, this.body);
    }
    
    1. ListView.separated

    比ListView.builder多了一个separatorBuilder参数(在生成的列表项之间添加分割组件)。

    示例(奇数行添加一条蓝色下划线,偶数行添加一条绿色下划线)

    class ListView3 extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        Widget divider1=Divider(color: Colors.blue,);
        Widget divider2=Divider(color: Colors.green);
        return ListView.separated(
            itemCount: 100,
            itemBuilder: (BuildContext context, int index) {
              return ListTile(title: Text("$index"));
            },
            separatorBuilder: (BuildContext context, int index) {  // 分割器构造器
              return index%2==0?divider1:divider2;
            },
        );
      }
    }
    

    示例2

    从数据源异步分批拉取一些数据,然后用ListView展示,当滑动到列表末尾时,判断是否需要再去拉取数据,如果是,则去拉取,拉取过程中在表尾显示一个loading,拉取成功后将数据插入列表;如果不需要再去拉取,则在表尾提示"没有更多"
    
    class InfiniteListView extends StatefulWidget {
      @override
      _InfiniteListViewState createState() => new _InfiniteListViewState();
    }
    class _InfiniteListViewState extends State<InfiniteListView> {
      static const loadingTag = "##loading##"; //表尾标记
      var _words = <String>[loadingTag];
      @override
      void initState() {
        super.initState();
        _retrieveData();
      }
      @override
      Widget build(BuildContext context) {
        return ListView.separated(
          itemCount: _words.length,
          itemBuilder: (context, index) {
            // 如果到了表尾
            if (_words[index] == loadingTag) {
              //不足100条,继续获取数据
              if (_words.length - 1 < 100) {
                // 获取数据
                _retrieveData();
                // 加载时显示loading
                return Container(
                  padding: const EdgeInsets.all(16.0),
                  alignment: Alignment.center,
                  child: SizedBox(
                      width: 24.0,
                      height: 24.0,
                      child: CircularProgressIndicator(strokeWidth: 2.0)
                  ),
                );
              } else {
                // 已经加载了100条数据,不再获取数据。
                return Container(
                    alignment: Alignment.center,
                    padding: EdgeInsets.all(16.0),
                    child: Text("没有更多了", style: TextStyle(color: Colors.grey),)
                );
              }
            }
            // 显示单词列表项
            return ListTile(title: Text(_words[index]));
          },
          separatorBuilder: (context, index) => Divider(height: .0),
        );
      }
      void _retrieveData() {
        Future.delayed(Duration(seconds: 2)).then((e) {
          setState(() {
            // 重新构建列表
            _words.insertAll(_words.length - 1,
              // 每次生成20个单词
              generateWordPairs().take(20).map((e) => e.asPascalCase).toList()
              );
          });
        });
      }
    }
    

    1. 添加固定的列表头
    不太好的写法:
    @override
    Widget build(BuildContext context) {
      return Column(children: <Widget>[
        ListTile(title:Text("商品列表")),
        SizedBox(
          // Material设计规范中状态栏、导航栏、ListTile高度分别为24、56、56 。避免底部留白
          height: MediaQuery.of(context).size.height-24-56-56,
          child: ListView.builder(itemBuilder: (BuildContext context, int index) {
            return ListTile(title: Text("$index"));
          }),
      )
      ]);
    }
    
    这种方法太不好,如果页面布局发生变化,比如表头布局调整导致表头高度改变,那么剩余空间的高度就得重新计算。修正:
    // 自动拉伸ListView以填充屏幕剩余空间
    @override
    Widget build(BuildContext context) {
      return Column(children: <Widget>[
        ListTile(title:Text("商品列表")),
        Expanded(
          child: ListView.builder(itemBuilder: (BuildContext context, int index) {
            return ListTile(title: Text("$index"));
          }),
        ),
      ]);
    }
    
    1. AutomaticKeepAlive组件
    将列表项的根RenderObject的keepAlive按需自动标记为true或false。
    列表组件的Viewport区域+cacheExtent预渲染区域 称为加载区域 :
        1. 当 keepAlive 标记为 false 时,如果列表项滑出加载区域时,列表组件将会被销毁。
        2. 当 keepAlive 标记为 true 时,当列表项滑出加载区域后,Viewport 会将列表组件缓存起来;当列表项进入加载区域时,Viewport 从先从缓存中查找是否已经缓存,如果有则直接复用,如果没有则重新创建列表项。
    
    子组件想改变是否需要缓存的状态时就向KeepAliveNotification通知,AutomaticKeepAlive收到消息后会去更改keepAlive的状态(从true变为false时,需要释放缓存)。
    
    1. 优化ListView
    1. 
    列表项较多或不确定(上拉加载更多)时不要使用默认的构造函数,应该使用ListView.builder
    2. 
    禁用addAutomaticKeepAlives(滑动过快时可能会出现短暂白屏)。
    禁用addRepaintBoundaries,当列表元素布局较简单时(可提高流畅度)。
    3.
    列表中不可变子组件使用const修饰。 // children: [const ListImage()],
    4.
    指定itemExtent值,当可以提前知道时。
    
    

    2. SingleChildScrollView

    内容不会超过屏幕太多时使用SingleChildScrollView,只能接收一个子组件。
    不支持基于Sliver的延迟实例化模型。

    SingleChildScrollView({
      this.scrollDirection = Axis.vertical, 
      this.reverse = false, 
      this.padding, 
      bool primary, 
      this.physics, 
      this.controller,
      this.child,
    })
    

    示例(将大写字母A-Z沿垂直方向显示)

    class SingleChildScrollViewTestRoute extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        String str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
        return Scrollbar( // 显示进度条
          child: SingleChildScrollView(
            padding: EdgeInsets.all(16.0),
            child: Center(
              child: Column( 
                // 动态创建一个List<Widget>  
                children: str.split("") 
                    // 每一个字母都用一个Text显示,字体为原来的两倍
                    .map((c) => Text(c, textScaleFactor: 2.0,)) 
                    .toList(),
              ),
            ),
          ),
        );
      }
    }
    
    运行结果

    3. GridView(二维网格列表)

    GridView({
      Axis scrollDirection = Axis.vertical,    // 滚动方向
      bool reverse = false,
      ScrollController controller,
      bool primary,
      ScrollPhysics physics,
      bool shrinkWrap = false,
      EdgeInsetsGeometry padding,  // 内边距
      // 控制子组件如何排列
      // SliverGridDelegate定义了GridView Layout相关接口,子类需要通过实现它们来实现具体的布局算法。
      // SliverGridDelegateWithFixedCrossAxisCount和SliverGridDelegateWithMaxCrossAxisExtent。
      @required SliverGridDelegate gridDelegate, 
      bool addAutomaticKeepAlives = true,
      bool addRepaintBoundaries = true,
      double cacheExtent,
      List<Widget> children = const <Widget>[],    // 子列表
    })
    和ListView的大多数参数都是相同的。
    
    1. SliverGridDelegateWithFixedCrossAxisCount、GridView.count

    横轴为固定数量子元素。
    GridView.count构造函数内部使用了SliverGridDelegateWithFixedCrossAxisCount。

    SliverGridDelegateWithFixedCrossAxisCount({
      @required double crossAxisCount, 
      double mainAxisSpacing = 0.0,
      double crossAxisSpacing = 0.0,
      double childAspectRatio = 1.0,
    })
    
    说明:
    1. crossAxisCount
      横轴子元素的数量。
      子元素在横轴的长度=ViewPort横轴长度/crossAxisCount。
    2. childAspectRatio
      子元素在横轴长度和主轴长度的比例。
      子元素的大小是通过crossAxisCount和childAspectRatio两个参数共同决定的。
      注意,这里的子元素指的是子组件的最大显示空间,确保子组件的实际大小不要超出子元素的空间。
    3. mainAxisSpacing
      主轴方向的间距。
    4. crossAxisSpacing
      横轴方向的间距。
    

    示例

    GridView(
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 3, // 横轴三个子widget
          childAspectRatio: 1.0 // 宽高比为1
      ),
      children:<Widget>[
        Icon(Icons.ac_unit),
        Icon(Icons.airport_shuttle),
        Icon(Icons.all_inclusive),
        Icon(Icons.beach_access),
        Icon(Icons.cake),
        Icon(Icons.free_breakfast)
      ]
    );
    上面的示例代码等价于(GridView.count):
    GridView.count( 
      crossAxisCount: 3,
      childAspectRatio: 1.0,
      children: <Widget>[
        Icon(Icons.ac_unit),
        Icon(Icons.airport_shuttle),
        Icon(Icons.all_inclusive),
        Icon(Icons.beach_access),
        Icon(Icons.cake),
        Icon(Icons.free_breakfast),
      ],
    );
    

    示例(GridView.count)

    import 'package:flutter/material.dart';
    void main() {
      runApp(new MyApp());
    }
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        final title = 'Grid List';
        return new MaterialApp(
          title: title,
          home: new Scaffold(
            appBar: new AppBar(
              title: new Text(title),
            ),
            body: new GridView.count(
              crossAxisCount: 2,
              children: new List.generate(100, (index) {
                return new Center(
                  child: new Text(
                    'Item $index',
                    style: Theme.of(context).textTheme.headline,
                  ),
                );
              }),
            ),
          ),
        );
      }
    }
    
    1. SliverGridDelegateWithMaxCrossAxisExtent、GridView.extent

    横轴子元素为固定最大长度。
    GridView.extent构造函数内部使用了SliverGridDelegateWithMaxCrossAxisExtent。

    SliverGridDelegateWithMaxCrossAxisExtent({
      double maxCrossAxisExtent,    // 子元素在横轴上的最大长度
      double mainAxisSpacing = 0.0,
      double crossAxisSpacing = 0.0,
      double childAspectRatio = 1.0,
    })
    

    示例

    GridView(
      padding: EdgeInsets.zero,
      gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
          maxCrossAxisExtent: 120.0,
          childAspectRatio: 2.0 // 宽高比为2
      ),
      children: <Widget>[
        Icon(Icons.ac_unit),
        Icon(Icons.airport_shuttle),
        Icon(Icons.all_inclusive),
        Icon(Icons.beach_access),
        Icon(Icons.cake),
        Icon(Icons.free_breakfast),
      ],
    );
    上面的示例代码等价于:
    GridView.extent(
       maxCrossAxisExtent: 120.0,
       childAspectRatio: 2.0,
       children: <Widget>[
         Icon(Icons.ac_unit),
         Icon(Icons.airport_shuttle),
         Icon(Icons.all_inclusive),
         Icon(Icons.beach_access),
         Icon(Icons.cake),
         Icon(Icons.free_breakfast),
       ],
     );
    
    1. GridView.builder

    通过GridView.builder来动态创建子widget。

    GridView.builder(
     ...
     @required SliverGridDelegate gridDelegate, 
     @required IndexedWidgetBuilder itemBuilder,  // 子widget构建器
    )
    

    示例

    从一个异步数据源(如网络)分批获取一些Icon,然后用GridView来展示
    
    class InfiniteGridView extends StatefulWidget {
      @override
      _InfiniteGridViewState createState() => new _InfiniteGridViewState();
    }
    class _InfiniteGridViewState extends State<InfiniteGridView> {
      List<IconData> _icons = []; //保存Icon数据
      @override
      void initState() {
        // 初始化数据  
        _retrieveIcons();
      }
      @override
      Widget build(BuildContext context) {
        return GridView.builder(
            gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                // mainAxisSpacing: 10.0,
                // crossAxisSpacing: 10.0,
                crossAxisCount: 3, // 每行三列
                childAspectRatio: 1.0 // 显示区域宽高相等
            ),
            itemCount: _icons.length,
            itemBuilder: (context, index) {
              // 如果显示到最后一个并且Icon总数小于200时继续获取数据
              if (index == _icons.length - 1 && _icons.length < 200) {
                _retrieveIcons();
              }
              return Icon(_icons[index]);
            }
        );
      }
      void _retrieveIcons() {
        Future.delayed(Duration(milliseconds: 200)).then((e) {
          setState(() {
            _icons.addAll([
              Icons.ac_unit,
              Icons.airport_shuttle,
              Icons.all_inclusive,
              Icons.beach_access, Icons.cake,
              Icons.free_breakfast
            ]);
          });
        });
      }
    }
    
    flutter_staggered_grid_view包可实现这种布局

    相关文章

      网友评论

        本文标题:Flutter了解之入门篇10-1(可滚动组件)

        本文链接:https://www.haomeiwen.com/subject/idrfortx.html