Flutter use Hero transition between Custom Painter
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.
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),
),
),
),
],
),
),
),
);
}
}
xenos92
Updated on November 26, 2022Comments
-
xenos92 over 1 year
Want I want
Hello, I want to realize a function in my application which is based on brainstorming applications.
What I do
Here is my application
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 about 2 yearsi dont think you could do that easily with
Hero
widget -
xenos92 about 2 yearsHero 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 about 2 yearsyou can use your
CustomPainter
for all the levels of your tree, going back can be implemented by means ofLocalHistoryRoute
mixin which is used byMaterialPageRoute
-
-
xenos92 about 2 yearsThanks for your time, I'll try this.