How to recognize diagonal swipes in flutter? - flutter

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

Related

Is the build function creating new widgets to render or reusing?

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);
}

How can i set Offset to specific value

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;
}
}

setState not really settng the state [flutter]

So I have this code that is supposed to draw to the canvas.
import 'dart:ui';
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(
debugShowCheckedModeBanner: false,
home: Home(),
);
}
}
// offsets
List<Offset?> offsets = [];
class Home extends StatefulWidget {
const Home({
Key? key,
}) : super(key: key);
#override
State<Home> createState() => _HomeState();
}
class _HomeState extends State<Home> {
// offsets
List<Offset?> offsets = [];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Drawing'),
),
body: GestureDetector(
onPanDown: (details) {
setState(() {
offsets.add(details.localPosition);
});
},
onPanUpdate: (details) {
setState(() {
offsets.add(details.localPosition);
});
},
onPanEnd: (details) {
setState(() {
offsets.add(null);
});
},
child: Container(
width: double.infinity,
height: MediaQuery.of(context).size.height,
color: Colors.grey,
child: CustomPaint(
foregroundPainter: Painter(offsets: offsets),
),
),
),
);
}
}
class Painter extends CustomPainter {
Painter({required this.offsets});
List<Offset?> offsets;
#override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..strokeWidth = 2
..style = PaintingStyle.stroke;
// for every offset and the next offset
for (int i = 0; i < offsets.length - 1; i++) {
// if both are not null
if (offsets[i] != null && offsets[i + 1] != null) {
canvas.drawLine(offsets[i]!, offsets[i + 1]!, paint);
} else if (offsets[i] != null && offsets[i + 1] == null) {
canvas.drawPoints(PointMode.points, [offsets[i]!], paint);
}
}
print('paint called');
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
}
How this is working is that it detects the gestures on the container and when there is a swipe on the container, it records the offsets of that and adds them to the offsets array. Later that array is being passed to the custom painter where we go through all the offsets and render the lines. So when I add the offsets to the array I use setState so that the new offsets are added, the screen is rendered again, new offsets are passed to the custom painter and it renders those lines. But it's not working when I make a swipe but the lines appear when I make a hot reload. Why is this? Why I need to manually hot reload to see those lines? Any help will be appreciated.
Edit: It seems like the offsets are being added properly and the setState is also working fine but somehow the custom painter is not being called.
NVM, returning true from shouldRepaint method solve the issue.
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}

Update CustomPaint drawing

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
),
);
},
);
}
}

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,
),
),
);
}
}