I am testing the performance in drawing using Flutter. I am using Path to draw line between each point detected by Listener because I have read that the performance would increase using it. I am using Listener because I tried also the Apple Pencil on iPad 2017 by changing the kind property to stylus.
The problem is that I was hoping to get a response in the stroke design similar to notability, it seems much slower, acceptable but not as much as I would like.
So I'm looking for tips to increase performance in terms of speed.
At the following link they recommended using NotifyListener(), but I didn't understand how to proceed. If it really improves performance I would like even an example to be able to implement it.
If Flutter has some limitations when it comes to drawing with your fingers or with a stylus then let me know.
performance issue in drawing using flutter
import 'dart:io';
import 'dart:ui';
import 'package:flutter/material.dart';
class DrawWidget extends StatefulWidget {
#override
_DrawWidgetState createState() => _DrawWidgetState();
}
class _DrawWidgetState extends State<DrawWidget> {
Color selectedColor = Colors.black;
double strokeWidth = 3.0;
List<MapEntry<Path, Object>> pathList = List();
StrokeCap strokeCap = (Platform.isAndroid) ? StrokeCap.butt : StrokeCap.round;
double opacity = 1.0;
Paint pa = Paint();
#override
Widget build(BuildContext context) {
return Listener(
child: CustomPaint(
size: Size.infinite,
painter: DrawingPainter(
pathList: this.pathList,
),
),
onPointerDown: (details) {
if (details.kind == PointerDeviceKind.touch) {
print('down');
setState(() {
Path p = Path();
p.moveTo(details.localPosition.dx, details.localPosition.dy);
pa.strokeCap = strokeCap;
pa.isAntiAlias = true;
pa.color = selectedColor.withOpacity(opacity);
pa.strokeWidth = strokeWidth;
pa.style = PaintingStyle.stroke;
var drawObj = MapEntry<Path,Paint>(p, pa);
pathList.add(drawObj);
});
}
},
onPointerMove: (details) {
if (details.kind == PointerDeviceKind.touch) {
print('move');
setState(() {
pathList.last.key.lineTo(details.localPosition.dx, details.localPosition.dy);
});
}
},
/*onPointerUp: (details) {
setState(() {
});
},*/
);
}
}
class DrawingPainter extends CustomPainter {
DrawingPainter({this.pathList});
List<MapEntry<Path, Object>> pathList;
#override
void paint(Canvas canvas, Size size) {
for(MapEntry<Path, Paint> m in pathList) {
canvas.drawPath(m.key, m.value);
}
}
#override
bool shouldRepaint(DrawingPainter oldDelegate) => true;
}
I think you should not use setState, rather use state management like Bloc or ChangeNotifier or smth.
Also, just drawing a path with this:
canvas.drawPath(m.key, m.value);
Works for only small stroke widths, it leaves a weird-looking line full of blank spaces when drawing.
I implemented this by using Bloc that updates the UI based on the gesture functions (onPanStart, onPanEnd, onPanUpdate). It holds a List of a data model that I called CanvasPath that represents one line (so from onPanStart to onPanEnd events), and it holds the resulting Path of that line, list of Offsets and Paint used to paint it.
In the end paint() method draws every single Path from this CanvasPath object and also a circle in every Offset.
` for every canvasPath do this:
canvas.drawPath(canvasPath.path, _paint);
for (int i = 0; i < canvasPath.drawPoints.length - 1; i++) {
//draw Circle on every Offset of user interaction
canvas.drawCircle(
canvasPath.drawPoints[i],
_raidus,
_paint);
}`
I made a blog about this, where it is explained in much more details:
https://ivanstajcer.medium.com/flutter-drawing-erasing-and-undo-with-custompainter-6d00fec2bbc2
Related
In Flutter, is a Custom Painter and all its class variables re-constructed from scratch every time the paint method is called? -- when setState is called in the parent?? I didn't expect that, but it seems to be the case:
I ask because I have a Custom Painter that contains an object inside the paint method (a sequence of points) that is the basis of all the subsequent painting effects. But... this object takes math in the first place just to be created... and it requires the canvas dimensions as part of that creation... which is why its inside the paint method.
So... I thought... instead of calculating the same exact skeleton shape every time paint is called, I'll make it a nullable class variable and initialize it once... then just check if its null every time in paint instead of recreating it every time.
I thought this was best practice to "move calculations out of the paint method when possible."
BUT... the unexpected result is that when I check, Flutter always says my object is null (it's recreated every time anyway).
Custom Painter:
import 'package:flutter/material.dart';
import 'dart:math';
class StackPainter02 extends CustomPainter {
final List<double> brightnessValues;
Paint backgroundPaint = Paint();
Paint segmentPaint = Paint();
List<Offset>? myShape;
StackPainter02({
required this.brightnessValues,
}) {
backgroundPaint.color = Colors.black;
backgroundPaint.style = PaintingStyle.fill;
segmentPaint.style = PaintingStyle.stroke;
}
#override
void paint(Canvas canvas, Size size) {
final W = size.width;
final H = size.height;
segmentPaint.strokeWidth = W / 100;
canvas.drawPaint(backgroundPaint);
// unfortunately, we must initialize this here because we need the view dimensions
if (myShape == null) {
myShape = _myShapePoints(brightnessValues.length, 0.8, W, H, Offset(W/2,H/2));
}
for (int i = 0; i<myShape!.length; i++) {
// bug fix... problem: using "i+1" results in index out-of-range on wrap-around
int modifiedIndexPlusOne = i;
if (modifiedIndexPlusOne == myShape!.length-1) {
modifiedIndexPlusOne = 0;
} else {
modifiedIndexPlusOne++;
}
// draw from point i to point i+1
Offset segmentStart = myShape![i];
Offset segmentEnd = myShape![modifiedIndexPlusOne];
double b = brightnessValues[i];
if (b < 0) {
b = 0; // !!!- temp debug... problem: brightness array algorithm is not perfect
}
int segmentAlpha = (255*b).toInt();
segmentPaint.color = Color.fromARGB(segmentAlpha, 255, 255, 200);
canvas.drawLine(segmentStart, segmentEnd, segmentPaint);
}
}
#override
bool shouldRepaint(covariant StackPainter02 oldDelegate) {
//return (oldDelegate.brightnessValues[32] != brightnessValues[32]); // nevermind
return true;
}
}
Build Method in Parent:
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: CustomPaint(
painter: //MyCustomPainter(val1: val1, val2: val2),
StackPainter02(
brightnessValues: brightnessValues,
),
size: Size.infinite,
),
),
);
}
PS - A ticker is used in parent and the "brightnessValues" are recalculated on every Ticker tick -> setState
I am developing an app using Flutter, where I simply want a rectangle to move on screen when I scroll with my fingers (on Android/iOS) or when I scroll with a mouse wheel or trackpad (on web).
This is what my code looks like:
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
/////////////////
void main()
{
runApp(MyCustomScroller());
}
/////////////////
class MyCustomScroller extends StatefulWidget
{
#override
_MyCustomScrollerState createState() => _MyCustomScrollerState();
}
class _MyCustomScrollerState extends State<MyCustomScroller> with TickerProviderStateMixin
{
AnimationController animController;
double scrollPos = 500;
#override
void initState()
{
super.initState();
animController = AnimationController.unbounded(
vsync: this,
value: 0,
);
animController.addListener(()
{
setState(()
{
scrollPos = animController.value;
});
});
}
#override
void dispose()
{
animController.dispose();
super.dispose();
}
void _onScrollUpdate(final double velPpf)
{
final double velPps = velPpf*60;
Simulation sim = ClampingScrollSimulation(
position: scrollPos,
velocity: velPps,
friction: 0.01,
);
animController.animateWith(sim);
scrollPos += velPpf;
}
#override
Widget build(BuildContext context)
{
return MaterialApp(
title: 'Scrolling Test',
home: Listener(
onPointerMove: (PointerMoveEvent moveEvent) // mobile, web click or web finger (on mobile)
{
_onScrollUpdate(moveEvent.delta.dy);
},
onPointerSignal: (PointerSignalEvent signalEvent) // web scrolling wheel/trackpad
{
if (signalEvent is PointerScrollEvent)
_onScrollUpdate(-signalEvent.scrollDelta.dy);
},
child: Container(
color: Colors.white,
child: CustomPaint(
painter: MyPainter(
pos: scrollPos,
),
child: Container(),
)
)
)
);
}
}
/////////////////
class MyPainter extends CustomPainter
{
final double pos;
MyPainter({
#required this.pos
});
#override
void paint(Canvas canvas, Size size)
{
Paint paint = Paint()
..color = Colors.black
..strokeWidth = 12
..style = PaintingStyle.stroke;
final double x = size.width/2;
final double y = (pos % size.height*1.5) - size.height*0.25;
final Offset offset0 = Offset(x, y);
final Offset offset1 = Offset(x, y+size.height*0.25);
canvas.drawLine(offset0, offset1, paint);
}
#override
bool shouldRepaint(CustomPainter oldDelegate)
{
return true;
}
}
Everything seems to work fine, I can see a rectangle that moves up and down as I scroll. When I stop scrolling, the AnimationController continues moving the rectangle. I know similar results can be achieved using a GestureDetector instead of a Listener, but a GestureDetector does not work on mouse scrolling on web.
The problem comes when I run my app in profiling mode: I turn on the Performance Overlay to see if the UI and Raster threads are executing fine and that I don't have any jank.
Most of the time the performance of the raster thread looks fine, as you can see in the following picture:
However sometimes, for some reason, the time taken to render a frame on the Raster thread goes up to around 16-17 ms, as you can see in the following image, and then stays around those values for the upcoming frames, until the animation finishes:
Anybody has any idea why this happens?
I have a CustomPainter that I want to render some items every few milliseconds. But I only want to render the items that have changed since the last draw. I plan on manually clearing the area that will be changing and redrawing just in the area. The problem is that the canvas in Flutter seems to be completely new every time paint() is called. I understand that I can keep track of the entire state and redraw everything every time, but for performance reasons and the specific use case that is not preferable. Below is sample code that could represent the issue:
I understand that everything will need to be redrawn when the canvas size changes.
import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
class CanvasWidget extends StatefulWidget {
CanvasWidget({Key key}) : super(key: key);
#override
_CanvasWidgetState createState() => _CanvasWidgetState();
}
class _CanvasWidgetState extends State<CanvasWidget> {
final _repaint = ValueNotifier<int>(0);
TestingPainter _wavePainter;
#override
void initState() {
_wavePainter = TestingPainter(repaint: _repaint);
Timer.periodic( Duration(milliseconds: 50), (Timer timer) {
_repaint.value++;
});
super.initState();
}
#override
Widget build(BuildContext context) {
return CustomPaint(
painter: _wavePainter,
);
}
}
class TestingPainter extends CustomPainter {
static const double _numberPixelsToDraw = 3;
final _rng = Random();
double _currentX = 0;
double _currentY = 0;
TestingPainter({Listenable repaint}): super(repaint: repaint);
#override
void paint(Canvas canvas, Size size) {
var paint = Paint();
paint.color = Colors.transparent;
if(_currentX + _numberPixelsToDraw > size.width)
{
_currentX = 0;
}
// Clear previously drawn points
var clearArea = Rect.fromLTWH(_currentX, 0, _numberPixelsToDraw, size.height);
canvas.drawRect(clearArea, paint);
Path path = Path();
path.moveTo(_currentX, _currentY);
for(int i = 0; i < _numberPixelsToDraw; i++)
{
_currentX++;
_currentY = _rng.nextInt(size.height.toInt()).toDouble();
path.lineTo(_currentX, _currentY);
}
// Draw new points in red
paint.color = Colors.red;
canvas.drawPath(path, paint);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
Redrawing the whole canvas, even on every frame, is completely efficient. Trying to reuse the previous frame will often not be more efficient.
Looking at the code you posted, there are certain areas with rooms for improvement, but trying to preserve parts of the canvas should not be one of them.
The real performance issue you are having, is from repeatedly changing a ValueNotifier from a Timer.periodic event, every 50 ms. A much better way to handle redrawing on every frame, is to use AnimatedBuilder with a vsync, so the paint method of the CustomPainter will be called on every frame. This is similar to Window.requestAnimationFrame in the web browser world, if you are familiar with that. Here vsync stands for "vertical sync", if you are familiar with how computer graphics work. Essentially, your paint method will be called 60 times per second, on a device with 60 Hz screen, and it'll paint 120 times per second on a 120 Hz screen. This is the correct and scalable way to achieve buttery smooth animation across different kind of devices.
There are other areas worth optimizing, before thinking about preserving parts of the canvas. For example, just briefly looking at your code, you have this line:
_currentY = _rng.nextInt(size.height.toInt()).toDouble();
Here I assume you want to have a random decimal between 0 and size.height, if so, you can simply write _rng.nextDouble() * size.height, instead of casting a double to int and back again, and (probably unintentionally) rounding it during that process. But the performance gain from stuff like these is negligible.
Think about it, if a 3D video game can run smoothly on a phone, with each frame being dramatically different from the previous one, your animation should run smoothly, without having to worry about manually clearing parts of the canvas. Trying to manually optimize the canvas will probably lead to performance loss instead.
So, what you really should be focusing, is to use AnimatedBuilder instead of Timer to trigger the canvas redraw in your project, as a starting point.
For example, here's a small demo I made using AnimatedBuilder and CustomPaint:
Full source code:
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage>
with SingleTickerProviderStateMixin {
List<SnowFlake> snowflakes = List.generate(100, (index) => SnowFlake());
AnimationController _controller;
#override
void initState() {
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 1),
)..repeat();
super.initState();
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
width: double.infinity,
height: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.blue, Colors.lightBlue, Colors.white],
stops: [0, 0.7, 0.95],
),
),
child: AnimatedBuilder(
animation: _controller,
builder: (_, __) {
snowflakes.forEach((snow) => snow.fall());
return CustomPaint(
painter: MyPainter(snowflakes),
);
},
),
),
);
}
}
class MyPainter extends CustomPainter {
final List<SnowFlake> snowflakes;
MyPainter(this.snowflakes);
#override
void paint(Canvas canvas, Size size) {
final w = size.width;
final h = size.height;
final c = size.center(Offset.zero);
final whitePaint = Paint()..color = Colors.white;
canvas.drawCircle(c - Offset(0, -h * 0.165), w / 6, whitePaint);
canvas.drawOval(
Rect.fromCenter(
center: c - Offset(0, -h * 0.35),
width: w * 0.5,
height: w * 0.6,
),
whitePaint);
snowflakes.forEach((snow) =>
canvas.drawCircle(Offset(snow.x, snow.y), snow.radius, whitePaint));
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
class SnowFlake {
double x = Random().nextDouble() * 400;
double y = Random().nextDouble() * 800;
double radius = Random().nextDouble() * 2 + 2;
double velocity = Random().nextDouble() * 4 + 2;
SnowFlake();
fall() {
y += velocity;
if (y > 800) {
x = Random().nextDouble() * 400;
y = 10;
radius = Random().nextDouble() * 2 + 2;
velocity = Random().nextDouble() * 4 + 2;
}
}
}
Here I'm generating 100 snowflakes, redrawing the whole screen every frame. You can easily change the number of snowflakes to 1000 or higher, and it would still run very smoothly. Here I'm also not using the device screen size as much as I should be, as you can see, there are some hardcoded values like 400 or 800. Anyway, hopefully this demo would give you some faith in Flutter's graphics engine. :)
Here is another (smaller) example, showing you everything you need to get going with Canvas and Animations in Flutter. It might be easier to follow:
import 'package:flutter/material.dart';
void main() {
runApp(DemoWidget());
}
class DemoWidget extends StatefulWidget {
#override
_DemoWidgetState createState() => _DemoWidgetState();
}
class _DemoWidgetState extends State<DemoWidget>
with SingleTickerProviderStateMixin {
AnimationController _controller;
#override
void initState() {
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 1),
)..repeat(reverse: true);
super.initState();
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (_, __) => CustomPaint(
painter: MyPainter(_controller.value),
),
);
}
}
class MyPainter extends CustomPainter {
final double value;
MyPainter(this.value);
#override
void paint(Canvas canvas, Size size) {
canvas.drawCircle(
Offset(size.width / 2, size.height / 2),
value * size.shortestSide,
Paint()..color = Colors.blue,
);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
The only solution currently available is to capture the progress as image and then draw the image instead of executing the whole canvas code.
for drawing the image you can use canvas.drawImage as mentioned by pskink in the above comment.
but the solution i would recommend is to wrap the CustomPaint with RenderRepaint to convert that widget to image. for detials refer to
Creating raw image from Widget or Canvas and (https://medium.com/flutter-community/export-your-widget-to-image-with-flutter-dc7ecfa6bafb for brief implementation), and have a condition to check if you are building for the first time or not.
class _CanvasWidgetState extends State<CanvasWidget> {
/// Just to track if its the first frame or not.
var _flag = false;
/// Will be used for generating png image.
final _globalKey = new GlobalKey();
/// Stores the image bytes
Uint8List _imageBytes;
/// No need for this actually;
/// final _repaint = ValueNotifier<int>(0);
TestingPainter _wavePainter;
Future<Uint8List> _capturePng() async {
try {
final boundary = _globalKey
.currentContext.findRenderObject();
ui.Image image = await boundary.toImage();
ByteData byteData =
await image.toByteData(format: ui.ImageByteFormat.png);
var pngBytes = byteData.buffer.asUint8List();
var bs64 = base64Encode(pngBytes);
print(pngBytes);
print(bs64);
setState(() {});
return pngBytes;
} catch (e) {
print(e);
}
}
#override
void initState() {
_wavePainter = TestingPainter();
Timer.periodic( Duration(milliseconds: 50), (Timer timer) {
if (!flag) flag = true;
/// Save your image before each redraw.
_imageBytes = _capturePng();
/// You don't need a listener if you are using a stful widget.
/// It will do just fine.
setState(() {});
});
super.initState();
}
#override
Widget build(BuildContext context) {
return RepaintBoundary(
key: _globalkey,
child: Container(
/// Use this if this is not the first frame.
decoration: _flag ? BoxDecoration(
image: DecorationImage(
image: MemoryImage(_imageBytes)
)
) : null,
child: CustomPainter(
painter: _wavePainter
)
)
);
}
}
This way the image will not be a part of your custom painter and let me tell you, i tried drawing image using canvas but it was not that efficient, the MemoryImage provided by flutter renders image in a much better way.
Can Flutter's inbuilt Canvas drawing methods be directly used to render variable-width strokes, for example to reflect pressure applied throughout each stroke in a handwriting app?
Ideally, in a manner compatible with saving in the XML-esque format of SVG (example at bottom).
What I think I've noticed / troubles I'm having / current attempts:
canvas.drawPath, canvas.drawPoints, canvas.drawPolygon, canvas.drawLines etc all take only a single Paint object, which can in turn have a single strokeWidth (as opposed to taking lists of Paint objects or strokeWidths, such that parameters of the path besides position could change point to point and be interpolated between).
Drawing lines, polygons or points of varying strokeWidths or radii by iterating over lists of position and pressure data and using the respective Canvas method results in no interpolation / paths not looking continuously stroked.
Screenshot from OneNote showing the behaviour I'd like:
Screenshot from the app the minimal working example below produces:
(Unoptimized) minimal working example:
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(Container(
color: Colors.white,
child: Writeable(),
));
}
class Writeable extends StatefulWidget {
#override
_WriteableState createState() => _WriteableState();
}
class _WriteableState extends State<Writeable> {
List<List<double>> pressures = List<List<double>>();
List<Offset> currentLine = List<Offset>();
List<List<Offset>> lines = List<List<Offset>>();
List<double> currentLinePressures = List<double>();
double pressure;
Offset position;
Color color = Colors.black;
Painter painter;
CustomPaint paintCanvas;
#override
Widget build(BuildContext context) {
painter = Painter(
lines: lines,
currentLine: currentLine,
pressures: pressures,
currentLinePressures: currentLinePressures,
color: color);
paintCanvas = CustomPaint(
painter: painter,
);
return Listener(
onPointerMove: (details) {
setState(() {
currentLinePressures.add(details.pressure);
currentLine.add(details.localPosition);
});
},
onPointerUp: (details) {
setState(() {
lines.add(currentLine.toList());
pressures.add(currentLinePressures.toList());
currentLine.clear();
currentLinePressures.clear();
});
},
child: paintCanvas,
);
}
}
class Painter extends CustomPainter {
Painter(
{#required this.lines,
#required this.currentLine,
#required this.color,
#required this.pressures,
#required this.currentLinePressures});
final List<List<Offset>> lines;
final List<Offset> currentLine;
final Color color;
final List<List<double>> pressures;
final List<double> currentLinePressures;
double scalePressures = 10;
Paint paintStyle = Paint();
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
// Paints here using drawPoints and PointMode.lines, but have also tried
// PointMode.points, PointMode.polygon and drawPath with a path variable and
// moveTo, lineTo methods.
#override
void paint(Canvas canvas, Size size) {
// Paint line currently being drawn (points added since pointer was
// last lifted)
for (int i = 0; i < currentLine.length - 1; i++) {
paintStyle.strokeWidth = currentLinePressures[i] * scalePressures;
canvas.drawPoints(
PointMode.lines, [currentLine[i], currentLine[i + 1]], paintStyle);
}
// Paint all completed lines drawn since app start
for (int i = 0; i < lines.length; i++) {
for (int j = 0; j < lines[i].length - 1; j++) {
paintStyle.strokeWidth = pressures[i][j] * scalePressures;
canvas.drawPoints(
PointMode.lines, [lines[i][j], lines[i][j + 1]], paintStyle);
}
}
}
}
I'm about to try writing my own implementation for rendering aesthetic SVG-friendly data from PointerEvents, but so many of the existing classes feel SVG/pretty-vectors-compatible (e.g. all the lineTos, moveTos, stroke types and endings and other parameters) that I thought it worth checking if there's something I've missed, and these methods can already do this?
Example of a few lines in an SVG file saved by Xournal++, with the stroke-width parameter changing for each line segment, and all other listed parameters presumably also having potential to change. Each line contains a moveTo command (M) and a lineTo command (L), where the latter draws a line from the current position (the last moveTo-ed or lineTo-ed), reminiscent of Flutter's segments/sub-paths and current point, to the specified offset:
<path style="fill:none;stroke-width:0.288794;stroke-linecap:round;stroke-linejoin:round;stroke:rgb(0%,100%,0%);stroke-opacity:1;comp-op:src;clip-to-self:true;stroke-miterlimit:10;" d="M 242.683594 45.519531 L 242.980469 45.476562 "/>
<path style="fill:none;stroke-width:0.295785;stroke-linecap:round;stroke-linejoin:round;stroke:rgb(0%,100%,0%);stroke-opacity:1;comp-op:src;clip-to-self:true;stroke-miterlimit:10;" d="M 242.980469 45.476562 L 243.28125 45.308594 "/>
<path style="fill:none;stroke-width:0.309105;stroke-linecap:round;stroke-linejoin:round;stroke:rgb(0%,100%,0%);stroke-opacity:1;comp-op:src;clip-to-self:true;stroke-miterlimit:10;" d="M 243.28125 45.308594 L 243.601562 45.15625 "/>
The approach seems to be 'draw a very short line, change stroke-width, draw the next very short line starting from the previous position', which I've tried to emulate with the paint method above.
I am very new to flutter development, and I have to make a fairly quick decision on whether or not it is the right platform for my internship project.
I have to create an interface which requires all directional swipes to navigate to different menus (I'm thinking of doing nested horizontal and vertical scrolling, which I have had trouble with in Android Studio) - but more importantly, I have to save the raw data from the touching/tapping/swiping. I can't just save "left swipe" or "right swipe", I also have to know pressure, velocity, location, exact path, etc.
Is this feasible in flutter? How does flutter handle this raw data as opposed to Android studio? Does flutter only determine the approximate direction of the swipe and that's it?
I have tried searching for answers all day, but I must be missing some key word, because I have been unable to find the answer so far.
GestureDetector is a very extensive Widget in this regard. It has all the capabilities you are searching for. A simpler version of it, which also has Material design built in, is InkWell, but this might be lacking some of the functionality you are searching for.
With a GestureDetector wrapped about your Widget you will be able to catch all hit events (you can even specify HitTestBehavior (with the behavior parameter).
For your custom interactions there are plenty of callbacks implemented. I linked you to the constructor, which contains a bunch of useful parameters, like onTapDown/Up, onVertical/HorizontalDragStart/Update/End.
This is not even everything, but using those you can programatically define your behavior. Let me explain the concept with a small example:
Offset start;
void verticalDragStart(DragStartDetails details) {
// process the start by working with the details
start = details.globalPosition;
// ...
}
void verticalDragUpdate(DragUpdateDetails details) {
// apply your logic
Offset delta = details.delta;
// ...
}
// use DragEnd, also for horizontal, pan etc.
#override
Widget build(BuildContext context) => GestureDectector(
onVerticalDragStart: verticalDragStart,
// ...
);
I hope that you can imagine all the possibilties this enables. You can also combine different callbacks. Just take a look at the parameters in the documentation and experiment with what fits for you.
I think that this is the raw data you asked for.
You can get the raw touch movements using Listener. The following example shows how to grab a list of points. It uses them to draw the line you just traced with your finger. You can't tell the pressure, but can tell the exact path and velocity (if you stored time with each point). The higher level detector, GestureDetector, hides these raw movements from you, but interprets them into the traditional swipes.
(Notes about the example... shouldRepaint should be smarter, points returned by Listener are in global co-ordinates so may need to be converted to local (this simple example works because there's no AppBar))
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Gesture',
home: new MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<Offset> points = [];
#override
Widget build(BuildContext context) {
return new Scaffold(
body: new Listener(
onPointerDown: (e) {
points = [];
points.add(e.position);
},
onPointerMove: (e) {
points.add(e.position);
},
onPointerUp: (e) {
points.add(e.position);
setState(() {});
},
child: new CustomPaint(
painter: new PathPainter(points),
child: new Container(
width: 300.0,
height: 300.0,
color: Colors.black12,
),
),
),
);
}
}
class PathPainter extends CustomPainter {
List<Offset> points;
Path path = new Path();
PathPainter(this.points) {
if (points.isEmpty) return;
Offset origin = points[0];
path.moveTo(origin.dx, origin.dy);
for (Offset o in points) {
path.lineTo(o.dx, o.dy);
}
}
#override
void paint(Canvas canvas, Size size) {
canvas.drawPath(
path,
new Paint()
..color = Colors.orange
..style = PaintingStyle.stroke
..strokeWidth = 4.0,
);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return true; // todo - determine if the path has changed
}
}