《Flutter》-- 6.高级组件

2022-04-07 16:23:22 浏览数 (1)

参阅书籍:

《Flutter跨平台开发入门与实践》-- 向治洪(著)

6. 高级组件

6.1 可滚动组件

对于列表和长布局的显示溢出问题,可以使用Flutter提供的可滚动组件来处理。

6.1.1 Scrollable组件

在Flutter中,一个可滚动的组件直接或间接包含一个Scrollable组件,它是可滚动组件的基础组件。

代码语言:javascript复制
Scrollable({
  this.axisDirection = AxisDirection.down,//滚动方向
  this.controller,//用于接收一个ScrollController对象,控制滚动位置和监听滚动事件
  this.physics,//用于接收一个ScrollPhysics对象,可以决定滚动组件响应用户操作的方式
  @required this.viewportBuilder
})
6.1.2 Scrollbar组件

Scrollbar是一个Material风格的滚动指示器组件,如果要给可滚动组件添加滚动条,只需将Scrollbar组件作为可滚动组件的父组件使用即可。

如果一个可滚动组件支持Sliver模型,那么该滚动可以将子组件分成多个部分,只有当子组件出现在视口中时才会去构建它。

目前,可滚动组件中的大部分组件都支持基于Sliver的延迟构建模型,如ListView、GridView。

6.1.3 SingleChildScrollView组件

是一个只能包含单一子组件的可滚动组件,其作用类似于iOS的UIScrollView组件或Android的ScrollView组件。

只能应用于内容不会超过屏幕尺寸太多的情况,因为SingleChildScrollView组件目前还不支持基于Sliver的延迟加载,如果视图内容超出屏幕尺寸太多会导致性能问题。

所谓基于Sliver的延迟加载,是Flutter中提出的薄片(Sliver)概念。如果一个可滚动组件支持Sliver,那么该可滚动组件可以将子组件分成多个Sliver,只有当Sliver出现在视图窗口时才会去构建它,从而提高渲染的性能。

SingleChildScrollView组件的构造函数:

代码语言:javascript复制
const SingleChildScrollView({
  Key key,
  this.scrollDirection = Axis.vertical,//滚动的方向,默认在垂直方向滚动
  this.reverse = false,//控制从头还是从尾开始滚动,默认false,即从头开始滚动
  this.padding,//插入子组件时的内边距
  bool primary,//是否是与父级关联的主滚动视图
  this.physics,//设置滚动效果
  this.controller,//控制滚动位置,当primary为true时,controller必须为null
  this.child,//列表项内容
  this.dragStrartBehavior = DragStrartBehavior.down,//处理拖拽开始行为的方式
})

示例代码:

代码语言:javascript复制
import 'package:flutter/material.dart';

void main() => runApp(ScollWidget());

class ScollWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '高级组件--可滚动组件',
      home: Scaffold(
        appBar: AppBar(title: Text('可滚动组件--SingleChildScrollView')),
        body: SingleChildScrollView(
          scrollDirection: Axis.horizontal,
          child: Row(
            children: <Widget>[Text('Hello Flutter ' * 100)]
          ),
        )
      )
    );
  }
}

示例效果:

6.1.4 CustomScrollView组件

可以使用Sliver模型实现自定义滚动组件,可以包含多个子组件,而且可以将这些子组件包裹起来实现一致的滚动效果。

CustomScrollView作为容器组件时,子组件不能是ListView、GridView等可滚动组件,会造成滚动冲突。在实际使用过程中,Flutter提供了SliverList、SliverGrid等可滚动组件的Sliver版本。

ListView、GridView自带滚动模型,SliverList、SliverGrid不包含滚动模型,不会造成滚动冲突。

CustomScrollView组件的构造函数:

代码语言:javascript复制
class CustomScrollView extends ScrollView {
  const CustomScrollView({
    Key key,
    Axis scrollDirection = Axis.vertical,//滚动的方向,默认在垂直方向滚动
    bool reverse = false,//控制从头还是从尾开始滚动,默认false,即从头开始滚动
    ScrollController controller,//控制滚动位置,当primary为true时,controller必须为null
    bool primary,//是否是与父级关联的主滚动视图
    ScrollPhysics physics,//设置滚动效果
    bool shrinkWrap = false,//子组件是否只满足自身大小
    Key center,//子组件的key值
    double anchor = 0.0,//开始滚动的偏移量,默认从坐标原点开始排列
    double cacheExtent,//缓存不可见的列表项,即使这部分区域不可见,也会被加载处理
    this.slivers = const <Widget>[],//列表子元素
    int semanticChildCount,//子项数量
    DragStartBehavior dragStartBehavior = DragStartBehavior.down,//开始处理拖拽行为的方式,默认为检测到拖拽手势时开始处理
  })
}

CustomScrollView组件通常被用于实现复杂的滚动效果,并且可以用来实现复杂的动画效果。

示例代码:

代码语言:javascript复制
import 'package:flutter/material.dart';

void main() => runApp(ScollWidget());

class ScollWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '高级组件--可滚动组件',
      home: Scaffold(
        appBar: AppBar(title: Text('可滚动组件--CustomScrollView')),
        body: CustomScrollView(
          slivers: <Widget>[
            // 头部
            SliverAppBar(
              pinned: true,
              expandedHeight: 160.0,
              flexibleSpace: FlexibleSpaceBar(
                title: Text('CustomScrollView'),
                background: Image.asset('images/test1.png'),
              ),
            ),
            // 中间
            SliverGrid(
              gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
                maxCrossAxisExtent: 200.0, 
                mainAxisSpacing: 10.0,
                childAspectRatio: 3.0,
              ),
              delegate: SliverChildBuilderDelegate(
                (BuildContext context, int index) {
                  return Card(
                    child: Container(
                      alignment: Alignment.centerLeft,
                      padding: EdgeInsets.all(10),
                      child: Text('grid $index'),
                    ),
                  );
                },
                childCount: 11,
              ),
            ),
            // 底部
            SliverFixedExtentList(
              itemExtent: 60.0,
              delegate: SliverChildListDelegate(
                List.generate(20, (int index) {
                  return GestureDetector(
                    onTap: () => print('单击$index'),
                    child: Card(
                      child: Container(
                        alignment: Alignment.centerLeft,
                        padding: EdgeInsets.all(15),
                        child: Text('list $index'),
                      )
                    )
                  );
                })
              )
            )
          ],
        )
      )
    );
  }
}

示例效果:

6.1.5 ScrollController组件

如果需要监听可滚动组件的滚动过程,可以使用ScrollController组件来进行监听。

ScrollController组件的构造函数:

代码语言:javascript复制
ScrollController({
  double initialScrollOffset = 0.0,//初始化滚动位置
  this.keepScrollOffset = true,//是否保持滚动位置
  this.debugLabel,
})

当keepScrollOffset的属性值为true时,可滚动组件的滚动位置会被存储到PageStorage中,当可滚动组件重新创建时可以使用PageStorage恢复存储的位置。

ScrollController组件还有如下属性和方法:

offset:可滚动组件当前的滚动位置;

jumpTo():用于跳转到指定的位置;

animateTo():跳转到指定位置,跳转时会执行设置的动画。

示例代码:

代码语言:javascript复制
import 'package:flutter/material.dart';

void main() => runApp(ScrollControllerPage());

class ScrollControllerPage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return ScrollControllerPageState();
  }
}

class ScrollControllerPageState extends State<ScrollControllerPage> {
  ScrollController controller = new ScrollController();
  bool showTopBtn = false;//是否显示按钮
  @override
  void initState() {
    super.initState();
    controller.addListener(() {
      if(controller.offset < 500 && showTopBtn) {
        setState(() {
          showTopBtn = false;
        });
      } else if (controller.offset >= 500 && !showTopBtn) {
        setState(() {
          showTopBtn = true;
        });
      }
    });
  }
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '高级组件--可滚动组件',
      home: Scaffold(
        appBar: AppBar(title: Text('可滚动组件--ScrollController')),
        body: ListView.builder(
          itemCount: 100,
          itemExtent: 50.0,
          controller: controller,
          itemBuilder: (context, index) {
            return ListTile(title: Text('列表Item $index'));
          },
        ),
        floatingActionButton: !showTopBtn ? null : 
          FloatingActionButton(
            child: Icon(Icons.arrow_upward),
            onPressed: () {
              controller.jumpTo(0);
            }
          ) 
      )
    );
  }
}

示例效果:

在有多个组件嵌套的组件树中,组件树的子组件可以通过发送通知来与父组件进行通信,父组件则可以通过NotificationListener组件来监听自己关注的通知,这种跨组件的通信方式通常被称为事件冒泡。

接收滚动事件的参数类型为ScrollNotification,它提供了一个metrics属性,该属性包含了当前可视窗口和滚动位置等信息。

NotificationListener组件支持的属性如下:

pixels:当前滚动位置;

maxScrollExtent:最大可滚动长度;

extentBefore:距离滚出视图窗口顶部的长度;

extentInside:视图窗口内部长度,大小等于屏幕显示的列表长度;

extentAfter:列表中未滑入视图窗口部分的长度;

atEdge:是否滚动到了可滚动组件的边界。 示例代码:

代码语言:javascript复制
import 'package:flutter/material.dart';

void main() => runApp(ScrollNotificationPage());

class ScrollNotificationPage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return ScrollNotificationPageState();
  }
}

class ScrollNotificationPageState extends State<ScrollNotificationPage> {
  String _progress = '0%';
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '高级组件--可滚动组件',
      home: Scaffold(
        appBar: AppBar(title: Text('可滚动组件--NotificationListener')),
        body: Scrollbar(
          child: NotificationListener<ScrollNotification>(
            onNotification: (ScrollNotification notification) {
              double progress = notification.metrics.pixels / notification.metrics.maxScrollExtent;
              setState(() {
                _progress = '${(progress * 100).toInt()}%';
              });
              return null;
            },
            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,
                )
              ]
            )
          )
        )
      )
    );
  }
}

示例效果:

NotificationListener组件和ScrollController组件都可以实现列表滚动的监听。NotificationListener组件可以监听可滚动组件的整个组件树,并且监听到的信息更多,ScrollController则只能监听关联的可滚动组件的相关信息。

6.2 列表组件

6.2.1 ListView

ListView,即列表组件,作用类似于Android的RecyclerView或ListView。ListView可以沿一个线性方向排布相同或相似的子组件元素,并支持基于Sliver的延迟。

ListView的默认构造函数:

代码语言:javascript复制
class ListView extends BoxScrollView {
  ListView({
    Key key,
    Axis scrollDirection = Axis.vertical,
    bool reverse = false,
    ScrollController controller,
    bool primary,
    ScrollPhysics physics,
    EdgeInsetsGeometry padding,
    bool shrinkWrap = false,//是否根据列表项的总长度来设置ListView的长度,默认为false
    this.itemExtent,//列表项的大小。如果滚动方向是垂直方向,则表示子组件的高度;如果滚动方向为水平方向,则表示子组件的长度。
    bool addAutomaticKeepAlives = true,//是否将列表项包裹在AutomaticKeepAlive组件中,默认值为true,表示列表项滑出视图窗口时不会被垃圾回收,会保存之前的状态。
    bool addRepaintBoundaries = true,//是否将列表项包裹在RepaintBoundary组件中,默认值为true,可以避免列表项的重绘,提高渲染的性能。
    bool addSemanticIndexes = true,
    double cacheExtent, 
    List<Widget> children = const <Widget>[],
    int semanticChildCount,
    DragStartBehavior dragStartBehavior = DragStartBehavior .down,
  })
}

示例代码:

代码语言:javascript复制
import 'package:flutter/material.dart';

void main() => runApp(ListViewWidget());

class ListViewWidget extends StatelessWidget {
  final _items = List<Widget>.generate(10, 
    (index) => Container(
      padding: EdgeInsets.all(16.0),
      child: Text('Item $index')
    )
  );
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '高级组件--列表组件',
      home: Scaffold(
        appBar: AppBar(title: Text('可滚动组件--列表组件')),
        body: ListView(children: _items)
      )
    );
  }
}

示例效果:

默认的构造函数适合只含有少量子组件的情况,因为它不支持基于Sliver的延迟加载,当列表的元素较多时,容易出现卡顿现象。

6.2.2 ListView.builder

使用ListView.builder创建的列表是基于Sliver的延迟加载创建的,渲染性能比较高,适合用于列表元素比较多的情况。

ListView.builder特有的属性:

1)itemBuilder:用于构建列表项的可见子组件构建器,只有索引>= 0且< itemCount时才会被调用;

2)itemCount:列表项的数量,如果为null,则列表为无限列表。

示例代码:

代码语言:javascript复制
import 'package:flutter/material.dart';

void main() => runApp(ListViewWidget());

class ListViewWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '高级组件--列表组件',
      home: Scaffold(
        appBar: AppBar(title: Text('高级组件--列表组件')),
        body: ListView.builder(
          itemCount: 100,
          itemExtent: 50.0,
          itemBuilder: (BuildContext context, int index) {
            return ListTile(title: Text('Item $index'));
          }
        )
      )
    );
  }
}

示例效果:

6.2.3 ListView.separated

和ListView.builder相比,ListView.separated多了一个separatorBuilder属性,该属性可以在生成的列表项之间添加一条分割线。

示例代码:

代码语言:javascript复制
import 'package:flutter/material.dart';

void main() => runApp(ListViewWidget());

class ListViewWidget extends StatelessWidget {
  Widget divider = Divider(color: Colors.grey);
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '高级组件--列表组件',
      home: Scaffold(
        appBar: AppBar(title: Text('高级组件--列表组件')),
        body: ListView.separated(
          itemCount: 100,
          itemBuilder: (BuildContext context, int index) {
            return ListTile(title: Text('Item $index'));
          },
          separatorBuilder: (BuildContext context, int index) {
            return divider;
          }, 
        )
      )
    );
  }
}

示例效果:

6.2.4 ListView.custom

ListView.custom适用于自定义列表的场景。其中,childrenDelegate是它的必传参数,需要传入一个实现了SliverChildDelegate抽象类的组件,用来给ListView组件添加列表项。

SliverChildDelegate是一个抽象类,它的实现类有SliverChildListDelegate和SliverChildBuilderDelegate,并且SliverChildDelegate的build()可以对单个子组件进行自定义样式处理。

示例代码:

代码语言:javascript复制
import 'package:flutter/material.dart';

void main() => runApp(ListViewWidget());

class ListViewWidget extends StatelessWidget {
  final _items = List<Widget>.generate(100, 
    (i) => Container(
      padding: EdgeInsets.all(16.0),
      child: Text('Item $i')
    )
  );
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '高级组件--列表组件',
      home: Scaffold(
        appBar: AppBar(title: Text('高级组件--列表组件')),
        body: ListView.custom(
          childrenDelegate: SliverChildListDelegate(_items),
        )
      )
    );
  }
}

示例效果:

如果滚动视图中出现列表嵌套的场景,为了不造成滚动时的冲突,需要对子组件添加禁止滚动属性。

代码语言:javascript复制
ListView.builder(
  ...
  physics: NeverScrollableScrollPhysics(),//禁止滚动
  ...
)
6.3 网格组件
6.3.1 GridView基础

GridView是一个可以构建二维网格的列表组件,作用类似于原生Android中的GridView/RecyclerView或者iOS的UICollectionView。

GridView的默认构造函数:

代码语言:javascript复制
GridView({
  Key key,
  Axis scrollDirection = Axis.vertical,
  bool reverse = false,
  ScrollController controller,
  bool primary, 
  ScrollPhysics physics,
  bool shrinkWrap = false,
  EdgeInsetsGeometry padding,
  @required this.gridDelegate,//类型是SliverGridDelegate,控制GridView子组件的排列方式
  bool addAutomaticKeepAlives = true,
  bool addRepaintBoundaries = true,
  bool addSemanticIndexes = true,
  double cacheExtent,
  List<Widget> children = const <Widget>[],
  int semanticChildCount,  
})

SliverGridDelegate是一个抽象类,是一个控制子元素排列方式的接口,有两个实现类:

1)SliverGridDelegateWithFixedCrossAxisCount:用于列数固定的场景

代码语言:javascript复制
SliverGridDelegateWithFixedCrossAxisCount({
  @required double crossAxisCount,//列数
  double mainAxisSpacing = 0.0,//主轴方向上子组件的间距
  double crossAxisSpacing = 0.0,//横轴方向上子组件的间距
  double childAspectRatio = 1.0,//子组件的宽高比  
})

示例代码:

代码语言:javascript复制
import 'package:flutter/material.dart';

void main() => runApp(GridWidget());

class GridWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '高级组件--网格组件',
      home: Scaffold(
        appBar: AppBar(title: Text('高级组件--网格组件')),
        body: GridView(
          gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
            crossAxisCount: 3,
            childAspectRatio: 1.5
          ),
          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),
          ],
        )
      )
    );
  }
}

示例效果:

SliverGridDelegateWithFixedCrossAxisCount还可以使用GridView.count进行代替:

代码语言:javascript复制
...
body: GridView.count(
  crossAxisCount: 3,
  childAspectRatio: 1.5,
  children: <Widget>[
    Icon(Icons.ac_unit),
    ...
  ],
)
...

2)SliverGridDelegateWithMaxCrossAxisExtent:用于子元素有最大宽度限制的场景

代码语言:javascript复制
SliverGridDelegateWithMaxCrossAxisExtent({
  @required this.maxCrossAxisExtent,//子元素在横轴上的最大长度
  this.mainAxisSpacing = 0.0,//主轴方向上子组件的间距
  this.crossAxisSpacing = 0.0,//横轴方向上子组件的间距
  this.childAspectRatio = 1.0,//子组件的宽高比 
})

示例代码:

代码语言:javascript复制
import 'package:flutter/material.dart';

void main() => runApp(GridWidget());

class GridWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '高级组件--网格组件',
      home: Scaffold(
        appBar: AppBar(title: Text('高级组件--网格组件')),
        body: GridView(
          padding: EdgeInsets.zero,
          gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
            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),
          ],
        )
      )
    );
  }
}

示例效果:

SliverGridDelegateWithFixedCrossAxisCount还可以使用GridView.extent()代替:

代码语言:javascript复制
...
body: GridView.extent(
  padding: EdgeInsets.zero,
  maxCrossAxisExtent: 120.0,
  childAspectRatio: 2.0,
  children: <Widget>[
    Icon(Icons.ac_unit),
    ...
  ],
)
...
6.3.2 GridView构造函数

GridView的构造函数一共有5个:

1)GridView():默认构造函数,适用于元素个数有限的场景,会一次性全部渲染children属性中的子元素组件;

2)GridView.builder():适用于构建大量或无限长的列表,它只会构建那些可见的组件,对于不可见的会动态销毁,减少内存销毁,渲染更高效;必须要传入gridDelegate和itemBuilder属性;

3)GridView.count():SliverGridDelegateWithFixedCrossAxisCount实现类的简写,用于创建横轴数量固定的网格视图;

4)GridView.extent():SliverGridDelegateWithFixedCrossAxisCount实现类的简写,用于创建横轴子元素宽度固定的网格视图;

5)GridView.custom():自定义的网格视图,需要同时传入gridDelegate和childrenDelegate。 示例代码:

代码语言:javascript复制
import 'package:flutter/material.dart';

void main() => runApp(GridViewWidget());

class ItemViewModel {
  final String icon;
  final String title;
  const ItemViewModel({this.icon, this.title});
}

class GridItem extends StatelessWidget {
  final ItemViewModel data;
  GridItem({Key key, this.data}): super(key: key);
  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.only(bottom: 5),
      child: Column(
        children: <Widget>[
          Image.asset(this.data.icon, width: 55, fit: BoxFit.fitWidth),
          Text(this.data.title)
        ],
      ),
    );
  }
}

const List<ItemViewModel> list = [
  ItemViewModel(title: '微信', icon: 'images/wx.png'),
  ItemViewModel(title: 'QQ', icon: 'images/qq.png'),
  ItemViewModel(title: '微信', icon: 'images/wx.png'),
  ItemViewModel(title: 'QQ', icon: 'images/qq.png'),
  ItemViewModel(title: '微信', icon: 'images/wx.png'),
  ItemViewModel(title: 'QQ', icon: 'images/qq.png'),
  ItemViewModel(title: '微信', icon: 'images/wx.png'),
  ItemViewModel(title: 'QQ', icon: 'images/qq.png'),
];

class GridViewWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '高级组件--网格组件',
      home: Scaffold(
        appBar: AppBar(title: Text('高级组件--网格组件')),
        body: GridView.builder(
          gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4), 
          itemCount: list.length,
          padding: EdgeInsets.symmetric(vertical: 10),
          itemBuilder: (context, index) {
            return GridItem(data: list[index]);
          }
        )
      )
    );
  }
}

示例效果:

6.4 滑动切换组件

PageView是一个滑动视图列表组件,它继承自CustomScrollView,作用类似于Android的ViewPager,可以用它实现视图的左右滑动切换功能。

PageView的构造函数:

1)PageView():默认构造函数,创建一个可滚动列表,适合子组件比较少的场景;

代码语言:javascript复制
PageView({
  Key key,
  this.scrollDirection = Axis.horizontal,
  this.reverse = false,
  PageController controller,
  this.physics,
  this.pageSnapping = true,
  this.onPageChanged,//页面滑动切换时调用
  this.semanticChildCount,//列表项的数量
  List<Widget> children = const <Widget>[],//PageView的列表项
  this.dragStartBehavior = DragStartBehavior.down,//处理拖拽开始行为的方式,默认为检测到拖拽手势时开始执行滚动拖拽行为
})

2)PageView.builder():创建一个滚动列表,适合子组件比较多的场景,需要指定子组件的数量;

3)PageView.custom():创建一个可滚动的列表,需要自定义子项。

示例代码:

代码语言:javascript复制
import 'package:flutter/material.dart';

void main() => runApp(PageViewWidget());

const List<String> items = [
  'images/test1.png',
  'images/test2.png',
];

class PageViewWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '高级组件--滑块切换组件',
      home: Scaffold(
        appBar: AppBar(title: Text('高级组件--滑块切换组件')),
        body: PageView.builder(
          onPageChanged: (index) {
            print('current page $index');
          },
          itemCount: items.length,
          itemBuilder: (context, index) {
            return Image.asset(items[index]);
          }
          )
      )
    );
  }
}

示例效果:

6.5 自定义组件
6.5.1 组合组件

按照从上到下、从左到右的方式去拆解布局结构即可。

6.5.2 自绘组件

在Flutter中创建自绘组件需要用到CustomPaint和CustomPainter两个类:CustomPaint在绘制阶段提供一个Canvas,即画布;CustomPainter在绘制阶段提供画笔,可配置画笔的颜色、样式和粗细等属性。

示例代码:

代码语言:javascript复制
import 'dart:math';
import 'package:flutter/material.dart';

void main() => runApp(PiePageWidget());

class PiePageWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '高级组件--自绘组件',
      home: Scaffold(
        appBar: AppBar(title: Text('高级组件--自绘组件')),
        body: Center(
          child: CustomPaint(
            size: Size(300, 300),
            painter: PiePainter()
          )
        )
      )
    );
  }
}

class PiePainter extends CustomPainter {
  Paint getPaint(Color color) {
    Paint paint = Paint();
    paint.color = color;
    return paint;
  }
  @override
  void paint(Canvas canvas, Size size) {
    double wheelSize = min(size.width, size.height) / 2;
    double nbElem = 6;
    double radius = (2 * pi) / nbElem;
    Rect boundingRect = Rect.fromCircle(center: Offset(wheelSize, wheelSize), radius: wheelSize);
    canvas.drawArc(boundingRect, 0, radius, true, getPaint(Colors.red));
    canvas.drawArc(boundingRect, radius, radius, true, getPaint(Colors.black38));
    canvas.drawArc(boundingRect, radius * 2, radius, true, getPaint(Colors.green));
    canvas.drawArc(boundingRect, radius * 3, radius, true, getPaint(Colors.amber));
    canvas.drawArc(boundingRect, radius * 4, radius, true, getPaint(Colors.blue));
    canvas.drawArc(boundingRect, radius * 5, radius, true, getPaint(Colors.purple));
  }
  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return true;//是否需要执行重绘
  }
}

示例效果:

创建Flutter自绘组件时,可以做以下两点性能优化:

1)尽可能利用好shouldRepaint()的返回值

如果绘制的内容不需要依赖外部状态,返回false即可;如果绘制过程需要依赖外部状态,可以在shouldRepaint()中判断依赖的状态是否改变,如果已改变,则返回true并执行重绘操作,反之则返回false不执行重绘;

2)绘制应尽可能多地进行分层

因为复杂的自绘组件都是由很多功能构成的,如果都写在一个方法中,不利于阅读,而且全部重绘带来的性能开销也很大。分层渲染可以降低视图渲染带来的性能开销。

无论是创建组合组件还是创建自绘组件,首先需要考虑如何将复杂的布局简化,把大问题拆分成若干小问题。

0 人点赞