I'm picking up flutter for what's supposed to be a quick bit of app work. I must be missing something. Can someone please explain how stateful components fit together? If I have a webservice API that provides a data structure as JSON and I want to display parts of that in my app and I want to break the display down into components, how do the components get associated with the different bits of JSON?
At its simplest, I have a component to display a battery level:
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class BatteryPainter extends CustomPainter {
double level;
BatteryPainter({this.level = 0.75});
#override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..color = Colors.green
..strokeWidth = 5
..strokeJoin = StrokeJoin.round
..style = PaintingStyle.stroke;
Rect rect = Rect.fromCenter(
center: Offset(size.width / 2, size.height),
width: size.width * 0.9,
height: 50,
);
RRect rr = RRect.fromRectAndRadius(rect, const Radius.circular(5));
canvas.drawRRect(rr, paint);
rect = Rect.fromCenter(
center: Offset(size.width / 2, size.height),
width: size.width * 0.9 - 10,
height: 40
);
double right = rect.left + (rect.right - rect.left) * level;
rect = Rect.fromLTRB(rect.left, rect.top, right, rect.bottom);
rr = RRect.fromRectAndRadius(rect, const Radius.circular(5));
paint
..style = PaintingStyle.fill;
canvas.drawRRect(rr, paint);
}
#override
bool shouldRepaint(BatteryPainter oldDelegate) => oldDelegate.level != level;
}
class Battery extends StatefulWidget {
#override
_BatteryWidgetState createState() => _BatteryWidgetState();
}
class _BatteryWidgetState extends State<Battery> {
Widget build (BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
return CustomPaint(
painter:BatteryPainter(),
size:Size(constraints.maxWidth, 50)
);
});
}
}
But now if I create one of these:
Widget build(BuildContext context) {
return Container(
child: Column(children: [
...,
Battery()
]
);
}
How do I pass updates to the battery state in so that the component gets redrawn? If I keep a reference to the Battery object, that does me no good because it's the state that keeps the current battery level, not the widget, so there's no obvious way to change the level value. If I pass a level into the Battery() constructor and the use that value in createState that doesn't really help either, since Dart is pass-by-value so changing the value in the top-level component which knows about changes to this value don't change the value in the battery level display component.
How are fine-grained components meant to be connected to a model like this?
How do I pass updates to the battery state in so that the component gets redrawn?
There is no simple answer. You pick one of the many state management methods and go with it.
I'm learning Flutter and am tasked with creating some 3D type effects for transitions and other "wow" factor type things. I have been using flutter's transforms to achieve this but have run into a problem. When I rotate about the Y axis the plane containing whatever tab was showing disappears around the 80 degree mark and shows back up around the 100 degree mark. This does not happen when rotated at -90 degrees.
I've seen other answers posted such as transform: rotateY() making element disappear but none seem to cover this issue.
The question is; is there something I should be setting to avoid this since it is pretty close to off screen when I do it. If not is this a quirk or known bug in flutter's transition?
I am unable to put the full code and I'm not really familiar enough with flutter to rip out a bit and rough up an example quickly. Below is a code snippet and I'll come back with a better code snippet once I have more time. My apologies for that.
#override
Container build (BuildContext context) {
return Container(
child: Transform(
transform: Matrix4.identity()
..setEntry(3, 2, 0.004)
..translate(position.x, position.y, position.z)
..rotateX(rotation.x)
..rotateY(rotation.y)
..rotateZ(rotation.z),
child: CustomPaint(
painter: MyPainter(),
),
),
);
}
class MyPainter extends CustomPainter {
Size rectSize = Size(100, 100);
#override
Future<void> paint(Canvas canvas, Size size) async {
final paint = Paint()
..style = PaintingStyle.fill
..color = Colors.green;
Rect rect = Offset(-rectSize.width / 2.0, -rectSize.height / 2.0) & this.rectSize;
canvas.drawRect(rect, paint);
}
}
EDIT 1: I have tried to get a minimal example working but the rotation actually works as expected. I used the above code with no changes into a statefulwidget but the plane only disappears at 90 and 270 degrees as expected. I'll update the question if I find out anything else. Thanks to all who have looked at this.
After quite a number of hours since yesterday I have discovered the cause of the disappearance. Both fragments of code are held in a custom Container class which uses alignment: FractionalOffset.center to align them into the center of the screen. When this is removed the widgets return to the upper left hand corner and rotates as expected.
I'm not sure why this happens and would love to know if anybody has any ideas. However, below is a minimal replication of the issue. Uncomment the aligment: FractionalOffset.center line in runApp() to see the error in action.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:vector_math/vector_math.dart' as Vector;
import 'dart:math';
void main() {
runApp(
MaterialApp(
theme: ThemeData.dark(),
home: Stack(
// alignment: FractionalOffset.center, // Remove the comment slashes to see issue
children: [
MyWidget(),
]
)
),
);
//runApp(MyApp());
}
class MyWidget extends StatefulWidget {
#override
State<StatefulWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
Vector.Vector3 position = Vector.Vector3.zero();
Vector.Vector3 rotation = Vector.Vector3.zero();
int degrees = 0;
#override
initState() {
super.initState();
new Timer.periodic(Duration(milliseconds: 100), (timer) => {timerCall()});
}
void timerCall() {
setState(() {
});
}
#override
Container build (BuildContext context) {
degrees += 1;
if (degrees >= 360) degrees = 0;
rotation.y = degrees * (pi / 180);
return Container(
child: Transform(
transform: Matrix4.identity()
..setEntry(3, 2, 0.004)
..translate(position.x, position.y, position.z)
..rotateX(rotation.x)
..rotateY(rotation.y)
..rotateZ(rotation.z),
child: CustomPaint(
painter: MyPainter(),
),
),
);
}
}
class MyPainter extends CustomPainter {
Size rectSize = Size(200, 200);
#override
Future<void> paint(Canvas canvas, Size size) async {
final paint = Paint()
..style = PaintingStyle.fill
..color = Colors.green;
Rect rect = Offset(-rectSize.width / 2.0, -rectSize.height / 2.0) & this.rectSize;
canvas.drawRect(rect, paint);
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
// TODO: implement shouldRepaint
return true;
}
}
To make a widget draggable, it has to be passed to a Draggable like so:
Draggable<String>(
data: "SomeDateToBeDragged",
child: Container(
width: 300,
height: 200,
alignment: Alignment.center,
color: Colors.purple,
child: Image.network(
'https://someServer.com/dog.jpg',
fit: BoxFit.cover,
),
),
...
As far as I know, a Widget is something rectangular. At least, I found nothing else than rectangular widgets.
I'd like to build a sketching app. Thus, I'd like to make a 'two dimensional' connector [a Line between two points] draggable.
How to I make a line draggable?
In other words: I'd like to make a click initiate a drag only if the drag e.g. is on a painted area and not on transparent background of the area. If I would draw a circle, it would drag if the circle would be clicked. If clicked some pixel outside the circle, it should not start a drag.
Flutter is incredible in making things that appear complex really easy.
Here is a solution for a draggable segment of line with two handles for its extremities.
As you see, you can grab either the line itself or its extremities.
A quick overview of the solution:
MyApp is the MaterialApp with the Scaffold
CustomPainterDraggable is the main Widget with the State Management
I used Hooks Riverpod
Have a look how to use freezed
LinePainter a basic Painter to paint the line and the two handles
Utils, a Utility class to calculate the distance between the cursor and the line or handles. The distance defines which part of the drawing should be dragged around.
Part, a Union class defining the different elements of the drawing (line and handles) to better structure the code.
1. Material App
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hooks_riverpod/all.dart';
part '66070975.sketch_app.freezed.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ProviderScope(
child: MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Draggable Custom Painter',
home: Scaffold(
body: CustomPainterDraggable(),
),
),
);
}
}
2. Part Union
#freezed
abstract class Part with _$Part {
const factory Part.line() = _Line;
const factory Part.a() = _A;
const factory Part.b() = _B;
const factory Part.noPart() = _NoPart;
}
3. CustomPainterDraggable
class CustomPainterDraggable extends HookWidget {
#override
Widget build(BuildContext context) {
final a = useState(Offset(50, 50));
final b = useState(Offset(150, 200));
final dragging = useState(Part.noPart());
return GestureDetector(
onPanStart: (details) => dragging.value =
Utils.shouldGrab(a.value, b.value, details.globalPosition),
onPanEnd: (details) {
dragging.value = Part.noPart();
},
onPanUpdate: (details) {
dragging.value.when(
line: () {
a.value += details.delta;
b.value += details.delta;
},
a: () => a.value += details.delta,
b: () => b.value += details.delta,
noPart: () {},
);
},
child: Container(
color: Colors.white,
child: CustomPaint(
painter: LinePainter(a: a.value, b: b.value),
child: Container(),
),
),
);
}
}
4. LinePainter
class LinePainter extends CustomPainter {
final Offset a;
final Offset b;
final double lineWidth;
final Color lineColor;
final double pointWidth;
final double pointSize;
final Color pointColor;
Paint get linePaint => Paint()
..color = lineColor
..strokeWidth = lineWidth
..style = PaintingStyle.stroke;
Paint get pointPaint => Paint()
..color = pointColor
..strokeWidth = pointWidth
..style = PaintingStyle.stroke;
LinePainter({
this.a,
this.b,
this.lineWidth = 5,
this.lineColor = Colors.black54,
this.pointWidth = 3,
this.pointSize = 12,
this.pointColor = Colors.red,
});
#override
void paint(Canvas canvas, Size size) {
canvas.drawLine(a, b, linePaint);
canvas.drawRect(
Rect.fromCenter(center: a, width: pointSize, height: pointSize),
pointPaint,
);
canvas.drawRect(
Rect.fromCenter(center: b, width: pointSize, height: pointSize),
pointPaint,
);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
5. Utils
class Utils {
static double maxDistance = 20;
static Part shouldGrab(Offset a, Offset b, Offset target) {
if ((a - target).distance < maxDistance) {
return Part.a();
}
if ((b - target).distance < maxDistance) {
return Part.b();
}
if (shortestDistance(a, b, target) < maxDistance) {
return Part.line();
}
return Part.noPart();
}
static double shortestDistance(Offset a, Offset b, Offset target) {
double px = b.dx - a.dx;
double py = b.dy - a.dy;
double temp = (px * px) + (py * py);
double u = ((target.dx - a.dx) * px + (target.dy - a.dy) * py) / temp;
if (u > 1) {
u = 1;
} else if (u < 0) {
u = 0;
}
double x = a.dx + u * px;
double y = a.dy + u * py;
double dx = x - target.dx;
double dy = y - target.dy;
double dist = math.sqrt(dx * dx + dy * dy);
return dist;
}
}
Full Source Code (Easy to copy paste)
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hooks_riverpod/all.dart';
part '66070975.sketch_app.freezed.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ProviderScope(
child: MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Draggable Custom Painter',
home: Scaffold(
body: CustomPainterDraggable(),
),
),
);
}
}
#freezed
abstract class Part with _$Part {
const factory Part.line() = _Line;
const factory Part.a() = _A;
const factory Part.b() = _B;
const factory Part.noPart() = _NoPart;
}
class CustomPainterDraggable extends HookWidget {
#override
Widget build(BuildContext context) {
final a = useState(Offset(50, 50));
final b = useState(Offset(150, 200));
final dragging = useState(Part.noPart());
return GestureDetector(
onPanStart: (details) => dragging.value =
Utils.shouldGrab(a.value, b.value, details.globalPosition),
onPanEnd: (details) {
dragging.value = Part.noPart();
},
onPanUpdate: (details) {
dragging.value.when(
line: () {
a.value += details.delta;
b.value += details.delta;
},
a: () => a.value += details.delta,
b: () => b.value += details.delta,
noPart: () {},
);
},
child: Container(
color: Colors.white,
child: CustomPaint(
painter: LinePainter(a: a.value, b: b.value),
child: Container(),
),
),
);
}
}
class LinePainter extends CustomPainter {
final Offset a;
final Offset b;
final double lineWidth;
final Color lineColor;
final double pointWidth;
final double pointSize;
final Color pointColor;
Paint get linePaint => Paint()
..color = lineColor
..strokeWidth = lineWidth
..style = PaintingStyle.stroke;
Paint get pointPaint => Paint()
..color = pointColor
..strokeWidth = pointWidth
..style = PaintingStyle.stroke;
LinePainter({
this.a,
this.b,
this.lineWidth = 5,
this.lineColor = Colors.black54,
this.pointWidth = 3,
this.pointSize = 12,
this.pointColor = Colors.red,
});
#override
void paint(Canvas canvas, Size size) {
canvas.drawLine(a, b, linePaint);
canvas.drawRect(
Rect.fromCenter(center: a, width: pointSize, height: pointSize),
pointPaint,
);
canvas.drawRect(
Rect.fromCenter(center: b, width: pointSize, height: pointSize),
pointPaint,
);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
class Utils {
static double maxDistance = 20;
static Part shouldGrab(Offset a, Offset b, Offset target) {
if ((a - target).distance < maxDistance) {
return Part.a();
}
if ((b - target).distance < maxDistance) {
return Part.b();
}
if (shortestDistance(a, b, target) < maxDistance) {
return Part.line();
}
return Part.noPart();
}
static double shortestDistance(Offset a, Offset b, Offset target) {
double px = b.dx - a.dx;
double py = b.dy - a.dy;
double temp = (px * px) + (py * py);
double u = ((target.dx - a.dx) * px + (target.dy - a.dy) * py) / temp;
if (u > 1) {
u = 1;
} else if (u < 0) {
u = 0;
}
double x = a.dx + u * px;
double y = a.dy + u * py;
double dx = x - target.dx;
double dy = y - target.dy;
double dist = math.sqrt(dx * dx + dy * dy);
return dist;
}
}
As a Container creates a Renderbox, the resulting draggable area will always be a rectangle.
You can create a line using a CustomPainter. However, the line created by this will by itself not be draggable. If you wrap it with a Container, the line will be draggable. But the area that is draggable is then again determined by the size of the container.
I'd suggest you use a Canvas and keep track of the state of your lines by yourself, like in this thread: How to draw custom shape in flutter and drag that shape around?
I am using Flutter's CustomPaint to draw a scatter plot the user can add points to by tapping. Since the whole chart cannot fit on a mobile screen, I want the user to be able to pan. How can I enable this? Currently, the chart is not panning, so I can only see a section of it. Here is my code:
import 'dart:io';
import 'dart:ui';
import 'package:flutter/material.dart';
class Draw extends StatefulWidget {
String eventName;
Draw({this.eventName});
#override
_DrawState createState() => _DrawState();
}
class _DrawState extends State<Draw> {
List<Offset> points = List();
#override
Widget build(BuildContext context) {
return Scaffold(
body: GestureDetector(
child: CustomPaint(
size: Size.infinite,
painter: DrawingPainter(),
),
),
);
}
}
class DrawingPainter extends CustomPainter {
DrawingPainter();
Paint tickSettings = Paint()
..strokeCap = (Platform.isAndroid) ? StrokeCap.butt : StrokeCap.round
..isAntiAlias = true
..color = Colors.grey
..strokeWidth = 5.0;
static double axisXCoord = 60;
static double axisHt = 1000;
var ticks = List<Offset>.generate(10, (i) => Offset(axisXCoord, i * (axisHt / 10) + 30));
#override
void paint(Canvas canvas, Size size) {
canvas.drawPoints(PointMode.points, ticks, tickSettings);
}
#override
bool shouldRepaint(DrawingPainter oldDelegate) => true;
}
(Note, I also saw Google's Flutter Charts library, but my understanding is that interactivity is limited to a few things like selecting existing points on the chart.)
Seems I solved it by wrapping the GestureDetector in a SingleChildScrollView.
I need to code these white intertwined circles (not the background):
I know how to draw one circle.
What eludes me is the math.
Note:
Yes, trigonometry is high school stuff, I'm perfectly aware.
As bereal said:
The coordinates of the k-th center will be (rcos kx, rsin kx) where r is the radius, and x = 2*pi/n where n is the number of circles you need.
Here is the example how to do it:
import 'dart:math';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: CustomPaint(
foregroundPainter: CustomCirclesPainter(),
),
),
),
);
}
}
class CustomCirclesPainter extends CustomPainter {
var myPaint = Paint()
..color = Colors.black
..style = PaintingStyle.stroke
..strokeWidth = 5.0;
double radius = 80;
#override
void paint(Canvas canvas, Size size) {
int n = 10;
var range = List<int>.generate(n, (i) => i + 1);
for (int i in range) {
double x = 2 * pi / n;
double dx = radius * cos(i * x);
double dy = radius * sin(i * x);
canvas.drawCircle(Offset(dx, dy), radius, myPaint);
}
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}