Flutter Framework怎么自定义渲染型 Widget

2021-09-27  本文已影响0人  笨笨11

本文将对Framework 的 Widget 、BuildOwner 、Element 、PaintingContext 、Layer 、PipelineOwner 、RenderObejct 进行总结,并动手实现一个渲染型 Widget (Render-Widget)。

Render-Widget 大致有三类:

Widget 间的继承关系如下图:

Widget、Element、RenderObject 间的对应关系如下:

其中,Element 与 RenderObject 间用的是虚线,因为它们间的对应关系是基于 RenderBox 系列下的一种建议 (不是强制)。

Sliver 系列就不是基于RenderBox,而是RenderSliver

通过Render-Widget#createRenderObject方法可以返回任意 RenderObject (如果你愿意)。

对于RenderBox系列来说,如果要自定义子类,根据自定义子类子节点模型的不同需要有不同的处理:

下面,我们一步步地来实现上面提到的评分组件。

Custom Leaf Render Widget


首先,我们来实现评分组件里的五星部分 (ScoreStar Widget):

LeafRenderObjectWidget

ScoreStar作为叶子节点,继承自LeafRenderObjectWidget,并实现了2个重要方法:createRenderObjectupdateRenderObject

1  class ScoreStar extends LeafRenderObjectWidget {
2    final Color backgroundColor;
3    final Color foregroundColor;
4    final double score;
5
6    ScoreStar(this.backgroundColor, this.foregroundColor, this.score);
7
8    @override
9    RenderObject createRenderObject(BuildContext context) {
10     return RenderScoreStar(backgroundColor, foregroundColor, score);
11   }
12
13   @override
14   void updateRenderObject(BuildContext context, covariant RenderScoreStar renderObject) {
15     renderObject
16       ..backgroundColor = backgroundColor
17       ..foregroundColor = foregroundColor
18       ..score = score;
19   }
20 }
复制代码

其中,updateRenderObject方法会在 Widget re-build 时调用,用于更新复用的 Render Object 的属性。

在本例中,score会随着用户点击不同的区域而变化,就需要通过updateRenderObject方法来更新RenderScoreStar#score,以便刷新 UI。

Leaf Render Object

从上一小节ScoreStar#createRenderObject可知,ScoreStar 对应的 Render Object 是RenderScoreStar

RenderScoreStar继承自RenderBox

如下代码:

1  // 为了缩减篇幅,精简了部分代码
2  //
3  class RenderScoreStar extends RenderBox {
4    Color _backgroundColor;
5    ...
6
7    Color _foregroundColor;
8    ...
9
10   double _score;
11   double get score => _score;
12   set score(double value) {
13     _score = value;
14    
15     // score 变化时需要re-paint
16     //
17     markNeedsPaint();
18   }
19
20   RenderScoreStar(this._backgroundColor, this._foregroundColor, this._score);
21
22   @override
23   bool get sizedByParent => false;
24
25   @override
26   void performLayout() {
27     double height = min(constraints.biggest.height, constraints.biggest.width / 5);
28     height = max(height, constraints.smallest.height);
29     size = Size(constraints.biggest.width, height);
30   }
31
32   // @override
33   // bool get sizedByParent => true;
34   //
35   // @override
36   // void performResize() {
37   //   double height = min(constraints.biggest.height, constraints.biggest.width / 5);
38   //   height = max(height, constraints.smallest.height);
39   //   size = Size(constraints.biggest.width, height);
40   // }
41
42   @override
43   double computeMaxIntrinsicWidth(double height) {
44     return constraints.biggest.width;
45   }
46
47   @override
48   double computeMaxIntrinsicHeight(double width) {
49     double height = min(constraints.biggest.height, constraints.biggest.width / 5);
50     height = max(height, constraints.smallest.height);
51
52     return height;
53   }
54
55   @override
56   bool hitTestSelf(Offset position) {
57     return true;
58   }
59
60   @override
61   void paint(PaintingContext context, Offset offset) {
62     void _backgroundStarPainter(PaintingContext context, Offset offset) {
63       _starPainter(context, offset, backgroundColor);
64     }
65
66     void _foregroundStarPainter(PaintingContext context, Offset offset) {
67       _starPainter(context, offset, foregroundColor);
68     }
69
70     _backgroundStarPainter(context, offset);
71     context.pushClipRect(
72       needsCompositing,
73       offset,
74       Rect.fromLTRB(0, 0, size.width * score / 5, size.height),
75       _foregroundStarPainter
76     );
77   }
78
79   void _starPainter(PaintingContext context, Offset offset, Color color) {
80     Paint paint = Paint();
81     paint.color = color;
82     paint.style = PaintingStyle.fill;
83
84     double radius = min(size.height / 2, size.width/ (2 * 5));
85 
86     Path path = Path();
87     _addStarLine(radius, path);
88     for (int i = 0; i < 4; i++) {
89       path = path.shift(Offset(radius * 2, 0.0));
90       _addStarLine(radius, path);
91     }
92
93     path = path.shift(offset);
94     path.close();
95 
96     context.canvas.drawPath(path, paint);
97   }
98 
99   void _addStarLine(double radius, Path path) {
100    ...
101  }
102 }
复制代码

至此,RenderScoreStar基本完成,完整代码请参见 [ Github:Score ]

动态评分


如下图,我们希望评分组件不仅能展示分数,还能评分:

在 Flutter UI 中,一个重要的思想就是:『 组合 』。

为了实现上图所示效果,只需组合StatefulWidget +ScoreStar即可:

1  typedef ScoreCallback = void Function(double score);
2
3  class Score extends StatefulWidget {
4    final double score;
5    final ScoreCallback callback;
6
7    const Score({Key key, this.score = 0, this.callback}) : super(key: key);
8
9    @override
10   _ScoreState createState() => _ScoreState();
11 }
12
13 class _ScoreState extends State<Score> {
14
15   double score;
16
17   @override
18   void initState() {
19     super.initState();
20
21     score = widget.score ?? 0;
22   }
23
24   @override
25   void didUpdateWidget(Score oldWidget) {
26     super.didUpdateWidget(oldWidget);
27
28     score = widget.score ?? 0;
29   }
30
31   @override
32   Widget build(BuildContext context) {
33     void _changeScore(Offset offset) {
34       Size _size = context.size;
35       double offsetX = min(offset.dx, _size.width);
36       offsetX = max(0, offsetX);
37
38       setState(() {
39         score = double.parse(((offsetX / _size.width) * 5).toStringAsFixed(1));
40       });
41
42       if (widget.callback != null) {
43         widget.callback(score);
44       }
45     }
46
47     return GestureDetector(
48       child: ScoreStar(Colors.grey, Colors.amber, score),
49       onTapDown: (TapDownDetails details) {
50         _changeScore(details.localPosition);
51       },
52       onLongPressMoveUpdate:(LongPressMoveUpdateDetails details) {
53         _changeScore(details.localPosition);
54       },
55     );
56   }
57 }
复制代码

代码比较简单,就不赘述了。

其中的关键还是上节介绍的RenderScoreStar#hitTestSelf需要返回true

Custom MultiChild RenderObject Widget


我们希望通过自定义 MultiChild RenderObject Widget 实现如上图所示的效果。

没错,就是加了一个显示分数的 Text。

本来,这完全没必要通过自定义 MultiChild RenderObject Widget 来实现,一般的 Widget 组合即可。

我们只是为了实践自定义 MultiChild RenderObject Widget 才这么做的。

MultiChildRenderObjectWidget

RichScore继承自MultiChildRenderObjectWidget

在其初始化方法中,向父类传递了2个 children:ScoreText

重写了createRenderObject方法,以便返回RenderRichScore实例。 由于RenderRichScore没有属性,故无需重写updateRenderObject方法。

1  class RichScore extends MultiChildRenderObjectWidget {
2    RichScore({
3      Key key,
4      double score,
5      ScoreCallback callback,
6    }) : super(
7      key: key,
8      children: [
9        Score(score: score, callback: callback),
10       Text('$score分', style: TextStyle(fontSize: 28)),
11     ]
12   );
13
14   @override
15   RenderObject createRenderObject(BuildContext context) {
16     return RenderRichScore();
17   }
18 }
复制代码

RichScoreParentData

对于含有子节点的 RenderObject,一般都需要自定义自己的 ParentData 子类,用于辅助 layout。

class RichScoreParentData extends ContainerBoxParentData<RenderBox> {
  double scoreTextWidth;
}
复制代码

RichScoreParentData继承自ContainerBoxParentData

/// Abstract ParentData subclass for RenderBox subclasses that want the
/// ContainerRenderObjectMixin.
///
/// This is a convenience class that mixes in the relevant classes with
/// the relevant type arguments.
abstract class ContainerBoxParentData<ChildType extends RenderObject> extends BoxParentData with ContainerParentDataMixin<ChildType> { }
复制代码

ContainerBoxParentData是抽象类,但其 mixinContainerParentDataMixin

/// Parent data to support a doubly-linked list of children.
mixin ContainerParentDataMixin<ChildType extends RenderObject> on ParentData {
  /// The previous sibling in the parent's child list.
  ChildType previousSibling;
  /// The next sibling in the parent's child list.
  ChildType nextSibling;
}
复制代码

ContainerParentDataMixin在子节点间提供了双向链接的支持。

RichScoreParentData中定义了唯一一个属性:scoreTextWidth,其作用在后面再介绍。

MultiChild RenderObject

RenderRichScore继承自RenderBox并 minix 了ContainerRenderObjectMixin以及RenderBoxContainerDefaultsMixin

1  class RenderRichScore extends RenderBox with ContainerRenderObjectMixin<RenderBox, RichScoreParentData>,
2      RenderBoxContainerDefaultsMixin<RenderBox, RichScoreParentData>,
3      DebugOverflowIndicatorMixin {
4
5    RenderRichScore({
6      List<RenderBox> children,
7    }) {
8      addAll(children);
9    }
10
11   @override
12   bool get sizedByParent => false;
13
14   final double horizontalSpace = 10;
15   final double scoreTextWidthDifference = 10;
16
17   @override
18   bool hitTestChildren(BoxHitTestResult result, { Offset position }) {
19     assert(childCount == 2);
20
21     RenderBox scoreChild = firstChild;
22     return scoreChild?.hitTest(result, position: position) ?? false;
23   }
24
25   @override
26   void performLayout() {
27     assert(childCount == 2);
28
29     RenderBox scoreStarChild = firstChild;
30     RenderBox scoreTextChild = lastChild;
31
32     if (scoreStarChild == null || scoreTextChild == null) {
33       size = constraints.smallest;
34       return;
35     }
36
37     // infinity constraints
38     //
39     BoxConstraints descConstraints = BoxConstraints();
40     scoreTextChild.layout(descConstraints, parentUsesSize: true);
41
42     final RichScoreParentData descChildParentData = scoreTextChild.parentData as RichScoreParentData;
43     double descWidth = descChildParentData.scoreTextWidth;
44     if (descWidth == null) {
45       descWidth = scoreTextChild.size.width + scoreTextWidthDifference;
46       descChildParentData.scoreTextWidth = descWidth;
47     }
48
49     BoxConstraints scoreConstraints = BoxConstraints(
50       minWidth: 0,
51       maxWidth: max(constraints.maxWidth - descWidth - horizontalSpace, 0),
52       minHeight: 0,
53       maxHeight: constraints.maxHeight
54     );
55     scoreStarChild.layout(scoreConstraints, parentUsesSize: true);
56
57     descChildParentData.offset = Offset(
58       scoreStarChild.size.width + horizontalSpace,
59       (scoreStarChild.size.height - scoreTextChild.size.height) / 2
60     );
61
62     if (constraints.isTight) {
63       size = constraints.biggest;
64     }
65     else {
66       double width = min(constraints.biggest.width, scoreStarChild.size.width + descWidth + horizontalSpace);
67       width = max(constraints.smallest.width, width);
68
69       double height = max(scoreStarChild.size.height, scoreTextChild.size.height);
70       height = min(constraints.biggest.height, height);
71       height = max(constraints.smallest.height, height);
72
73       size = Size(width, height);
74     }
75   }
76  
77   ...
78
79   @override
80   double computeDistanceToActualBaseline(TextBaseline baseline) {
81     return defaultComputeDistanceToFirstActualBaseline(baseline);
82   }
83
84   @override
85   void paint(PaintingContext context, Offset offset) {
86     assert(childCount == 2);
87
88     if (childCount != 2) {
89       return;
90     }
91
92     defaultPaint(context, offset);
93   }
94
95   @override
96   void setupParentData(RenderObject child) {
97     if (child.parentData is! RichScoreParentData) {
98       child.parentData = RichScoreParentData();
99     }
100  }
101 }
复制代码

RichScoreParentData#scoreTextWidth

上面我们提到RichScoreParentData有唯一一个属性:scoreTextWidth。 那么它的作用是啥呢?

根据RenderRichScore的排版算法,先计算 text 的宽度,★★★★★ 的宽度等于 constraints.biggest.width - textWidth。

这个算法有点小问题:

由于 textWidth 会因分数的不同,而有细微的差异,最终导致 ★★★★★ 有点闪烁。

为了解决这个问题,我们将 textWidth 的宽度固定为首次计算的 text 宽度+10,并将其存储在RichScoreParentData中(上述代码第42~47行)。

这种解决方法不一定是最好的,这里主要是演示一下 ParentData 的作用。

至此,自定义 MultiChild RenderObject 基本完成了。

小结


本文通过实现评分组件,逐步实践了如何自定义 Leaf Render Widget 以及 MultiChild Render Widget。

在这过程中,自定义了 Widget 以及 Render Object,但并没有涉及 Element。

原因是 Element 作为 Widget 与 Render Object 间的桥梁,逻辑相对内聚、独立。 当自定义 Widget 继承自LeafRenderObjectWidgetSingleChildRenderObjectWidgetMultiChildRenderObjectWidget时,一般不用自定义 Element。

自定义 Leaf Render Widget,一般需要以下步骤:

自定义 MultiChild Render Widget,一般需要以下步骤:

写该文章围绕 Widget、Element 以及 RenderObject 展开讨论,对 Flutter Framework 有了一个简单的认识。

在此过程中对相关的 BuildOwner、PaintingContext、Layer 以及 PipelineOwner 等也进行了一定的讨论。

作者:峰之巅
链接:https://juejin.cn/post/7004305315814440968/
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

上一篇下一篇

猜你喜欢

热点阅读