Flutter use Hero transition between Custom Painter

Want to do

Hello, I want to realize a function in my application which is based on brainstorming applications.

enter image description here


What I do

Here is my application

enter image description here

I have a wheel which is the first page, when I click on one of the “balls” it opens and shows me the second page.


My problem

I don’t see how to animate the transition like on the example application. I have to use the “Hero” transitions but I don’t see how to use that in a custom painter ?

I used 2 pages to do my ball opening because I want to be able to use my android back button to go back.


The code

Here my Views :

import 'package:provider/provider.dart';
import 'package:flutter/material.dart';
import 'package:touchable/touchable.dart';

class FirstView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {

    var controller = Provider.of<CompteurProvider>(context);

    return SafeArea(
      child: Scaffold(
        body: LayoutBuilder(
          builder: (context, constraints){
            return Column(
              children: [
                Hero(
                  tag: "Hero",
                  child: ChangeNotifierProvider<CompteurProvider>(
                    create: (context) => CompteurProvider(),
                    child: CanvasTouchDetector(
                        builder: (context) {
                          return CustomPaint(
                            size: Size(constraints.maxWidth, constraints.maxHeight),
                            painter: AreasPainter(
                              context,
                              controller.areas,
                            ),
                          );
                        }
                    ),
                  ),
                ),
              ],
            );
          },
        )
      ),
    );
  }
}

class SecondView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {

    var controller = Provider.of<CompteurProvider>(context);

    return SafeArea(
      child: Scaffold(
          body: LayoutBuilder(
            builder: (context, constraints){
              return Column(
                children: [
                  Hero(
                    tag: "Hero",
                    child: ChangeNotifierProvider<CompteurProvider>(
                      create: (context) => CompteurProvider(),
                      child: CanvasTouchDetector(
                          builder: (context) {
                            return CustomPaint(
                              size: Size(constraints.maxWidth, constraints.maxHeight),
                              painter: SubAreasPainter(
                                context,
                                controller.area!,
                              ),
                            );
                          }
                      ),
                    ),
                   ),
                ],
              );
            },
          )
      ),
    );
  }
}

Here my Painter:

import 'dart:math';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:touchable/touchable.dart';
import 'package:provider/provider.dart';

class AreasPainter extends CustomPainter
{
  final List<AreaEntity> areas;
  final BuildContext context;
  final CompteurProvider controller;
  final int dotsPerRing;

  AreasPainter(this.context, this.areas)
      : dotsPerRing = areas.length,
        controller = Provider.of<CompteurProvider>(context);

  final double dotRadius = 40;

  @override
  void paint(Canvas canvas, Size size) {
    TouchyCanvas touchyCanvas = TouchyCanvas(context,canvas);

    // General variable
    final Offset centerOffset = Offset(size.width / 2, size.height / 2);
    final double centerCircleRadius = size.height / 2 - dotRadius - 10;
    final double betweenAngle = 2 * pi / dotsPerRing;

    // Center circle  ----------------------------------------------------------
    drawCenterCircle(
      canvas: canvas,
      centerOffset: controller.circlePosition ?? centerOffset,
      radius: centerCircleRadius,
    );
    // Big Circle   ------------------------------------------------------------
    drawBigCircle(
      canvas: canvas,
      centerOffset: centerOffset,
      radius: centerCircleRadius,
    );

    // Balls around   ----------------------------------------------------------
    areas.forEach((area) {
      drawBall(
          canvas: canvas,
          touchyCanvas : touchyCanvas,
          centerOffset : centerOffset,
          offsetPositionOnCircle : getPositionOnCircle(betweenAngle: betweenAngle, id: area.id, radius: centerCircleRadius),
          subValues: area.subAreas,
          name: area.text,
          dotRadius: dotRadius,
          onTapAction: (){
            print(area.text);
            Navigator.pushNamed(context, RouterName.kTest2, arguments: area);
          }
      );
    });
  }

  void drawCenterCircle({
    required Canvas canvas,
    required Offset centerOffset,
    required double radius
  }){
    // -------------------------------------------------------------------------
    Paint outCirclePaint = Paint()
      ..color = AppColors.kcolor_bleu
      ..strokeCap = StrokeCap.round
      ..style = PaintingStyle.stroke
      ..strokeWidth = 2;
    canvas.drawCircle(centerOffset, 20, outCirclePaint);
    // -------------------------------------------------------------------------
    Paint inCirclePaint = Paint()
      ..color = AppColors.kcolor_bleu
      ..strokeCap = StrokeCap.round;
    canvas.drawCircle(centerOffset, 5, inCirclePaint);
    // -------------------------------------------------------------------------
    final text = measureText(text: "Areas", style: TextStyle(color: AppColors.kcolor_bleu, fontSize: 10));
    text.paint(canvas, centerOffset - Offset(text.width / 2, -25));
  }

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

class SubAreasPainter extends CustomPainter
{
  final AreaEntity area;
  final BuildContext context;
  final CompteurProvider controller;
  final int dotsPerRing;

  SubAreasPainter(this.context, this.area)
      : dotsPerRing = area.subAreas.length,
        controller = Provider.of<CompteurProvider>(context);

  final double dotRadius = 40;

  @override
  void paint(Canvas canvas, Size size) {
    TouchyCanvas touchyCanvas = TouchyCanvas(context,canvas);

    // General variable
    final Offset centerOffset = Offset(size.width / 2, size.height / 2);
    final double centerCircleRadius = size.height / 2 - dotRadius - 10;
    final double betweenAngle = 2 * pi / dotsPerRing;

    // Center circle  ----------------------------------------------------------
    drawBall(
        canvas: canvas,
        touchyCanvas : touchyCanvas,
        centerOffset : centerOffset,
        subValues: area.subAreas,
        name: area.text,
        dotRadius: dotRadius,
        onTapAction: (){}
    );

    // Big Circle   ------------------------------------------------------------
    drawBigCircle(
      canvas: canvas,
      centerOffset: centerOffset,
      radius: centerCircleRadius,
    );

    // Balls around   ----------------------------------------------------------
    area.subAreas.forEach((subArea) {
      drawBall(
          canvas: canvas,
          touchyCanvas : touchyCanvas,
          centerOffset : centerOffset,
          offsetPositionOnCircle : getPositionOnCircle(betweenAngle: betweenAngle, id: subArea.id, radius: centerCircleRadius),
          subValues: area.subAreas,
          name: subArea.text,
          dotRadius: dotRadius,
          onTapAction: (){
            Navigator.pop(context);
          }
      );
    });
  }

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


Offset getPositionOnCircle({
  required double betweenAngle,
  required int id,
  required double radius
}){
  double angleFromStart = betweenAngle * id;
  return Offset(radius * cos(angleFromStart), radius * sin(angleFromStart));
}

TextPainter measureText({
  required String text,
  TextStyle? style
})
{
  final textSpan = TextSpan(text: text, style: style != null ? style : TextStyle(color: Colors.white));
  final textPainter = TextPainter(text: textSpan, textDirection: TextDirection.ltr);
  textPainter.layout(minWidth: 0, maxWidth: double.maxFinite);
  return textPainter;
}

 drawBall({
  required Canvas canvas,
  required TouchyCanvas touchyCanvas,
  required Offset centerOffset,
  Offset offsetPositionOnCircle = const Offset(0,0),
  required List subValues,
  required String name,
  required dotRadius,
  Function? onTapAction
}){
  // Dot background --------------------------------------------------------
  Paint dotBackgroundPaint = Paint()
    ..color = AppColors.kBg_dark;
  touchyCanvas.drawCircle(centerOffset + offsetPositionOnCircle, dotRadius + 4, dotBackgroundPaint);

  // Dot -------------------------------------------------------------------
  Paint dotPaint = Paint()
    ..color = AppColors.kBg_light
    ..strokeCap = StrokeCap.round
    ..style = PaintingStyle.stroke
    ..strokeWidth = 2;

  touchyCanvas.drawCircle(centerOffset + offsetPositionOnCircle, dotRadius, dotPaint);

  // Text ------------------------------------------------------------------
  final textSize = measureText(
      text: name,
      style: TextStyle(
        color: AppColors.kFont_grey,
        fontSize: 8.0,
        fontWeight: FontWeight.bold,
      )
  );
  final Offset offsetCenterText = Offset(- textSize.width / 2.0 , - textSize.height / 2.0);
  textSize.paint(canvas, centerOffset + offsetPositionOnCircle + offsetCenterText);

  // Touch area ----------------------------------------------------------------
  Paint toucheAreaPaint = Paint()
    ..color = Colors.transparent;
  touchyCanvas.drawCircle(centerOffset + offsetPositionOnCircle, dotRadius, toucheAreaPaint,
      onTapDown: (t) {
        onTapAction!();
      }
  );
}

void drawBigCircle({
  required Canvas canvas,
  required Offset centerOffset,
  required double radius
}){
  Paint defaultCirclePaint = Paint()
    ..color = AppColors.kBg_normal.withOpacity(1)
    ..strokeCap = StrokeCap.round
    ..style = PaintingStyle.stroke
    ..strokeWidth = 3;
  canvas.drawCircle(centerOffset, radius, defaultCirclePaint);
}

The provider:

class CompteurProvider with ChangeNotifier {
  // Variables
  // ---------------------------------------------------------------------------
  Future? dataToLoad;
  late List<AreaEntity> areas = [];
  late double centerRingSize;
  late AreaEntity? area;

  // Constructor
  // ---------------------------------------------------------------------------
  CompteurProvider({
    this.area
  }){
    _initialise();
  }

  // Initialisation
  // ---------------------------------------------------------------------------
  Future _initialise() async
  {
    dataToLoad = await loadingData();
    notifyListeners();
  }

  Future loadingData() async
  {
    centerRingSize = 20;
    areas.add(AreaEntity(
        id: 0,
        text: "AREA 1",
        subAreas : [
          SubAreaEntity(id: 0, text: "E1"),
          SubAreaEntity(id: 1, text: "E2")
        ]
    ));
    areas.add(AreaEntity(
        id: 1,
        text: "AREA 2",
        subAreas : []
    ));
    areas.add(AreaEntity(
        id: 2,
        text: "AREA 3",
        subAreas : []
    ));
    areas.add(AreaEntity(
        id: 3,
        text: "AREA 4",
        subAreas : []
    ));
    areas.add(AreaEntity(
        id: 4,
        text: "AREA 5",
        subAreas : []
    ));

    if(area != null){
      area = area;
    }

    notifyListeners();
  }
}

The AreaEntity:

import 'package:equatable/equatable.dart';

class AreaEntity extends Equatable{
  int id;
  String text;
  List<SubAreaEntity> subAreas;

  AreaEntity({
    required this.id,
    required this.text,
    required this.subAreas,
  });

  @override
  List<Object> get props{
    return [
      id,
      text,
      subAreas
    ];
  }

  Map<String, dynamic> toJson() => {
    "id" : id,
    "text" : text,
    "subAreas" : subAreas,
  };
}

class SubAreaEntity extends Equatable{
  int id;
  String text;

  SubAreaEntity({
    required this.id,
    required this.text,
  });

  @override
  List<Object> get props{
    return [
      id,
      text,
    ];
  }

  Map<String, dynamic> toJson() => {
    "id" : id,
    "text" : text,
  };
}

Any guidance on the best way to accomplish this would be appreciated.