Update CustomPaint drawing - flutter

I have a Problem with the CustomPainter Widget. I want to draw a PieChart which works fine, then I added a Variable which draws the Chart to until it reached this angle. Now I want to animate it, I used the Future.delayed function and in there with setState I wanted to update the variable but that doesn't work unfortunately.
I am developing for the web. Thanks for helping!
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:stats/data/listLanguages.dart';
import 'painter/pieChartPainter.dart';
class Chart extends StatefulWidget {
ListLanguages listLanguages;
Chart({ListLanguages listLanguages}) {
if (listLanguages == null) {
listLanguages = new ListLanguages();
}
this.listLanguages = listLanguages;
}
#override
_ChartState createState() => _ChartState();
}
class _ChartState extends State<Chart> {
#override
Widget build(BuildContext context) {
List angles = widget.listLanguages.calcCounts();
int angle = 0;
Future.delayed(new Duration(seconds: 2), (){
setState(() {
angle = 360;
print("test");
});
});
return Column(
children: [
Spacer(flex: 2),
Row(
children: [
Spacer(),
CustomPaint(
size: Size.square(400),
painter: PieChartPainter(
angles: angles,
colors: new List()
..add(Colors.green)
..add(Colors.blue)
..add(Colors.brown)
..add(Colors.pink)
..add(Colors.orange)
..add(Colors.grey.shade700),
angle: angle,
),
),
Spacer(flex: 10),
],
),
Spacer(flex: 3),
],
);
}
}
import 'package:flutter/material.dart';
import 'package:vector_math/vector_math.dart' as vm;
class PieChartPainter extends CustomPainter {
List angles, colors;
int angle;
PieChartPainter(
{#required List angles, #required List colors, int angle: 360}) {
this.angles = angles;
this.colors = colors;
this.angle = angle;
}
#override
void paint(Canvas canvas, Size size) {
Paint p = new Paint();
double start = -90;
double tmp = 0;
for (int i = 0; i < angles.length; i++) {
if (i < 5) {
p.color = colors[i];
} else {
p.color = colors[5];
}
if (tmp + angles[i] < angle) {
canvas.drawArc(Rect.fromLTRB(0, 0, size.width, size.height),
vm.radians(start), vm.radians(angles[i]), true, p);
start = start + angles[i];
tmp = tmp + angles[i];
} else {
double x = angle - tmp;
canvas.drawArc(Rect.fromLTRB(0, 0, size.width, size.height),
vm.radians(start), vm.radians(x), true, p);
return;
}
}
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
this is the complete code I have to create the Pie Chart

You can copy paste run full code below
In your case, to work with Future.delayed, you can move logic from build to initState and use addPostFrameCallback
working demo change angle in 2, 4, 6 seconds and angle is 150, 250, 360
code snippet
class _ChartState extends State<Chart> {
int angle = 0;
List angles;
#override
void initState() {
angles = widget.listLanguages.calcCounts();
WidgetsBinding.instance.addPostFrameCallback((_) {
Future.delayed(Duration(seconds: 2), () {
setState(() {
angle = 150;
});
});
Future.delayed(Duration(seconds: 4), () {
setState(() {
angle = 250;
});
});
Future.delayed(Duration(seconds: 6), () {
setState(() {
angle = 360;
});
});
});
working demo
full code
import 'package:flutter/material.dart';
import 'package:vector_math/vector_math.dart' as vm;
class ListLanguages {
List calcCounts() {
return [10.0, 20.0, 100.0, 150.0, 250.0, 300.0];
}
}
class Chart extends StatefulWidget {
ListLanguages listLanguages;
Chart({ListLanguages listLanguages}) {
if (listLanguages == null) {
listLanguages = ListLanguages();
}
this.listLanguages = listLanguages;
}
#override
_ChartState createState() => _ChartState();
}
class _ChartState extends State<Chart> {
int angle = 0;
List angles;
#override
void initState() {
angles = widget.listLanguages.calcCounts();
WidgetsBinding.instance.addPostFrameCallback((_) {
Future.delayed(Duration(seconds: 2), () {
print("delay");
setState(() {
angle = 150;
print("test");
});
});
Future.delayed(Duration(seconds: 4), () {
print("delay");
setState(() {
angle = 250;
print("test");
});
});
Future.delayed(Duration(seconds: 6), () {
print("delay");
setState(() {
angle = 360;
print("test");
});
});
});
super.initState();
}
#override
Widget build(BuildContext context) {
return Column(
children: [
Spacer(flex: 2),
Row(
children: [
Spacer(),
CustomPaint(
size: Size.square(400),
painter: PieChartPainter(
angles: angles,
colors: List()
..add(Colors.green)
..add(Colors.blue)
..add(Colors.brown)
..add(Colors.pink)
..add(Colors.orange)
..add(Colors.grey.shade700),
angle: angle,
),
),
Spacer(flex: 10),
],
),
Spacer(flex: 3),
],
);
}
}
class PieChartPainter extends CustomPainter {
List angles, colors;
int angle;
PieChartPainter(
{#required List angles, #required List colors, int angle: 360}) {
this.angles = angles;
this.colors = colors;
this.angle = angle;
}
#override
void paint(Canvas canvas, Size size) {
Paint p = Paint();
double start = -90;
double tmp = 0;
for (int i = 0; i < angles.length; i++) {
if (i < 5) {
p.color = colors[i];
} else {
p.color = colors[5];
}
if (tmp + angles[i] < angle) {
canvas.drawArc(Rect.fromLTRB(0, 0, size.width, size.height),
vm.radians(start), vm.radians(angles[i]), true, p);
start = start + angles[i];
tmp = tmp + angles[i];
} else {
double x = angle - tmp;
canvas.drawArc(Rect.fromLTRB(0, 0, size.width, size.height),
vm.radians(start), vm.radians(x), true, p);
return;
}
}
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: Chart(
listLanguages: ListLanguages(),
),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}

I can not use your code so that I can run it (since it's only a small part) but what you need is:
Define an animation and animation controller in your state
Surround your CustomPainter with an "AnimatedBuilder" which will use this animation and will pass the value between 0 to 360 to your CustomPainter in 2 seconds.
Below is an example with comments (which you will have to take parts from and put in to your widget).
class Test extends StatefulWidget {
#override
_TestState createState() => _TestState();
}
// NOTE: You need to add "SingleTickerProviderStateMixin" for animation to work
class _TestState extends State<Test> with SingleTickerProviderStateMixin {
Animation _animation; // Stores animation
AnimationController _controller; // Stores controller
#override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 2),
); // Create a 2 second duration controller
_animation = IntTween(begin: 0, end: 360)
.animate(_controller); // Create the animation using controller with a tween from 0 to 360
WidgetsBinding.instance.addPostFrameCallback((_) {
_controller.forward(); // Start the animation when widget is displayed
});
}
#override
void dispose() {
_controller.dispose(); // Don't forget to dispose your controller
super.dispose();
}
#override
Widget build(BuildContext context) {
return AnimatedBuilder( // AnimatedBuilder using the animation
animation: _animation,
builder: (context, _){
return CustomPaint(
size: Size.square(400),
painter: PieChartPainter(
angles: angles,
colors: new List()
..add(Colors.green)
..add(Colors.blue)
..add(Colors.brown)
..add(Colors.pink)
..add(Colors.orange)
..add(Colors.grey.shade700),
angle: _animation.value, // Pass _animation.value (0 to 360) as your angle
),
);
},
);
}
}

Related

Flutter: How can I navigate to a screen with page turn / page curl effect in Flutter

how to make a Page Curl effect in a Flutter app as in this photo
I tried this package but it renders the screen as an image So can not interact with the screen as usual.
https://github.com/fluttercommunity/page_turn
I used the Flip_Widget package.
I would navigate from splash to home screen, as below:
import 'dart:math' as math;
import 'package:flip_widget/flip_widget.dart';
class SplashScreen extends StatefulWidget {
const SplashScreen({Key? key}) : super(key: key);
#override
State<SplashScreen> createState() => _SplashScreenState();
}
const double _MinNumber = 0.008;
double _clampMin(double v) {
if (v < _MinNumber && v > -_MinNumber) {
if (v >= 0) {
v = _MinNumber;
} else {
v = -_MinNumber;
}
}
return v;
}
class _SplashScreenState extends State<SplashScreen> {
late Timer _timer;
_goNext() async {
_flipKey.currentState?.startFlip();
List<double> percent = [
0.1,
0.2,
0.3,
0.4,
0.5,
0.6,
0.7,
0.8,
];
List<double> tilt = [
1.0,
1.5,
2.0,
2.0,
3.0,
-13,
-16,
-20,
];
for (int i = 0; i < percent.length; i++) {
await Future.delayed(Duration(milliseconds: 100));
_flipKey.currentState?.flip(percent[i], tilt[i]);
}
Navigator.pushReplacementNamed(context, Routes.home);
}
_startDelay() {
_timer = Timer(
const Duration(seconds: AppConstants.splashDelay), () => _goNext());
}
#override
void initState() {
super.initState();
_startDelay();
}
#override
void dispose() {
_timer.cancel();
super.dispose();
}
GlobalKey<FlipWidgetState> _flipKey = GlobalKey();
Offset _oldPosition = Offset.zero;
bool _visible = true;
#override
Widget build(BuildContext context) {
Size size = Size(100.w, 100.h);
return MaterialApp(
home: Scaffold(
backgroundColor: ColorManager.bgColor,
body: Container(
color: ColorManager.bgColor,
child: GestureDetector(
child: FlipWidget(
leftToRight: true,
key: _flipKey,
rollSize: 30,
child: yourWidget(),
),
),
),
),
);
}
}

Animation affect other animations in Flutter CustomPainter

So, i can't achieve two independents animation in customPainter. The animation of the invaders is affecting the animation of the spaceship.The animation of the invaders consist of a TweenSequence, that produces an effect of move and stop just like space invaders original game.
But the movement of the spaceShip should be continuos. The problem is that the space ship also is having the movement pattern of the invaders, eventhough the animation has a different Tween and a different controller.
The curious thing is that if I comment all the lines that draw the invaders( so the animation value is not being used) the spaceship has a continuos movement(the movement expected)
Just to clarify:
The animation controller and animation of the spaceship are: controllerAsync and animationAsync of SpaceInvadersMainView.dart
Here is the code:
SpaceInvadersMainView.dart
import 'package:croco/SpaceInvaders/InvadersPositionManager.dart';
import '../SpaceInvaders/InvadersConstruction.dart';
import 'package:flutter/material.dart';
import '../SpaceInvaders/InvadersAnimationManager.dart';
class SpaceInvadersMainView extends StatelessWidget {
const SpaceInvadersMainView({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Space Invaders',
theme: ThemeData(
scaffoldBackgroundColor: Colors.black,
fontFamily: 'Segoe UI',
primarySwatch: Colors.lightBlue,
),
home: SpaceCanvas(),
);
}
}
class SpaceCanvas extends StatefulWidget {
SpaceCanvas({Key? key}) : super(key: key);
#override
State<SpaceCanvas> createState() => _SpaceCanvasState();
}
class _SpaceCanvasState extends State<SpaceCanvas> with TickerProviderStateMixin {
late AnimationController controller;
double animationStateValue = 0;
String keyLabel = "";
late AnimationController controllerAsync;
late double animationStateValueAsync;
late var animation = TweenSequence<double>(InvadersAnimationManager.getTweenRightMovement(-600, 500, 24)).animate(controller);
late var animationAsync = Tween<double>(begin: -700, end: 700).animate(controllerAsync);
#override
void initState() {
super.initState();
controllerAsync = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 10000)
);
animationAsync
.addListener(() {
setState(() {
print(animationAsync.value);
});
});
controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 10000 )
);
animation
.addListener(() {
setState(() {
});
});
controller.forward();
controllerAsync.forward();
}
#override
void dispose() {
controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return RawKeyboardListener(
autofocus: true,
focusNode: FocusNode(),
onKey: (event) {
keyLabel = event.logicalKey.keyLabel;
},
child: Scaffold(
body: Center(
child: Column(
children: <Widget>[
Align(
alignment: Alignment.topCenter,
child: Container(
padding: const EdgeInsets.only(top: 20),
child: const Text(
'Welcome to space Invaders!',
style: TextStyle(
fontWeight: FontWeight.w300,
fontSize: 25,
color: Colors.white70
)
),
),
),
Stack(
children: <Widget>[
CustomPaint(
painter : InvadersPaint(animation, animationAsync)
),
],
)
])
)
),
);
}
}
class InvadersPaint extends CustomPainter {
Paint basicSquarePaint = Paint();
Path originPath = Path();
late Animation animation;
late double animationStateValue;
late Animation animationAsync;
InvadersPaint(this.animation, this.animationAsync) : super(repaint: animation);
#override
void paint(Canvas canvas, Size size) {
Path testPath = Path();
Paint paint = Paint();
paint.color = Colors.greenAccent;
testPath = InvadersConstruction.drawInvader(animationAsync.value, 670, "spaceShip");
canvas.drawPath(testPath, paint);
InvadersPositionManager.placeInvadersOnLine(canvas, animation.value + 6 + 2, 100, "squid", 58, Colors.purpleAccent);
InvadersPositionManager.placeInvadersOnLine(canvas, animation.value + 2, 144, "crab", 58, Colors.lightBlueAccent);
InvadersPositionManager.placeInvadersOnLine(canvas, animation.value + 2, 188, "crab", 58, Colors.lightBlueAccent);
InvadersPositionManager.placeInvadersOnLine(canvas, animation.value, 232, "octopus", 58, Colors.yellowAccent);
InvadersPositionManager.placeInvadersOnLine(canvas, animation.value, 276, "octopus", 58, Colors.yellowAccent);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}
InvadersContruction.dart
import 'package:flutter/material.dart';
class InvadersConstruction {
static Path drawInvader(double originX, double originY, String typeOfInvader) {
var transitionPath = Path();
var pathList = <List<int>>[];
const List<List<int>> octopusMatrix = [
[1,1,0,0,0,0,0,0,0,0,1,1],
[0,0,1,1,0,1,1,0,1,1,0,0],
[0,0,0,1,1,0,0,1,1,0,0,0],
[1,1,1,1,1,1,1,1,1,1,1,1],
[1,1,1,0,0,1,1,0,0,1,1,1],
[1,1,1,1,1,1,1,1,1,1,1,1],
[0,1,1,1,1,1,1,1,1,1,1,0],
[0,0,0,0,1,1,1,1,0,0,0,0],
];
const List<List<int>> crabMatrix = [
[0,0,0,1,1,0,1,1,0,0,0],
[1,0,1,0,0,0,0,0,1,0,1],
[1,0,1,1,1,1,1,1,1,0,1],
[1,1,1,1,1,1,1,1,1,1,1],
[0,1,1,0,1,1,1,0,1,1,0],
[0,0,1,1,1,1,1,1,1,0,0],
[0,0,0,1,0,0,0,1,0,0,0],
[0,0,1,0,0,0,0,0,1,0,0]
];
const List<List<int>> squidMatrix = [
[1,0,1,0,0,1,0,1],
[0,1,0,1,1,0,1,0],
[0,0,1,0,0,1,0,0],
[1,1,1,1,1,1,1,1],
[1,1,0,1,1,0,1,1],
[0,1,1,1,1,1,1,0],
[0,0,1,1,1,1,0,0],
[0,0,0,1,1,0,0,0]
];
const List<List<int>> spaceShipMatrix = [
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0],
[0,0,0,0,0,0,1,1,1,1,1,0,0,0,0,0,0],
[0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0],
];
void drawSquare( int row, int isItSquare, int sqrPosition ) {
double y;
double x;
Path secondaryPath = Path();
y = originY + row * -4;
if (sqrPosition == 0) {
x = originX;
} else {
x = sqrPosition * 4 + originX;
}
if (isItSquare == 1) {
secondaryPath.moveTo(x, y);
secondaryPath.relativeLineTo(4, 0);
secondaryPath.relativeLineTo(0, -4);
secondaryPath.relativeLineTo(-4, 0);
secondaryPath.close();
transitionPath = Path.combine(PathOperation.union, transitionPath, secondaryPath);
transitionPath.fillType = PathFillType.evenOdd;
}
}
int counterRow = -1;
int counterCol = -1;
if(typeOfInvader == "octopus") {
pathList = octopusMatrix;
} else if(typeOfInvader == "crab") {
pathList = crabMatrix;
} else if(typeOfInvader == "squid") {
pathList = squidMatrix;
} else if(typeOfInvader == "spaceShip") {
pathList = spaceShipMatrix;
}
for ( var row in pathList) {
counterRow++;
// print("This is the counter of the row: $counterRow");
for (var sqr in row) {
counterCol++;
// print("This is the counter of the square position: $counterCol");
drawSquare(counterRow, sqr, counterCol);
if (counterCol == row.length -1) {
counterCol = -1;
}
}
}
return transitionPath;
}
}
InvadersAnimationManager.dart
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'dart:html';
class InvadersAnimationManager {
//This method maps the right direction movement(-x -> x) of invaders in a List of Tween Sequence Items, that it's
//going to be procesed by the TweenSequence constructor '../Views/SpaceInvadersMainView.dart' line: 37
static List<TweenSequenceItem<double>> getTweenRightMovement (double originX, double finalX, double pixelsToRight) {
double steps = 16;
var tweenSequence = <TweenSequenceItem<double>>[];
for(int i = 1; i <= steps; i++ ) {
var subCounter = i - 1;
var tweenItem = TweenSequenceItem<double>(
tween: Tween<double>(begin: originX + pixelsToRight * subCounter, end: originX + pixelsToRight * i),
weight: 100/ steps
);
tweenSequence.add(tweenItem);
}
return tweenSequence;
}
}
InvadersPositionManager.dart
import 'package:flutter/material.dart';
import 'InvadersConstruction.dart';
class InvadersPositionManager {
static void placeInvadersOnLine(Canvas canvas, double originX, double originY, String invader, double inBetweenPixels, Color color) {
int _rowElements = 12;
Path _internalInvader;
Paint _paint = Paint();
double _invaderLength = 48;
_paint.color = color;
for(int i = 1; i <= 12 + 1; i++) {
if(i == 1) {
_internalInvader = InvadersConstruction.drawInvader(originX, originY, invader);
} else {
_internalInvader = InvadersConstruction.drawInvader(originX + _invaderLength + i * inBetweenPixels, originY, invader);
canvas.drawPath(_internalInvader, _paint);
}
}
}
}

Different behavior when nesting Transform.scale and Transform.translate

I want to implement scale and translate behavior. I write it in _TransformScaleAndTranslate().
But I got different behavior between
[Transform.translate outside Transform.rotate]
vs
[Transform.rotate outside Transform.translate]
The result shows that if I translate after scale, [Transform.translate outside Transform.rotate] can't translate follow my pointer correctly.
Is there something wrong? or anything I didn't get it?
import 'package:flutter/material.dart';
class Home extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Test1'),
),
body: const _BodyWidget(),
);
}
}
class _BodyWidget extends StatefulWidget {
const _BodyWidget({
Key key,
}) : super(key: key);
#override
__BodyWidgetState createState() => __BodyWidgetState();
}
class __BodyWidgetState extends State<_BodyWidget> {
Offset _startFocalPoint = Offset.zero;
Offset _lastOffset = Offset.zero;
Offset _currentOffset = Offset.zero;
double _lastScale = 1.0;
double _currentScale = 1.0;
#override
Widget build(BuildContext context) {
return GestureDetector(
onScaleStart: _onScaleStart,
onScaleUpdate: _onScaleUpdate,
child: Stack(
fit: StackFit.expand,
children: <Widget>[
_TransformScaleAndTranslate(_currentOffset, _currentScale),
],
),
);
}
void _onScaleStart(ScaleStartDetails details) {
_startFocalPoint = details.focalPoint;
_lastOffset = _currentOffset;
_lastScale = _currentScale;
}
void _onScaleUpdate(ScaleUpdateDetails details) {
if (details.scale != 1.0) {
double currentScale = _lastScale * details.scale;
if (currentScale < 0.5) {
currentScale = 0.5;
}
setState(() {
_currentScale = currentScale;
});
// The place I calculate for translate
} else if (details.scale == 1.0) {
Offset currentOffset =
_lastOffset + (details.focalPoint - _startFocalPoint) / _lastScale;
setState(() {
_currentOffset = currentOffset;
});
}
}
}
class _TransformScaleAndTranslate extends StatelessWidget {
final Offset _currentOffset;
final double _currentScale;
const _TransformScaleAndTranslate(
this._currentOffset,
this._currentScale, {
Key key,
}) : super(key: key);
//Put Transform.translate or Transform.scale outside got different behavior when translate after scaled.
#override
Widget build(BuildContext context) {
return Transform.translate(
offset: _currentOffset,
child: Transform.scale(
scale: _currentScale,
child: Image.asset(
'assets/images/elephant.jpg',
fit: BoxFit.contain,
),
),
);
}
}

How to add Signature in flutter?

I have implemented signature_pad in my flutter project and it works fine.
Unfortunately when I place it inside SingleChildScrollView, the signature was not drawn. It scrolled instead of signed.
It seems like is the GestureDetector but I have no idea how to fix it.
Can someone give me some clue on this?
Thanks.
Signature Class need to be modified to respond to VerticalDrag , I renamed it to Signature1
now signature area pad should not scroll , you can check the complete code below as it behaves. you will find out that Signature area is no more scrolling with the SingleChildScrollView.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'dart:async';
import 'dart:ui' as ui;
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
var color = Colors.black;
var strokeWidth = 3.0;
final _sign = GlobalKey<Signature1State>();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body:
SingleChildScrollView(
child: Column(
children: <Widget>[
_showCategory(),
SizedBox(height: 15),
_showCategory(),
SizedBox(height: 15),
_showCategory(),
SizedBox(height: 15),
_showCategory(),
SizedBox(height: 15),
_showCategory(),
_showCategory(),
SizedBox(height: 15),
_showCategory(),
SizedBox(height: 15),
_showCategory(),
SizedBox(height: 15),
_showCategory(),
SizedBox(height: 15),
_showCategory(),
_showSignaturePad()
],
),
)
,
);
}
Widget _showCategory() {
return TextField(
onTap: () {
FocusScope.of(context).requestFocus(FocusNode());
},
style: TextStyle(fontSize: 12.0, height: 1.0),
decoration: InputDecoration(hintText: "TextView"));
}
Widget _showSignaturePad() {
return Container(
width: double.infinity,
height: 200,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
height: 200,
//color: Colors.red,
child: Signature1(
color: color,
key: _sign,
strokeWidth: strokeWidth,
),
),
),
color: Colors.grey.shade300,
);
}
}
class Signature1 extends StatefulWidget {
final Color color;
final double strokeWidth;
final CustomPainter backgroundPainter;
final Function onSign;
Signature1({
this.color = Colors.black,
this.strokeWidth = 5.0,
this.backgroundPainter,
this.onSign,
Key key,
}) : super(key: key);
Signature1State createState() => Signature1State();
static Signature1State of(BuildContext context) {
return context.findAncestorStateOfType<Signature1State>();
}
}
class _SignaturePainter extends CustomPainter {
Size _lastSize;
final double strokeWidth;
final List<Offset> points;
final Color strokeColor;
Paint _linePaint;
_SignaturePainter({#required this.points, #required this.strokeColor, #required this.strokeWidth}) {
_linePaint = Paint()
..color = strokeColor
..strokeWidth = strokeWidth
..strokeCap = StrokeCap.round;
}
#override
void paint(Canvas canvas, Size size) {
_lastSize = size;
for (int i = 0; i < points.length - 1; i++) {
if (points[i] != null && points[i + 1] != null) canvas.drawLine(points[i], points[i + 1], _linePaint);
}
}
#override
bool shouldRepaint(_SignaturePainter other) => other.points != points;
}
class Signature1State extends State<Signature1> {
List<Offset> _points = <Offset>[];
_SignaturePainter _painter;
Size _lastSize;
Signature1State();
void _onDragStart(DragStartDetails details){
RenderBox referenceBox = context.findRenderObject();
Offset localPostion = referenceBox.globalToLocal(details.globalPosition);
setState(() {
_points = List.from(_points)
..add(localPostion)
..add(localPostion);
});
}
void _onDragUpdate (DragUpdateDetails details) {
RenderBox referenceBox = context.findRenderObject();
Offset localPosition = referenceBox.globalToLocal(details.globalPosition);
setState(() {
_points = List.from(_points)..add(localPosition);
if (widget.onSign != null) {
widget.onSign();
}
});
}
void _onDragEnd (DragEndDetails details) => _points.add(null);
#override
Widget build(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((_) => afterFirstLayout(context));
_painter = _SignaturePainter(points: _points, strokeColor: widget.color, strokeWidth: widget.strokeWidth);
return ClipRect(
child: CustomPaint(
painter: widget.backgroundPainter,
foregroundPainter: _painter,
child: GestureDetector(
onVerticalDragStart: _onDragStart,
onVerticalDragUpdate: _onDragUpdate,
onVerticalDragEnd: _onDragEnd,
onPanStart: _onDragStart,
onPanUpdate: _onDragUpdate,
onPanEnd: _onDragEnd
),
),
);
}
Future<ui.Image> getData() {
var recorder = ui.PictureRecorder();
var origin = Offset(0.0, 0.0);
var paintBounds = Rect.fromPoints(_lastSize.topLeft(origin), _lastSize.bottomRight(origin));
var canvas = Canvas(recorder, paintBounds);
if(widget.backgroundPainter != null) {
widget.backgroundPainter.paint(canvas, _lastSize);
}
_painter.paint(canvas, _lastSize);
var picture = recorder.endRecording();
return picture.toImage(_lastSize.width.round(), _lastSize.height.round());
}
void clear() {
setState(() {
_points = [];
});
}
bool get hasPoints => _points.length > 0;
List<Offset> get points => _points;
afterFirstLayout(BuildContext context) {
_lastSize = context.size;
}
}
you need to create a CustomGestureDetector.
Check this updated version of Signature that I just changed to you:
import 'dart:async';
import 'dart:ui' as ui;
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
class Signature extends StatefulWidget {
final Color color;
final double strokeWidth;
final CustomPainter backgroundPainter;
final Function onSign;
Signature({
this.color = Colors.black,
this.strokeWidth = 5.0,
this.backgroundPainter,
this.onSign,
Key key,
}) : super(key: key);
SignatureState createState() => SignatureState();
static SignatureState of(BuildContext context) {
return context.findAncestorStateOfType<SignatureState>();
}
}
class CustomPanGestureRecognizer extends OneSequenceGestureRecognizer {
final Function onPanStart;
final Function onPanUpdate;
final Function onPanEnd;
CustomPanGestureRecognizer({#required this.onPanStart, #required this.onPanUpdate, #required this.onPanEnd});
#override
void addPointer(PointerEvent event) {
onPanStart(event.position);
startTrackingPointer(event.pointer);
resolve(GestureDisposition.accepted);
}
#override
void handleEvent(PointerEvent event) {
if (event is PointerMoveEvent) {
onPanUpdate(event.position);
}
if (event is PointerUpEvent) {
onPanEnd(event.position);
stopTrackingPointer(event.pointer);
}
}
#override
String get debugDescription => 'customPan';
#override
void didStopTrackingLastPointer(int pointer) {}
}
class _SignaturePainter extends CustomPainter {
Size _lastSize;
final double strokeWidth;
final List<Offset> points;
final Color strokeColor;
Paint _linePaint;
_SignaturePainter({#required this.points, #required this.strokeColor, #required this.strokeWidth}) {
_linePaint = Paint()
..color = strokeColor
..strokeWidth = strokeWidth
..strokeCap = StrokeCap.round;
}
#override
void paint(Canvas canvas, Size size) {
_lastSize = size;
for (int i = 0; i < points.length - 1; i++) {
if (points[i] != null && points[i + 1] != null) canvas.drawLine(points[i], points[i + 1], _linePaint);
}
}
#override
bool shouldRepaint(_SignaturePainter other) => other.points != points;
}
class SignatureState extends State<Signature> {
List<Offset> _points = <Offset>[];
_SignaturePainter _painter;
Size _lastSize;
SignatureState();
#override
Widget build(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((_) => afterFirstLayout(context));
_painter = _SignaturePainter(points: _points, strokeColor: widget.color, strokeWidth: widget.strokeWidth);
return ClipRect(
child: CustomPaint(
painter: widget.backgroundPainter,
foregroundPainter: _painter,
child: RawGestureDetector(
gestures: {
CustomPanGestureRecognizer: GestureRecognizerFactoryWithHandlers<CustomPanGestureRecognizer>(
() => CustomPanGestureRecognizer(
onPanStart: (position) {
RenderBox referenceBox = context.findRenderObject();
Offset localPostion = referenceBox.globalToLocal(position);
setState(() {
_points = List.from(_points)..add(localPostion)..add(localPostion);
});
return true;
},
onPanUpdate: (position) {
RenderBox referenceBox = context.findRenderObject();
Offset localPosition = referenceBox.globalToLocal(position);
setState(() {
_points = List.from(_points)..add(localPosition);
if (widget.onSign != null) {
widget.onSign();
}
});
},
onPanEnd: (position) {
_points.add(null);
},
),
(CustomPanGestureRecognizer instance) {},
),
},
),
),
);
}
Future<ui.Image> getData() {
var recorder = ui.PictureRecorder();
var origin = Offset(0.0, 0.0);
var paintBounds = Rect.fromPoints(_lastSize.topLeft(origin), _lastSize.bottomRight(origin));
var canvas = Canvas(recorder, paintBounds);
if (widget.backgroundPainter != null) {
widget.backgroundPainter.paint(canvas, _lastSize);
}
_painter.paint(canvas, _lastSize);
var picture = recorder.endRecording();
return picture.toImage(_lastSize.width.round(), _lastSize.height.round());
}
void clear() {
setState(() {
_points = [];
});
}
bool get hasPoints => _points.length > 0;
List<Offset> get points => _points;
afterFirstLayout(BuildContext context) {
_lastSize = context.size;
}
}
Special attention to CustomPanGestureRecognizer
You can read more in:
Gesture Disambiguation
This is happening because the gesture from SingleChildScrollView overrides your Signature widget’s gesture as the SingleChildScrollView is the parent. There are few ways to solve it as in the other responses in this thread. But the easiest one is using the existing package. You can simply use the below Syncfusion's Flutter SignaturePad widget which I am using now for my application. This widget will work on Android, iOS, and web platforms.
Package - https://pub.dev/packages/syncfusion_flutter_signaturepad
Features - https://www.syncfusion.com/flutter-widgets/flutter-signaturepad
Documentation - https://help.syncfusion.com/flutter/signaturepad/getting-started

Flutter: How to implement Rotate and Pan/Move gesture for any container?

I have implemented the Scale gesture for the container. Also, I have added onHorizontalDragUpdate and onVerticalDragUpdate. But when I try to add both, I get an exception saying can't implement both with Scale gesture. Even for Pan gesture, it throws the same exception. Below is my code:
import 'package:flutter/material.dart';
import 'package:vector_math/vector_math_64.dart' hide Colors;
import 'dart: math' as math;
class HomeScreen extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return HomeState();
}
}
class HomeState extends State<HomeScreen> {
double _scale = 1.0;
double _previousScale;
var yOffset = 400.0;
var xOffset = 50.0;
var rotation = 0.0;
var lastRotation = 0.0;
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.amber,
body: Stack(
children: <Widget>[
stackContainer(),
],
),
);
}
Widget stackContainer() {
return Stack(
children: <Widget>[
Positioned.fromRect(
rect: Rect.fromPoints( Offset(xOffset, yOffset),
Offset(xOffset+250.0, yOffset+100.0)),
child: GestureDetector(
onScaleStart: (scaleDetails) {
_previousScale = _scale;
print(' scaleStarts = ${scaleDetails.focalPoint}');
},
onScaleUpdate: (scaleUpdates){
//ScaleUpdateDetails
rotation += lastRotation - scaleUpdates.rotation;
lastRotation = scaleUpdates.rotation;
print("lastRotation = $lastRotation");
print(' scaleUpdates = ${scaleUpdates.scale} rotation = ${scaleUpdates.rotation}');
setState(() => _scale = _previousScale * scaleUpdates.scale);
},
onScaleEnd: (scaleEndDetails) {
_previousScale = null;
print(' scaleEnds = ${scaleEndDetails.velocity}');
},
child:
Transform(
transform: Matrix4.diagonal3( Vector3(_scale, _scale, _scale))..rotateZ(rotation * math.pi/180.0),
alignment: FractionalOffset.center,
child: Container(
color: Colors.red,
),
)
,
),
),
],
);
}
}
I wanted to move around the red subview and rotate along with the scale.
In scale-related events, you can use the focalPoint to calculate panning, in addition to scaling (zooming). Panning while zooming can also be supported.
Demo:
Here's the code used for the above demo:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: ZoomAndPanDemo(),
);
}
}
class ZoomAndPanDemo extends StatefulWidget {
#override
_ZoomAndPanDemoState createState() => _ZoomAndPanDemoState();
}
class _ZoomAndPanDemoState extends State<ZoomAndPanDemo> {
Offset _offset = Offset.zero;
Offset _initialFocalPoint = Offset.zero;
Offset _sessionOffset = Offset.zero;
double _scale = 1.0;
double _initialScale = 1.0;
#override
Widget build(BuildContext context) {
return GestureDetector(
onScaleStart: (details) {
_initialFocalPoint = details.focalPoint;
_initialScale = _scale;
},
onScaleUpdate: (details) {
setState(() {
_sessionOffset = details.focalPoint - _initialFocalPoint;
_scale = _initialScale * details.scale;
});
},
onScaleEnd: (details) {
setState(() {
_offset += _sessionOffset;
_sessionOffset = Offset.zero;
});
},
child: Transform.translate(
offset: _offset + _sessionOffset,
child: Transform.scale(
scale: _scale,
child: FlutterLogo(),
),
),
);
}
}
Side note: even though events like onHorizontalDragUpdate do not cause runtime exception when used with scale-related events, they still cause conflict and will result in an inferior UX.
It's also worth-noting that InteractiveViewer is a built-in Flutter widget that can handle most of your needs, so you might not need to use GestureDetector and Transform at all.
We can use the focalPoint field of ScaleUpdateDetails object, which we get as an argument in the onScaleUpdate function.
Solution related to the above example:
We need to update the onScaleUpdate method.
onScaleUpdate: (scaleUpdates) {
lastRotation += scaleUpdates.rotation;
var offset = scaleUpdates.focalPoint;
xOffset = offset.dx;
yOffset = offset.dy;
setState(() => _scale = _previousScale * scaleUpdates.scale);
}
Change 'rect' field of Positioned Widget in above code.
rect: Rect.fromPoints(Offset(xOffset - 125.0, yOffset - 50.0),
Offset(xOffset + 250.0, yOffset + 100.0))
Default GestureRecognizer does not support the recognition of pan/drag and scaling at the same time. I think this is the bug and it should be fixed. To implement such behavior - you need to build you own recogizer RawGestureDetector based on ImmediateMultiDragGestureRecognizer gestures.
I have already implemented class PanAndScalingGestureRecognizer here: https://gist.github.com/comm1x/8ffffd08417053043e079878b4bd8d03
So you can see full example or just copypaste and use.
I using Pointer Listener to implement my custom gesture detector.
For anyone facing the same problem, just take a look on package gesture_x_detector
Supports (tap, double-tap, scale(start, update, end), move(start, update, end) and long-press. All types can be used simultaneously.
Example:
import 'package:flutter/material.dart';
import 'package:gesture_x_detector/gesture_x_detector.dart';
void main() {
runApp(
MaterialApp(
home: XGestureExample(),
),
);
}
class XGestureExample extends StatefulWidget {
#override
_XGestureExampleState createState() => _XGestureExampleState();
}
class _XGestureExampleState extends State<XGestureExample> {
String lastEventName = 'Tap on screen';
#override
Widget build(BuildContext context) {
return XGestureDetector(
child: Material(
child: Center(
child: Text(
lastEventName,
style: TextStyle(fontSize: 30),
),
),
),
doubleTapTimeConsider: 300,
longPressTimeConsider: 350,
onTap: onTap,
onDoubleTap: onDoubleTap,
onLongPress: onLongPress,
onMoveStart: onMoveStart,
onMoveEnd: onMoveEnd,
onMoveUpdate: onMoveUpdate,
onScaleStart: onScaleStart,
onScaleUpdate: onScaleUpdate,
onScaleEnd: onScaleEnd,
bypassTapEventOnDoubleTap: false,
);
}
void onScaleEnd() {
setLastEventName('onScaleEnd');
print('onScaleEnd');
}
void onScaleUpdate(changedFocusPoint, scale) {
setLastEventName('onScaleUpdate');
print(
'onScaleUpdate - changedFocusPoint: $changedFocusPoint ; scale: $scale');
}
void onScaleStart(initialFocusPoint) {
setLastEventName('onScaleStart');
print('onScaleStart - initialFocusPoint: ' + initialFocusPoint.toString());
}
void onMoveUpdate(localPos, position, localDelta, delta) {
setLastEventName('onMoveUpdate');
print('onMoveUpdate - pos: ' + localPos.toString());
}
void onMoveEnd(pointer, localPos, position) {
setLastEventName('onMoveEnd');
print('onMoveEnd - pos: ' + localPos.toString());
}
void onMoveStart(pointer, localPos, position) {
setLastEventName('onMoveStart');
print('onMoveStart - pos: ' + localPos.toString());
}
void onLongPress(pointer, localPos, position) {
setLastEventName('onLongPress');
print('onLongPress - pos: ' + localPos.toString());
}
void onDoubleTap(localPos, position) {
setLastEventName('onDoubleTap');
print('onDoubleTap - pos: ' + localPos.toString());
}
void onTap(pointer, localPos, position) {
setLastEventName('onTap');
print('onTap - pos: ' + localPos.toString());
}
void setLastEventName(String eventName) {
setState(() {
lastEventName = eventName;
});
}
}