I'm trying to control an animation on a widget (widget 2 in the pic) using gestures captured from another widget (widget 1 in the pic) and I'm having trouble finding the right way to do that.
The approach I currently think about is having the AnimationController in a common parent. widget 1 would, via callbacks, update the value of this controller in the parent. widget 2 would receive that controller a a parameter and therefore the animation in widget 2 would correspond to the values provided by the gestures in widget 1
Is that the right way to do it ? If not, what would be a better approach ?
Edit: After trying this method, I notice that an exception get thrown
Another exception was thrown: AnimationController.dispose() called more than once.
I suspect the AnimationController object to be reconstructed on rebuild causing the error. Any idea on how to do it to avoid that ?
Thanks ! :)
Here is a full sample of what I think you're trying to achieve, using AnimatedWidget. Might not be the most efficient way to do it, but it works.
import 'package:flutter/material.dart';
void main() => runApp(App());
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text("Test")),
body: SafeArea(child: Parent()),
),
);
}
}
class Parent extends StatefulWidget {
#override
_ParentState createState() => _ParentState();
}
class _ParentState extends State<Parent> with SingleTickerProviderStateMixin {
AnimationController _animationController;
Animation<double> _animation;
#override
void initState() {
super.initState();
_animationController = AnimationController(vsync: this);
_animation =
Tween<double>(begin: 0, end: 100).animate(_animationController);
}
#override
void dispose() {
super.dispose();
_animationController.dispose();
}
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Expanded(child: Widget1(animation: _animation)),
Widget2(onSliderChanged: (value) {
_animationController.value = value / 100;
}),
],
);
}
}
class Widget1 extends AnimatedWidget {
Widget1({Key key, #required Animation<double> animation})
: super(key: key, listenable: animation);
#override
Widget build(BuildContext context) {
Animation<double> animation = listenable;
return Opacity(
opacity: animation.value / 100,
child: Center(
child: Text(
"Hi",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 40.0),
),
),
);
}
}
class Widget2 extends StatefulWidget {
final ValueChanged<double> onSliderChanged;
const Widget2({Key key, this.onSliderChanged}) : super(key: key);
#override
State<StatefulWidget> createState() => _Widget2State();
}
class _Widget2State extends State<Widget2> {
double _value = 0;
#override
Widget build(BuildContext context) {
return Slider(
value: _value,
min: 0,
max: 100,
onChanged: (value) {
setState(() {
_value = value;
});
widget.onSliderChanged(value);
},
);
}
}
Related
Im using a SideBar() widget inside the SideBarWidget() and using it in my main screen SideBarScreen()
but i cant initialize the controller in the Sidebar() widget... How can i fix this
sidebar_Screen
class _SideBarScreenState extends State<SideBarScreen> {
#override
Widget build(BuildContext context) {
return SafeArea(
child: Stack(
children: [
ScaffoldScreen(
iconButton: IconButton(
onPressed: () {SideBarWidget().toggle();},
icon: kScaffoldScreenButtonIcon,
),
),
SideBarWidget(),
],
),
);
}
}
sideBarWidget
class SideBarWidget extends StatelessWidget {
SideBarWidget({Key? key}) : super(key: key);
void toggle() {
SideBarState().toggle();
}
late final SideBar sideBarWidget = SideBar();
#override
Widget build(BuildContext context) {
return sideBarWidget;
}
}
SideBar
class SideBar extends StatefulWidget {}
class SideBarState extends State<SideBar> with SingleTickerProviderStateMixin{
late AnimationController controller;
late Animation<Offset> _offsetAnimation;
#override
void initState() {
super.initState();
controller = AnimationController(vsync: this, duration: const Duration(seconds: 2));
_offsetAnimation = Tween<Offset>(
begin: const Offset(-1.0, 0.0),
end: const Offset(0.0, 0.0),
).animate(CurvedAnimation(parent: controller, curve: Curves.easeOut));
}
#override
void dispose() {
controller.dispose();
super.dispose();
}
void toggle() {
if (controller.isCompleted) {
controller.reverse();
}
else {controller.forward();}
}
#override
Widget build(BuildContext context) {
return SlideTransition();
}
}
This gives the error LateInitializationError: Field 'controller' has not been initialized.
I tried passing the controller as a parameter to the SideBar() but it gives off an exception:
This widget has been unmounted, so the State no longer has a context (and should be considered defunct).
The problem is that you're creating a new instance of SideBar every time you call SideBarWidget().toggle(). Instead, you should be using the same instance of SideBar so that the state is retained.
class SideBarWidget extends StatelessWidget {
SideBarWidget({Key? key}) : super(key: key);
final sideBar = SideBar();
void toggle() {
sideBar.state.toggle();
}
#override
Widget build(BuildContext context) {
return sideBar;
}
}
So now you are using the same instance of SideBar every time and you don't need to pass any parameters.
Note:
To use like this, you also need to change the SideBar class to a StatefulWidget
class SideBar extends StatefulWidget {
#override
_SideBarState createState() => _SideBarState();
}
I have a custom widget that changes color when tapped inside a gridview. When I scroll to the bottom and scroll back up to the top selected widget its animation is reversed.
I'm pretty sure that it has something to do with the widget being disposed of when out of view but I don't have a solution to overcome it. See my code below:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Thirty Seconds',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
// Page with the gridview
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key});
#override
Widget build(BuildContext context) {
return Scaffold(
body: GridView.count(
crossAxisCount: 2,
children: List.generate(20, (index) {
return MyCustomWidget(
key: GlobalKey(),
index: index + 1,
);
}),
),
);
}
}
// Custom Widget
class MyCustomWidget extends StatefulWidget {
const MyCustomWidget({
super.key,
required this.index,
});
final int index;
#override
State<MyCustomWidget> createState() => _MyCustomWidgetState();
}
class _MyCustomWidgetState extends State<MyCustomWidget>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<Color?> _colorAnimation;
#override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
);
_colorAnimation = ColorTween(begin: Colors.white, end: Colors.yellow)
.animate(_animationController)
..addListener(() => setState(() {}));
}
#override
void dispose() {
_animationController.dispose();
super.dispose();
}
void _toggleAnimation() {
if (_animationController.isCompleted) {
_animationController.reverse();
} else {
_animationController.forward();
}
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
_toggleAnimation();
},
child: Container(
color: _colorAnimation.value,
child: Center(
child: Text("Custom Widget ${widget.index}"),
),
),
);
}
}
GridView dispose the widget that aren't visible on UI. You can use cacheExtent(not suitable for this case) or AutomaticKeepAliveClientMixin on _MyCustomWidgetState.
class _MyCustomWidgetState extends State<MyCustomWidget>
with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin {
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
super.build(context);
You may prefer handing it parent widget and passing a bool to check active state or state-management or project level depends on scenario.
This code is part of online training of flutter by Google team. The original code can be accessed in https://github.com/afitz0/exploration_planner. I am new on flutter and I´ve got some dificulties to use statefull widget. I still do not have enough confidence. I made some modification on original code to add action to the indicator bar, it works fine but I dont think my solution is ideal...
My question is related to the right way to make a change in the state of the taskitem give an
update on the linearProgressIndicator ? Thanks in advance..
import 'package:flutter/material.dart';
double _percentual = 0; //variable to hold progress bar values from zero to 1 step 0.2
// first comes root run appp
void main() => runApp(MyApp()
//MaterialApp
//Scaffold
//AppBar
//Text
//body: Column
//text, text, text
//image
//Row
//text, text, bttom
//....
);
// second comes materialapp
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Exploration!',
theme: ThemeData(primarySwatch: Colors.blueGrey),
home: MyHomePage(),
);
}
}
//third comes home page describes visual of app
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
late AnimationController controller;
#override
void initState() {
controller = AnimationController(
vsync: this,
)..addListener(() {
setState(() {
controller.value = _percentual;
});
});
super.initState();
}
#override
void dispose() {
controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Space Exploration planner'),
),
body: Column(
children: [
Progress(),
TaskList(),
],
),
);
}
}
class Progress extends StatefulWidget {
const Progress({super.key});
#override
State<Progress> createState() => _ProgressState();
}
class _ProgressState extends State<Progress> {
#override
Widget build(BuildContext context) {
return Column(
children: [
Text('You are this far away from exploring the whole universe'),
LinearProgressIndicator(
value: _percentual,
)
],
);
}
}
class TaskList extends StatelessWidget {
const TaskList({super.key});
#override
Widget build(BuildContext context) {
return Column(
children: [
TaskItem(label: "Load rocket with supplies"),
TaskItem(label: "Launch rocket"),
TaskItem(label: "Circle the home planet"),
TaskItem(label: "Head out to de first moon"),
TaskItem(label: "Launch moon lander #1"),
],
);
}
}
class TaskItem extends StatefulWidget {
final String label;
const TaskItem({Key? key, required this.label}) : super(key: key);
#override
State<TaskItem> createState() => _TaskItemState();
}
class _TaskItemState extends State<TaskItem> {
bool? _value = false;
#override
Widget build(BuildContext context) {
return Row(
children: [
Checkbox(
onChanged: (newValue) => setState(() => {
_value = newValue,
if (_value == true)
{
_percentual = double.parse(
(_percentual + 0.2).toStringAsPrecision(1)),
_ProgressState(),
}
else if (_value == false)
{
_percentual = double.parse(
(_percentual - 0.2).toStringAsPrecision(1)),
_ProgressState(),
},
main(), *//<-- worked like hot-reload but I dont think is the right way to do it.*
}),
value: _value,
),
Text(widget.label),
],
);
}
}
Currently, the value of the elapsed time is printing a gazillion times per second, ok maybe more like 10 times. That's unnecessary. I just want it to print once every 5 seconds or if the user navigates away from the page, print. So that I can record the amount of time spent here.
How do i achieve that?
Attached is my code below:
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: TimerTest(),
);
}
}
class TimerTest extends StatefulWidget {
#override
_TimerTestState createState() => _TimerTestState();
}
class _TimerTestState extends State<TimerTest> with TickerProviderStateMixin {
AnimationController _controller;
int levelClock = 5400;
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(
seconds:
levelClock)
);
_controller.forward();
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Color(0xff04072E),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Countup(
animation: StepTween(
begin: 0, // THIS IS A USER ENTERED NUMBER
end: levelClock,
).animate(_controller),
),
],
),
),
);
}
}
class Countup extends AnimatedWidget {
Countup({Key key, this.animation}) : super(key: key, listenable: animation);
Animation<int> animation;
#override
build(BuildContext context) {
Duration clockTimer = Duration(seconds: animation.value);
String timerText =
'${clockTimer.inMinutes.remainder(60).toString()}:${clockTimer.inSeconds.remainder(60).toString().padLeft(2, '0')}';
print('animation.value ${animation.value} ');
return Text(
"$timerText",
style: TextStyle(
fontSize: 45,
color: Theme.of(context).primaryColor,
),
);
}
}
You can use Timer periodic function for this. For more detail : https://api.flutter.dev/flutter/dart-async/Timer/Timer.periodic.html
I made a simple class for animating a widget into the screen:
class CustomSlideAnimation extends StatefulWidget {
static const String FROM_LEFT = 'FROM_LEFT';
static const String FROM_RIGHT = 'FROM_RIGHT';
final String from;
final Widget child;
const CustomSlideAnimation(
{Key key, this.from = FROM_LEFT, #required this.child})
: super(key: key);
#override
_CustomSlideAnimationState createState() => _CustomSlideAnimationState();
}
class _CustomSlideAnimationState extends State<CustomSlideAnimation>
with SingleTickerProviderStateMixin {
Animation<Offset> _offsetAnimation;
AnimationController _animationController;
#override
void initState() {
super.initState();
_animationController =
AnimationController(vsync: this, duration: Duration(milliseconds: 300));
_offsetAnimation = widget.from == CustomSlideAnimation.FROM_LEFT
? Tween<Offset>(
begin: Offset(-1.0, 0.0),
end: Offset.zero,
).animate(CurvedAnimation(
curve: Curves.linear,
parent: _animationController,
))
: Tween<Offset>(
begin: Offset(1.0, 0.0),
end: Offset.zero,
).animate(CurvedAnimation(
curve: Curves.linear,
parent: _animationController,
));
_animationController.forward();
}
#override
void dispose() {
super.dispose();
_animationController.dispose();
}
#override
Widget build(BuildContext context) {
return SlideTransition(
position: _offsetAnimation,
child: widget.child,
);
}
}
And a simple app to demonstrate the problem:
void main() => runApp(Test());
class Test extends StatefulWidget {
#override
_TestState createState() => _TestState();
}
class _TestState extends State<Test> {
bool widget1 = true;
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: widget1
? CustomSlideAnimation(
child: InkWell(
child: Text('widget1'),
onTap: () {
setState(() {
widget1 = false;
});
},
),
from: CustomSlideAnimation.FROM_LEFT,
)
: CustomSlideAnimation(
child: InkWell(
child: Text('widget2'),
onTap: () {
setState(() {
widget1 = true;
});
},
),
from: CustomSlideAnimation.FROM_LEFT,
),
),
),
);
}
}
When I start the app for first time the Text widget as expected slides in to the screen,
but then I tap it and thus calling setState() the second Text widget just appears suddenly into the screen and no slide transition happens.
I printed the times when build get called in all the classes and I see that setState() is triggering builds in Test and CustomSlideTransition classes but not triggering initState() in the CustomSlideTransition.
EDIT for more clarification: even putting controller.forward() inside the build() method didn't work.
So what am I missing?
You just need to add a UniqueKey() to every CustomSlideAnimation() to tell flutter to treat them like different widgets.
void main() => runApp(Test());
class Test extends StatefulWidget {
#override
_TestState createState() => _TestState();
}
class _TestState extends State<Test> {
bool widget1 = true;
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: widget1
? CustomSlideAnimation(
key: UniqueKey(),
child: InkWell(
child: Text('widget1'),
onTap: () {
setState(() {
widget1 = false;
});
},
),
from: CustomSlideAnimation.FROM_LEFT,
)
: CustomSlideAnimation(
key: UniqueKey(),
child: InkWell(
child: Text('widget2'),
onTap: () {
setState(() {
widget1 = true;
});
},
),
from: CustomSlideAnimation.FROM_LEFT,
),
),
),
);
}
}
Also, keep controller.forward() inside initState()
I figured it out, the reason is in the keys and widget trees as explained in this video.
When the setState was called without CustomSlideTransition having a unique key, then the framework did not recreate the State object of this class again and it instead used the State object of the previous widget which had the AnimationStatus as completed
But now with keys each time the framework creates a new State object and calls initState() where the controller is reset and the animation starts normally.
I also found a workaround but I don't know if it is as efficient as the "keys" solution:
Without adding a key to the CustomSlideTransition widget,I can, in the build() method of this widget, do these two commands:
_animationController.reset();
_animationController.forward();
and remove the call to _animationController.forward(); in initState().