Flutter use Hero transition between Custom Painter

229

This is sample static code - using AnimatedPositioned and AnimatedContainer, and i write code for all component in a single file in a single class, with help of this you can create dynamically, this is just an idea.

enter image description here

import 'package:flutter/material.dart';

class GlobeExample extends StatefulWidget {
  const GlobeExample({Key? key}) : super(key: key);

  @override
  State<GlobeExample> createState() => _GlobeExampleState();
}

class _GlobeExampleState extends State<GlobeExample> {
  bool isExpand = false;
  double sizeValue = 75.0, lineSize = 0.0;
  int lineDuration = 1000;
  @override
  Widget build(BuildContext context) {
    final Size size = MediaQuery.of(context).size;
    return Scaffold(
      backgroundColor: Colors.white,
      body: SizedBox(
        height: size.height,
        width: size.width,
        child: GestureDetector(
          onTap: () => setState(() {
            isExpand = !isExpand;
            isExpand == true ? sizeValue = 100.0 : sizeValue = 75.0;
            isExpand == true ? lineDuration = 800 : lineDuration = 1000;
            isExpand == true ? lineSize = 400.0 : lineSize = 0.0;
          }),
          child: Stack(
            alignment: Alignment.center,
            children: [
              AnimatedContainer(
                duration: const Duration(seconds: 1),
                height: isExpand == true ? 400.0 : 250.0,
                width: isExpand == true ? 400.0 : 250.0,
                decoration: BoxDecoration(
                  border: Border.all(width: 2.5, color: Colors.blueGrey),
                  borderRadius: BorderRadius.circular(250.0),
                ),
              ),
              Container(
                height: 100.0,
                width: 100.0,
                decoration: BoxDecoration(
                  color: Colors.blueGrey,
                  borderRadius: BorderRadius.circular(100.0),
                ),
              ),
              AnimatedContainer(
                duration: Duration(milliseconds: lineDuration),
                height: lineSize,
                width: 2.0,
                decoration: BoxDecoration(
                  border: Border.all(width: 2.5, color: Colors.blueGrey),
                ),
              ),
              AnimatedContainer(
                duration: Duration(milliseconds: lineDuration),
                height: 2.0,
                width: lineSize,
                decoration: BoxDecoration(
                  border: Border.all(width: 2.5, color: Colors.blueGrey),
                ),
              ),
              Transform.rotate(
                angle: 0.77,
                child: AnimatedContainer(
                  duration: Duration(milliseconds: lineDuration),
                  height: lineSize,
                  width: 2.0,
                  decoration: BoxDecoration(
                    border: Border.all(width: 2.5, color: Colors.blueGrey),
                  ),
                ),
              ),
              Transform.rotate(
                angle: -0.77,
                child: AnimatedContainer(
                  duration: Duration(milliseconds: lineDuration),
                  height: lineSize,
                  width: 2.0,
                  decoration: BoxDecoration(
                    border: Border.all(width: 2.5, color: Colors.blueGrey),
                  ),
                ),
              ),
              AnimatedPositioned(
                duration: const Duration(seconds: 1),
                top: isExpand == true ? 200.0 : 290.0,
                child: AnimatedContainer(
                  duration: const Duration(seconds: 1),
                  height: sizeValue,
                  width: sizeValue,
                  decoration: BoxDecoration(
                    color: Colors.blueGrey,
                    borderRadius: BorderRadius.circular(sizeValue),
                  ),
                ),
              ),
              AnimatedPositioned(
                duration: const Duration(seconds: 1),
                bottom: isExpand == true ? 200.0 : 290.0,
                child: AnimatedContainer(
                  duration: const Duration(seconds: 1),
                  height: sizeValue,
                  width: sizeValue,
                  decoration: BoxDecoration(
                    color: Colors.blueGrey,
                    borderRadius: BorderRadius.circular(sizeValue),
                  ),
                ),
              ),
              AnimatedPositioned(
                duration: const Duration(seconds: 1),
                right: isExpand == true ? -38.0 : 50.0,
                child: AnimatedContainer(
                  duration: const Duration(seconds: 1),
                  height: sizeValue,
                  width: sizeValue,
                  decoration: BoxDecoration(
                    color: Colors.blueGrey,
                    borderRadius: BorderRadius.circular(sizeValue),
                  ),
                ),
              ),
              AnimatedPositioned(
                duration: const Duration(seconds: 1),
                left: isExpand == true ? -38.0 : 50.0,
                child: AnimatedContainer(
                  duration: const Duration(seconds: 1),
                  height: sizeValue,
                  width: sizeValue,
                  decoration: BoxDecoration(
                    color: Colors.blueGrey,
                    borderRadius: BorderRadius.circular(sizeValue),
                  ),
                ),
              ),
              AnimatedPositioned(
                duration: const Duration(seconds: 1),
                bottom: isExpand == true ? 256.0 : 324.0,
                right: isExpand == true ? 20.0 : 80.0,
                child: AnimatedContainer(
                  duration: const Duration(seconds: 1),
                  height: sizeValue,
                  width: sizeValue,
                  decoration: BoxDecoration(
                    color: Colors.blueGrey,
                    borderRadius: BorderRadius.circular(sizeValue),
                  ),
                ),
              ),
              AnimatedPositioned(
                duration: const Duration(seconds: 1),
                bottom: isExpand == true ? 256.0 : 324.0,
                left: isExpand == true ? 20.0 : 80.0,
                child: AnimatedContainer(
                  duration: const Duration(seconds: 1),
                  height: sizeValue,
                  width: sizeValue,
                  decoration: BoxDecoration(
                    color: Colors.blueGrey,
                    borderRadius: BorderRadius.circular(sizeValue),
                  ),
                ),
              ),
              AnimatedPositioned(
                duration: const Duration(seconds: 1),
                top: isExpand == true ? 256.0 : 324.0,
                right: isExpand == true ? 20.0 : 80.0,
                child: AnimatedContainer(
                  duration: const Duration(seconds: 1),
                  height: sizeValue,
                  width: sizeValue,
                  decoration: BoxDecoration(
                    color: Colors.blueGrey,
                    borderRadius: BorderRadius.circular(sizeValue),
                  ),
                ),
              ),
              AnimatedPositioned(
                duration: const Duration(seconds: 1),
                top: isExpand == true ? 256.0 : 324.0,
                left: isExpand == true ? 20.0 : 80.0,
                child: AnimatedContainer(
                  duration: const Duration(seconds: 1),
                  height: sizeValue,
                  width: sizeValue,
                  decoration: BoxDecoration(
                    color: Colors.blueGrey,
                    borderRadius: BorderRadius.circular(sizeValue),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
Share:
229
xenos92
Author by

xenos92

Updated on November 26, 2022

Comments

  • xenos92
    xenos92 over 1 year

    Want I want

    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.

    • pskink
      pskink about 2 years
      i dont think you could do that easily with Hero widget
    • xenos92
      xenos92 about 2 years
      Hero widget or whatever, I just want the result. How then to be able to explore my tree structure and go back or go directly to a deep point in the tree structure?
    • pskink
      pskink about 2 years
      you can use your CustomPainter for all the levels of your tree, going back can be implemented by means of LocalHistoryRoute mixin which is used by MaterialPageRoute
  • xenos92
    xenos92 about 2 years
    Thanks for your time, I'll try this.