目录

Flutter自定义组件

在使用Flutter开发app的时候,是使用各种各样的Widget 组合绘制出的页面,一些最基础的如ContainerPaddingText 等等,由官方封装好的比较复杂的组件如AppBar日历选择器等等。仅仅使用这些组件也确实能写出一些app,但是官方封装好的拓展性我觉得不太好,有些属性根本无法改变;另外一旦和设计稿出入比较大,那根不就玩不了了,所以这就涉及到自定义组件。Flutter自定义组件的方式我了解到有三种,一是通过组合其它组件来达到你想要的效果;二是自绘,这块牵扯到Canvas;三是实现RenderObject

1.原理基本介绍

Flutter引擎会将我们所写组件生成一个Widget Tree,而实际渲染出来的结果又有一个RenderObject TreeRenderObject是继承Widget的。在项目运行中Widget Tree是不断变化的,如果每次变化都要导致整个RenderObject Tree变化,这对性能来说是一个很大的消耗,于是就有了一个Element Trre,这就相当于一个中间层,由WidgetElmentRenderObject。每次Widget变化时,与Element做对比,找出最小最优的变化,作用于RenderObject。我们创建的Widget基本继承于StatelessWidgetStatefulWidget,他们仅负责属性、生命周期等的管理,最终也还是会继承于RenderObjectWidget

RenderObjectWidget下面又有三个子类

SngleChildRenderObjectWidgetRenderObject只有一个 child

MultiChildRenderObjectWidget:可以有多个 child

LeafRenderObjectWidgetRenderObject是一个叶子节点,没有child

官方所提供的组件很多都是继承singleChildRenderObjectWidget,所以我们通常只能传一个child,找到singleChildRenderObjectWidget定义

1
2
3
4
5
6
7
abstract class SingleChildRenderObjectWidget extends RenderObjectWidget {
  const SingleChildRenderObjectWidget({ Key key, this.child }) : super(key: key);
  final Widget child;

  @override
  SingleChildRenderObjectElement createElement() => SingleChildRenderObjectElement(this);
}

可以看到每次创建一个SingleChildRenderObjectWidget就会调用``CreateElement生成一个对应的Elment,当然MultiChildRenderObjectWidget也是类似,这也就说明了他们一对一的关系。所以我们实现自定义组件必须得继承SingleChildRenderObjectWidget或者MultiChildRenderObjectWidget或者调用官方实现好的自绘类,这也是Flutter`引擎渲染的基本结构。

2..组合其它组件

这种方式是最基本最简单的方式,说白了就是将一些原有的、封装好的组件合并再封装一次。Flutter本身就有很多组合组件,比如常用的Container,查看它的源码就知道,它是由DecoratedBoxConstrainedBoxTransformPaddingAlign等组件组成,其内部做了很多判断处理。这里实现一个自定义宽度的drawer组件,创建一个类SmartDrawer继承StatelessWidget,并实现其具体方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class SmartDrawer extends StatelessWidget {
  final double elevation;
  final Widget child;
  final String semanticLabel;
  final double widthPercent;
  const SmartDrawer({
    Key key,
    this.elevation = 16.0,
    this.child,
    this.semanticLabel,
    this.widthPercent = 0.7,
  }) : 
   assert(widthPercent != null && widthPercent < 1.0 && widthPercent> 0.0)
   ,super(key: key);

  @override
  Widget build(BuildContext context) {
    assert(debugCheckHasMaterialLocalizations(context));
    String label = semanticLabel;
    final double _width = MediaQuery.of(context).size.width * widthPercent;
    
    return Semantics(
      scopesRoute: true,
      namesRoute: true,
      explicitChildNodes: true,
      label: label,
      child: ConstrainedBox(
        constraints: BoxConstraints.expand(width: _width),
        child: Material(
          elevation: elevation,
          child: child,
        ),
      ),
    );
  }
}

可以看到,上述组件就是将ConstrainedBox以及传入的child组合成一个新的组件,其他属性则是来控制样式的。这里需要特别说明的,最后返回的Semantics,它继承SingleChildRenderObjectWidget,它上面定义的属性有点带有语义化的意思,而它上面定义的child属性则是返回的组件,这样return Semantices{}实际就表示返回了带有一些语义的组件。

4.自绘组件

Flutter跨平台的实现方式就是在不同操作系统的上层绘制一个中间的UI系统,对不同的操作系统的API进行适配,风格统一,这样就能实现一个跨平台应用了,这也是和React Native的最大的差异以及优于React Native的地方。当你无法用现有的组件来描绘你所需要UI时,可以考虑使用自绘组件来实现。下面是一个自绘的验证码组件示例

首先创建一个名为CodeReviewStatefulWidget

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class CodeReview extends StatefulWidget {

  final String text;
  final callback;

  CodeReview({Key key, this.text, this.callback}) : super(key: key);

  _CodeReviewState createState() => _CodeReviewState();

}

然后实现具体代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
class _CodeReviewState extends State<CodeReview> {
	
  // 存放每个验证码字符上的横线的位置
  List<Offset> _lineOffsets = <Offset>[];

  // 验证码的长度,由外部传入
  int _textLength;
  // 验证码宽度
  double _width;
  // 验证码高度
  double _height;

  // 生成验证码上的横线遮挡物的位置
  void _randLines() {
    _lineOffsets.clear();
    for (var i = 0; i < _textLength; i++) {
      double fromX = randomBetween(10, 20).toDouble();
      double fromY = randomBetween(3, 33).toDouble();
      Offset from = Offset(fromX, fromY);
      _lineOffsets.add(from);

      double endX = randomBetween(60, _width.toInt() - 10).toDouble();
      double endY = randomBetween(3, 33).toDouble();
      Offset end = Offset(endX, endY);
      _lineOffsets.add(end);
    }
  }

  @override
  void initState() {
    super.initState();
    _textLength = widget.text.length ?? 4;
    _width = _textLength.toDouble() * 22;
    _height = 36;
    _randLines();
  }

  void _changeCode() {
    setState(() {
      _randLines();
    });
  }
	
  // 对每个字符进行随机rotate操作
  Container _subString(index) {
    return Container(
      padding: EdgeInsets.only(left: 2, right: 2, top: randomBetween(0, 14).toDouble()),
      child: Transform.rotate(
        angle: pi / randomBetween(3, 30) * randomBetween(0, 1),
        child: Text(widget.text[index], style: TextStyle(fontSize: randomBetween(20, 22).toDouble(), color: Color(0xFF4abdcc))),
      ),
    );
  }
	
 	// 描绘验证码上横线遮挡物
  Container _backLines() {
    return Container(
      width: _width,
      height: _height,
      child: CustomPaint(
        painter: CodePaint(_lineOffsets, Tool.randomColor()),
        foregroundPainter: CodePaint(_lineOffsets, Tool.randomColor()),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      width: _width,
      height: _height,
      color: Colors.grey[200],
      child: Stack(
        alignment: Alignment.center,
        children: <Widget>[
          _backLines(),
          _backLines(),
          GestureDetector(
            behavior: HitTestBehavior.opaque,
            onTap: () {
              _changeCode();
              widget.callback();
            },
            child: Container(
              width: _width,
              height: _height,
              child: Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: List.generate(_textLength, (int index) {
                  return _subString(index);
                }),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

可以看到上面用到了CustomPaint,这个类继承SingleChildRenderObjectWidget,这让我们直接能使用Canvas来绘制你所需要的UI

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class CustomPaint extends SingleChildRenderObjectWidget {
  /// Creates a widget that delegates its painting.
  const CustomPaint({
    Key key,
    this.painter,
    this.foregroundPainter,
    this.size = Size.zero,
    this.isComplex = false,
    this.willChange = false,
    Widget child,
  })......
    ......
    ......

传入painterforegroundPainter对应类,在上述代码中,是创建一个类的CodePaint继承CustomPainer,重写paint方法,然后设置画笔的属性,最后调用对应的Canvasapi即可,这里使用的是drawPoints,还有其他的,如drawCircledrawLine等等。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class CodePaint extends CustomPainter {
  final List<Offset> lineOffsets;
  final Color ranColor;
  CodePaint(this.lineOffsets, this.ranColor);

  @override
  void paint(Canvas canvas, Size size) {
    // debugPrint(canvas.runtimeType.toString());
    canvas.save();
    Paint _paint = Paint()
      ..color = ranColor // 画笔颜色
      ..strokeCap = StrokeCap.round // 画笔笔触类型
      ..isAntiAlias = true // 是否启动抗锯齿
      ..blendMode = BlendMode.exclusion // 颜色混合模式
      ..style = PaintingStyle.fill // 绘画风格,默认为填充
      ..colorFilter = ColorFilter.mode(ranColor, BlendMode.exclusion) // 颜色渲染模式,一般是矩阵效果来改变的,但是flutter中只能使用颜色混合模式
      ..maskFilter = MaskFilter.blur(BlurStyle.inner, 1.0) // 模糊遮罩效果
      ..filterQuality = FilterQuality.high // 颜色渲染模式的质量
      // ..strokeWidth = randomBetween(1, 3).toDouble(); // 暂时固定
      ..strokeWidth = 1;

    final pointMode = PointMode.lines;
    canvas.drawPoints(pointMode, lineOffsets, _paint);
    canvas.restore();
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return false;
  }
}

综上,自绘逐渐的核心是继承CustomPaint,然后使用Canvas实现UI的绘制

5.实现RenderObject

实现RenderObject即是自己重写一整套渲染树,首先得继承一个RenderObject,实现其内置仿佛,还得继承Element,实现其内置方法,复杂的自定义组件最终也是通过Canvas API来绘制的,而上面说的CustomPaint只是为了方便开发者封装的一个代理类,它直接继承自SingleChildRenderObjectWidget,通过RenderCustomPaintpaint方法将Canvas和画笔Painter连接起来实现自绘组件。这种方式操作起来实在比较麻烦,我也没怎么用过,所以就不提供示例展示了。