0

I want to implement a CustomPainter that draws a clock given images of hour, minute, and second hands, as well as an image of an empty clock face. To account for the possibility of the image being too large or small, I used drawImageRect to resize it to a specific size before drawing it. However, when I drew a second hand on an empty clock as an experiment, I couldn't draw a second hand. I thought the image of the second hand was the problem, so I tried using canvas.drawCircle, but this was also not drawn. How can I draw the hands of a clock on an empty clock?

Below is the code I used. I used the image stored in the asset, and converted to ui.Image and imported to this page.

class CustomClockPage extends StatefulWidget {
  final ui.Image clockImage;
  final ui.Image secondImage;

  const CustomClockPage({
    required this.clockImage,
    required this.secondImage,
    Key? key,
  }) : super(key: key);

  @override
  State<CustomClockPage> createState() => _CustomClockPageState();
}

class _CustomClockPageState extends State<CustomClockPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          Positioned(
            left: 50,
            top: 100,
            child: CustomClock(
              clockImage: widget.clockImage,
              secondImage: widget.secondImage,
            ),
          ),
        ],
      ),
    );
  }
}

class CustomClock extends StatefulWidget {
  final ui.Image clockImage;
  final ui.Image secondImage;

  const CustomClock({
    required this.clockImage,
    required this.secondImage,
    Key? key,
  }) : super(key: key);

  @override
  State<CustomClock> createState() => _CustomClockState();
}

class _CustomClockState extends State<CustomClock> {
  late DateTime now;

  @override
  void initState() {
    super.initState();

    now = DateTime.now();
    Timer.periodic(const Duration(seconds: 1), (timer) {
      setState(() {
        now = DateTime.now();
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: MediaQuery.of(context).size.width / 2,
      height: ((MediaQuery.of(context).size.width / 2) / widget.clockImage.width) * widget.clockImage.height,
      child: CustomPaint(
        painter: WatchPainter(
          now: now,
          width: MediaQuery.of(context).size.width / 2,
          clockImage: widget.clockImage,
          secondImage: widget.secondImage,
        ),
      ),
    );
  }
}

class WatchPainter extends CustomPainter {
  final DateTime now;
  final double width;
  final ui.Image clockImage;
  final ui.Image secondImage;

  WatchPainter({
    required this.now,
    required this.width,
    required this.clockImage,
    required this.secondImage,
  });

  @override
  void paint(Canvas canvas, Size size) async {
    final xCenter = size.width / 2;
    final yCenter = size.height / 2;
    final reduceRatio = width /clockImage.width.toDouble();

    renderClock(canvas, size);
    renderHands(canvas, size, xCenter, yCenter, reduceRatio);
  }

  renderClock(Canvas canvas, Size size) {
    final imageSize = Size(clockImage.width.toDouble(), clockImage.height.toDouble());

    final src = Rect.fromLTWH(0, 0, imageSize.width, imageSize.height);
    final dst = Rect.fromLTWH(0, 0, size.width, size.height);

    canvas.drawImageRect(clockImage, src, dst, Paint());
  }

  renderHands(Canvas canvas, Size size, double xCenter, double yCenter, double reduceRatio) {
    final secondRotation = now.second * (2 * pi / 60);

    canvas.save();
    canvas.rotate(secondRotation);

    final secondOriginSize = Size(secondImage.width.toDouble(), secondImage.height.toDouble());
    final secondSrc = Rect.fromLTWH(yCenter, xCenter, secondOriginSize.width, secondOriginSize.height);
    final secondDst = Rect.fromLTWH(yCenter, xCenter, (secondOriginSize.width*reduceRatio), (secondOriginSize.height*reduceRatio));
    canvas.drawImageRect(secondImage, secondSrc, secondDst, Paint());

    canvas.restore();
  }

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

I wondered if the image itself was a problem, so I drew a basic function like drawCircle on the image, but it was not drawn.

  • the way you try to rotate the canvas is most likely broken, check https://www.flutterclutter.dev/flutter/tutorials/2022-04-17-rotate-custom-paint-canvas-flutter/ – pskink Mar 03 '23 at 08:23
  • btw you can get some ideas from this gist: https://gist.github.com/pskink/d1f591eca19359c09d38bf8cc35df4ca – pskink Mar 03 '23 at 08:26
  • thanks so much! I solved it by moving the canvas to the center and rotating it – hyeongrae-kim Mar 03 '23 at 08:34
  • I also want to implement it in a way that draws directly like the code you sent me, but I think it will be difficult because I have to implement the clock hands using a lot of images. – hyeongrae-kim Mar 03 '23 at 08:56
  • no, you need just one image per hand, for normal clock you need 3 images then (hour, minute, second) – pskink Mar 03 '23 at 08:58
  • In that code, What does anchor stand for in composeMatrixFromOffsets? – hyeongrae-kim Mar 05 '23 at 07:56
  • see https://gist.github.com/pskink/10c8fd372113f0b36570db96d58d818e#file-flow_painter-dart-L66 also there is an image on the bottom explaining this – pskink Mar 05 '23 at 09:21
  • if still unclear the same is explained in other words in `flame` package: https://pub.dev/documentation/flame/latest/components/PositionComponent-class.html - find the paragraph starting with "The main properties of this class is " – pskink Mar 05 '23 at 09:27
  • At first, I didn't fully understand the composeMatrixFromOffsets, but thanks to the link you sent me, I fully understand now. I wanted to reflect the actual time in the code you sent me as an example, so I put the actual time in the rotation part using DateTime instead of the animation controller value, and it works well! Thank you so much! – hyeongrae-kim Mar 05 '23 at 16:30

0 Answers0