Is it possible to rotate pie charts with the charts_flutter package? - dart

Is it possible to have this same effect using the package charts_flutter? In this case the user can rotate the pie chart.
User rotating pie chart

It's not possible with the current implementation of the charting library you're using unless you used their code and changed. You might be able to get it to work with the flutter circular chart plugin by hooking up your gesture detecting code and animating the value for startAngle, but I'm not sure it would do exactly what you want (or might try to redraw the entire thing each time which isn't overly performant).
I had some old code lying around that implemented most of what you want so I fixed it up a little - here's an example of just writing your own pie chart. You can copy/paste it into a file and run it as-is.
Your mileage may vary with this - I haven't tested it extensively or anything, but you're welcome to use it at least as a starting point - it has the code for drawing a pie chart and rotating according to gestures at least.
There's quite a lot of stuff in here so I'd encourage you to give it a deep read-through to see exactly what I'm doing. I don't have time to add documentation right now, but if you have any questions feel free to ask.
import 'dart:math';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: SafeArea(
child: Material(
child: RotatingPieChart(
items: [
PieChartItem(30, "one", Colors.red),
PieChartItem(210, "two", Colors.green),
PieChartItem(60, "three", Colors.blue),
PieChartItem(35, "four", Colors.teal),
PieChartItem(25, "five", Colors.orange)
],
toText: (item, _) => TextPainter(
textAlign: TextAlign.center,
text: TextSpan(
style: TextStyle(color: Colors.black, fontSize: 8.0),
text: "${item.name}\n${item.val}",
),
textDirection: TextDirection.ltr),
),
),
),
);
}
}
class PieChartItem {
final num val;
final String name;
final Color color;
PieChartItem(this.val, this.name, this.color) : assert(val != 0);
}
typedef TextPainter PieChartItemToText(PieChartItem item, double total);
class RotatingPieChart extends StatelessWidget {
final double accellerationFactor;
final List<PieChartItem> items;
final PieChartItemToText toText;
const RotatingPieChart({Key key, this.accellerationFactor = 1.0, #required this.items, #required this.toText})
: super(key: key);
#override
Widget build(BuildContext context) {
return Center(
child: AspectRatio(
aspectRatio: 1.0,
child: _RotatingPieChartInternal(
items: items,
toText: toText,
accellerationFactor: accellerationFactor,
),
),
);
}
}
class _RotationEndSimulation extends Simulation {
final double initialVelocity;
final double initialPosition;
final double accelleration;
_RotationEndSimulation({
#required this.initialVelocity,
#required double decelleration,
#required this.initialPosition,
}) : accelleration = decelleration * -1.0;
#override
double dx(double time) => initialVelocity + (accelleration * time);
#override
bool isDone(double time) => initialVelocity > 0 ? dx(time) < 0.001 : dx(time) > -0.001;
#override
double x(double time) => (initialPosition + (initialVelocity * time) + (accelleration * time * time / 2)) % 1.0;
}
class _RotatingPieChartInternal extends StatefulWidget {
final double accellerationFactor;
final List<PieChartItem> items;
final PieChartItemToText toText;
const _RotatingPieChartInternal(
{Key key, this.accellerationFactor = 1.0, #required this.items, #required this.toText})
: super(key: key);
#override
_RotatingPieChartInternalState createState() => _RotatingPieChartInternalState();
}
class _RotatingPieChartInternalState extends State<_RotatingPieChartInternal> with SingleTickerProviderStateMixin {
AnimationController _controller;
Animation<double> _animation;
#override
void initState() {
_controller = AnimationController(vsync: this);
_animation = new Tween(begin: 0.0, end: 2.0 * pi).animate(_controller);
_controller.animateTo(2 * pi, duration: Duration(seconds: 10));
super.initState();
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
Offset lastDirection;
Offset getDirection(Offset globalPosition) {
RenderBox box = context.findRenderObject();
Offset offset = box.globalToLocal(globalPosition);
Offset center = Offset(context.size.width / 2.0, context.size.height / 2.0);
return offset - center;
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onPanDown: (details) {
lastDirection = getDirection(details.globalPosition);
},
onPanUpdate: (details) {
Offset newDirection = getDirection(details.globalPosition);
double diff = newDirection.direction - lastDirection.direction;
var value = _controller.value + (diff / pi / 2);
_controller.value = value % 1.0;
lastDirection = newDirection;
},
onPanEnd: (details) {
// non-angular velocity
Offset velocity = details.velocity.pixelsPerSecond;
var top = (lastDirection.dx * velocity.dy) - (lastDirection.dy * velocity.dx);
var bottom = (lastDirection.dx * lastDirection.dx) + (lastDirection.dy * lastDirection.dy);
var angularVelocity = top / bottom;
var angularRotation = angularVelocity / pi / 2;
var decelleration = angularRotation * widget.accellerationFactor;
_controller.animateWith(
_RotationEndSimulation(
decelleration: decelleration,
initialPosition: _controller.value,
initialVelocity: angularRotation,
),
);
},
child: AnimatedBuilder(
animation: _animation,
builder: (context, widget) {
return Stack(
fit: StackFit.passthrough,
children: [
Transform.rotate(
angle: _animation.value,
child: widget,
),
CustomPaint(
painter:
_PieTextPainter(items: this.widget.items, rotation: _animation.value, toText: this.widget.toText),
)
],
);
},
child: CustomPaint(
painter: _PieChartPainter(
items: widget.items,
),
),
),
);
}
}
abstract class _AlignedCustomPainter extends CustomPainter {
#override
void paint(Canvas canvas, Size size) {
// for convenience I'm doing all the drawing in a 100x100 square then moving it rather than worrying
// about the actual size.
// Also, using a 100x100 square for convenience so we can hardcode values.
FittedSizes fittedSizes = applyBoxFit(BoxFit.contain, Size(100.0, 100.0), size);
var dest = fittedSizes.destination;
canvas.translate((size.width - dest.width) / 2 + 1, (size.height - dest.height) / 2 + 1);
canvas.scale((dest.width - 2) / 100.0);
alignedPaint(canvas, Size(100.0, 100.0));
}
void alignedPaint(Canvas canvas, Size size);
}
class _PieChartPainter extends _AlignedCustomPainter {
final List<PieChartItem> items;
final double total;
final double rotation;
_PieChartPainter({this.rotation = 0.0, #required this.items})
: total = items.fold(0.0, (total, el) => total + el.val);
#override
void alignedPaint(Canvas canvas, Size size) {
Rect rect = Offset.zero & size;
double soFar = rotation;
Paint outlinePaint = Paint()
..color = Colors.white
..style = PaintingStyle.stroke;
for (int i = 0; i < items.length; ++i) {
PieChartItem item = items[i];
double arcRad = item.val / total * 2 * pi;
canvas.drawArc(rect, soFar, arcRad, true, Paint()..color = item.color);
canvas.drawArc(rect, soFar, arcRad, true, outlinePaint);
soFar += arcRad;
}
}
#override
bool shouldRepaint(_PieChartPainter oldDelegate) {
return oldDelegate.rotation != rotation || oldDelegate.items != items;
}
}
class _PieTextPainter extends _AlignedCustomPainter {
final List<PieChartItem> items;
final double total;
final double rotation;
final List<double> middles;
final PieChartItemToText toText;
static final double textDisplayCenter = 0.7;
_PieTextPainter._(this.items, this.total, this.rotation, this.middles, this.toText);
factory _PieTextPainter(
{double rotation = 0.0, #required List<PieChartItem> items, #required PieChartItemToText toText}) {
double total = items.fold(0.0, (prev, el) => prev + el.val);
var middles = (() {
double soFar = rotation;
return items.map((item) {
double arcRad = item.val / total * 2 * pi;
double middleRad = (soFar) + (arcRad / 2);
soFar += arcRad;
return middleRad;
}).toList(growable: false);
})();
return _PieTextPainter._(items, total, rotation, middles, toText);
}
#override
void alignedPaint(Canvas canvas, Size size) {
for (int i = 0; i < items.length; ++i) {
var middleRad = middles[i];
var item = items[i];
var rad = size.width / 2;
var middleX = rad + rad * textDisplayCenter * cos(middleRad);
var middleY = rad + rad * textDisplayCenter * sin(middleRad);
TextPainter textPainter = toText(item, total)..layout();
textPainter.paint(canvas, Offset(middleX - (textPainter.width / 2), middleY - (textPainter.height / 2)));
}
}
#override
bool shouldRepaint(_PieTextPainter oldDelegate) {
// note that just checking items != items might not be enough.
return oldDelegate.rotation != rotation || oldDelegate.items != items || oldDelegate.toText != toText;
}
}

Related

How to add a time delay when printing multiple lines using canvas.drawLine()

I am learning Flutter/Dart, specifically, how to draw on the Canvas.
I want to draw multiple lines using a FOR loop. I want a delay between iterations, so that I see each line painted individually, one after another.
Referencing these two articles:
Asynchronous Programming: Futures https://www.dartlang.org/tutorials/language/futures
Asynchronous Programming: Streams
https://www.dartlang.org/tutorials/language/streams
I was able to write a 'text version' line painter to show I could delay a running FOR loop and print some text.
// print 10 lines 'text version'
Future printTenLines() async {
for (int z = 0; z <= 9; z++) {
await addDelay();
print('Line $z');
}
}
main() {
printTenLines();
}
const delayTime = Duration(milliseconds: 1000);
Future addDelay() => Future.delayed(delayTime);
But when I substitute the actual canvas.drawLine() function for the 'text version' print() routine, I get exception errors.
const delayTime = Duration(milliseconds: 10);
Future<String> addDelay() => Future.delayed(delayTime);
// draw 10 lines on the canvas
Future<void> drawTenLines() async {
for (double i = 0; i <= 100; i += 10) {
await addDelay();
print('Line $i');
canvas.drawLine(
Offset(size.width/2, size.height/2),
Offset(i, 0),
paint,
);
}
drawTenLines();
I found this problem to be similar to trying to print the output of two nested loops. Code executes, and the output of both loops is printed when the outer loop is completed.
I tried nested loops for painting the lines, with the outer loop painting the lines, and the inner loop acting as the delay timer. The program waits until both loops complete, and all the lines are drawn at once.
Using this example of asynchronous Futures and Streams is the closest I got to achieving the desired result. The 'text version" line printer is delayed and works correctly. But the delay will not work with canvas.drawLine().
Here is my complete code. It works as is and draws 10 lines. Un-comment the delay call to see the exception. I just want canvas to delay for some given time before drawing the next line.
`
import 'package:flutter/material.dart';
import 'dart:async';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: "Drawing Lines",
home: DrawingPage(),
);
}
}
class DrawingPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Draw Ten Lines"),
),
body: CustomPaint(
painter: LinePainter(),
child: Center(
child: Text(
"",
style: TextStyle(fontSize: 20, fontStyle: FontStyle.italic),
),
),
));
}
}
class LinePainter extends CustomPainter {
#override
void paint(Canvas canvas, Size size) {
var paint = Paint();
paint.color = Colors.blue;
paint.strokeWidth = 1;
paint.style = PaintingStyle.stroke;
const delayTime = Duration(milliseconds: 1000);
Future<void> addDelay() => Future.delayed(delayTime);
Future<void> drawTenLines() async {
for (double i = 0; i <= 100; i += 10) {
//await addDelay();
print('Line $i');
canvas.drawLine(
Offset(size.width / 2, size.height / 2),
Offset(i, 0),
paint,
);
//await addDelay();
}
}
drawTenLines();
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
`
Try this way..
Future.delayed(const Duration(milliseconds: 500), () {
// Here you can write your code
setState(() {
// Here you can write your code for open new view
});
});
second way.. Used Timer
Timer _timer;
_timer = new Timer(const Duration(milliseconds: 400), () {
setState(() {
// Here you can write your code for open new view
});
});
Timer import 'dart:async'

Scroll multiple pages in PageView easier

I am building a horizontally scrollable list in flutter using PageView, however, I would like to be able to scroll multiple pages at the same time. It is currently possible if I scroll really fast, but it is far from ideal. Another option is to set pageSnapping to false, but then it does not snap at all, which is not what I want.
I am thinking that it might be possible to change pageSnapping from false to true if the scroll velocity is under a certain treshhold, but i don't know how I would get said velocity.
The app i am building looks something like this.
All help appreciated!
To anyone coming here in the future, i finally solved this using a Listener insted of a GestureDetector to calculate the code manually.
Here is the relevant code:
class HomeWidget extends StatefulWidget {
#override
_HomeWidgetState createState() => _HomeWidgetState();
}
class _HomeWidgetState extends State<HomeWidget> {
int _currentPage = 0;
PageController _pageController;
int t; //Tid
double p; //Position
#override
initState() {
super.initState();
_pageController = PageController(
viewportFraction: 0.75,
initialPage: 0,
);
}
#override
Widget build(BuildContext context) {
return Container(
child: Listener(
onPointerMove: (pos) { //Get pointer position when pointer moves
//If time since last scroll is undefined or over 100 milliseconds
if (t == null || DateTime.now().millisecondsSinceEpoch - t > 100) {
t = DateTime.now().millisecondsSinceEpoch;
p = pos.position.dx; //x position
} else {
//Calculate velocity
double v = (p - pos.position.dx) / (DateTime.now().millisecondsSinceEpoch - t);
if (v < -2 || v > 2) { //Don't run if velocity is to low
//Move to page based on velocity (increase velocity multiplier to scroll further)
_pageController.animateToPage(_currentPage + (v * 1.2).round(),
duration: Duration(milliseconds: 800), curve: Curves.easeOutCubic);
}
}
},
child: PageView(
controller: _pageController,
physics: ClampingScrollPhysics(), //Will scroll to far with BouncingScrollPhysics
scrollDirection: Axis.horizontal,
children: <Widget>[
//Pages
],
),
),
);
}
}
Interesting problem!
To gain the velocity of a swipe you can use a GestureDetector, unfortunately when trying to use both a GestureDetector and PageView then the PageView steals the focus from the GestureDetector so they can not be used in unison.
GestureDetector(
onPanEnd: (details) {
Velocity velocity = details.velocity;
print("onPanEnd - velocity: $velocity");
},
)
Another way however was to use DateTime in the PageView's onPageChanged to measure the change in time, instead of velocity. However this is not ideal, the code I wrote is like a hack. It has a bug (or feature) that the first swipe after a standstill on a page will only move one page, however consecutive swipes will be able to move multiple pages.
bool pageSnapping = true;
List<int> intervals = [330, 800, 1200, 1600]; // Could probably be optimised better
DateTime t0;
Widget timeBasedPageView(){
return PageView(
onPageChanged: (item) {
// Obtain a measure of change in time.
DateTime t1 = t0 ?? DateTime.now();
t0 = DateTime.now();
int millisSincePageChange = t0.difference(t1).inMilliseconds;
print("Millis: $millisSincePageChange");
// Loop through the intervals, they only affect how much time is
// allocated before pageSnapping is enabled again.
for (int i = 1; i < intervals.length; i++) {
bool lwrBnd = millisSincePageChange > intervals[i - 1];
bool uprBnd = millisSincePageChange < intervals[i];
bool withinBounds = lwrBnd && uprBnd;
if (withinBounds) {
print("Index triggered: $i , lwr: $lwrBnd, upr: $uprBnd");
// The two setState calls ensures that pageSnapping will
// always return to being true.
setState(() {
pageSnapping = false;
});
// Allows some time for the fast pageChanges to proceed
// without being pageSnapped.
Future.delayed(Duration(milliseconds: i * 100)).then((val){
setState(() {
pageSnapping = true;
});
});
}
}
},
pageSnapping: pageSnapping,
children: widgets,
);
}
I hope this helps in some way.
Edit: another answer based upon Hannes' answer.
class PageCarousel extends StatefulWidget {
#override
_PageCarouselState createState() => _PageCarouselState();
}
class _PageCarouselState extends State<PageCarousel> {
int _currentPage = 0;
PageController _pageController;
int timePrev; //Tid
double posPrev; //Position
List<Widget> widgets = List.generate(
10,
(item) =>
Container(
padding: EdgeInsets.all(8),
child: Card(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text("Index $item"),
],
),
),
));
#override
void initState() {
super.initState();
_pageController = PageController(
viewportFraction: 0.75,
initialPage: 0,
);
}
int boundedPage(int newPage){
if(newPage < 0){
return 0;
}
if(newPage >= widgets.length){
return widgets.length - 1;
}
return newPage;
}
#override
Widget build(BuildContext context) {
return Container(
child: Listener(
onPointerDown: (pos){
posPrev = pos.position.dx;
timePrev = DateTime.now().millisecondsSinceEpoch;
print("Down");
print("Time: $timePrev");
},
onPointerUp: (pos){
int newTime = DateTime.now().millisecondsSinceEpoch;
int timeDx = newTime - timePrev;
double v = (posPrev - pos.position.dx) / (timeDx);
int newPage = _currentPage + (v * 1.3).round();
print("Velocity: $v");
print("New Page: $newPage, Old Page: $_currentPage");
if (v < 0 && newPage < _currentPage || v >= 0 && newPage > _currentPage) {
_currentPage = boundedPage(newPage);
}
_pageController.animateToPage(_currentPage,
duration: Duration(milliseconds: 800), curve: Curves.easeOutCubic);
},
child: PageView(
controller: _pageController,
physics: ClampingScrollPhysics(), //Will scroll to far with BouncingScrollPhysics
scrollDirection: Axis.horizontal,
children: widgets,
),
),
);
}
}
This should ensure a decent multi page navigation.
The easiest and most natural way is to customize the physics of the PageView. You can set velocityPerOverscroll (logical pixels per second) according to your needs.
PageView(
pageSnapping: false,
physics: const PageOverscrollPhysics(velocityPerOverscroll: 1000),
Extend ScrollPhysics to control the snap behavior:
class PageOverscrollPhysics extends ScrollPhysics {
///The logical pixels per second until a page is overscrolled.
///A satisfying value can be determined by experimentation.
///
///Example:
///If the user scroll velocity is 3500 pixel/second and [velocityPerOverscroll]=
///1000, then 3.5 pages will be overscrolled/skipped.
final double velocityPerOverscroll;
const PageOverscrollPhysics({
ScrollPhysics? parent,
this.velocityPerOverscroll = 1000,
}) : super(parent: parent);
#override
PageOverscrollPhysics applyTo(ScrollPhysics? ancestor) {
return PageOverscrollPhysics(
parent: buildParent(ancestor)!,
);
}
double _getTargetPixels(ScrollMetrics position, double velocity) {
double page = position.pixels / position.viewportDimension;
page += velocity / velocityPerOverscroll;
double pixels = page.roundToDouble() * position.viewportDimension;
return pixels;
}
#override
Simulation? createBallisticSimulation(
ScrollMetrics position, double velocity) {
// If we're out of range and not headed back in range, defer to the parent
// ballistics, which should put us back in range at a page boundary.
if ((velocity <= 0.0 && position.pixels <= position.minScrollExtent) ||
(velocity >= 0.0 && position.pixels >= position.maxScrollExtent)) {
return super.createBallisticSimulation(position, velocity);
}
final double target = _getTargetPixels(position, velocity);
if (target != position.pixels) {
return ScrollSpringSimulation(spring, position.pixels, target, velocity,
tolerance: tolerance);
}
return null;
}
#override
bool get allowImplicitScrolling => false;
}
Note that it is also required to set pageSnapping: false, this is done intentionally to disable the internal physics of PageView, which we are overwriting with PageOverscrollPhysics. Although PageOverscrollPhysics might look crazy complicated, it's essentially just an adjustment of the PageScrollPhysics()class.

passing string as FontAwesomeIcons

am using font_awesome_flutter in my app to display some icons..
am getting the name of the icons from json as strings .. how i can pass it to the icon?
there is a way to achieve this?
for example:
am getting from the json:
String icon = 'ad';
and then i want to use it like this:
new Icon(FontAwesomeIcons.icon),
i know it doesn't work like this .. but how can i do this? is it doable?
I found one way may that help you. edit font_awesome_flutter.dart file as following and also access as below.
I just demonstrate with two Icon you can go on with as much as you need or for all.
font_awesome_flutter.dart
library font_awesome_flutter;
import 'package:flutter/widgets.dart';
import 'package:font_awesome_flutter/icon_data.dart';
// THIS FILE IS AUTOMATICALLY GENERATED!
class FontAwesomeIcons {
static const createDoc = {
'fiveHundredPx': IconDataBrands(0xf26e),
'accessibleIcon': IconDataBrands(0xf368),
//.......add all Icons HERE
};
static const IconData fiveHundredPx = const IconDataBrands(0xf26e);
static const IconData accessibleIcon = const IconDataBrands(0xf368);
static const IconData accusoft = const IconDataBrands(0xf369);
static const IconData acquisitionsIncorporated = const IconDataBrands(0xf6af);
static const IconData ad = const IconDataSolid(0xf641);
static const IconData addressBook = const IconDataRegular(0xf2b9);
static const IconData solidAddressBook = const IconDataSolid(0xf2b9);
static const IconData addressCard = const IconDataRegular(0xf2bb);
static const IconData solidAddressCard = const IconDataSolid(0xf2bb);
//.......
//.......add all Icons HERE To as already in your file
}
Now you can use as Following Code:
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
void main() {
runApp(new FontAwesomeGalleryApp());
}
class FontAwesomeGalleryApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Font Awesome Flutter Gallery',
theme: new ThemeData.light().copyWith(
iconTheme: new IconThemeData(size: 36.0, color: Colors.black87),
textTheme: new TextTheme(
body1: new TextStyle(fontSize: 16.0, color: Colors.black87),
),
),
home: new Home(),
);
}
}
class Home extends StatelessWidget {
String data = 'fiveHundredPx';
#override
Widget build(BuildContext context) {
return Scaffold(
body: new Container(
child: Center(
child: Icon(FontAwesomeIcons.createDoc[data.toString()]),
),
),
);
}
}
I know That This Way is little bit tough but i found this way only.
You can save the unicode data in the Json. For example :
static const IconData clock = const IconDataRegular(0xf017);
save f017 in the Json.
Later use the following function for conversion into an int.
int getHexFromStr(String fontCode) {
int val = 0;
int len = fontCode.length;
for (int i = 0; i < len; i++) {
int hexDigit = fontCode.codeUnitAt(i);
if (hexDigit >= 48 && hexDigit <= 57) {
val += (hexDigit - 48) * (1 << (4 * (len - 1 - i)));
} else if (hexDigit >= 65 && hexDigit <= 70) {
// A..F
val += (hexDigit - 55) * (1 << (4 * (len - 1 - i)));
} else if (hexDigit >= 97 && hexDigit <= 102) {
// a..f
val += (hexDigit - 87) * (1 << (4 * (len - 1 - i)));
} else {
throw new FormatException("An error occurred when converting");
}
}
return val;
}
And finally use the following :
Icon(IconDataSolid(getHexFromStr(' f017')))

How can I customize / rotate a BoxDecoration for a Container widget in Flutter?

I have a Widget that builds a circular profile picture for bus stops, and as of right now it has a circular border surrounding the profile picture like such. I want to change the circular border to be dashed instead and to also animate by circling/rotating around the profile picture in its dashed form. Is there any easy way to do that? Thanks so much for any help you can provide!
return new Transform(
transform: new Matrix4.diagonal3Values(
animation.avatarSize.value,
animation.avatarSize.value,
1.0,
),
alignment: Alignment.center,
child: new Container(
width: 110.0,
height: 110.0,
decoration: new BoxDecoration(
shape: BoxShape.circle,
border: new Border.all(
color: Colors.white30,
),
),
margin: const EdgeInsets.only(
top: 24.0,
left: 16.0,
),
padding: const EdgeInsets.all(3.0),
child: new ClipOval(
child: new Image.asset(
stopName.avatar,
),
),
),
);
Unfortunately the simple answer is that there is no easy way to do that. The flutter people in their infinite wisdom have made the conclusion that dashed lines are not performant enough to be included in flutter and therefore no-one will ever need to draw a dashed line. (Yes, the logical discontinuity in that sentence is intended. Don't get me wrong - I love flutter and the devs have done a great job, but they do seem to have made a few semi-arbitrary decisions based on performance rather than functionality).
The interpretation I've seen of why is that basically the C++ version would be doing something similar to dart code (unless it were done directly on the GPU), and so they expect someone to eventually implement dashed lines in dart instead (possibly as part of a library?). See this github bug for progress and discussion... and +1 it if you want to see dashes in future. If enough people do that then eventually the flutter people may decide to implement it.
But for now, that means that there is no easy way to make CircleBorder, or any other type of border, with a dashed line.
However, with maximum effort, exactly what you want can be achieved =). Below is a quick cut of the code for you that does what you want. Be warned - it isn't very optimized, and there are probably easier ways to do it, and you could probably use a Decoration and implement the paint etc there or something... but this does work.
import 'dart:math';
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: new SafeArea(
child: Column(
children: [
new DashedCircle(
child: new ClippedDrawing(),
)
],
),
),
);
}
}
class ClippedDrawing extends StatelessWidget {
#override
Widget build(BuildContext context) => new ClipOval(
child: new Container(
color: Colors.red,
),
);
}
class DashedCircle extends StatefulWidget {
final Widget child;
const DashedCircle({Key key, this.child}) : super(key: key);
#override
DashedBorderState createState() => new DashedBorderState();
}
class DashedBorderState extends State<DashedCircle> with TickerProviderStateMixin<DashedCircle> {
AnimationController controller;
Animation<double> animation;
#override
void initState() {
super.initState();
controller = new AnimationController(vsync: this, duration: Duration(seconds: 10));
animation = new Tween(begin: 0.0, end: pi * 2.0).animate(controller);
controller.repeat();
}
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: animation,
builder: (context, child) {
return new CustomPaint(
painter: OvalPainter(
color: Colors.blue, borderWidth: 1.0, dashLength: 5.0, spaceLength: 2.0, offset: animation.value),
child: child,
);
},
child: Container(
width: 110.0,
height: 110.0,
padding: EdgeInsets.all(3.0),
child: widget.child,
),
);
}
}
class OvalPainter extends CustomPainter {
final Color color;
final double borderWidth;
final double dashLength;
final double spaceLength;
final double offset;
OvalPainter(
{#required this.borderWidth,
#required this.dashLength,
#required this.spaceLength,
#required this.offset,
#required this.color});
double lastShortestSide;
double lastDashLength;
double lastSpaceLength;
Path lastPath;
#override
void paint(Canvas canvas, Size size) {
Rect rect = Offset.zero & size;
var radius = rect.shortestSide / 2;
canvas.translate(radius, radius);
canvas.rotate(offset);
Path path;
if (lastShortestSide == rect.shortestSide &&
dashLength == lastDashLength &&
spaceLength == lastSpaceLength &&
lastPath != null) {
path = lastPath;
} else {
path = _getDashedCircularPath(rect.shortestSide / 2, dashLength, spaceLength);
lastPath = path;
lastShortestSide = rect.shortestSide;
lastDashLength = dashLength;
lastSpaceLength = spaceLength;
}
canvas.drawPath(
path,
new Paint()
..style = PaintingStyle.stroke
..color = color
..strokeWidth = borderWidth,
);
}
#override
bool shouldRepaint(OvalPainter oldDelegate) {
return offset != oldDelegate.offset ||
color != oldDelegate.color ||
borderWidth != oldDelegate.borderWidth ||
dashLength != oldDelegate.dashLength ||
spaceLength != oldDelegate.spaceLength;
}
static Path _getDashedCircularPathFromNumber(double radius, int numSections, double dashPercentage) {
var tau = 2 * pi;
var actualTotalLength = tau / numSections;
var actualDashLength = actualTotalLength * dashPercentage;
double offset = 0.0;
Rect rect = new Rect.fromCircle(center: Offset.zero, radius: radius);
Path path = new Path();
for (int i = 0; i < numSections; ++i) {
path.arcTo(rect, offset, actualDashLength, true);
offset += actualTotalLength;
}
return path;
}
static Path _getDashedCircularPath(double radius, double dashLength, double spaceLength) {
// first, find number of radians that dashlength + spacelength take
var tau = 2 * pi;
var circumference = radius * tau;
var dashSpaceLength = dashLength + spaceLength;
var num = circumference / (dashSpaceLength);
// we'll floor the value so that it's close-ish to the same amount as requested but
// instead will be the same all around
var closeNum = num.floor();
return _getDashedCircularPathFromNumber(radius, closeNum, dashLength / dashSpaceLength);
}
}

Flutter - I am looking for a way to do a pulse animation

i am trying to achieved some thing like this in flutter
One way is with CustomPainter and an animation. Also look at SpriteWidget.
import 'dart:math';
import 'package:flutter/material.dart';
class SpritePainter extends CustomPainter {
final Animation<double> _animation;
SpritePainter(this._animation) : super(repaint: _animation);
void circle(Canvas canvas, Rect rect, double value) {
double opacity = (1.0 - (value / 4.0)).clamp(0.0, 1.0);
Color color = Color.fromRGBO(0, 117, 194, opacity);
double size = rect.width / 2;
double area = size * size;
double radius = sqrt(area * value / 4);
final Paint paint = Paint()..color = color;
canvas.drawCircle(rect.center, radius, paint);
}
#override
void paint(Canvas canvas, Size size) {
Rect rect = Rect.fromLTRB(0.0, 0.0, size.width, size.height);
for (int wave = 3; wave >= 0; wave--) {
circle(canvas, rect, wave + _animation.value);
}
}
#override
bool shouldRepaint(SpritePainter oldDelegate) {
return true;
}
}
class SpriteDemo extends StatefulWidget {
#override
SpriteDemoState createState() => SpriteDemoState();
}
class SpriteDemoState extends State<SpriteDemo>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
#override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
);
//_startAnimation();
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
void _startAnimation() {
_controller
..stop()
..reset()
..repeat(period: const Duration(seconds: 1));
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Pulse')),
body: CustomPaint(
painter: SpritePainter(_controller),
child: SizedBox(
width: 200.0,
height: 200.0,
),
),
floatingActionButton: FloatingActionButton(
onPressed: _startAnimation,
child: new Icon(Icons.play_arrow),
),
);
}
}
void main() {
runApp(
MaterialApp(
home: SpriteDemo(),
),
);
}
You can also use Flare with: flare_flutter. It's more simple.
For those looking to create an animation which is not circular, but rectangular with possible rounded borders, you can replace the SpritePainter from the top answer with:
class SpritePainter extends CustomPainter {
final Animation<double> _animation;
SpritePainter(this._animation) : super(repaint: _animation);
void roundedRect(Canvas canvas, Rect rect, double animValue, int waveAmount) {
double opacity = (1.0 - (animValue / waveAmount)).clamp(0.0, 1.0);
Color color = new Color.fromRGBO(0, 117, 194, opacity);
final pixelMiltiplier = 20;
final newWidth = rect.width + animValue*pixelMiltiplier;
final newHeight = rect.height + animValue*pixelMiltiplier;
final widthIncrease = newWidth/rect.width;
final heightIncrease = newHeight/rect.height;
final widthOffset = (widthIncrease - 1) / 2;
final heightOffet = (heightIncrease - 1) / 2;
final Paint paint = new Paint()..color = color;
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromLTWH(-rect.width * widthOffset, -rect.height * heightOffet,
rect.width * widthIncrease, rect.height * heightIncrease),
Radius.circular(15.0)),
paint);
}
#override
void paint(Canvas canvas, Size size) {
Rect rect = new Rect.fromLTRB(0.0, 0.0, size.width, size.height);
final waveAmount = 1;
if (!_animation.isDismissed) {
for (int wave = waveAmount-1; wave >= 0; wave--) {
roundedRect(canvas, rect, wave + _animation.value, waveAmount);
}
}
}
#override
bool shouldRepaint(SpritePainter oldDelegate) {
return true;
}
}

Resources