I have a .flr animation of a minion. Is it possible to change colors of his body, pants, eyes, etc dynamicaly and separately in flutter app?
PS Minion is just an example i found on rive.app .There will be another character with lots of different parts.
PPS Maybe there is a better way to make a simple animated character in flutter? For now, i have a stack with positioned colorfilterd images, but i guess it should be easier with rive.
Yes you can. There is an example in the Flare github:
https://github.com/2d-inc/Flare-Flutter/tree/master/example/change_color
import 'package:flare_flutter/flare_controller.dart';
import 'package:flare_flutter/flare.dart';
import 'package:flare_dart/math/mat2d.dart';
import 'package:flare_flutter/flare_actor.dart';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
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();
}
List<Color> exampleColors = <Color>[Colors.red, Colors.green, Colors.blue];
class _MyHomePageState extends State<MyHomePage> with FlareController {
FlutterColorFill _fill;
void initialize(FlutterActorArtboard artboard) {
// Find our "Num 2" shape and get its fill so we can change it programmatically.
FlutterActorShape shape = artboard.getNode("Num 2");
_fill = shape?.fill as FlutterColorFill;
}
void setViewTransform(Mat2D viewTransform) {}
bool advance(FlutterActorArtboard artboard, double elapsed) {
// advance is called whenever the flare artboard is about to update (before it draws).
Color nextColor = exampleColors[_counter % exampleColors.length];
if (_fill != null) {
_fill.uiColor = nextColor;
}
// Return false as we don't need to be called again. You'd return true if you wanted to manually animate some property.
return false;
}
// We're going to use the counter to iterate the color.
int _counter = 0;
void _incrementCounter() {
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State, which causes it to rerun the build method below
// so that the display can reflect the updated values. If we changed
// _counter without calling setState(), then the build method would not be
// called again, and so nothing would appear to happen.
_counter++;
// advance the controller
isActive.value = true;
});
}
#override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: FlareActor("assets/change_color_example.flr", // You can find the example project here: https://www.2dimensions.com/a/castor/files/flare/change-color-example
fit: BoxFit.contain, alignment: Alignment.center, controller: this),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
If you're using the beta of Rive it's a bit different. I'm not sure if it's the best approach but I'm doing the following:
artboard.forEachComponent((child){
if (child.name == 'Fill') {
Fill fill = child;
fill.paint.color = Colors.red.withOpacity(0.25);
}
else if (child.name == 'Stroke') {
Stroke stroke = child;
stroke.paint.color = Colors.red;
}
});
As Kretin already outlined with the Flutter rive package it is possible even though it's beta as of writing.
To get this to work, you have to know thinks about your animation, for example the name of the shape you want to change (first), and the fill color you want to change.
Let's assume, I have an animation with a shape which name is 'Button1'.
This shape has one fill color that I'd like to change, then my code looks like this:
artboard.forEachComponent((child) {
if (child is Shape && child.name == 'Button1') {
final Shape shape = child;
shape.fills.first.paint.color = Colors.red;
}
});
I have tried the forEachComponent method that the other answers here mention, but it didn't work exactly like I expected. It iterated the components over and over again, spending a lot of process power and also because I was testing merging colors, it ended up merging again and again.
So I ended up checking the properties of the classes to find how to get the colors precisely and changing.
In my code, I'm changing a part of a color of linear gradients. But to get a solid color you can change
shape.fills.first.paintMutator.children.[first/last].color
to
shape.fills.first.paint.color
My code is like this:
final rootBone =
artboard.children.firstWhereOrNull((e) => e.name == 'Root Bone');
final rootBoneChildren = rootBone?.artboard?.drawables;
// Changing hair color
final hair =
rootBoneChildren?.firstWhereOrNull((e) => e.name == 'hair');
if (hair is Shape) {
((hair.fills.first.paintMutator as dynamic)?.children.first
as dynamic)
.color = AppColors.accent;
}
// Changing head color
final head =
rootBoneChildren?.firstWhereOrNull((e) => e.name == 'head');
if (head is Shape) {
final colorParts =
(head.fills.first.paintMutator as dynamic)?.children;
const mergeColor = AppColors.brown;
const timeline = 0.9;
// center
colorParts.first.color =
Color.lerp(colorParts.first.color, mergeColor, timeline);
// border
colorParts.last.color =
Color.lerp(colorParts.last.color, mergeColor, timeline);
}
ps: I'm casting as dynamic because it is a class called LinearGradient, extended from the paintMutator original class, and the class LinearGradient from rive has the same name as LinearGradient from flutter and I didn't want to use aliases everywhere in the code just because of those lines.
Related
In Flutter, when creating a CustomPainter, there is an override method, shouldRepaint() that you can return either true or false... presumably to tell the system whether or not to repaint the view.
And in the docs, the description for the method is:
shouldRepaint(covariant CustomPainter oldDelegate) → bool Called
whenever a new instance of the custom painter delegate class is
provided to the RenderCustomPaint object, or any time that a new
CustomPaint object is created with a new instance of the custom
painter delegate class (which amounts to the same thing, because the
latter is implemented in terms of the former). [...]
I basically don't understand any of that other than the fact that it returns a bool. That makes my head hurt! I also suspect that delving deeper into the definition of "custom painter delegate class," or "RenderCustomPaint object," will not be an enlightening experience.
I'm confused because:
I thought we didn't have to worry about when a widget "should repaint" because Flutter was supposed to decide where and when to re-render the widget tree based on it's own complex optimization decisions.
I thought the paint() method was where you define "this is how this view paints itself, (always and whenever that is necessary)"
All the examples I have found simply return false from this method... but I have noticed different behavior when using true vs false.
If we are always returning false, then how does it ever repaint? (And it does repaint even when false)
If the only possible logic available to us is comparing the "oldDelegate" to (something?) then why are we required to override the method at all?
I haven't seen any example that demonstrates why or how you would return TRUE, and what the logic of such an example would look like in order to make that decision.
Why and how would a knowledgable person decide to return false?
Why and how would a knowledgable person decide to return true?
Can anyone explain it like you're talking to a 13 year old (not Linus Torvalds)?
A simple code example and counter-example would be great (as opposed to an exhaustive explicit explanation!)
I have used CustomPainter extensively, and here is my answer.
Firstly, here is the full doc. You may have only read the starting sentences instead of the full doc. https://api.flutter.dev/flutter/rendering/CustomPainter/shouldRepaint.html
Why and how would a knowledgeable person decide to return false/true?
Here is the rule: If the new instance represents different information than the old instance, then the method should return true, otherwise it should return false.
Example:
class MyPainter extends CustomPainter {
MyPainter() : super();
#override
void paint(Canvas canvas, Size size) => canvas.drawRect(Offset.zero & size, Paint());
// false since all instances of MyPainter contain same information
#override
bool shouldRepaint(MyPainter oldDelegate) => false;
}
class MyPainter extends CustomPainter {
final Color color;
final double width;
MyPainter(this.color, this.width) : super();
#override
void paint(Canvas canvas, Size size) => canvas.drawRect(
Offset.zero & size,
Paint()
..color = color
..strokeWidth = width);
#override
bool shouldRepaint(MyPainter oldDelegate) => oldDelegate.color != this.color || oldDelegate.width != this.width;
}
I thought we didn't have to worry about when a widget "should repaint" because Flutter was supposed to decide where and when to re-render the widget tree based on it's own complex optimization decisions.
Yes and no. This shouldRepaint() is basically an optimization for speed. You can return constantly true if you do not care about performance.
I thought the paint() method was where you define "this is how this view paints itself, (always and whenever that is necessary)"
"this is how this view paints itself" - yes. "always and whenever that is necessary" - partially no. If you provide wrong information to shouldRepaint() you may miss some paints.
All the examples I have found simply return false from this method... but I have noticed different behavior when using true vs false.
What??? I see people returning true, or returning with comparison (see my example below). But when returning false, it can cause problems. Even simply look at comments of this function you will see it should cause problems with constant false. But anyway, if your painter really does not contain any information that can change, it is ok...
If we are always returning false, then how does it ever repaint? (And it does repaint even when false)
/// If the method returns false, then the [paint] call might be optimized
/// away.
///
/// It's possible that the [paint] method will get called even if
/// [shouldRepaint] returns false (e.g. if an ancestor or descendant needed to
/// be repainted). It's also possible that the [paint] method will get called
/// without [shouldRepaint] being called at all (e.g. if the box changes
/// size).
Notice the "might", and the paragraph below. Flutter can choose to paint or not to paint.
If the only possible logic available to us is comparing the "oldDelegate" to (something?) then why are we required to override the method at all?
See my example
I haven't seen any example that demonstrates why or how you would return TRUE, and what the logic of such an example would look like in order to make that decision.
See my example
By the way, it would be great if you gain some insights of Flutter, such as the widget/layout/paint logic etc, then you will easily understand the problem. But anyway I have answered above using words that are hopefully easy to understand even without deep understanding of Flutter.
It means always repaint if setState() method is called from the State class
The widgets in the State class will be updated if setState() changes their data.
shouldRepaint() EXAMPLE:
check at the bottom for inline comments!!
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
// true means Always repaint if this CustomPainter is updated anywhere else
// e.g. from the State class
// State class can be called as below
}
Complete code ...
import 'package:flutter/material.dart';
import 'dart:math';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Custom Paint example ',
//theme: ThemeData( //use theme if needed
// brightness: Brightness.dark,
// primaryColor: Colors.indigo,
// accentColor: Colors.indigoAccent
// ),
home: _CustomPainterView() // _is implemented in this file
//first return the StatefulWidget
);
}
}
class _CustomPainterView extends StatefulWidget{ //must be a stateful widget
#override
State<StatefulWidget> createState() {
return _CustomViewState(); //here return the State widget
}
}
//State class below references the Stateful widget from which it was called...
class _CustomViewState extends State<_CustomPainterView>{
Map<String, Object> painterData = {'screenMessage': 'not yet swiped', 'var_1': 0, 'var_2': 0.0 };
//all the needed parameters for the instance of CustomPainter class must be defined here in the State Class.
//they will be passed by setState() method to the CustomPainter which will provides the actual canvas
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar( title: Text("Custom Painter Example") ) ,
body: Container(
width: MediaQuery.of(context).size.width,//get width from device
height: MediaQuery.of(context).size.width*2, //i use the the custom width from half of the height
// ignore: sort_child_properties_last
child: GestureDetector( //to allow screen swipe or drag
child: CustomPaint(//CustomPaint() is just a container for actual painter.
//note the spelling
painter: _CustomPainterExample(painterData)
//return CustomPainter()
//supply constructor data
),
onVerticalDragUpdate: (details) {
int sensitivity = 1;// = 1 every pixel swiped will be detected by below code
setState( (){
if (details.delta.dy > sensitivity) {
debugPrint("swipe down"); //print to the debug console
painterData['screenMessage'] = "swipe down";
//this change only the key-value that needs to be changed in the key-value pairs, then repaint.
// painterData map will change but inside setState() will cause other effect
//setState recalls CustomPainter consctructor every time
//setState force a repaint in CustomPainter
}
if(details.delta.dy < -sensitivity){
debugPrint("swipe up");
painterData['screenMessage'] = "swipe up";
}
}
);
},
onHorizontalDragUpdate: (details) {
int sensitivity = 1;
setState( (){
if (details.delta.dx > sensitivity) {
debugPrint("swipe right");
painterData['screenMessage'] = "swipe right";
}
if(details.delta.dx < -sensitivity){
debugPrint("swipe left");
painterData['screenMessage'] = "swipe left";
}
}
);
},
),
//color: Color.fromARGB(255, 50, 57, 126)
// ignore: prefer_const_constructors
color: Color.fromARGB(255, 1, 108, 141),
),
);
}
}
class _CustomPainterExample extends CustomPainter {
Map<String, Object> painterData = new Map<String, Object>();
_CustomPainterExample(Map<String, Object> pdata){
painterData = pdata;
}
#override
void paint(Canvas canvas, Size size) {
var centerX = size.width/2;
var centerY = size.height/2;
var center = Offset(centerX,centerY);
var radius = min(centerX,centerY);
var fillBrush = Paint()
// ignore: prefer_const_constructors
..color = Color.fromARGB(255, 202, 122, 29);
canvas.drawCircle( center, radius, fillBrush );
//can also draw using the data from constructor method
var textPainter = TextPainter(
text: TextSpan(
text: painterData['screenMessage'].toString(),
style: TextStyle(color: Color.fromARGB(255, 245, 242, 242),fontSize: 30,),
),
textDirection: TextDirection.ltr,
);
textPainter.layout(minWidth: 0, maxWidth: size.width);//<<< needed method
textPainter.paint(canvas, Offset(5.0, (90/100)*size.height));
} //customPainter
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true; //always repaint if setState() is called from the State class. Look for setState() method in the class: _CustomViewState
//It will update canvas since the _CustomPainterExample is one of widgets in the _CustomViewState which is the State class. All widgets in the State class will be updated if SetState() changes their data.
}
}
Minimal reproducible code:
void main() => runApp(MaterialApp(home: HomePage()));
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final List<Offset> _points = [];
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () => setState(() {}), // This setState works
child: Icon(Icons.refresh),
),
body: GestureDetector(
onPanUpdate: (details) => setState(() => _points.add(details.localPosition)), // but this doesn't...
child: CustomPaint(
painter: MyCustomPainter(_points),
size: Size.infinite,
),
),
);
}
}
class MyCustomPainter extends CustomPainter {
final List<Offset> points;
MyCustomPainter(this.points);
#override
void paint(Canvas canvas, Size size) {
final paint = Paint()..color = Colors.red;
for (var i = 0; i < points.length; i++) {
if (i + 1 < points.length) {
final p1 = points[i];
final p2 = points[i + 1];
canvas.drawLine(p1, p2, paint);
}
}
}
#override
bool shouldRepaint(MyCustomPainter oldDelegate) => false;
}
Try to draw something by long dragging on the screen, you won't see anything drawn. Now, press the FAB which will reveal the drawn painting maybe because FAB calls setState but onPanUpdate also calls setState and that call doesn't paint anything on the screen. Why?
Note: I'm not looking for a solution on how to enable the paint, a simple return true does the job. What I need to know is why one setState works (paints on the screen) but the other fails.
To understand why setState() in onPanUpdate is not working you might want to look into the widget paint Renderer i.e., CustomPaint.
The CustomPaint (As stated by docs as well) access the painter object (in your case MyCustomPainter) after finishing up the rendering of that frame. To confirm we can check the source of CustomPainter. we can see markNeedsPaint() is called only while we are accessing painter object through setter. For more clarity you might want to look into source of RenderCustomPaint , you will definitely understand it :
void _didUpdatePainter(CustomPainter? newPainter, CustomPainter? oldPainter) {
// Check if we need to repaint.
if (newPainter == null) {
assert(oldPainter != null); // We should be called only for changes.
markNeedsPaint();
} else if (oldPainter == null ||
newPainter.runtimeType != oldPainter.runtimeType ||
newPainter.shouldRepaint(oldPainter)) { //THIS
markNeedsPaint();
}
.
.
.
}
While on every setState call your points are updating but every time creating new instances of 'MyCustomPainter` is created and the widget tree is already rendered but painter have not yet painted due to reason mentioned above.
That is why the only way to call markNeedPaint()(i.e., to paint your object), is by returning true to shouldRepaint or Either oldDeleagate is null which only happens and Fist UI build of the CustomPainter, you can verify this providing some default points in the list.
It is also stated that
It's possible that the [paint] method will get called even if
[shouldRepaint] returns false (e.g. if an ancestor or descendant
needed to be repainted). It's also possible that the [paint] method
will get called without [shouldRepaint] being called at all (e.g. if
the box changes size).
So the only reason of setState of Fab to be working here (which seams valid) is that Fab is somehow rebuilding the any parent of the custom painter. You can also try to resize the UI in 'web build' or using dartpad you will find that as parent rebuilds itself the points will become visible So setState directly have nothing to do with shouldRepaint. Even hovering on the fab (in dartpad) button will cause the ui to rebuild and hence points will be visible.
I am new to flutter and the way I get StatefulWidget's state is add a state property to widget, eg:
// ignore: must_be_immutable
class _CustomContainer extends StatefulWidget {
_CustomContainer({Key key}) : super(key: key);
#override
__CustomContainerState createState() {
state = __CustomContainerState();
return state;
}
__CustomContainerState state;
void changeColor() {
if (state == null) return;
// call state's function
this.state.changeColor();
}
}
class __CustomContainerState extends State<_CustomContainer> {
var bgColor = Colors.red;
#override
Widget build(BuildContext context) {
return Container(
width: 200,
height: 200,
color: bgColor,
);
}
void changeColor() {
setState(() {
bgColor = Colors.blue;
});
}
}
usage:
final redContainer = _CustomContainer();
final button = FlatButton(
onPressed: () {
// call widget function
redContainer.changeColor();
},
child: Text('change color'),
);
It works, but I wonder is there any hidden danger?
You'll notice it's very awkward to manipulate Flutter widgets in an imperative fashion, like in the question. This is because of the declarative approach Flutter has taken to building screens.
Declarative vs. Imperative
The approach / philosophy of Flutter UI is a declarative UI vs. an imperative UI.
The example in the question above leans toward an imperative approach.
create an object
object holds state (information)
object exposes method
use method to impose change on object → UI changes
A declarative approach:
there is state (information) above your object
your object is declared (created) from that state
if the state changes...
your object is recreated with the changed state
Below I've tried to convert the imperative approach above, into a declarative one.
CustomContainer is declared with a color; state known / kept outsideCustomContainer & used in its construction.
After construction, you cannot impose a color change on CustomContainer. In an imperative framework you would expose a method, changeColor(color) and call that method and the framework would do magic to show a new color.
In Flutter, to change color of CustomContainer, you declare CustomContainer with a new color.
import 'package:flutter/material.dart';
/// UI object holds no modifiable state.
/// It configures itself once based on a declared color.
/// If color needs to change, pass a new color for rebuild
class CustomContainer extends StatelessWidget {
final Color color;
CustomContainer(this.color);
#override
Widget build(BuildContext context) {
return Container(
color: color,
child: Text('this is a colored Container'),
);
}
}
/// A StatefulWidget / screen to hold state above your UI object
class DeclarativePage extends StatefulWidget {
#override
_DeclarativePageState createState() => _DeclarativePageState();
}
class _DeclarativePageState extends State<DeclarativePage> {
var blue = Colors.blueAccent.withOpacity(.3);
var red = Colors.redAccent.withOpacity(.3);
Color color;
// state (e.g. color) is held in a context above your UI object
#override
void initState() {
super.initState();
color = blue;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Declarative Page'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CustomContainer(color),
// color "state" ↑ is passed in to build/rebuild your UI object
RaisedButton(
child: Text('Swap Color'),
onPressed: () {
setState(() {
toggleColor();
});
}
)
],
),
),
);
}
void toggleColor() {
color = color == blue ? red : blue;
}
}
Read more on declarative vs imperative on Flutter.dev.
setState() Rebuilds & Performance
Performance-wise it seems wasteful to rebuild the entire screen when a single widget, way down in the widget tree needs rebuilding.
When possible (and sensible) it's better to wrap the particular elements that have state & need rebuilding in a StatefulWidget, rather than wrapping your entire page in a StatefulWidget and rebuilding everything. (Likely, this wouldn't even be a problem, I'll discuss further below.)
Below I've modified the above example, moving the StatefulWidget from being the entire DeclarativePage, to a ChangeWrapper widget.
ChangeWrapper will wrap the CustomContainer (which changes color).
DeclarativePage is now a StatelessWidget and won't be rebuilt when toggling color of CustomContainer.
import 'package:flutter/material.dart';
class ChangeWrapper extends StatefulWidget {
#override
_ChangeWrapperState createState() => _ChangeWrapperState();
}
class _ChangeWrapperState extends State<ChangeWrapper> {
final blue = Colors.blueAccent.withOpacity(.3);
final red = Colors.redAccent.withOpacity(.3);
Color _color; // this is state that changes
#override
void initState() {
super.initState();
_color = blue;
}
#override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CustomContainer(_color),
RaisedButton(
child: Text('Swap Color'),
onPressed: () {
setState(() {
toggleColor();
});
}
)
],
);
}
void toggleColor() {
_color = _color == blue ? red : blue;
}
}
/// UI object holds no state, it configures itself once based on input (color).
/// If color needs to change, pass a new color for rebuild
class CustomContainer extends StatelessWidget {
final Color color;
CustomContainer(this.color);
#override
Widget build(BuildContext context) {
return Container(
color: color,
child: Text('this is a colored Container'),
);
}
}
class DeclarativePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
print('Declarative Page re/built');
return Scaffold(
appBar: AppBar(
title: Text('Declarative Page'),
),
body: Center(
child: ChangeWrapper(),
),
);
}
}
When running this version of the code, only the ChangeWrapper widget is rebuilt when swapping colors via button press. You can watch the console output for "Declarative Page re/built" which is written to debug console only once upon first screen build/view.
If DeclarativePage was huge with hundreds of widgets, isolating widget rebuilds in the above manner could be significant or useful. On small screens like in the first example at top or even or average screens with a couple dozen widgets, the difference in savings are likely negligible.
Flutter was designed to operate at 60 frames per second. If your screen can build / rebuild all widgets within 16 milliseconds (1000 milliseconds / 60 frames = 16.67 ms per frame), the user will not see any jankiness.
When you use animations, those are designed to run at 60 frames (ticks) per second. i.e. the widgets in your animation will be rebuilt 60 times each second the animation runs.
This is normal Flutter operation for which it was designed & built. So when you're considering whether your widget architecture could be optimized it's useful to think about its context or how that group of widgets will be used. If the widget group isn't in an animation, built once per screen render or once per human button tap... optimization is likely not a big concern.
A large group of widgets within an animation... likely a good candidate to consider optimization & performance.
Btw, this video series is a good overview of the Flutter architecture. I'm guessing Flutter has a bunch of hidden optimizations such as Element re-use when a Widget hasn't materially/substantially changed, in order to save on CPU cycles instantiating, rendering & positioning Element objects.
add setState() method where you want to add state
So I have been trying to solve this issue for a couple of days now and I seem to have hit a real dead end here.
The issue (which I have simplified in the code below) is that when I try to remove an item from a List<"SomeDataObject">, it does actually remove the correct object from the list. This is evident because the ID that I arbitrarily assigned to the objects does shift just as I would expect it to when I remove something. However, oddly enough, even though the IDs shift in the list and the correct ID is removed, the states of all the widgets seem to act as though the last item is always removed, even when the deleted object is at the middle or even beginning of the list.
An example would be if the list looked as such:
Data(id: 630) // This one starts blue
Data(id: 243) // Let's say the user made this one red
Data(id: 944) // Also blue
And let's say I tried to remove the middle item from this list. Well what happens is that the list will look like this now:
Data(id: 630)
Data(id: 944)
This seems at first to be great because it looks like exactly what I wanted, but for some reason, the colors did not change their order and are still Red first, then Blue. The color state data seems to function independently from the actual objects and I could not find a clear solution.
I have code of an example program to reproduce this problem and I also have some pictures below of the program so that it is clearer what the issue is.
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: HomeScreen(),
);
}
}
// Home screen class to display app
class HomeScreen extends StatefulWidget {
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
List<DataWidget> widgetsList = List<DataWidget>();
// Adds a new data object to the widgets list
void addData() {
setState(() {
widgetsList.add(DataWidget(removeData));
});
}
// Removes a given widget from the widgets list
void removeData(DataWidget toRemove) {
setState(() {
widgetsList = List.from(widgetsList)..remove(toRemove);
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
/* I used a SingleChildScrollView instead of ListView because ListView
resets an objects state when it gets out of view so I wrapped the whole
list in a column and then passed that to the SingleChildScrollView to
force it to stay in memory. It is worth noting that this problem still
exists even when using a regular ListView. */
body: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
// Added a SizedBox just to make column take up whole screen width
children: [...widgetsList, SizedBox(width: double.infinity)],
),
),
// FloatingActionButton just to run addData function for example
floatingActionButton: FloatingActionButton(onPressed: addData),
);
}
}
// Simple class that is a red square that when clicked turns blue when pressed
class DataWidget extends StatefulWidget {
DataWidget(this.onDoubleTap);
final Function(DataWidget) onDoubleTap;
// Just a random number to keep track of which widget this is
final String id = Random.secure().nextInt(1000).toString();
#override
String toStringShort() {
return id;
}
#override
_DataWidgetState createState() => _DataWidgetState();
}
class _DataWidgetState extends State<DataWidget> {
/* Here the example state information is just a color, but in the full version
of this problem this actually has multiple fields */
Color color = Colors.red;
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: GestureDetector(
// onDoubleTap this passes itself to the delete method of the parent
onDoubleTap: () => widget.onDoubleTap(widget),
// Changes color on tap to whatever color it isn't
onTap: () {
setState(() {
color = color == Colors.red ? Colors.blue : Colors.red;
});
},
child: Container(
child: Center(child: Text(widget.id)),
width: 200,
height: 200,
color: color,
),
),
);
}
}
Before user changes any colors (object ID is displayed as text on container):
User changes middle container to blue by tapping:
User attempts to delete middle (blue) container by double tapping:
As you can see from the image above, the ID was deleted and the ID from below was moved up on the screen, but the color information from the state did not get deleted.
Any ideas for things to try would be greatly appreciated.
You need to make a few changes to your DataWidget class to make it work:
// Simple class that is a red square that when clicked turns blue when pressed
class DataWidget extends StatefulWidget {
Color color = Colors.red; // moved from _DataWidgetState class
DataWidget(this.onDoubleTap);
final Function(DataWidget) onDoubleTap;
// Just a random number to keep track of which widget this is
final String id = Random.secure().nextInt(1000).toString();
#override
String toStringShort() {
return id;
}
#override
_DataWidgetState createState() => _DataWidgetState();
}
class _DataWidgetState extends State<DataWidget> {
/* Here the example state information is just a color, but in the full version
of this problem this actually has multiple fields */
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: GestureDetector(
// onDoubleTap this passes itself to the delete method of the parent
onDoubleTap: () => widget.onDoubleTap(widget),
// Changes color on tap to whatever color it isn't
onTap: () {
setState(() {
widget.color =
widget.color == Colors.red ? Colors.blue : Colors.red;
});
},
child: Container(
child: Center(child: Text(widget.id)),
width: 200,
height: 200,
color: widget.color,
),
),
);
}
}
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
}
}