简言
在使用Flutter
开发app的时候,是使用各种各样的Widget
组合绘制出的页面,一些最基础的如Container
、Padding
、Text
等等,由官方封装好的比较复杂的组件如AppBar
、日历选择器
等等。仅仅使用这些组件也确实能写出一些app,但是官方封装好的拓展性我觉得不太好,有些属性根本无法改变;另外一旦和设计稿出入比较大,那根不就玩不了了,所以这就涉及到自定义组件。Flutter
自定义组件的方式我了解到有三种,一是通过组合其它组件来达到你想要的效果;二是自绘,这块牵扯到Canvas
;三是实现RenderObject
。
1.原理基本介绍
Flutter
引擎会将我们所写组件生成一个Widget Tree
,而实际渲染出来的结果又有一个RenderObject Tree
,RenderObject
是继承Widget
的。在项目运行中`Widget Tree
是不断变化的,如果每次变化都要导致整个RenderObject Tree
变化,这对性能来说是一个很大的消耗,于是就有了一个Element Trre
,这就相当于一个中间层,由Widget
→Elment
→RenderObject
。每次Widget
变化时,与Element
做对比,找出最小最优的变化,作用于RenderObject
。我们创建的Widget
基本继承于StatelessWidget
、StatefulWidget
,他们仅负责属性、生命周期等的管理,最终也还是会继承于RenderObjectWidget
RenderObjectWidget
下面又有三个子类
SngleChildRenderObjectWidget
:RenderObject
只有一个 child
MultiChildRenderObjectWidget
:可以有多个 child
LeafRenderObjectWidget
:RenderObject
是一个叶子节点,没有child
官方所提供的组件很多都是继承singleChildRenderObjectWidget
,所以我们通常只能传一个child
,找到singleChildRenderObjectWidget
定义
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
,查看它的源码就知道,它是由DecoratedBox
、ConstrainedBox
、Transform
、Padding
、Align
等组件组成,其内部做了很多判断处理。这里实现一个自定义宽度的drawer
组件,创建一个类SmartDrawer
继承StatelessWidget
,并实现其具体方法
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时,可以考虑使用自绘组件来实现。下面是一个自绘的验证码组件示例
首先创建一个名为CodeReview
的StatefulWidget
class CodeReview extends StatefulWidget {
final String text;
final callback;
CodeReview({Key key, this.text, this.callback}) : super(key: key);
_CodeReviewState createState() => _CodeReviewState();
}
然后实现具体代码
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
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,
})......
......
......
传入painter
、foregroundPainter
对应类,在上述代码中,是创建一个类的CodePaint
继承CustomPainer
,重写paint
方法,然后设置画笔的属性,最后调用对应的Canvas
api即可,这里使用的是drawPoints
,还有其他的,如drawCircle
、drawLine
等等。
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
,通过RenderCustomPaint
的paint
方法将Canvas
和画笔Painter
连接起来实现自绘组件。这种方式操作起来实在比较麻烦,我也没怎么用过,所以就不提供示例展示了。