Related
I have a working snippet that I've wrote, but I kinda don't understand how flutter is (re)using the widgets creating in the build method:
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(const MyGame());
}
class MyGame extends StatelessWidget {
const MyGame({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(home: GameWidget());
}
}
class GameWidget extends StatefulWidget {
const GameWidget({Key? key}) : super(key: key);
static const squareWidth = 50.0;
static const squareHeight = 50.0;
#override
State<GameWidget> createState() => _GameWidgetState();
}
class _GameWidgetState extends State<GameWidget> {
List<Offset> offsets = [];
#override
Widget build(BuildContext context) {
if (offsets.isEmpty) {
for(int i = 0; i < 20; i++) {
offsets.add(calculateNextOffset());
}
}
List<Widget> squareWidgets = [];
for (int j = 0; j < offsets.length; j++) {
squareWidgets.add(AnimatedPositioned(
left: offsets[j].dx,
top: offsets[j].dy,
curve: Curves.easeIn,
duration: const Duration(milliseconds: 500),
child: GestureDetector(
onTapDown: (tapDownDetails) {
setState(() {
offsets.removeAt(j);
for (int k = 0; k < offsets.length; k++) {
offsets[k] = calculateNextOffset();
}
});
},
behavior: HitTestBehavior.opaque,
child: Container(
width: GameWidget.squareWidth,
height: GameWidget.squareHeight,
color: Colors.blue,
),
),
));
}
return Stack(
children: squareWidgets,
);
}
Offset calculateNextOffset() {
return randomOffset(
MediaQuery.of(context).size,
const Size(GameWidget.squareWidth, GameWidget.squareHeight),
MediaQuery.of(context).viewPadding.top);
}
double randomNumber(double min, double max) =>
min + Random().nextDouble() * (max - min);
Offset randomOffset(
Size parentSize, Size childSize, double statusBarHeight) {
var parentWidth = parentSize.width;
var parentHeight = parentSize.height;
var randomPosition = Offset(
randomNumber(parentWidth, childSize.width),
randomNumber(statusBarHeight,parentHeight - childSize.height),
);
return randomPosition;
}
}
Every time I click on a container, i expect my "offsets" state to be updated, but I also expect all the AnimationPositioned widgets, GestureDetector widgets and the square widgets that you see would be rerendered.
With rerendered i mean they would disappear from the screen and new ones would be rerendered (and the animation from the first widgets would be cancelled and never displayed)
However it works? Could someone explain this to me?
EDIT: I've updated my snippet of code in my question to match what i'm asking, which i'm also going to rephrase here:
Every time I click on a square, i want that square to disappear and all the other square to randomly animate to another position. But every time I click on a square, another random square is deleted, and the one i'm clicking is animating.
I want the square that I click on disappears and the rest will animate.
Following up from the comments - Actually, the square you click is disappears. However, to see this visually add this to your container color:
color:Colors.primaries[Random().nextInt(Colors.primaries.length)],
Now, your offsets are generating just fine and random. However, because you have an AnimatedContainer widget. This widget will remember the last x & y position of your square and animate the new square starting from that old x,y value to the new on you passed it. So if you really want the square you click on disappear - you will need to either use Positioned widget:
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(const MyGame());
}
class MyGame extends StatelessWidget {
const MyGame({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(home: GameWidget());
}
}
class GameWidget extends StatefulWidget {
const GameWidget({Key? key}) : super(key: key);
static const squareWidth = 100.0;
static const squareHeight = 100.0;
#override
State<GameWidget> createState() => _GameWidgetState();
}
class _GameWidgetState extends State<GameWidget> {
List<Offset> offsets = [];
#override
Widget build(BuildContext context) {
if (offsets.isEmpty) {
offsets.add(calculateNextOffset());
}
print(offsets);
List<Widget> squareWidgets = [];
for (var offset in offsets) {
squareWidgets.add(Positioned(
left: offset.dx,
top: offset.dy,
//curve: Curves.easeIn,
//duration: const Duration(milliseconds: 500),
child: GestureDetector(
onTapDown: (tapDownDetails) {
setState(() {
for (var i = 0; i < offsets.length; i++) {
offsets[i] = calculateNextOffset();
}
offsets.add(calculateNextOffset());
});
},
behavior: HitTestBehavior.opaque,
child: Container(
width: GameWidget.squareWidth,
height: GameWidget.squareHeight,
color:Colors.primaries[Random().nextInt(Colors.primaries.length)],
),
),
));
}
return Stack(
children: squareWidgets,
);
}
Offset calculateNextOffset() {
return randomOffset(
MediaQuery.of(context).size,
const Size(GameWidget.squareWidth, GameWidget.squareHeight),
MediaQuery.of(context).viewPadding.top);
}
double randomNumber(double min, double max) =>
min + Random().nextDouble() * (max - min);
Offset randomOffset(
Size parentSize, Size childSize, double statusBarHeight) {
var parentWidth = parentSize.width;
var parentHeight = parentSize.height;
var randomPosition = Offset(
randomNumber(parentWidth, childSize.width),
randomNumber(statusBarHeight,parentHeight - childSize.height),
);
return randomPosition;
}
}
If you want the rest of the squares to animate while only the one that is clicked disappears. You will need to rethink your implementation and track all square perhaps using unique keys and custom animations. Hope that helps!
I've finally found it:
In the context of the snippet inside the question: Every time you click on a square, it will correctly remove that item, but the widgets are rerendered from that new list, and the last widget that was previously rendered will be removed instead of the one that I clicked.
This has to do because every widget in the widget tree is rendered as an element inside the element tree. If the state of the element inside the element tree is the same, it will not rerender that one. and they are all just blue squares in the end, so there is no distinction.
You can find a very nice video made by the flutter devs here:
When to Use Keys - Flutter Widgets 101 Ep. 4
Long story short: Here is the snippet with the fix, which is to add a Key to each widget, then the state will change on the element inside the element tree and it will rerender (and remove) the correct widgets/elements:
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(const MyGame());
}
class MyGame extends StatelessWidget {
const MyGame({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(home: GameWidget());
}
}
class GameWidget extends StatefulWidget {
const GameWidget({Key? key}) : super(key: key);
static const squareWidth = 50.0;
static const squareHeight = 50.0;
#override
State<GameWidget> createState() => _GameWidgetState();
}
class _GameWidgetState extends State<GameWidget> {
List<OffsetData> offsets = [];
#override
Widget build(BuildContext context) {
if (offsets.isEmpty) {
for(int i = 0; i < 20; i++) {
offsets.add(OffsetData(UniqueKey(), calculateNextOffset()));
}
}
List<Widget> squareWidgets = [];
for (int j = 0; j < offsets.length; j++) {
squareWidgets.add(AnimatedPositioned(
key: offsets[j].key, // This line is the trick
left: offsets[j].offset.dx,
top: offsets[j].offset.dy,
curve: Curves.easeIn,
duration: const Duration(milliseconds: 500),
child: GestureDetector(
onTapDown: (tapDownDetails) {
setState(() {
offsets.removeAt(j);
for (var offsetData in offsets) {
offsetData.offset = calculateNextOffset();
}
});
},
behavior: HitTestBehavior.opaque,
child: Container(
width: GameWidget.squareWidth,
height: GameWidget.squareHeight,
color: Colors.blue,
),
),
));
}
return Stack(
children: squareWidgets,
);
}
Offset calculateNextOffset() {
return randomOffset(
MediaQuery.of(context).size,
const Size(GameWidget.squareWidth, GameWidget.squareHeight),
MediaQuery.of(context).viewPadding.top);
}
double randomNumber(double min, double max) =>
min + Random().nextDouble() * (max - min);
Offset randomOffset(
Size parentSize, Size childSize, double statusBarHeight) {
var parentWidth = parentSize.width;
var parentHeight = parentSize.height;
var randomPosition = Offset(
randomNumber(parentWidth, childSize.width),
randomNumber(statusBarHeight,parentHeight - childSize.height),
);
return randomPosition;
}
}
class OffsetData {
Offset offset;
final Key key;
OffsetData(this.key, this.offset);
}
i have found this simple full code
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const Exp3();
}
}
typedef OffsetBuilder = Offset Function(Size size);
class Exp3 extends StatefulWidget {
const Exp3({Key? key}) : super(key: key);
#override
State<Exp3> createState() => _Exp3State();
}
class _Exp3State extends State<Exp3> {
OffsetBuilder _offsetBuilder = (_) => Offset.zero;
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Builder(builder: (context) {
return Container( // parent container
color: Colors.red,
child: GestureDetector(
onPanUpdate: (details) {
final currentBuilder = _offsetBuilder;
_offsetBuilder = (Size containerSize) {
return currentBuilder.call(containerSize) + details.delta;
};
setState(() {});
},
child: CustomSingleChildLayout(
delegate: MyCustomSingleChildLayoutDelegate(
canGoOffParentBounds: false,
padding: const EdgeInsets.all(8.0),
offsetBuilder: _offsetBuilder,
),
child: Container(
width: 100,
height: 100,
color: Colors.yellow,
),
),
),
);
}
),
);
}
}
now my widget located at Offset.zero; for first snapshot by it's variable OffsetBuilder _offsetBuilder = (_) => Offset.zero;
Question: How i set OffsetBuilder _offsetBuilder = (_) => Offset.zero; to located at following value by first snapshot or first initialize
last point on the screen (max buttom) - 300
and here the class i use for SingleChildLayoutDelegate
class MyCustomSingleChildLayoutDelegate extends SingleChildLayoutDelegate {
final Offset Function(Size childSize) offsetBuilder;
final EdgeInsets padding;
final bool canGoOffParentBounds;
MyCustomSingleChildLayoutDelegate({
required this.offsetBuilder,
required this.padding,
required this.canGoOffParentBounds,
});
#override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
return BoxConstraints.loose(constraints.biggest).deflate(padding);
}
#override
Offset getPositionForChild(Size size, Size childSize) {
Offset childTopLeft = offsetBuilder.call(childSize);
if (canGoOffParentBounds) {
// no more checks on the position needed
return childTopLeft;
}
if (childTopLeft.dx + childSize.width > size.width - padding.right) {
final distance = -(childTopLeft.dx - (size.width - padding.right - childSize.width));
childTopLeft = childTopLeft.translate(distance, 0);
}
if (childTopLeft.dx < padding.left) {
final distance = padding.left - childTopLeft.dx;
childTopLeft = childTopLeft.translate(distance, 0);
}
if (childTopLeft.dy + childSize.height > size.height - padding.bottom) {
final distance = -(childTopLeft.dy - (size.height - padding.bottom - childSize.height));
childTopLeft = childTopLeft.translate(0, distance);
}
if (childTopLeft.dy < padding.top) {
final distance = padding.top - childTopLeft.dy;
childTopLeft = childTopLeft.translate(0, distance);
}
return childTopLeft;
}
#override
bool shouldRelayout(MyCustomSingleChildLayoutDelegate oldDelegate) {
return oldDelegate.offsetBuilder != offsetBuilder;
}
}
I attempted to detect diagonal swipe direction using GestureDetector. I used the onPanStart method to detect the screen's corners.
However, it is not perfect because it only considers the beginning of the gesture.
Right now, if I swipe bottom left to top right in the top right corner or part, it gives me the incorrect top right direction. In this case, the correct direction should be bottom left.
So, how can we identify diagonal swipe direction using GestureDetector or any other method?
I looked at several other StackOverflow posts, but they only detect top, left, right, and bottom directions.
Here is an example of code.
class _HomePageState extends State<HomePage> {
#override
Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
String? swipeDirection;
return Scaffold(
body: SafeArea(
child: Scaffold(
backgroundColor: Colors.black,
body: GestureDetector(
child: Container(),
onPanStart: (details) {
final halfScreenWidth = screenSize.width / 2;
final halfScreenHeight = screenSize.height / 2;
final position = details.localPosition;
if ((position.dx < halfScreenWidth) &&
(position.dy < halfScreenHeight)) {
swipeDirection = 'topLeft';
} else if ((position.dx > halfScreenWidth) &&
(position.dy < halfScreenHeight)) {
swipeDirection = 'topRight';
} else if ((position.dx < halfScreenWidth) &&
(position.dy > halfScreenHeight)) {
swipeDirection = 'bottomLeft';
} else {
swipeDirection = 'bottomRight';
}
},
),
),
));
}
}
You can use pan update to find the position of the pointer the same manner you did for pan start and get the starting and ending points. Then on pan end execute with the final results
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(
home: MyHome(),
);
}
}
class MyHome extends StatefulWidget {
const MyHome({Key? key}) : super(key: key);
#override
State<MyHome> createState() => _MyHomeState();
}
class _MyHomeState extends State<MyHome> {
String? swipeStart;
String? swipeEnd;
#override
void initState() {
// TODO: implement initState
super.initState();
}
#override
Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
return SafeArea(
child: Scaffold(
backgroundColor: Colors.black,
body: GestureDetector(
child: Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
color: Colors.grey,
),
onPanStart: (details) {
final halfScreenWidth = screenSize.width / 2;
final halfScreenHeight = screenSize.height / 2;
final position = details.localPosition;
if ((position.dx < halfScreenWidth) &&
(position.dy < halfScreenHeight)) {
swipeStart = 'topLeft';
} else if ((position.dx > halfScreenWidth) &&
(position.dy < halfScreenHeight)) {
swipeStart = 'topRight';
} else if ((position.dx < halfScreenWidth) &&
(position.dy > halfScreenHeight)) {
swipeStart = 'bottomLeft';
} else {
swipeStart = 'bottomRight';
}
// print(swipeStart);
},
onPanUpdate: (details) {
final halfScreenWidth = screenSize.width / 2;
final halfScreenHeight = screenSize.height / 2;
final position = details.localPosition;
if ((position.dx < halfScreenWidth) &&
(position.dy < halfScreenHeight)) {
swipeEnd = 'topLeft';
} else if ((position.dx > halfScreenWidth) &&
(position.dy < halfScreenHeight)) {
swipeEnd = 'topRight';
} else if ((position.dx < halfScreenWidth) &&
(position.dy > halfScreenHeight)) {
swipeEnd = 'bottomLeft';
} else {
swipeEnd = 'bottomRight';
}
//print(swipeEnd);
},
onPanEnd: (details) {
print("Final direction was from $swipeStart to $swipeEnd");
},
),
),
);
}
}
I have printed output like this
flutter: Final direction was from topLeft to bottomRight
flutter: Final direction was from bottomLeft to topRight
EDIT
You can find the offset when the pan starts and updates. Then on pan end calculate the direction like this
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(
home: MyHome(),
);
}
}
class MyHome extends StatefulWidget {
const MyHome({Key? key}) : super(key: key);
#override
State<MyHome> createState() => _MyHomeState();
}
class _MyHomeState extends State<MyHome> {
String? horizontalDirection;
String? verticalDirection;
late Offset startPoint;
late Offset endPoint;
#override
void initState() {
// TODO: implement initState
super.initState();
}
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
backgroundColor: Colors.black,
body: GestureDetector(
child: Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
color: Colors.grey,
),
onPanStart: (details) {
startPoint = details.localPosition;
},
onPanUpdate: (details) {
endPoint = details.localPosition;
},
onPanEnd: (details) {
if (startPoint.dx < endPoint.dx) {
horizontalDirection = "right";
} else {
horizontalDirection = "left";
}
if (startPoint.dy < endPoint.dy) {
verticalDirection = "bottom";
} else {
verticalDirection = "top";
}
print("Final direction was $horizontalDirection$verticalDirection");
},
),
),
);
}
}
Looking into the GestureDetector widget there isn't any kind of feature your'e searching for. Also there is no other widget in my mind which is able to do that.
Maybe you can try to add the following:
Save the position of the onPanStart of one of the directions. Add onPanEnd and do the exact same you did for onPanStart and also save these positions. Now add onPanUpdate and with this you will compare the first and last point which was recognized. If you are working with screensizes as I can see, you can divide your screen in 4 blocks (top, left, right, bottom) and with the informations of the first and last position you can detect the diagonal swipe
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
),
);
},
);
}
}
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;
});
}
}