通过RenderObject的方式来进行UI绘制、动画调度和事件处理。本篇通过这种方式实现一个打勾动画Widget。
class SSLDoneWidget extends LeafRenderObjectWidget{
//线条宽度
final double strokeWidth;
//轮廓颜色或填充色
final Color color;
//如果为true则没有填充色,color为轮廓色,反之依然
final bool outline;
final bool value;
final ValueChanged<bool>? onChanged;
const SSLDoneWidget({
Key? key,
this.strokeWidth = 2.0,
this.color = Colors.green,
this.outline = false,
this.onChanged,
this.value = false
}):super(key: key);
@override
RenderObject createRenderObject(BuildContext context) {
// TODO: implement createRenderObject
return SSLRenderDoneObject(
strokeWidth: strokeWidth,
color: color,
outline: outline,
onChanged: onChanged,
value: value
);
}
@override
void updateRenderObject(BuildContext context, covariant SSLRenderDoneObject renderObject) {
// TODO: implement updateRenderObject
// super.updateRenderObject(context, renderObject);
if (renderObject.value != value){
//执行动画
debugPrint("ssl chick checkbox 5\n");
renderObject.animationStatus = value ? AnimationStatus.forward : AnimationStatus.reverse;
}
renderObject
..strokeWidth = strokeWidth
..color = color
..outline = outline
..onChanged = onChanged
..value = value;
}
}
DoneWidget有两种outline模式,该模式背景没有填充色,此时color表示轮廓线条的颜色;如果是非outline模式,则color表示填充的背景色,勾选为白色。
接下来实现RenderDoneObject。由于需要使用动画,直接使用上篇中SSLRenderObjectAnimationMixin:
class SSLRenderDoneObject extends RenderBox with SSLRenderObjectAnimationMixin{
double strokeWidth;
Color color;
bool outline;
bool value;
ValueChanged<bool>? onChanged;
SSLRenderDoneObject({
required this.strokeWidth,
required this.color,
required this.outline,
this.onChanged,
this.value = false,
});
@override
Duration get duration => const Duration(milliseconds: 300);
int pointerId = -1;
@override
void doPaint(PaintingContext context, Offset offset) {
// TODO: implement doPaint
Curve curve = Curves.easeIn;
final progressTem = curve.transform(progress);
Rect rect = offset & size;
final paint = Paint()
..isAntiAlias = true
..style = outline ? PaintingStyle.stroke : PaintingStyle.fill
..color = color;
if (outline){
paint.strokeWidth = strokeWidth;
rect = rect.deflate(strokeWidth/2);
}
//画圆背景
context.canvas.drawCircle(rect.center, rect.shortestSide / 2, paint);
paint
..style = PaintingStyle.stroke
..color = outline ? color : Colors.white
..strokeWidth = strokeWidth;
final path = Path();
Offset firstOffset = Offset(rect.left + rect.width/6, rect.top + rect.height / 2.1);
final secondOffset = Offset(rect.left + rect.width/2.5, rect.bottom - rect.height/3.3);
path.moveTo(firstOffset.dx, firstOffset.dy);
const adjustProgress = 0.6;
debugPrint("ssl current progress$progress, tem $progressTem");
//画勾
if (progressTem < adjustProgress){
//第一个点到第二个点的连线做动画(第二个点不停的变)
Offset secondOffset0 = Offset.lerp(firstOffset, secondOffset, progressTem / adjustProgress)!;
path.lineTo(secondOffset0.dx, secondOffset0.dy);
}else{
path.lineTo(secondOffset.dx, secondOffset.dy);
final lastOffset = Offset(
rect.right - rect.width/5,
rect.top + rect.height / 3.5
);
Offset lastOffset0 = Offset.lerp(secondOffset, lastOffset, (progress - adjustProgress)/(1 - adjustProgress))!;
path.lineTo(lastOffset0.dx, lastOffset0.dy);
}
context.canvas.drawPath(path, paint..style = PaintingStyle.stroke);
}
@override
void performLayout() {
// TODO: implement performLayout
// super.performLayout();
size = constraints.constrain(
constraints.isTight ? Size.infinite : const Size(25, 25),
);
}
@override
bool hitTestSelf(Offset position) {
// TODO: implement hitTestSelf
return true;
}
@override
void handleEvent(PointerEvent event, covariant HitTestEntry<HitTestTarget> entry) {
// TODO: implement handleEvent
// super.handleEvent(event, entry);
if (event.down){
pointerId = event.pointer;
}else if (pointerId == event.pointer){
onChanged?.call(!value);
}
}
}
测试示例:
class SSLDoneWidgetRoute extends StatefulWidget{
const SSLDoneWidgetRoute({Key? key}):super(key: key);
@override
State<StatefulWidget> createState() {
// TODO: implement createState
return SSLDoneWidgetRouteState();
}
}
class SSLDoneWidgetRouteState extends State<SSLDoneWidgetRoute>{
bool checked = false;
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
appBar: AppBar(
title: const Text("SSL Done Widget"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SSLDoneWidget(
color: Colors.purple,
value: checked,
strokeWidth: 3,
onChanged: onChangedAction,
),
SSLDoneWidget(
outline: true,
value: checked,
color: Colors.green,
onChanged: onChangedAction
)
],
),
),
);
}
void onChangedAction(value){
debugPrint("ssl chick $value");
setState(() {
checked = value;
});
}
}
注意:
- 对动画应用了easeIn曲线,可以看到RenderObject中对动画应用曲线,曲线本质就是对动画的进度加一层映射,通过不同的映射规则就可以控制动画在不同阶段的快慢。
- 重写了SSLRenderObjectAnimationMixin中的duration,用于指定动画时长。
- adjustProgress作用是控制打勾动画的两部分的时长,第一部分背景色占总动画时长60%,第二部分是连线占40%。