我正在参与「启航计划」

自定义组件介绍

  • 何时需求自定义组件
    • Flutter供给的现有组件无法满足业务需求,或许需求封装一些通用组件,多处复用时
  • Flutter中自定义组件有三种办法:
    • 经过组合其它组件
    • 自绘组件
    • 完成RenderObject

自定义组件类型

  • 组合其它Widget
    • 该办法经过拼装其它组件来组合成一个新的组件。
  • 自绘组件
    • 若遇到无法经过现有的组件来完成需求的UI时,能够经过自绘组件的办法来完成,例如一个颜色突变的圆形进度条。
  • 完成RenderObject
    • Flutter供给的本身具有UI外观的组件,如TextImage都是经过相应的RenderObject渲染出来的。

自绘组件

  • 关于一些杂乱或不规则的UI,或许无法经过组合其它组件的办法来完成
    • 比如需求一个突变的圆形进度条、一个棋盘等。
  • 几乎一切的UI体系都供给一个自绘UI的接口,该接口通常会供给一块2D画布Canvas
    • Canvas内部封装一些基本制作的API,开发者能够经过Canvas制作各种自定义图形。
    • 在Flutter中,供给了一个CustomPaint 组件,可结合画笔CustomPainter来完成自定义图形制作。

CustomPaint类

  • CustomPaint结构函数:

    CustomPaint({
      Key key,
      this.painter, 
      this.foregroundPainter,
      this.size = Size.zero, 
      this.isComplex = false, 
      this.willChange = false, 
      Widget child, //子节点,能够为空
    })
    
  • 论述一下参数意义
    • painter: 背景画笔,会显现在子节点后面;
    • foregroundPainter: 远景画笔,会显现在子节点前面
    • size:当child为null时,代表默许制作区域巨细,假如有child则忽略此参数,画布尺寸则为child尺寸。假如有child可是想指定画布为特定巨细,能够使用SizeBox包裹CustomPaint完成。
    • isComplex:是否杂乱的制作,假如是,Flutter会使用一些缓存策略来减少重复渲染的开销。
    • willChange:和isComplex配合使用,当启用缓存时,该属性代表鄙人一帧中制作是否会改动。

示例:五子棋盘

  • 下面是五子棋游戏中棋盘和棋子的制作来演示自绘UI的过程:
    import 'package:flutter/material.dart';
    import 'dart:math';
    class CustomPaintRoute extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Center(
          child: CustomPaint(
            size: Size(300, 300), //指定画布巨细
            painter: MyPainter(),
          ),
        );
      }
    }
    class MyPainter extends CustomPainter {
      @override
      void paint(Canvas canvas, Size size) {
        double eWidth = size.width / 15;
        double eHeight = size.height / 15;
        //画棋盘背景
        var paint = Paint()
          ..isAntiAlias = true
          ..style = PaintingStyle.fill //填充
          ..color = Color(0x77cdb175); //背景为纸黄色
        canvas.drawRect(Offset.zero & size, paint);
        //画棋盘网格
        paint
          ..style = PaintingStyle.stroke //线
          ..color = Colors.black87
          ..strokeWidth = 1.0;
        for (int i = 0; i <= 15; ++i) {
          double dy = eHeight * i;
          canvas.drawLine(Offset(0, dy), Offset(size.width, dy), paint);
        }
        for (int i = 0; i <= 15; ++i) {
          double dx = eWidth * i;
          canvas.drawLine(Offset(dx, 0), Offset(dx, size.height), paint);
        }
        //画一个黑子
        paint
          ..style = PaintingStyle.fill
          ..color = Colors.black;
        canvas.drawCircle(
          Offset(size.width / 2 - eWidth / 2, size.height / 2 - eHeight / 2),
          min(eWidth / 2, eHeight / 2) - 2,
          paint,
        );
        //画一个白子
        paint.color = Colors.white;
        canvas.drawCircle(
          Offset(size.width / 2 + eWidth / 2, size.height / 2 - eHeight / 2),
          min(eWidth / 2, eHeight / 2) - 2,
          paint,
        );
      }
      //在实践场景中正确利用此回调能够避免重绘开销,本示例咱们简单的回来true
      @override
      bool shouldRepaint(CustomPainter oldDelegate) => true;
    }
    

制作功能问题

  • 在完成自绘控件时应该考虑到功能开销,下面是关于功能优化的建议:
    • 利用好shouldRepaint回来值;在UI树从头build时,控件在制作前都会先调用该办法以确定是否有必要重绘;若制作的UI不依靠外部状况,就应该始终回来false,因为外部状况改动导致从头build时不会影响UI外观;
    • 若制作依靠外部状况,则应该在shouldRepaint中判别依靠的状况是否改动,假如已改动则应回来true来重绘,反之则应回来false不需求重绘。
    • 制作尽或许多的分层;在上面五子棋示例中,将棋盘和棋子的制作放在了一起,这样会有一个问题:因为棋盘始终是不变的,用户每次落子时变的只是棋子,可是假如依照上面的代码来完成,每次制作棋子时都要从头制作一次棋盘,是没必要的。优化的办法就是将棋盘独自抽为一个组件,并设置其shouldRepaint回调值为false,将棋盘组件作为背景。将棋子的制作放到另一个组件中,这样每次落子时只需求制作棋子。
  • 自绘控件理论上能够完成任何2D图形外观
    • Flutter供给的一切组件最终都是经过调用Canvas制作出来的,只不过制作的逻辑被封装起来。