Flutter - Creating a custom control with Flutter - dart

I need to create a custom control which allows a user to drag a pointer within a bounded rectangle. Very like this joystick control here :https://github.com/zerokol/JoystickView
I have managed to cobble something together using a CustomPainter to draw the control point and a GestureDetector to track where the user drags the pointer on the view. However, I can't get this to capture the panning input. I can't get it to capture any input at all. I don't know if what I am doing is the best approach. I could be on the totally wrong track. Here is the code.
import 'package:flutter/material.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(new TouchTest());
}
class TouchTest extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Touch Test',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new Scaffold(
appBar: new AppBar(
title: const Text('Test'),
),
body: new Container(
decoration: new BoxDecoration(
color: Colors.white,
border: new Border.all(
color: Colors.black,
width: 2.0,
),
),
child: new Center(
child: new TouchControl()
),
),
)
);
}
}
class TouchControl extends StatefulWidget {
final double xPos;
final double yPos;
final ValueChanged<Offset> onChanged;
const TouchControl({Key key,
this.onChanged,
this.xPos:0.0,
this.yPos:0.0}) : super(key: key);
#override
TouchControlState createState() => new TouchControlState();
}
/**
* Draws a circle at supplied position.
*
*/
class TouchControlState extends State<TouchControl> {
double xPos = 0.0;
double yPos = 0.0;
GestureDetector _gestureDetector;
TouchControl() {
_gestureDetector = new GestureDetector(
onPanStart:_handlePanStart,
onPanEnd: _handlePanEnd,
onPanUpdate: _handlePanUpdate);
}
void onChanged(Offset offset) {
setState(() {
widget.onChanged(offset);
xPos = offset.dx;
yPos = offset.dy;
});
}
#override
bool hitTestSelf(Offset position) => true;
#override
void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
if (event is PointerDownEvent ) {
// ??
}
}
void _handlePanStart(DragStartDetails details) {
onChanged(details.globalPosition);
}
void _handlePanEnd(DragEndDetails details) {
// TODO
}
void _handlePanUpdate(DragUpdateDetails details) {
onChanged(details.globalPosition);
}
#override
Widget build(BuildContext context) {
return new Center(
child: new CustomPaint(
size: new Size(xPos, yPos),
painter: new TouchControlPainter(xPos, yPos),
),
);
}
}
class TouchControlPainter extends CustomPainter {
static const markerRadius = 10.0;
final double xPos;
final double yPos;
TouchControlPainter(this.xPos, this.yPos);
#override
void paint(Canvas canvas, Size size) {
final paint = new Paint()
..color = Colors.blue[400]
..style = PaintingStyle.fill;
canvas.drawCircle(new Offset(xPos, yPos), markerRadius, paint);
}
#override
bool shouldRepaint(TouchControlPainter old) => xPos != old.xPos && yPos !=old.yPos;
}

Your code isn't using the GestureDetector anywhere.
You should use it to wrap the CustomPaint the build() function of TouchControlState.
import 'package:flutter/material.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(new TouchTest());
}
class TouchTest extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Touch Test',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new Scaffold(
appBar: new AppBar(
title: const Text('Test'),
),
body: new Container(
decoration: new BoxDecoration(
color: Colors.white,
border: new Border.all(
color: Colors.black,
width: 2.0,
),
),
child: new Center(
child: new TouchControl()
),
),
)
);
}
}
class TouchControl extends StatefulWidget {
final double xPos;
final double yPos;
final ValueChanged<Offset> onChanged;
const TouchControl({Key key,
this.onChanged,
this.xPos:0.0,
this.yPos:0.0}) : super(key: key);
#override
TouchControlState createState() => new TouchControlState();
}
/**
* Draws a circle at supplied position.
*
*/
class TouchControlState extends State<TouchControl> {
double xPos = 0.0;
double yPos = 0.0;
GlobalKey _painterKey = new GlobalKey();
void onChanged(Offset offset) {
final RenderBox referenceBox = context.findRenderObject();
Offset position = referenceBox.globalToLocal(offset);
if (widget.onChanged != null)
widget.onChanged(position);
setState(() {
xPos = position.dx;
yPos = position.dy;
});
}
#override
bool hitTestSelf(Offset position) => true;
#override
void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
if (event is PointerDownEvent ) {
// ??
}
}
void _handlePanStart(DragStartDetails details) {
onChanged(details.globalPosition);
}
void _handlePanEnd(DragEndDetails details) {
print('end');
// TODO
}
void _handlePanUpdate(DragUpdateDetails details) {
onChanged(details.globalPosition);
}
#override
Widget build(BuildContext context) {
return new ConstrainedBox(
constraints: new BoxConstraints.expand(),
child: new GestureDetector(
behavior: HitTestBehavior.opaque,
onPanStart:_handlePanStart,
onPanEnd: _handlePanEnd,
onPanUpdate: _handlePanUpdate,
child: new CustomPaint(
size: new Size(xPos, yPos),
painter: new TouchControlPainter(xPos, yPos),
),
),
);
}
}
class TouchControlPainter extends CustomPainter {
static const markerRadius = 10.0;
final double xPos;
final double yPos;
TouchControlPainter(this.xPos, this.yPos);
#override
void paint(Canvas canvas, Size size) {
final paint = new Paint()
..color = Colors.blue[400]
..style = PaintingStyle.fill;
canvas.drawCircle(new Offset(xPos, yPos), markerRadius, paint);
}
#override
bool shouldRepaint(TouchControlPainter old) => xPos != old.xPos && yPos !=old.yPos;
}

Related

How to build animated headers like this GIF in Flutter?

In iOS, I wrote a somewhat complex custom UIViewController that handles transitioning between unique child controllers; most notably, a special header view at the top of each one. I'm still trying to really wrap my head around end to end architecture in Flutter, and would like some suggestions on how to accomplish this. There are two types of headers - Arc and Profile, and each one goes from an expanded to a collapsed state as the user scrolls. Additionally, navigation between any combination of type and state can have a transition defined.
Here is how it looks when used in a TabBar for example. Transitions are handled gracefully wether nested in Tab/NavigationControllers or not.
This is what I threw together, I hope it helps (click for video):
Note:
It would be better to reduce the amount of animation controllers, ideally to a single controller that controls both the header extent and the arc curvature
There is no animation for the content below the header, but I'm sure you could add that as well.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Anim playground',
theme: ThemeData(
brightness: Brightness.dark,
),
home: AnimatedPageTest(),
);
}
}
class AnimatedPageTest extends StatefulWidget {
#override
_AnimatedPageTestState createState() => _AnimatedPageTestState();
}
class _AnimatedPageTestState extends State<AnimatedPageTest> {
bool _arc = true;
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(child: AnimatedPage(
appearance: _arc ? HeaderAppearance.arc : HeaderAppearance.profile,
backgroundImage: _arc ? 'assets/earth.jpg' : 'assets/moon.jpg',
children: List.generate(30, (index) => ListTile(title: Text('index'),)),
),),
persistentFooterButtons: <Widget>[
FlatButton(
child: Text('Switch'),
onPressed: () {
setState(() {
_arc = !_arc;
});
},
)
],
);
}
}
enum HeaderAppearance { arc, profile }
double _getTargetMaxExtent(HeaderAppearance appearance) {
if (appearance == HeaderAppearance.arc) {
return 150.0;
} else {
return 75.0;
}
}
double _getTargetArcAnimationValue(HeaderAppearance appearance) {
if (appearance == HeaderAppearance.arc) {
return 1.0;
} else {
return 0.0;
}
}
class AnimatedPage extends StatefulWidget {
AnimatedPage({Key key, this.appearance, this.backgroundImage, this.children}) : super(key: key);
final HeaderAppearance appearance;
final String backgroundImage;
final List<Widget> children;
#override
_AnimatedPageState createState() => _AnimatedPageState();
}
class _AnimatedPageState extends State<AnimatedPage> with SingleTickerProviderStateMixin {
AnimationController _maxExtentAnimation;
#override
void initState() {
super.initState();
_maxExtentAnimation = AnimationController.unbounded(vsync: this, value: _getTargetMaxExtent(widget.appearance));
}
#override
void didUpdateWidget(AnimatedPage oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.appearance != oldWidget.appearance) {
_maxExtentAnimation.animateTo(
_getTargetMaxExtent(widget.appearance),
duration: Duration(milliseconds: 600),
curve: Curves.easeInOut,
);
}
}
#override
void dispose() {
_maxExtentAnimation.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _maxExtentAnimation,
builder: (context, child) {
return CustomScrollView(
slivers: <Widget>[
SliverPersistentHeader(
pinned: true,
delegate: AnimatedHeaderDelegate(
appearance: widget.appearance,
backgroundImage: widget.backgroundImage,
minExtent: 50.0,
maxExtent: _maxExtentAnimation.value,
),
),
child,
],
);
},
child: SliverList(delegate: SliverChildListDelegate(widget.children)),
);
}
}
class AnimatedHeaderDelegate extends SliverPersistentHeaderDelegate {
AnimatedHeaderDelegate({this.appearance, this.backgroundImage, this.minExtent, this.maxExtent});
final HeaderAppearance appearance;
final String backgroundImage;
#override
final double minExtent;
#override
final double maxExtent;
#override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
final shrinkRelative = shrinkOffset / (maxExtent - minExtent);
return AnimatedHeader(
appearance: appearance,
backgroundImage: backgroundImage,
curvatureMultiplier: 1.0 - shrinkRelative,
);
}
#override
bool shouldRebuild(AnimatedHeaderDelegate oldDelegate) {
return appearance != oldDelegate.appearance ||
minExtent != oldDelegate.minExtent ||
maxExtent != oldDelegate.maxExtent;
}
}
class AnimatedHeader extends StatefulWidget {
AnimatedHeader({Key key, this.appearance, this.backgroundImage, this.curvatureMultiplier}) : super(key: key);
final HeaderAppearance appearance;
final String backgroundImage;
final double curvatureMultiplier;
#override
_AnimatedHeaderState createState() => _AnimatedHeaderState();
}
class _AnimatedHeaderState extends State<AnimatedHeader> with TickerProviderStateMixin {
AnimationController _arcAnimation;
#override
void initState() {
super.initState();
_arcAnimation = AnimationController(
vsync: this,
value: _getTargetArcAnimationValue(widget.appearance),
duration: Duration(milliseconds: 600),
);
}
#override
void didUpdateWidget(AnimatedHeader oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.appearance != oldWidget.appearance) {
_arcAnimation.animateTo(_getTargetArcAnimationValue(widget.appearance));
}
}
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: CurvedAnimation(parent: _arcAnimation, curve: Curves.linear),
builder: (context, child) {
return ClipPath(
clipper: ArcClipper(
curvature: _arcAnimation.value * widget.curvatureMultiplier,
),
clipBehavior: Clip.antiAlias,
child: child,
);
},
child: Stack(
fit: StackFit.expand,
children: <Widget>[
AnimatedSwitcher(
duration: Duration(milliseconds: 600),
child: Container(
key: ValueKey(widget.backgroundImage),
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(widget.backgroundImage),
fit: BoxFit.cover,
),
),
),
),
Center(
child: Text(
'TITLE',
style: TextStyle(fontSize: 30.0, fontWeight: FontWeight.w500),
),
),
],
),
);
}
}
class ArcClipper extends CustomClipper<Path> {
ArcClipper({this.curvature});
final double curvature;
#override
Path getClip(Size size) {
if (curvature == 0.0) {
return Path()..addRect(Offset.zero & size);
} else {
return Path()
..moveTo(0.0, 0.0)
..lineTo(size.width, 0.0)
..lineTo(size.width, size.height)
..quadraticBezierTo(size.width / 2, size.height - size.height * 0.4 * curvature, 0.0, size.height)
..close();
}
}
#override
bool shouldReclip(ArcClipper oldClipper) {
return curvature != oldClipper.curvature;
}
}

How to write clickable functions for slider images of image_carousel package in flutter?

I am using imagecarousel package for displaying images from the network. I want to keep onPressed function for images in the slide.
new ImageCarousel(
<ImageProvider>[
new NetworkImage('http://www.hilversum.ferraridealers.com/siteasset/ferraridealer/54f07ac8c35b6/961/420/selected/0/0/0/54f07ac8c35b6.jpg'),
new NetworkImage('http://auto.ferrari.com/en_EN/wp-content/uploads/sites/5/2017/08/ferrari-portofino-reveal-2017-featured-new.jpg'),
new NetworkImage('http://www.hilversum.ferraridealers.com/siteasset/ferraridealer/54f07ac8c35b6/961/420/selected/0/0/0/54f07ac8c35b6.jpg'),
],
interval: new Duration(seconds: 1),
)
After making some modifications to Image Carousel, I was able to implement click event (other events also possible). Here is the sample code.
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
class ImageCarousel extends StatefulWidget {
final List<ImageProvider> imageProviders;
final double height;
final TargetPlatform platform;
final Duration interval;
final TabController tabController;
final BoxFit fit;
// Images will shrink according to the value of [height]
// If you prefer to use the Material or Cupertino style activity indicator set the [platform] parameter
// Set [interval] to let the carousel loop through each photo automatically
// Pinch to zoom will be turned on by default
ImageCarousel(this.imageProviders,
{this.height = 250.0, this.platform, this.interval, this.tabController, this.fit = BoxFit.cover});
#override
State createState() => new _ImageCarouselState();
}
TabController _tabController;
class _ImageCarouselState extends State<ImageCarousel> with SingleTickerProviderStateMixin {
#override
void initState() {
super.initState();
_tabController = widget.tabController ?? new TabController(vsync: this, length: widget.imageProviders.length);
if (widget.interval != null) {
new Timer.periodic(widget.interval, (_) {
_tabController.animateTo(_tabController.index == _tabController.length - 1 ? 0 : ++_tabController.index);
});
}
}
#override
void dispose() {
_tabController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return new SizedBox(
height: widget.height,
child: new TabBarView(
controller: _tabController,
children: widget.imageProviders.map((ImageProvider provider) {
return new CarouselImageWidget(widget, provider, widget.fit, widget.height);
}).toList(),
),
);
}
}
class CarouselImageWidget extends StatefulWidget {
final ImageCarousel carousel;
final ImageProvider imageProvider;
final BoxFit fit;
final double height;
CarouselImageWidget(this.carousel, this.imageProvider, this.fit, this.height);
#override
State createState() => new _CarouselImageState();
}
class _CarouselImageState extends State<CarouselImageWidget> {
bool _loading = true;
Widget _getIndicator(TargetPlatform platform) {
if (platform == TargetPlatform.iOS) {
return new CupertinoActivityIndicator();
} else {
return new Container(
height: 40.0,
width: 40.0,
child: new CircularProgressIndicator(),
);
}
}
#override
void initState() {
super.initState();
widget.imageProvider.resolve(new ImageConfiguration()).addListener((i, b) {
if (mounted) {
setState(() {
_loading = false;
});
}
});
}
#override
Widget build(BuildContext context) {
return new Container(
height: widget.height,
child: _loading
? _getIndicator(widget.carousel.platform == null ? defaultTargetPlatform : widget.carousel.platform)
: new GestureDetector(
child: new Image(
image: widget.imageProvider,
fit: widget.fit,
),
onTap: () {
int index = int.parse(_tabController.index.toString());
switch(index){
//Implement you case here
case 0:
case 1:
case 2:
default:
print(_tabController.index.toString());
}
},
),
);
}
}
void main(){
runApp(new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: new Text("Demo"),
),
body: new ImageCarousel(
<ImageProvider>[
new NetworkImage(
'http://wallpaper-gallery.net/images/images/images-2.jpg'),
new NetworkImage(
'http://wallpaper-gallery.net/images/images/images-10.jpg'),
new NetworkImage(
'http://wallpaper-gallery.net/images/images/images-4.jpg'),
],
interval: new Duration(seconds: 5),
)
),
));
}
Hope it helps..!!

How to Calculate the Coordinates of touch in Flutter?

I want show a popup at touch Coordinates. I am using Stack and Positioned widgets to place the popup.
You can add a GestureDetector as parent of stack, and register onTapDownDetails listener. This should call your listener on every tapdown event, with global offset of the tap in TapDownDetails parameter of the your listener.
Here is sample code demonstrating the same.
import 'package:flutter/material.dart';
void main() {
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
home: new MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
MyHomePageState createState() => new MyHomePageState();
}
class MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Popup Demo'),
),
body: new MyWidget());
}
}
class MyWidget extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return new MyWidgetState();
}
}
class MyWidgetState extends State<MyWidget> {
double posx = 100.0;
double posy = 100.0;
void onTapDown(BuildContext context, TapDownDetails details) {
print('${details.globalPosition}');
final RenderBox box = context.findRenderObject();
final Offset localOffset = box.globalToLocal(details.globalPosition);
setState(() {
posx = localOffset.dx;
posy = localOffset.dy;
});
}
#override
Widget build(BuildContext context) {
return new GestureDetector(
onTapDown: (TapDownDetails details) => onTapDown(context, details),
child: new Stack(fit: StackFit.expand, children: <Widget>[
// Hack to expand stack to fill all the space. There must be a better
// way to do it.
new Container(color: Colors.white),
new Positioned(
child: new Text('hello'),
left: posx,
top: posy,
)
]),
);
}
}
You can simply use a Listener as the parent of your Stack and listen to it's onPointerDown event like so:
Listener(
onPointerDown: (event) {
// use event.localPosition.dx or event.localPosition.dy
},
child: Stack(
children: [
],
),
)

How to touch paint a canvas?

Flutter newbie here. I'm currently trying to build a simple touch-drawing app with Flutter, but cannot work out how to trigger a canvas re-draw.
What I have is this:
I have a CustomPaint widget which contains a GestureDetector child. The CustomPaint's painter is getting a message whenever a touch event occurs, and stores the touch coordinates to draw a path on re-paint. Problem is, the paint method is never called.
This is the code I have so far:
import 'package:flutter/material.dart';
class WriteScreen extends StatefulWidget {
#override
_WriteScreenState createState() => new _WriteScreenState();
}
class KanjiPainter extends CustomPainter {
Color strokeColor;
var strokes = new List<List<Offset>>();
KanjiPainter( this.strokeColor );
void startStroke(Offset position) {
print("startStroke");
strokes.add([position]);
}
void appendStroke(Offset position) {
print("appendStroke");
var stroke = strokes.last;
stroke.add(position);
}
void endStroke() {
}
#override
void paint(Canvas canvas, Size size) {
print("paint!");
var rect = Offset.zero & size;
Paint fillPaint = new Paint();
fillPaint.color = Colors.yellow[100];
fillPaint.style = PaintingStyle.fill;
canvas.drawRect(
rect,
fillPaint
);
Paint strokePaint = new Paint();
strokePaint.color = Colors.black;
strokePaint.style = PaintingStyle.stroke;
for (var stroke in strokes) {
Path strokePath = new Path();
// Iterator strokeIt = stroke.iterator..moveNext();
// Offset start = strokeIt.current;
// strokePath.moveTo(start.dx, start.dy);
// while (strokeIt.moveNext()) {
// Offset off = strokeIt.current;
// strokePath.addP
// }
strokePath.addPolygon(stroke, false);
canvas.drawPath(strokePath, strokePaint);
}
}
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
class _WriteScreenState extends State<WriteScreen> {
GestureDetector touch;
CustomPaint canvas;
KanjiPainter kanjiPainter;
void panStart(DragStartDetails details) {
print(details.globalPosition);
kanjiPainter.startStroke(details.globalPosition);
}
void panUpdate(DragUpdateDetails details) {
print(details.globalPosition);
kanjiPainter.appendStroke(details.globalPosition);
}
void panEnd(DragEndDetails details) {
kanjiPainter.endStroke();
}
#override
Widget build(BuildContext context) {
touch = new GestureDetector(
onPanStart: panStart,
onPanUpdate: panUpdate,
onPanEnd: panEnd,
);
kanjiPainter = new KanjiPainter( const Color.fromRGBO(255, 255, 255, 1.0) );
canvas = new CustomPaint(
painter: kanjiPainter,
child: touch,
// child: new Text("Custom Painter"),
// size: const Size.square(100.0),
);
Container container = new Container(
padding: new EdgeInsets.all(20.0),
child: new ConstrainedBox(
constraints: const BoxConstraints.expand(),
child: new Card(
elevation: 10.0,
child: canvas,
)
)
);
return new Scaffold(
appBar: new AppBar(
title: new Text("Draw!")
),
backgroundColor: const Color.fromRGBO(200, 200, 200, 1.0),
body: container,
);
}
}
According to CustomPainter docs you must notify paint widget whenever repainting is needed
The most efficient way to trigger a repaint is to either extend this class and supply a repaint argument to the constructor of the CustomPainter, where that object notifies its listeners when it is time to repaint, or to extend Listenable (e.g. via ChangeNotifier) and implement CustomPainter, so that the object itself provides the notifications directly. In either case, the CustomPaint widget or RenderCustomPaint render object will listen to the Listenable and repaint whenever the animation ticks, avoiding both the build and layout phases of the pipeline.
E.g. KanjiPainter should extend ChangeNotifier and implement CustomPainter. And when you change strokes, invoke notifyListeners
And also build function always creates new KanjiPainter, this will remove all old data. You can init painter in initState once.
Working example:
class WriteScreen extends StatefulWidget {
#override
_WriteScreenState createState() => _WriteScreenState();
}
class KanjiPainter extends ChangeNotifier implements CustomPainter {
Color strokeColor;
var strokes = <List<Offset>>[];
KanjiPainter(this.strokeColor);
bool hitTest(Offset position) => true;
void startStroke(Offset position) {
print("startStroke");
strokes.add([position]);
notifyListeners();
}
void appendStroke(Offset position) {
print("appendStroke");
var stroke = strokes.last;
stroke.add(position);
notifyListeners();
}
void endStroke() {
notifyListeners();
}
#override
void paint(Canvas canvas, Size size) {
print("paint!");
var rect = Offset.zero & size;
Paint fillPaint = Paint();
fillPaint.color = Colors.yellow[100]!;
fillPaint.style = PaintingStyle.fill;
canvas.drawRect(rect, fillPaint);
Paint strokePaint = new Paint();
strokePaint.color = Colors.black;
strokePaint.style = PaintingStyle.stroke;
for (var stroke in strokes) {
Path strokePath = new Path();
// Iterator strokeIt = stroke.iterator..moveNext();
// Offset start = strokeIt.current;
// strokePath.moveTo(start.dx, start.dy);
// while (strokeIt.moveNext()) {
// Offset off = strokeIt.current;
// strokePath.addP
// }
strokePath.addPolygon(stroke, false);
canvas.drawPath(strokePath, strokePaint);
}
}
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
#override
// TODO: implement semanticsBuilder
SemanticsBuilderCallback? get semanticsBuilder => null;
#override
bool shouldRebuildSemantics(covariant CustomPainter oldDelegate) {
// TODO: implement shouldRebuildSemantics
return true;
}
}
class _WriteScreenState extends State<WriteScreen> {
late GestureDetector touch;
late CustomPaint canvas;
late KanjiPainter kanjiPainter;
void panStart(DragStartDetails details) {
print(details.globalPosition);
kanjiPainter.startStroke(details.globalPosition);
}
void panUpdate(DragUpdateDetails details) {
print(details.globalPosition);
kanjiPainter.appendStroke(details.globalPosition);
}
void panEnd(DragEndDetails details) {
kanjiPainter.endStroke();
}
#override
void initState() {
super.initState();
kanjiPainter = new KanjiPainter(const Color.fromRGBO(255, 255, 255, 1.0));
}
#override
Widget build(BuildContext context) {
touch = new GestureDetector(
onPanStart: panStart,
onPanUpdate: panUpdate,
onPanEnd: panEnd,
);
canvas = new CustomPaint(
painter: kanjiPainter,
child: touch,
// child: new Text("Custom Painter"),
// size: const Size.square(100.0),
);
Container container = new Container(
padding: new EdgeInsets.all(20.0),
child: new ConstrainedBox(
constraints: const BoxConstraints.expand(),
child: new Card(
elevation: 10.0,
child: canvas,
)));
return new Scaffold(
appBar: new AppBar(title: new Text("Draw!")),
backgroundColor: const Color.fromRGBO(200, 200, 200, 1.0),
body: container,
);
}
}
this library does it well.
example :
myCanvas.drawRect(Rect.fromLTRB(x * 0.88, y / 2, 25, 25), paint,
onTapDown: (t) {
print('tap');
});

Flutter - Create a countdown widget

I am trying to build a countdown widget. Currently, I got the structure to work. I only struggle with the countdown itself. I tried this approach using the countdown plugin:
class _Countdown extends State<Countdown> {
int val = 3;
void countdown(){
CountDown cd = new CountDown(new Duration(seconds: 4));
cd.stream.listen((Duration d) {
setState((){
val = d.inSeconds;
});
});
}
#override
build(BuildContext context){
countdown();
return new Scaffold(
body: new Container(
child: new Center(
child: new Text(val.toString(), style: new TextStyle(fontSize: 150.0)),
),
),
);
}
}
However, the value changes very weirdly and not smooth at all. It start twitching. Any other approach or fixes?
It sounds like you are trying to show an animated text widget that changes over time. I would use an AnimatedWidget with a StepTween to ensure that the countdown only shows integer values.
import 'package:flutter/material.dart';
void main() {
runApp(new MaterialApp(
home: new MyApp(),
));
}
class Countdown extends AnimatedWidget {
Countdown({ Key key, this.animation }) : super(key: key, listenable: animation);
Animation<int> animation;
#override
build(BuildContext context){
return new Text(
animation.value.toString(),
style: new TextStyle(fontSize: 150.0),
);
}
}
class MyApp extends StatefulWidget {
State createState() => new _MyAppState();
}
class _MyAppState extends State<MyApp> with TickerProviderStateMixin {
AnimationController _controller;
static const int kStartValue = 4;
#override
void initState() {
super.initState();
_controller = new AnimationController(
vsync: this,
duration: new Duration(seconds: kStartValue),
);
}
#override
Widget build(BuildContext context) {
return new Scaffold(
floatingActionButton: new FloatingActionButton(
child: new Icon(Icons.play_arrow),
onPressed: () => _controller.forward(from: 0.0),
),
body: new Container(
child: new Center(
child: new Countdown(
animation: new StepTween(
begin: kStartValue,
end: 0,
).animate(_controller),
),
),
),
);
}
}
The countdown() method should be called from the initState() method of the State object.
class _CountdownState extends State<CountdownWidget> {
int val = 3;
CountDown cd;
#override
void initState() {
super.initState();
countdown();
}
...
Description of initState() from the Flutter docs:
The framework calls initState. Subclasses of State should override
initState to perform one-time initialization that depends on the
BuildContext or the widget, which are available as the context and
widget properties, respectively, when the initState method is called.
Here is a full working example:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:countdown/countdown.dart';
void main() {
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Countdown Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new CountdownWidget();
}
}
class _CountdownState extends State<CountdownWidget> {
int val = 3;
CountDown cd;
#override
void initState() {
super.initState();
countdown();
}
void countdown(){
print("countdown() called");
cd = new CountDown(new Duration(seconds: 4));
StreamSubscription sub = cd.stream.listen(null);
sub.onDone(() {
print("Done");
});
sub.onData((Duration d) {
if (val == d.inSeconds) return;
print("onData: d.inSeconds=${d.inSeconds}");
setState((){
val = d.inSeconds;
});
});
}
#override
build(BuildContext context){
return new Scaffold(
body: new Container(
child: new Center(
child: new Text(val.toString(), style: new TextStyle(fontSize: 150.0)),
),
),
);
}
}
class CountdownWidget extends StatefulWidget {
#override
_CountdownState createState() => new _CountdownState();
}
based on #raju-bitter answer, alternative to use async/await on countdown stream
void countdown() async {
cd = new CountDown(new Duration(seconds:4));
await for (var v in cd.stream) {
setState(() => val = v.inSeconds);
}
}
Why not use a simple TweenAnimationBuilder its easy to use and you don't need to manage any stream controllers or worry about using streams and disposing them off etc;
TweenAnimationBuilder<double>(
duration: Duration(seconds: 10),
tween: Tween(begin: 100.0, end: 0.0),
onEnd: () {
print('Countdown ended');
},
builder: (BuildContext context, double value, Widget child) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 5),
child: Text('${value.toInt()}',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
fontSize: 40)));
}),
here's the dartpad example to playaround
output:
originally answered here
Countdown example using stream, not using setState(...) therefore its all stateless.
this borrow idea from example flutter_stream_friends
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:countdown/countdown.dart';
void main() {
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
static String appTitle = "Count down";
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: appTitle,
theme: new ThemeData(
primarySwatch: Colors.purple,
),
home: new StreamBuilder(
stream: new CounterScreenStream(5),
builder: (context, snapshot) => buildHome(
context,
snapshot.hasData
// If our stream has delivered data, build our Widget properly
? snapshot.data
// If not, we pass through a dummy model to kick things off
: new Duration(seconds: 5),
appTitle)),
);
}
// The latest value of the CounterScreenModel from the CounterScreenStream is
// passed into the this version of the build function!
Widget buildHome(BuildContext context, Duration duration, String title) {
return new Scaffold(
appBar: new AppBar(
title: new Text(title),
),
body: new Center(
child: new Text(
'Count down ${ duration.inSeconds }',
),
),
);
}
}
class CounterScreenStream extends Stream<Duration> {
final Stream<Duration> _stream;
CounterScreenStream(int initialValue)
: this._stream = createStream(initialValue);
#override
StreamSubscription<Duration> listen(
void onData(Duration event),
{Function onError,
void onDone(),
bool cancelOnError}) =>
_stream.listen(onData,
onError: onError, onDone: onDone, cancelOnError: cancelOnError);
// The method we use to create the stream that will continually deliver data
// to the `buildHome` method.
static Stream<Duration> createStream(int initialValue) {
var cd = new CountDown(new Duration(seconds: initialValue));
return cd.stream;
}
}
The difference from stateful is that reload the app will restart counting. When using stateful, in some cases, it may not restart when reload.

Resources