I built a custom timer widget and am calling it through the main.dart file. My timer widget essentially takes an argument totalDuration and using that starts running the timer. In the main.dart file I created a variable called counter and am passing it as the value to totalDuration. Till here it works fine. Now when I create a button, which on being clicked increments the counter variable and calls the setState method, my counter varibale is being incremented but widget is not being rebuilt. Why is that so and how could I go about solving this problem? For reference I have attached the codes from my both my main and timer file here.
main.dart
import 'package:flutter/material.dart';
import 'package:flutter_app_test_counter/timer.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 60;
void _incrementCounter() {
setState(() {
print(_counter);
_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:',
),
Expanded(
child: Timer(
totalDuration: _counter,
),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
timer.dart
import 'dart:ui';
import 'package:flutter/material.dart';
class Timer extends StatefulWidget {
final int totalDuration;
const Timer({Key key, this.totalDuration}) : super(key: key);
#override
_Timer createState() => _Timer();
}
class _Timer extends State<Timer> with TickerProviderStateMixin {
double _progress = 0.0;
bool _reversed = true;
bool _stopped = false;
Duration duration;
Animation<double> animation;
AnimationController controller;
String get _timeRemaining {
if (controller.lastElapsedDuration != null) {
duration = _reversed
? controller.duration - controller.lastElapsedDuration
: controller.lastElapsedDuration + Duration(seconds: 1);
}
return '${(duration.inHours).toString().padLeft(2, '0')}:${(duration.inMinutes % 60).toString().padLeft(2, '0')}:${(duration.inSeconds % 60).toString().padLeft(2, '0')}';
}
#override
void initState() {
super.initState();
controller = AnimationController(
vsync: this,
duration: Duration(seconds: widget.totalDuration),
);
animation = Tween(begin: 1.0, end: 0.0).animate(controller)
..addListener(() {
setState(() {
_progress = animation.value;
});
});
controller.forward();
}
#override
void dispose() {
controller.dispose();
_stopped = !_stopped;
super.dispose();
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => _reversed = !_reversed,
child: Scaffold(
body: CustomPaint(
painter:
ShapePainter(progress: _progress, timeRemaining: _timeRemaining),
child: Container(),
),
),
);
}
}
class ShapePainter extends CustomPainter {
double progress;
String timeRemaining;
ShapePainter({this.progress, this.timeRemaining});
#override
void paint(Canvas canvas, Size size) {
final rectBounds = Rect.fromLTRB(0, 0, size.width, size.height);
final rectPaint = Paint()
..strokeWidth = 1
..style = PaintingStyle.fill
..color = Colors.orange;
canvas.drawRRect(
RRect.fromRectAndRadius(rectBounds, Radius.circular(10)),
rectPaint,
);
var paintProgressBar = Paint()
..color = Colors.white
..strokeWidth = 6
..strokeCap = StrokeCap.round;
Offset progressStartingPoint = Offset(42, size.height - 60);
Offset progressEndingPoint = Offset(size.width - 42, size.height - 60);
canvas.drawLine(
progressStartingPoint, progressEndingPoint, paintProgressBar);
var paintDoneBar = Paint()
..color = Colors.deepOrange
..strokeWidth = 6
..strokeCap = StrokeCap.round;
Offset doneStartingPoint = Offset(42, size.height - 60);
Offset doneEndingPoint =
Offset(((size.width - 84) * (1.0 - progress) + 42), size.height - 60);
canvas.drawLine(doneStartingPoint, doneEndingPoint, paintDoneBar);
final timerTextStyle = TextStyle(
color: Colors.indigo,
fontSize: 30,
);
final timerTextSpan = TextSpan(
text: timeRemaining,
style: timerTextStyle,
);
final timerTextPainter = TextPainter(
text: timerTextSpan,
textDirection: TextDirection.ltr,
);
timerTextPainter.layout(
minWidth: 0,
maxWidth: size.width,
);
final timerOffset = Offset(size.width / 2, size.height / 2 - 40);
timerTextPainter.paint(canvas, timerOffset);
final textStyle = TextStyle(
color: Colors.black,
fontSize: 30,
);
final textSpan = TextSpan(
text: 'time left',
style: textStyle,
);
final textPainter = TextPainter(
text: textSpan,
textDirection: TextDirection.ltr,
);
textPainter.layout(
minWidth: 0,
maxWidth: size.width,
);
final offset = Offset((size.width - 20) / 2, (size.height - 20) / 2);
textPainter.paint(canvas, offset);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
One quick dirty fix is to add a key to the Timer widget
Timer(
key: UniqueKey(),
totalDuration: _counter,
),
Other way is to use didUpdateWidget function in your child Timer() widget.
In that function, you can make the changes.
#override
void didUpdateWidget(Timer oldWidget) {
print("didUpdateWidget called");
super.didUpdateWidget(oldWidget);
}
https://api.flutter.dev/flutter/widgets/State/didUpdateWidget.html
Related
Is it possible to somehow do in flutter what is shown in the GIF?
And so that the starting point always displays a number (in what position this point is at the moment), in the range from 0-100.
EDIT: The animation is now similar to the one in GIF
import 'dart:ui';
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatefulWidget {
const MyApp({super.key});
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
late final AnimationController _controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 3)
);
late final Size size = const Size(400,400);
late Path path = Path()
..moveTo(3, size.height-4.5)
..quadraticBezierTo(
size.width, size.height,
size.width-4.5, 3,
);
late PathAnimation pathAnimation = PathAnimation(path: path);
#override
void initState() {
super.initState();
_controller.forward();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Material App',
home: Scaffold(
appBar: AppBar(
title: const Text('Material App Bar'),
),
body: Center(
child: ClipRect(
child: AnimatedBuilder(
animation: _controller,
child: Container(
alignment: Alignment.center,
height: size.height,
width: size.width,
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(
color: Colors.black,
width: 3,
)
),
),
builder: (context, child) {
return CustomPaint(
foregroundPainter: PathPainter(
animation: _controller.value,
pathAnimation: pathAnimation,
),
child: child
);
}
),
),
),
),
);
}
}
class PathAnimation {
final Path path;
Path currentPath = Path();
late final List<PathMetric> pathSegments;
late final double totalLength;
double currentLength = 0;
int currentPathIndex = 0;
PathAnimation({
required this.path
}) {
pathSegments = path.computeMetrics().toList();
totalLength = pathSegments.fold(0, (value, element) => value + element.length);
}
Path getCurrentPath(double animation) {
while (animation > (currentLength + pathSegments[currentPathIndex].length) / totalLength) {
double segmentLength = pathSegments[currentPathIndex].length;
currentPath.addPath(
pathSegments[currentPathIndex].extractPath(0, segmentLength),
Offset.zero
);
currentPathIndex++;
currentLength += segmentLength;
}
if (currentPathIndex >= pathSegments.length) return currentPath;
double missingPartLength = animation - currentLength / totalLength;
double newSegmentLength = pathSegments[currentPathIndex].length;
return
currentPath..addPath(
pathSegments[currentPathIndex].extractPath(0, missingPartLength * newSegmentLength),
Offset.zero
);
}
}
class PathPainter extends CustomPainter{
double animation;
PathAnimation pathAnimation;
PathPainter({
required this.animation,
required this.pathAnimation,
});
#override
void paint(Canvas canvas, Size size) {
canvas.drawPath(
pathAnimation.getCurrentPath(animation),
Paint()
..style = PaintingStyle.stroke
..strokeWidth = 3
..color = Colors.red
);
}
#override
bool shouldRepaint(PathPainter oldDelegate) {
return animation != oldDelegate.animation;
}
}
I have created a SamplePainter that inherits from CustomPainter as shown below, and in it, I am trying to create cartoonish screen tones using PictureRecorder.
When I try to use that screen tone to draw a shape using the paintImage function, I get an exception ("Object has been disposed.").
If I try to use canvas.drawImage instead, I get the same exception.
What is the problem?
import 'package:flutter/material.dart';
import 'dart:ui' as ui;
void main() {
runApp(const MyApp());
}
// https://www.flutter-study.dev/widgets/button-widget
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
appBar: AppBar(
title: const Text('Flutter Demo'),
),
body: SizedBox(
width: 400,
height: 400,
child: CustomPaint(
painter: _SamplePainter(),
),
),
floatingActionButton: FloatingActionButton(
backgroundColor: Colors.green,
onPressed: () {},
child: const Icon(Icons.add),
),
),
);
}
}
class _SamplePainter extends CustomPainter {
#override
void paint(Canvas canvas, Size size) async {
final paint = Paint()..color = Colors.blue;
final boundsRect = const Offset(50, 50) & const Size(100, 100);
canvas.drawRect(boundsRect, paint);
var aPattern = await getPattern();
canvas.drawImage(aPattern, Offset.zero, paint);
// paintImage(
// canvas: canvas,
// rect: boundsRect,
// image: aPattern,
// repeat: ImageRepeat.repeat,
// );
}
Future<ui.Image> getPattern() async {
var pictureRecorder = ui.PictureRecorder();
Canvas patternCanvas = Canvas(pictureRecorder);
List<Offset> points = const [Offset(0, 0), Offset(1, 1)];
final patternPaint = Paint()
..color = Colors.black
..strokeWidth = 1
..style = PaintingStyle.stroke
..strokeJoin = StrokeJoin.round
..isAntiAlias = false;
patternCanvas.drawPoints(ui.PointMode.points, points, patternPaint);
final aPatternPicture = pictureRecorder.endRecording();
return aPatternPicture.toImage(2, 2);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
Try like this, the main problem you have, is that paint, should be sync method, not async
void main() {
runApp(const MyApp());
}
// https://www.flutter-study.dev/widgets/button-widget
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
appBar: AppBar(
title: const Text('Flutter Demo'),
),
body: SizedBox(
width: 400,
height: 400,
child: FutureBuilder(
future: getPattern()
builder: (BuildContext context, AsyncSnapshot<ui.Image>
snapshot) {
return CustomPaint(
painter: _SamplePainter(snapshot.data),
);
},
),
),
floatingActionButton: FloatingActionButton(
backgroundColor: Colors.green,
onPressed: () {},
child: const Icon(Icons.add),
),
),
);
}
}
Future<ui.Image> getPattern() async {
var pictureRecorder = ui.PictureRecorder();
Canvas patternCanvas = Canvas(pictureRecorder);
List<Offset> points = const [Offset(0, 0), Offset(1, 1)];
final patternPaint = Paint()
..color = Colors.black
..strokeWidth = 1
..style = PaintingStyle.stroke
..strokeJoin = StrokeJoin.round
..isAntiAlias = false;
patternCanvas.drawPoints(ui.PointMode.points, points, patternPaint);
final aPatternPicture = pictureRecorder.endRecording();
return aPatternPicture.toImage(2, 2);
}
class _SamplePainter extends CustomPainter {
final ui.Image? aPattern;
_SamplePainter(this.aPattern);
#override
void paint(Canvas canvas, Size size) {
final paint = Paint()..color = Colors.blue;
final boundsRect = const Offset(50, 50) & const Size(100, 100);
canvas.drawRect(boundsRect, paint);
if (aPattern != null) {
canvas.drawImage(aPattern!, Offset.zero, paint);
}
// paintImage(
// canvas: canvas,
// rect: boundsRect,
// image: aPattern,
// repeat: ImageRepeat.repeat,
// );
}
#override
bool shouldRepaint(_SamplePainter oldDelegate) {
return aPattern != oldDelegate.aPattern;
}
}
I'm trying to Drag a widget on top of an InteractiveViewer.
The code works fine when the scale is 1.0.
However, if zoomed in, when I drag the circle:
the feedback widget is offset by a few pixels to the bottom right
when I let go, the circle moves up
Why does that happen?
Here's a demo illustrating what I'm talking about:
Below is the code of this app
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
enum _Action { scale, pan }
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
const _defaultMarkerSize = 48.0;
class _MyHomePageState extends State<MyHomePage> {
Offset _pos = Offset.zero; // Position could go from -1 to 1 in both directions
_Action action; // pan or pinch, useful to know if we need to scale down pin
final _transformationController = TransformationController();
#override
Widget build(BuildContext context) {
final scale = _transformationController.value.getMaxScaleOnAxis();
final size = _defaultMarkerSize / scale;
return Scaffold(
appBar: AppBar(title: Text(widget.title)),
body: InteractiveViewer(
transformationController: _transformationController,
maxScale: 5,
minScale: 1,
child: Stack(
children: [
Center(child: Image.asset('image/board.png')),
DraggablePin(
pos: _pos,
size: size,
onDragEnd: (details) {
final matrix = Matrix4.inverted(_transformationController.value);
final height = AppBar().preferredSize.height;
final sceneY = details.offset.dy - height;
final viewportPoint = MatrixUtils.transformPoint(
matrix,
Offset(details.offset.dx, sceneY) + Offset(_defaultMarkerSize / 2, _defaultMarkerSize / 2),
);
final screenSize = MediaQuery.of(context).size;
final x = viewportPoint.dx * 2 / screenSize.width - 1;
final y = viewportPoint.dy * 2 / screenSize.height - 1;
setState(() {
_pos = Offset(x, y);
});
},
),
],
),
onInteractionStart: (details) {
// No need to call setState as we don't need to rebuild
action = null;
},
onInteractionUpdate: (details) {
if (action == null) {
if (details.scale == 1)
action = _Action.pan;
else
action = _Action.scale;
}
if (action == _Action.scale) {
// Need to resize the pins so that they keep the same size
setState(() {});
}
},
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.restore),
onPressed: () {
setState(() {
_pos = Offset.zero;
});
},
),
);
}
}
class DraggablePin extends StatelessWidget {
final Offset pos;
final double size;
final void Function(DraggableDetails) onDragEnd;
const DraggablePin({this.pos, this.size, this.onDragEnd, Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
final offset = size / 2;
Widget pinWidget = Pin(size);
final screenSize = MediaQuery.of(context).size;
final height = screenSize.height - AppBar().preferredSize.height;
pinWidget = Draggable(
child: pinWidget,
feedback: Pin(_defaultMarkerSize),
childWhenDragging: Container(),
onDragEnd: onDragEnd,
);
return Positioned(
top: pos.dy * height / 2 + height / 2 - offset,
left: pos.dx * screenSize.width / 2 + screenSize.width / 2 - offset,
child: pinWidget,
);
}
}
class Pin extends StatelessWidget {
const Pin(this.size, {Key key}) : super(key: key);
final double size;
#override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: Center(
child: Ink(
decoration: ShapeDecoration(
color: Colors.green,
shape: const CircleBorder(),
),
child: IconButton(
constraints: BoxConstraints.tightFor(width: size, height: size),
padding: const EdgeInsets.all(0),
iconSize: size,
splashRadius: size / 2,
splashColor: Colors.white,
icon: Container(),
onPressed: () {},
),
),
),
);
}
}
I am trying since some days to connect an animated stateful child widget with a countdown timer to the parent stateful widget with the user interaction. I found this answer from Andrey on a similar question (using Tween which I do not) that already helped a lot, but I still don't get it to work. My assumption is, the child's initState could be the reason. The timer's code comes from here.
I have removed quite some code including some referenced functions/classes. This should provide a clearer picture on the logic:
In MainPageState I declare and init the _controller of the animation
In MainPageState I call the stateless widget CreateKeypad hosting among others the "go" key
When go is clicked, this event is returned to MainPageState and _controller.reverse(from: 1.0); executed
In MainPageState I call the stateful widget CountDownTimer to render the timer
In _CountDownTimerState I am not sure if my initState is correct
In _CountDownTimerState I build the animation with CustomTimerPainter from the timer code source
The animation shall render a white donut and a red, diminishing arc on top. However, I only see the white donut, not the red timer's arc. Any hint is highly appreciated.
class MainPage extends StatefulWidget {
MainPage({Key key, this.title}) : super(key: key);
final String title;
#override
_MainPageState createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> with TickerProviderStateMixin {
AnimationController _controller;
var answer="0", correctAnswer = true, result = 0;
#override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: Duration(seconds: 7));
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
),
child: SafeArea(
child: Container(
child: Column(
children: <Widget>[
CreateKeypad( // creates a keypad with a go button. when go is clicked, countdown shall start
prevInput: int.parse((answer != null ? answer : "0")),
updtedInput: (int val) {
setState(() => answer = val.toString());
},
goSelected: () {
setState(() {
if (answer == result.toString()) {
correctAnswer = true;
}
final problem = createProblem();
result = problem.result;
});
_controller.reverse(from: 1.0); // start the countdown animation
Future.delayed(const Duration(milliseconds: 300,),
() => setState(() => correctAnswer = true));
},
),
CountDownTimer(_controller), // show countdown timer
]
),
),
)
);
}
}
// CREATE KEYPAD - all keys but "1! and "go" removed
class CreateKeypad extends StatelessWidget {
final int prevInput;
final VoidCallback goSelected;
final Function(int) updtedInput;
CreateKeypad({#required this.prevInput, #required this.updtedInput, this.goSelected});
#override
Widget build(BuildContext context) {
return Row(
children: <Widget> [
Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(2.0),
child: SizedBox(
width: 80.0, height: 80.0,
child: CupertinoButton(
child: Text("1", style: TextStyle(color: CupertinoColors.black)),
onPressed: () {
updtedInput(1);
},
),
),
),
Padding(
padding: const EdgeInsets.all(2.0),
child: SizedBox(
width: 80.0, height: 80.0,
child: CupertinoButton(
child: Text("Go!", style: TextStyle(color: CupertinoColors.black)),
onPressed: () => goSelected(),
),
),
),
],
),
]
);
}
}
// CREATE COUNTDOWN https://medium.com/flutterdevs/creating-a-countdown-timer-using-animation-in-flutter-2d56d4f3f5f1
class CountDownTimer extends StatefulWidget {
CountDownTimer(this._controller);
final AnimationController _controller;
#override
_CountDownTimerState createState() => _CountDownTimerState();
}
class _CountDownTimerState extends State<CountDownTimer> with TickerProviderStateMixin {
#override
void initState() {
super.initState(); // here I have some difference to Andrey's answer because I do not use Tween
}
String get timerString {
Duration duration = widget._controller.duration * widget._controller.value;
return '${duration.inMinutes}:${(duration.inSeconds % 60)
.toString()
.padLeft(2, '0')}';
}
#override
Widget build(BuildContext context) {
return Container(
child: AnimatedBuilder(
animation: widget._controller,
builder:
(BuildContext context, Widget child) {
return CustomPaint(
painter: CustomTimerPainter( // this draws a white donut and a red diminishing arc on top
animation: widget._controller,
backgroundColor: Colors.white,
color: Colors.red,
));
},
),
);
}
}
You can copy paste run full code below
Step 1: You can put controller inside CountDownTimerState
Step 2: Use GlobalKey
CountDownTimer(key: _key)
Step 3: Call function start() inside _CountDownTimerState with _key.currentState
goSelected: () {
setState(() {
...
_controller.reverse(from: 10.0); // start the countdown animation
final _CountDownTimerState _state = _key.currentState;
_state.start();
...
class _CountDownTimerState extends State<CountDownTimer>
with TickerProviderStateMixin {
AnimationController _controller;
#override
void initState() {
_controller =
AnimationController(vsync: this, duration: Duration(seconds: 7));
super
.initState(); // here I have some difference to Andrey's answer because I do not use Tween
}
...
void start() {
setState(() {
_controller.reverse(from: 1.0);
});
}
working demo
full code
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'dart:math' as math;
class MainPage extends StatefulWidget {
MainPage({Key key, this.title}) : super(key: key);
final String title;
#override
_MainPageState createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> with TickerProviderStateMixin {
AnimationController _controller;
var answer = "0", correctAnswer = true, result = 0;
GlobalKey _key = GlobalKey();
#override
void initState() {
super.initState();
_controller =
AnimationController(vsync: this, duration: Duration(seconds: 7));
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
//navigationBar: CupertinoNavigationBar(),
child: SafeArea(
child: Container(
color: Colors.blue,
child: Column(children: <Widget>[
CreateKeypad(
// creates a keypad with a go button. when go is clicked, countdown shall start
prevInput: int.parse((answer != null ? answer : "0")),
updtedInput: (int val) {
setState(() => answer = val.toString());
},
goSelected: () {
setState(() {
if (answer == result.toString()) {
correctAnswer = true;
}
/*final problem = createProblem();
result = problem.result;*/
});
print("go");
_controller.reverse(from: 10.0); // start the countdown animation
final _CountDownTimerState _state = _key.currentState;
_state.start();
/* Future.delayed(
const Duration(
milliseconds: 300,
),
() => setState(() => correctAnswer = true));*/
},
),
Container(
height: 400,
width: 400,
child: CountDownTimer(key: _key)), // show countdown timer
]),
),
));
}
}
// CREATE KEYPAD - all keys but "1! and "go" removed
class CreateKeypad extends StatelessWidget {
final int prevInput;
final VoidCallback goSelected;
final Function(int) updtedInput;
CreateKeypad(
{#required this.prevInput, #required this.updtedInput, this.goSelected});
#override
Widget build(BuildContext context) {
return Row(children: <Widget>[
Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(2.0),
child: SizedBox(
width: 80.0,
height: 80.0,
child: CupertinoButton(
child:
Text("1", style: TextStyle(color: CupertinoColors.black)),
onPressed: () {
updtedInput(1);
},
),
),
),
Padding(
padding: const EdgeInsets.all(2.0),
child: SizedBox(
width: 80.0,
height: 80.0,
child: CupertinoButton(
child:
Text("Go!", style: TextStyle(color: CupertinoColors.black)),
onPressed: () => goSelected(),
),
),
),
],
),
]);
}
}
// CREATE COUNTDOWN https://medium.com/flutterdevs/creating-a-countdown-timer-using-animation-in-flutter-2d56d4f3f5f1
class CountDownTimer extends StatefulWidget {
CountDownTimer({Key key}) : super(key: key);
//final AnimationController _controller;
#override
_CountDownTimerState createState() => _CountDownTimerState();
}
class _CountDownTimerState extends State<CountDownTimer>
with TickerProviderStateMixin {
AnimationController _controller;
#override
void initState() {
_controller =
AnimationController(vsync: this, duration: Duration(seconds: 7));
super
.initState(); // here I have some difference to Andrey's answer because I do not use Tween
}
String get timerString {
Duration duration = _controller.duration * _controller.value;
return '${duration.inMinutes}:${(duration.inSeconds % 60).toString().padLeft(2, '0')}';
}
void start() {
setState(() {
_controller.reverse(from: 1.0);
});
}
#override
Widget build(BuildContext context) {
return Container(
color: Colors.green,
child: AnimatedBuilder(
animation: _controller,
builder: (BuildContext context, Widget child) {
return CustomPaint(
painter: CustomTimerPainter(
// this draws a white donut and a red diminishing arc on top
animation: _controller,
backgroundColor: Colors.green,
color: Colors.red,
));
},
),
);
}
}
class CustomTimerPainter extends CustomPainter {
CustomTimerPainter({
this.animation,
this.backgroundColor,
this.color,
}) : super(repaint: animation);
final Animation<double> animation;
final Color backgroundColor, color;
#override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..color = backgroundColor
..strokeWidth = 10.0
..strokeCap = StrokeCap.butt
..style = PaintingStyle.stroke;
canvas.drawCircle(size.center(Offset.zero), size.width / 2.0, paint);
paint.color = color;
double progress = (1.0 - animation.value) * 2 * math.pi;
//print("progress ${progress}");
canvas.drawArc(Offset.zero & size, math.pi * 1.5, -progress, false, paint);
}
#override
bool shouldRepaint(CustomTimerPainter old) {
//print(animation.value);
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: MainPage(),
);
}
}
Is it possible to change text paint like the below image? I need to add two lines and without rotating.
Container(
foregroundDecoration: const BadgeDecoration(
badgeColor: Colors.green,
badgeSize: 50,
textSpan: TextSpan(
text: '42',
style: TextStyle(color: Colors.black, fontSize: 10),
),
),
),
Decoration
import 'package:flutter/material.dart';
import 'dart:math' as math;
class BadgeDecoration extends Decoration {
final Color badgeColor;
final double badgeSize;
final TextSpan textSpan;
const BadgeDecoration({#required this.badgeColor, #required this.badgeSize, #required this.textSpan});
#override
BoxPainter createBoxPainter([onChanged]) => _BadgePainter(badgeColor, badgeSize, textSpan);
}
class _BadgePainter extends BoxPainter {
static const double BASELINE_SHIFT = 1;
static const double CORNER_RADIUS = 4;
final Color badgeColor;
final double badgeSize;
final TextSpan textSpan;
_BadgePainter(this.badgeColor, this.badgeSize, this.textSpan);
#override
void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
canvas.save();
canvas.translate(offset.dx + configuration.size.width - badgeSize, offset.dy);
canvas.drawPath(buildBadgePath(), getBadgePaint());
// draw text
final hyp = math.sqrt(badgeSize * badgeSize + badgeSize * badgeSize);
final textPainter = TextPainter(text: textSpan, textDirection: TextDirection.ltr, textAlign: TextAlign.center);
textPainter.layout(minWidth: hyp, maxWidth: hyp);
final halfHeight = textPainter.size.height / 2;
final v = math.sqrt(halfHeight * halfHeight + halfHeight * halfHeight) + BASELINE_SHIFT;
canvas.translate(v, -v);
canvas.rotate(0.785398); // 45 degrees
textPainter.paint(canvas, Offset.zero);
canvas.restore();
}
Paint getBadgePaint() => Paint()
..isAntiAlias = true
..color = badgeColor;
Path buildBadgePath() => Path.combine(
PathOperation.difference,
Path()..addRRect(RRect.fromLTRBAndCorners(0, 0, badgeSize, badgeSize, topRight: Radius.circular(CORNER_RADIUS))),
Path()
..lineTo(0, badgeSize)
..lineTo(badgeSize, badgeSize)
..close());
}
You can copy paste run full code below
You can paint before rotate and provide offset
code snippet
TextSpan span = new TextSpan(style: new TextStyle(color: Colors.black, fontSize: 10), text: "Point");
final textPainter1 = TextPainter(text: span, textDirection: TextDirection.ltr, textAlign: TextAlign.center);
textPainter1.layout(minWidth: hyp, maxWidth: hyp);
textPainter1.paint(canvas, Offset(0.0, 5.0));
textPainter.paint(canvas, Offset(3.0, 17.0));
canvas.translate(v, -v);
canvas.rotate(0.785398);
working demo
full code
import 'package:flutter/material.dart';
import 'dart:math' as math;
class BadgeDecoration extends Decoration {
final Color badgeColor;
final double badgeSize;
final TextSpan textSpan;
const BadgeDecoration({#required this.badgeColor, #required this.badgeSize, #required this.textSpan});
#override
BoxPainter createBoxPainter([onChanged]) => _BadgePainter(badgeColor, badgeSize, textSpan);
}
class _BadgePainter extends BoxPainter {
static const double BASELINE_SHIFT = 1;
static const double CORNER_RADIUS = 4;
final Color badgeColor;
final double badgeSize;
final TextSpan textSpan;
_BadgePainter(this.badgeColor, this.badgeSize, this.textSpan);
#override
void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
canvas.save();
canvas.translate(offset.dx + configuration.size.width - badgeSize, offset.dy);
canvas.drawPath(buildBadgePath(), getBadgePaint());
// draw text
final hyp = math.sqrt(badgeSize * badgeSize + badgeSize * badgeSize);
final textPainter = TextPainter(text: textSpan, textDirection: TextDirection.ltr, textAlign: TextAlign.center);
textPainter.layout(minWidth: hyp, maxWidth: hyp);
final halfHeight = textPainter.size.height / 2;
final v = math.sqrt(halfHeight * halfHeight + halfHeight * halfHeight) + BASELINE_SHIFT;
TextSpan span = new TextSpan(style: new TextStyle(color: Colors.black, fontSize: 10), text: "Point");
final textPainter1 = TextPainter(text: span, textDirection: TextDirection.ltr, textAlign: TextAlign.center);
textPainter1.layout(minWidth: hyp, maxWidth: hyp);
textPainter1.paint(canvas, Offset(0.0, 5.0));
textPainter.paint(canvas, Offset(3.0, 17.0));
canvas.translate(v, -v);
canvas.rotate(0.785398);
canvas.restore();
}
Paint getBadgePaint() => Paint()
..isAntiAlias = true
..color = badgeColor;
Path buildBadgePath() => Path.combine(
PathOperation.difference,
Path()..addRRect(RRect.fromLTRBAndCorners(0, 0, badgeSize, badgeSize, topRight: Radius.circular(CORNER_RADIUS))),
Path()
..lineTo(0, badgeSize)
..lineTo(badgeSize, badgeSize)
..close());
}
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: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
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>[
Container(
foregroundDecoration: const BadgeDecoration(
badgeColor: Colors.green,
badgeSize: 50,
textSpan: TextSpan(
text: '42',
style: TextStyle(color: Colors.black, fontSize: 10),
),
),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}