Flutter-自绘组件DoneWidget

204 阅读2分钟

通过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;
    });
  }
}

注意:

  1. 对动画应用了easeIn曲线,可以看到RenderObject中对动画应用曲线,曲线本质就是对动画的进度加一层映射,通过不同的映射规则就可以控制动画在不同阶段的快慢。
  2. 重写了SSLRenderObjectAnimationMixin中的duration,用于指定动画时长。
  3. adjustProgress作用是控制打勾动画的两部分的时长,第一部分背景色占总动画时长60%,第二部分是连线占40%。