How to show a Flutter widget as a result of action, and fade it out with no action - flutter-animation

I am trying to have some text show (or quickly fade in) as a result of user action (e.g. button click), and after a second fade out without user action. I understand that fading is accomplished by AnimatedOpacity class with opacity being set by a state variable, however it's not clear to me how to accomplish this particular scenario. Many thanks.

first, text its show it self while not fade its can be considered animated too, or create some async function
check this code :
import 'package:flutter/material.dart';
class FadeTextAuto extends StatefulWidget {
const FadeTextAuto({Key? key}) : super(key: key);
#override
_FadeTextAutoState createState() => _FadeTextAutoState();
}
class _FadeTextAutoState extends State<FadeTextAuto> with SingleTickerProviderStateMixin{
late Animation _animationFadeInOut;
late AnimationController _animationController;
bool _textShouldPlay = false;
late Animation _animationText;
String info = "This text appear in";
#override
void initState() {
super.initState();
_animationController = AnimationController(vsync: this, duration: const Duration(milliseconds: 2000));
_animationFadeInOut = Tween<double>(
begin: 0.0, end: 1.0).animate(CurvedAnimation(parent: _animationController, curve: Curves.linear));
_animationText = StepTween(
begin: 1, end: 3
).animate(CurvedAnimation(parent: _animationController, curve: Curves.linear));
}
#override
Widget build(BuildContext context) {
return Material(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextButton(onPressed: (){
setState(() {
info = "This text appear in";
_textShouldPlay = false;
});
print(_animationText.value);
_animationController.forward().then((value){
if(mounted){
setState(() {
_textShouldPlay = true;
info = "Hey read this Information";
});
Future.delayed(const Duration(milliseconds: 2000),(){
//wait a little for text can be read
if(mounted){
setState(() {
_textShouldPlay = false;
info = "this Text will be disappear in";
});
}
}).then((value){
if(mounted){
_animationController.reverse();
}
});
}
});
}, child: const Text("User Click")),
AnimatedBuilder(
animation: _animationController,
builder: (context, child){
return Opacity(
opacity: _animationFadeInOut.value,
child: Text(
"$info ${_textShouldPlay?"":_animationText.value} ", style: const TextStyle(color: Colors.cyan, fontSize: 20),
),
);
},
),
],
),
);
}
#override
void dispose() {
_animationController.dispose();
super.dispose();
}
}
result :

Related

How can I use Animated switcher without a button in Flutter

I have multiple widgets I want to fade in one after another but without needing a button. Every tutorial I have seen has required the use of a button in order to transition to the next widget. The widgets I want to transition take up the whole screen .
How do you want to trigger the transition?
If time is a suitable trigger.
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({Key? key}) : super(key: key);
#override
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
late Timer timer;
int count = 0;
#override
void initState() {
super.initState();
timer = Timer.periodic(Duration(seconds: 1), (_) => setState(() => count += 1));
}
#override
void dispose() {
super.dispose();
timer.cancel();
}
#override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
AnimatedSwitcher(
duration: const Duration(milliseconds: 500),
transitionBuilder: (Widget child, Animation<double> animation) {
return ScaleTransition(child: child, scale: animation);
},
child: Text(
'$count',
key: ValueKey<int>(count),
style: Theme.of(context).textTheme.headline4,
),
),
],
),
);
}
}

how to disable button after first click in flutter?

Clicked Button multiple times same time, open pages multiple times. How to fix this issue? I also uploaded the gif file on my application(double click on the image).
Container(
padding: EdgeInsets.all(10.0),
child: ButtonTheme(
minWidth: 10.0,
height: 40.0,
child: RaisedButton(
child: Text(
AppTranslations.of(context)
.text("loginpage_button"),
style: TextStyle(
color: Colors.white, fontSize: 15.0),
),
onPressed: () async{
(isOffline)
? _showSnackBar()
: checking2(usernameController, context, _url);
},
color: Colors.blue,
padding: EdgeInsets.all(20.0),
),
),
margin: EdgeInsets.only(top: 0.0),
)
I used this code, it's working, but user types username incorrectly, user cant click button second type. this is my code.
onPressed: () async {
if (_firstClick) {
_firstClick = false;
(isOffline)
? _showSnackBar()
: checking2(usernameController, context, _url);
}
Solved this in my application based on calculating time difference.
First, declare a DateTime variable and define the function as follows:
DateTime loginClickTime;
bool isRedundentClick(DateTime currentTime) {
if (loginClickTime == null) {
loginClickTime = currentTime;
print("first click");
return false;
}
print('diff is ${currentTime.difference(loginClickTime).inSeconds}');
if (currentTime.difference(loginClickTime).inSeconds < 10) {
// set this difference time in seconds
return true;
}
loginClickTime = currentTime;
return false;
}
In the login button call the function as follows to check for redundancy:
RaisedButton(
child: Text('Login'),
onPressed: () {
if (isRedundentClick(DateTime.now())) {
print('hold on, processing');
return;
}
print('run process');
},
),
Create a bool variable which will be true when the button is pressed, (hence, initial value is set to false).
bool _clicked = false;
#override
Widget build(BuildContext context) {
return Scaffold(
body: RaisedButton(
child: Text('Button'),
onPressed: _clicked
? null
: () {
setState(() => _clicked = true); // set it to true now
},
),
);
}
You can use a bool variable to save the state of your RaisedButton:
First create the variable a set its initial value :
var _firstPress = true;
Then add _firstPress inside your onPressed function :
Container(
padding: EdgeInsets.all(10.0),
child: ButtonTheme(
minWidth: 10.0,
height: 40.0,
child: RaisedButton(
child: Text(
AppTranslations.of(context).text("loginpage_button"),
style: TextStyle(color: Colors.white, fontSize: 15.0),
),
onPressed: () async {
// This is what you should add in your code
if (_firstPress) {
_firstPress = false;
(isOffline) ? _showSnackBar() : checking2(usernameController, context, _url);
}
},
color: Colors.blue,
padding: EdgeInsets.all(20.0),
),
),
margin: EdgeInsets.only(top: 0.0),
),
This way your onPressed function will only respond to the RaisedButton's first click.
I've written two classes for myself that may be helpful for others. They encapsulate the answer given by others in this thread so that you don't have a bunch of bools and assignment statements floating everywhere.
You pass your function to the class, and use the class' "invoke" method in place of the function. This currently does not support functions that need parameters, but is useful for the void case.
typedef void CallOnceFunction();
class CallOnce {
bool _inFunction = false;
final CallOnceFunction function;
CallOnce(CallOnceFunction function) :
assert(function != null),
function = function
;
void invoke() {
if (_inFunction)
return;
_inFunction = true;
function();
_inFunction = false;
}
}
typedef Future<void> CallOnceFuture();
class CallFutureOnce {
bool _inFunction = false;
final CallOnceFuture future;
CallFutureOnce(CallOnceFuture future) :
assert(future != null),
future = future
;
Future<void> invoke() async {
if (_inFunction)
return;
_inFunction = true;
await this.future();
_inFunction = false;
}
}
Update: Here's an example of both of these classes in action
/*Example*/
import 'package:flutter/material.dart';
class MyWidget extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return new MyWidgetState();
}
}
class MyWidgetState extends State<MyWidget> {
CallOnce _callOnce;
CallFutureOnce _callFutureOnce;
void myFunction() {
/*Custom Code*/
}
Future<void> myFutureFunction() async {
/*Custom Code*/
//await something()
}
#override
void initState() {
super.initState();
this._callOnce = CallOnce(this.myFunction);
this._callFutureOnce = CallFutureOnce(this.myFutureFunction);
}
#override
Widget build(BuildContext context) {
return Scaffold (
body: Center (
child: RaisedButton (
child: Text('Try Me'),
onPressed: this._callOnce.invoke,
),
),
floatingActionButton: FloatingActionButton (
child: Icon(Icons.save),
onPressed: this._callFutureOnce.invoke,
),
);
}
}
Some of the other solutions do not work for me, and some of them are not isolated in their own state and I implemented my solution to encapsulate the functionality in my custom widget. I implemented it for IconButton but you could modify it with any tappable widget. Cheers:
import 'package:flutter/material.dart';
class AppIconButton extends StatefulWidget {
const AppIconButton({
Key? key,
required this.onPressed,
required this.icon,
this.disableAfterClick = const Duration(milliseconds: 500),
}) : super(key: key);
final Function onPressed;
final Widget icon;
final Duration disableAfterClick;
#override
State<AppIconButton> createState() => _AppIconButtonState();
}
class _AppIconButtonState extends State<AppIconButton> {
bool _acceptsClicks = true;
#override
Widget build(BuildContext context) {
return IconButton(
onPressed: () {
if (_acceptsClicks) {
//if you want to disable the button
//use the variable with setState method
//but it's not my case
_acceptsClicks = false;
widget.onPressed();
Future.delayed(widget.disableAfterClick, () {
if (mounted) {
_acceptsClicks = true;
}
});
}
// else {
// debugPrint("Click ignored");
// }
},
icon: widget.icon,
);
}
}
Disabling multiple click events in a flutter with StatelessWidget.
Using as a shareable widget.
Simple example:
class SingleTapEvent extends StatelessWidget {
final Widget child;
final Function() onTap;
bool singleTap = false;
SingleTapEvent(
{Key? key, required this.child, required this.onTap, singleTap = false})
: super(key: key);
#override
Widget build(BuildContext context) {
return InkWell(
onTap: () {
if (!singleTap) {
Function.apply(onTap, []);
singleTap = true;
Future.delayed(const Duration(seconds: 3)).then((value) => singleTap = false);
}
},
child: child);
}
}
Usage:
SingleTapEvent(
onTap: () {
print("Clicked");
},
child: Text("Click me"),
);
This question is answered here How do I disable a Button in Flutter?
All you need to use statefulWidget and create a variable to hold your condition, And change it according to your event. Your button will be enable or disable according to your variable's value.
Suppose initial state of your variable, isDisable = false,that means - your button is enable by default. And after first clicking change the value of your state variable isDisable = true.
Instead of using RaisedButton directly, you can turn it into a StatefulWidget. Then use the ChangeNotifier to change it state from enable to disable and control button press function.It will also help you to reuse it in different places. Here is an example how can you do that
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();
}
class _MyHomePageState extends State<MyHomePage> {
final ValueNotifier<MyButtonState> _myButtonStateChangeNotifier =
ValueNotifier(MyButtonState.enable);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: MyButton(
buttonStateChangeNotifier: _myButtonStateChangeNotifier,
onPressed: _onButtonPressed,
text: "Click Me",
),
),
);
}
_onButtonPressed() {
print("Button Pressed");
_myButtonStateChangeNotifier.value = MyButtonState.disable;
}
}
enum MyButtonState {enable, disable}
class MyButton extends StatefulWidget {
final VoidCallback onPressed;
final String text;
final TextStyle textStyle;
final ValueNotifier<MyButtonState> buttonStateChangeNotifier;
MyButton({
#required this.onPressed,
this.text = "",
this.textStyle,
this.buttonStateChangeNotifier,
});
#override
_MyButtonState createState() => _MyButtonState();
}
class _MyButtonState extends State<MyButton> {
MyButtonState _myButtonState = MyButtonState.enable;
#override
void initState() {
super.initState();
if (widget.buttonStateChangeNotifier != null) {
widget.buttonStateChangeNotifier.addListener(_handleButtonStateChange);
_myButtonState = widget.buttonStateChangeNotifier.value;
}
}
#override
Widget build(BuildContext context) {
return RaisedButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(4)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(widget.text)
],
),
onPressed: _myButtonState == MyButtonState.enable
? _handleOnPress
: null,
);
}
_handleButtonStateChange() {
setState(() {
_myButtonState = widget.buttonStateChangeNotifier.value;
});
}
_handleOnPress() {
if (_myButtonState == MyButtonState.enable) {
widget.onPressed();
}
}
}
Thanks to #Mazin Ibrahim's suggestion above, setting a basic bool toggle flag works fine.
This implementation is based on handling the enable/disable logic at the callback level, independent of the widget layout details.
bool _isButtonEnabled = true;
MessageSql _messageSql = new MessageSql(); // DB helper class
final TextEditingController eCtrl = new TextEditingController();
_onSendMessage(String message) {
if (! _isButtonEnabled) {
return;
}
_isButtonEnabled = false;
_messageSql.insert(message).then((resultId) {
// only update all if save is successful
eCtrl.clear();
AppUi.dismissKeyboard();
_isButtonEnabled = true;
Future.delayed(const Duration(milliseconds: 400), () {
setState(() {});
})
.catchError((error, stackTrace) {
print("outer: $error");
});
}
Similar to what PreciseSpeech and Sharman implemented, I made a few changes and it works.
DateTime loginClickTime = DateTime.now();
#override
void initState() {
loginClickTime;
super.initState();
}
bool isRedundentClick(DateTime currentTime) {
if (loginClickTime == '') {
loginClickTime = currentTime;
print("first click");
return false;
}
print('diff is ${currentTime.difference(loginClickTime).inSeconds}');
if (currentTime.difference(loginClickTime).inSeconds < 10) {
//set this difference time in seconds
return true;
}
loginClickTime = currentTime;
return false;
}
Then in the OnPressed function
MaterialButton(
child:Text('Login'),
onPressed: (){
if(isRedundentClick(DateTime.now())){
print('hold on, processing');
return;
}
print('run process');
},
)

Flutter - Collapsing ExpansionTile after choosing an item

I'm trying to get ExpansionTile to collapse after I choose an item, but it does not close the list that was opened.
I tried to use the onExpansionChanged property but I did not succeed
How could you solve this problem?
Insert a gif demonstrating that ExpansionTile does not collapse after choosing an item, and below is also the code used.
import 'package:flutter/material.dart';
void main() {
runApp(new ExpansionTileSample());
}
class ExpansionTileSample extends StatefulWidget {
#override
ExpansionTileSampleState createState() => new ExpansionTileSampleState();
}
class ExpansionTileSampleState extends State<ExpansionTileSample> {
String foos = 'One';
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: const Text('ExpansionTile'),
),
body: new ExpansionTile(
title: new Text(this.foos),
backgroundColor: Theme.of(context).accentColor.withOpacity(0.025),
children: <Widget>[
new ListTile(
title: const Text('One'),
onTap: () {
setState(() {
this.foos = 'One';
});
},
),
new ListTile(
title: const Text('Two'),
onTap: () {
setState(() {
this.foos = 'Two';
});
},
),
new ListTile(
title: const Text('Three'),
onTap: () {
setState(() {
this.foos = 'Three';
});
},
),
]
),
),
);
}
}
Here is a workaround. Just add a global key (or a value key that changes after selecting an item) and it will force ExpansionTile to rebuild. The downside is losing animation for collapsing.
ExpansionTile(
key: GlobalKey(),
title: Text(title),
children: listTiles,
...
)
Here is a solution. We just add a expand, collapse and toggle functionality to ExpansionTile.
import 'package:flutter/material.dart';
import 'package:meta/meta.dart';
void main() {
runApp(new ExpansionTileSample());
}
class ExpansionTileSample extends StatefulWidget {
#override
ExpansionTileSampleState createState() => new ExpansionTileSampleState();
}
class ExpansionTileSampleState extends State<ExpansionTileSample> {
final GlobalKey<AppExpansionTileState> expansionTile = new GlobalKey();
String foos = 'One';
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: const Text('ExpansionTile'),
),
body: new AppExpansionTile(
key: expansionTile,
title: new Text(this.foos),
backgroundColor: Theme
.of(context)
.accentColor
.withOpacity(0.025),
children: <Widget>[
new ListTile(
title: const Text('One'),
onTap: () {
setState(() {
this.foos = 'One';
expansionTile.currentState.collapse();
});
},
),
new ListTile(
title: const Text('Two'),
onTap: () {
setState(() {
this.foos = 'Two';
expansionTile.currentState.collapse();
});
},
),
new ListTile(
title: const Text('Three'),
onTap: () {
setState(() {
this.foos = 'Three';
expansionTile.currentState.collapse();
});
},
),
]
),
),
);
}
}
// --- Copied and slightly modified version of the ExpansionTile.
const Duration _kExpand = const Duration(milliseconds: 200);
class AppExpansionTile extends StatefulWidget {
const AppExpansionTile({
Key key,
this.leading,
#required this.title,
this.backgroundColor,
this.onExpansionChanged,
this.children: const <Widget>[],
this.trailing,
this.initiallyExpanded: false,
})
: assert(initiallyExpanded != null),
super(key: key);
final Widget leading;
final Widget title;
final ValueChanged<bool> onExpansionChanged;
final List<Widget> children;
final Color backgroundColor;
final Widget trailing;
final bool initiallyExpanded;
#override
AppExpansionTileState createState() => new AppExpansionTileState();
}
class AppExpansionTileState extends State<AppExpansionTile> with SingleTickerProviderStateMixin {
AnimationController _controller;
CurvedAnimation _easeOutAnimation;
CurvedAnimation _easeInAnimation;
ColorTween _borderColor;
ColorTween _headerColor;
ColorTween _iconColor;
ColorTween _backgroundColor;
Animation<double> _iconTurns;
bool _isExpanded = false;
#override
void initState() {
super.initState();
_controller = new AnimationController(duration: _kExpand, vsync: this);
_easeOutAnimation = new CurvedAnimation(parent: _controller, curve: Curves.easeOut);
_easeInAnimation = new CurvedAnimation(parent: _controller, curve: Curves.easeIn);
_borderColor = new ColorTween();
_headerColor = new ColorTween();
_iconColor = new ColorTween();
_iconTurns = new Tween<double>(begin: 0.0, end: 0.5).animate(_easeInAnimation);
_backgroundColor = new ColorTween();
_isExpanded = PageStorage.of(context)?.readState(context) ?? widget.initiallyExpanded;
if (_isExpanded)
_controller.value = 1.0;
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
void expand() {
_setExpanded(true);
}
void collapse() {
_setExpanded(false);
}
void toggle() {
_setExpanded(!_isExpanded);
}
void _setExpanded(bool isExpanded) {
if (_isExpanded != isExpanded) {
setState(() {
_isExpanded = isExpanded;
if (_isExpanded)
_controller.forward();
else
_controller.reverse().then<void>((Null value) {
setState(() {
// Rebuild without widget.children.
});
});
PageStorage.of(context)?.writeState(context, _isExpanded);
});
if (widget.onExpansionChanged != null) {
widget.onExpansionChanged(_isExpanded);
}
}
}
Widget _buildChildren(BuildContext context, Widget child) {
final Color borderSideColor = _borderColor.evaluate(_easeOutAnimation) ?? Colors.transparent;
final Color titleColor = _headerColor.evaluate(_easeInAnimation);
return new Container(
decoration: new BoxDecoration(
color: _backgroundColor.evaluate(_easeOutAnimation) ?? Colors.transparent,
border: new Border(
top: new BorderSide(color: borderSideColor),
bottom: new BorderSide(color: borderSideColor),
)
),
child: new Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
IconTheme.merge(
data: new IconThemeData(color: _iconColor.evaluate(_easeInAnimation)),
child: new ListTile(
onTap: toggle,
leading: widget.leading,
title: new DefaultTextStyle(
style: Theme
.of(context)
.textTheme
.subhead
.copyWith(color: titleColor),
child: widget.title,
),
trailing: widget.trailing ?? new RotationTransition(
turns: _iconTurns,
child: const Icon(Icons.expand_more),
),
),
),
new ClipRect(
child: new Align(
heightFactor: _easeInAnimation.value,
child: child,
),
),
],
),
);
}
#override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
_borderColor.end = theme.dividerColor;
_headerColor
..begin = theme.textTheme.subhead.color
..end = theme.accentColor;
_iconColor
..begin = theme.unselectedWidgetColor
..end = theme.accentColor;
_backgroundColor.end = widget.backgroundColor;
final bool closed = !_isExpanded && _controller.isDismissed;
return new AnimatedBuilder(
animation: _controller.view,
builder: _buildChildren,
child: closed ? null : new Column(children: widget.children),
);
}
}
solution below would work, but it is quite hacky and might not be the best one:
import 'package:flutter/material.dart';
import 'dart:math';
void main() {
runApp(new ExpansionTileSample());
}
class ExpansionTileSample extends StatefulWidget {
#override
ExpansionTileSampleState createState() => new ExpansionTileSampleState();
}
class ExpansionTileSampleState extends State {
String foos = 'One';
int _key;
_collapse() {
int newKey;
do {
_key = new Random().nextInt(10000);
} while(newKey == _key);
}
#override
void initState() {
super.initState();
_collapse();
}
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: const Text('ExpansionTile'),
),
body: new ExpansionTile(
key: new Key(_key.toString()),
initiallyExpanded: false,
title: new Text(this.foos),
backgroundColor: Theme
.of(context)
.accentColor
.withOpacity(0.025),
children: [
new ListTile(
title: const Text('One'),
onTap: () {
setState(() {
this.foos = 'One';
_collapse();
});
},
),
new ListTile(
title: const Text('Two'),
onTap: () {
setState(() {
this.foos = 'Two';
_collapse();
});
},
),
new ListTile(
title: const Text('Three'),
onTap: () {
setState(() {
this.foos = 'Three';
_collapse();
});
},
),
]
),
),
);
}
}
I found that ExpansionTile has initiallyExpanded property, which is the only way to make it collapsed. As property works only initially you want to make ExpansionTile to be recreated everytime build is called. To force it you just assign different key everytime you build it. This might not be best solution performance wise, but ExpansionTile is quite simple, so this should not be a problem.
None of the provided solutions pleased me.
I ended up creating a custom ExpandableListTile. As you can see below, its code is very brief and easy to customize.
I also had to create two supporting classes (that only handle the required animations) to build my widget:
ExpandableSection: a widget that can be easily controlled by one parameter "expanded".
RotatableSection: a widget to rotate the "Expand More" icon based on one parameter.
The main class:
class ExpandableListTile extends StatelessWidget {
const ExpandableListTile({Key key, this.title, this.expanded, this.onExpandPressed, this.child}) : super(key: key);
final Widget title;
final bool expanded;
final Widget child;
final Function onExpandPressed;
#override
Widget build(BuildContext context) {
return Column(children: <Widget>[
ListTile(
title: title,
onTap: onExpandPressed,
trailing: IconButton(
onPressed: onExpandPressed,
// icon: Icon(Icons.expand_more),
icon: RotatableSection(
rotated: expanded,
child: SizedBox(height: 30, width: 30, child: Icon(Icons.expand_more),)
),
),
),
ExpandableSection(child: child, expand: expanded,)
]);
}
}
Usage (simplified):
//...
return ExpandableListTile(
onExpandPressed: (){ setState((){ _expandedItem = 0;}) },
title: Text('Item'),
expanded: _expandedItem==0,
child: Padding(
padding: const EdgeInsets.fromLTRB(8,0,0,0),
child: Container(
color: Color.fromRGBO(0, 0, 0, .2),
child: Column(children: <Widget>[
ListTile(title: Text('Item 1')),
ListTile(title: Text('Item 2')),
ListTile(title: Text('Item 3')),
ListTile(title: Text('Item 4'))
],),
),
),
),
//...
The ExpandableSection class:
class ExpandableSection extends StatefulWidget {
final Widget child;
final bool expand;
ExpandableSection({this.expand = false, this.child});
#override
_ExpandableSectionState createState() => _ExpandableSectionState();
}
class _ExpandableSectionState extends State<ExpandableSection> with SingleTickerProviderStateMixin {
AnimationController animationController;
Animation<double> sizeAnimation;
Animation<double> opacityAnimation;
#override
void initState() {
super.initState();
prepareAnimations();
_runExpandCheck();
}
///Setting up the animation
void prepareAnimations() {
animationController = AnimationController(vsync: this, duration: Duration(milliseconds: 300),);
sizeAnimation = CurvedAnimation(parent: animationController, curve: Curves.fastOutSlowIn,);
opacityAnimation = CurvedAnimation(parent: animationController, curve: Curves.slowMiddle,);
}
void _runExpandCheck() {
if(widget.expand) { animationController.forward(); }
else { animationController.reverse(); }
}
#override
void didUpdateWidget(ExpandableSection oldWidget) {
super.didUpdateWidget(oldWidget);
_runExpandCheck();
}
#override
void dispose() {
animationController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return FadeTransition(
opacity: opacityAnimation,
child: SizeTransition(
axisAlignment: 1.0,
sizeFactor: sizeAnimation,
child: widget.child
)
);
}
}
The RotatableSection class:
class RotatableSection extends StatefulWidget {
final Widget child;
final bool rotated;
final double initialSpin;
final double endingSpin;
RotatableSection({this.rotated = false, this.child, this.initialSpin=0, this.endingSpin=0.5});
#override
_RotatableSectionState createState() => _RotatableSectionState();
}
class _RotatableSectionState extends State<RotatableSection> with SingleTickerProviderStateMixin {
AnimationController animationController;
Animation<double> animation;
#override
void initState() {
super.initState();
prepareAnimations();
_runCheck();
}
final double _oneSpin = 6.283184;
///Setting up the animation
void prepareAnimations() {
animationController = AnimationController(vsync: this, duration: Duration(milliseconds: 300),
lowerBound: _oneSpin * widget.initialSpin, upperBound: _oneSpin * widget.endingSpin, );
animation = CurvedAnimation( parent: animationController, curve: Curves.linear, );
}
void _runCheck() {
if(widget.rotated) { animationController.forward(); }
else { animationController.reverse(); }
}
#override
void didUpdateWidget(RotatableSection oldWidget) {
super.didUpdateWidget(oldWidget);
_runCheck();
}
#override
void dispose() {
animationController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: animationController,
child: widget.child,
builder: (BuildContext context, Widget _widget) {
return new Transform.rotate(
angle: animationController.value,
child: _widget,
);
},
);
}
}
Use this package and follow my code. Hope this will help you :). Easy to use. https://pub.dev/packages/expansion_tile_card/example
final List<GlobalKey<ExpansionTileCardState>> cardKeyList = [];
... ListView.builder(
itemCount: 10,
itemBuilder: (BuildContext context, int index) {
cardKeyList.add(GlobalKey(debugLabel: "index :$index"));
return ExpansionTileCard(
title: Text('title'),
key: cardKeyList[index],
onExpansionChanged: (value) {
if (value) {
Future.delayed(const Duration(milliseconds: 500), () {
for (var i = 0; i < cardKeyList.length; i++) {
if (index != i) {
cardKeyList[i].currentState?.collapse();
}
}
});
}
},
);
}),
Use UniqueKey:
ExpansionTile(
key: UniqueKey(),
// Other properties
)
I've made a TreeView widget.
It uses ExpansionTile to simulate the hierarchy.
Each ExpansionTile could host a collection of ExpansionTile which can host ...etc.
Everything worked fine until I wanted to add 2 features : expand all / collapse all.
What helped me to overcame this problem is the GlobalKey.
My TreeView widget, is hosted in a page and is used with a global key.
I expose a VoidCallback. The implementation sets a new key in the setState method.
// TreeView host page
GlobalKey<TreeViewState> _key = GlobalKey();
void redrawWidgetCallback() {
setState(() {
// Triggers a rebuild of the whole TreeView.
_key = GlobalKey();
});
}
[...]
// In the Scaffold body :
TreeView(
key: _key,
treeViewItems: widget.treeViewItems,
redrawWidgetCallback: redrawWidgetCallback,
)
Then in my collapse/expand method in the widget, at the end, I call widget.redrawWidgetCallback.
No need to deal with a key for each level of the treeView : the root element widget is enough.
It may have perf issues / not the right way to go. But since my TreeView won't be used with more than 50 nodes, it's ok for me until I found a better solution which doesn't involve to create an ExpandableTile because I believe this behavior will be available oneday on the ExpansionTile itself.
PS : notice that this workaround doesn't run the expand animation.
Create a clone from ExpansionTile class and replace build method code by the following:
#override
Widget build(BuildContext context) {
final bool closed = !_isExpanded && _controller.isDismissed;
return AnimatedBuilder(
animation: _controller.view,
builder: _buildChildren,
child: closed ? null : GestureDetector(
child: Column(children: widget.children),
onTap: _handleTap,
),
);
}
and then ExpansionTile will collapse after click on each item.
Note:
if one of children has onTap call back, this solution doesn't work.
in this case you must provide onChildTap handler to pass index of tapped child in use case.(contact me for complete code)
I have modified the custom code, And its works fine for me.
Here is the solution.
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
const Duration _kExpand = Duration(milliseconds: 200);
/// A single-line [ListTile] with a trailing button that expands or collapses
/// the tile to reveal or hide the [children].
///
/// This widget is typically used with [ListView] to create an
/// "expand / collapse" list entry. When used with scrolling widgets like
/// [ListView], a unique [PageStorageKey] must be specified to enable the
/// [AppExpansionTile] to save and restore its expanded state when it is scrolled
/// in and out of view.
///
/// This class overrides the [ListTileTheme.iconColor] and [ListTileTheme.textColor]
/// theme properties for its [ListTile]. These colors animate between values when
/// the tile is expanded and collapsed: between [iconColor], [collapsedIconColor] and
/// between [textColor] and [collapsedTextColor].
///
/// See also:
///
/// * [ListTile], useful for creating expansion tile [children] when the
/// expansion tile represents a sublist.
/// * The "Expand and collapse" section of
/// <https://material.io/components/lists#types>
class AppExpansionTile extends StatefulWidget {
/// Creates a single-line [ListTile] with a trailing button that expands or collapses
/// the tile to reveal or hide the [children]. The [initiallyExpanded] property must
/// be non-null.
const AppExpansionTile({
GlobalKey<AppExpansionTileState>? key,
this.leading,
required this.title,
this.subtitle,
this.onExpansionChanged,
this.children = const <Widget>[],
this.trailing,
this.initiallyExpanded = false,
this.maintainState = false,
this.tilePadding,
this.expandedCrossAxisAlignment,
this.expandedAlignment,
this.childrenPadding,
this.backgroundColor,
this.collapsedBackgroundColor,
this.textColor,
this.collapsedTextColor,
this.iconColor,
this.collapsedIconColor,
}) : assert(initiallyExpanded != null),
assert(maintainState != null),
assert(
expandedCrossAxisAlignment != CrossAxisAlignment.baseline,
'CrossAxisAlignment.baseline is not supported since the expanded children '
'are aligned in a column, not a row. Try to use another constant.',
),
super(key: key);
/// A widget to display before the title.
///
/// Typically a [CircleAvatar] widget.
final Widget? leading;
/// The primary content of the list item.
///
/// Typically a [Text] widget.
final Widget title;
/// Additional content displayed below the title.
///
/// Typically a [Text] widget.
final Widget? subtitle;
/// Called when the tile expands or collapses.
///
/// When the tile starts expanding, this function is called with the value
/// true. When the tile starts collapsing, this function is called with
/// the value false.
final ValueChanged<bool>? onExpansionChanged;
/// The widgets that are displayed when the tile expands.
///
/// Typically [ListTile] widgets.
final List<Widget> children;
/// The color to display behind the sublist when expanded.
final Color? backgroundColor;
/// When not null, defines the background color of tile when the sublist is collapsed.
final Color? collapsedBackgroundColor;
/// A widget to display instead of a rotating arrow icon.
final Widget? trailing;
/// Specifies if the list tile is initially expanded (true) or collapsed (false, the default).
final bool initiallyExpanded;
/// Specifies whether the state of the children is maintained when the tile expands and collapses.
///
/// When true, the children are kept in the tree while the tile is collapsed.
/// When false (default), the children are removed from the tree when the tile is
/// collapsed and recreated upon expansion.
final bool maintainState;
/// Specifies padding for the [ListTile].
///
/// Analogous to [ListTile.contentPadding], this property defines the insets for
/// the [leading], [title], [subtitle] and [trailing] widgets. It does not inset
/// the expanded [children] widgets.
///
/// When the value is null, the tile's padding is `EdgeInsets.symmetric(horizontal: 16.0)`.
final EdgeInsetsGeometry? tilePadding;
/// Specifies the alignment of [children], which are arranged in a column when
/// the tile is expanded.
///
/// The internals of the expanded tile make use of a [Column] widget for
/// [children], and [Align] widget to align the column. The `expandedAlignment`
/// parameter is passed directly into the [Align].
///
/// Modifying this property controls the alignment of the column within the
/// expanded tile, not the alignment of [children] widgets within the column.
/// To align each child within [children], see [expandedCrossAxisAlignment].
///
/// The width of the column is the width of the widest child widget in [children].
///
/// When the value is null, the value of `expandedAlignment` is [Alignment.center].
final Alignment? expandedAlignment;
/// Specifies the alignment of each child within [children] when the tile is expanded.
///
/// The internals of the expanded tile make use of a [Column] widget for
/// [children], and the `crossAxisAlignment` parameter is passed directly into the [Column].
///
/// Modifying this property controls the cross axis alignment of each child
/// within its [Column]. Note that the width of the [Column] that houses
/// [children] will be the same as the widest child widget in [children]. It is
/// not necessarily the width of [Column] is equal to the width of expanded tile.
///
/// To align the [Column] along the expanded tile, use the [expandedAlignment] property
/// instead.
///
/// When the value is null, the value of `expandedCrossAxisAlignment` is [CrossAxisAlignment.center].
final CrossAxisAlignment? expandedCrossAxisAlignment;
/// Specifies padding for [children].
///
/// When the value is null, the value of `childrenPadding` is [EdgeInsets.zero].
final EdgeInsetsGeometry? childrenPadding;
/// The icon color of tile's [trailing] expansion icon when the
/// sublist is expanded.
///
/// Used to override to the [ListTileTheme.iconColor].
final Color? iconColor;
/// The icon color of tile's [trailing] expansion icon when the
/// sublist is collapsed.
///
/// Used to override to the [ListTileTheme.iconColor].
final Color? collapsedIconColor;
/// The color of the tile's titles when the sublist is expanded.
///
/// Used to override to the [ListTileTheme.textColor].
final Color? textColor;
/// The color of the tile's titles when the sublist is collapsed.
///
/// Used to override to the [ListTileTheme.textColor].
final Color? collapsedTextColor;
#override
AppExpansionTileState createState() => AppExpansionTileState();
}
class AppExpansionTileState extends State<AppExpansionTile>
with SingleTickerProviderStateMixin {
static final Animatable<double> _easeOutTween =
CurveTween(curve: Curves.easeOut);
static final Animatable<double> _easeInTween =
CurveTween(curve: Curves.easeIn);
static final Animatable<double> _halfTween =
Tween<double>(begin: 0.0, end: 0.5);
final ColorTween _borderColorTween = ColorTween();
final ColorTween _headerColorTween = ColorTween();
final ColorTween _iconColorTween = ColorTween();
final ColorTween _backgroundColorTween = ColorTween();
late AnimationController _controller;
late Animation<double> _iconTurns;
late Animation<double> _heightFactor;
late Animation<Color?> _borderColor;
late Animation<Color?> _headerColor;
late Animation<Color?> _iconColor;
late Animation<Color?> _backgroundColor;
bool _isExpanded = false;
#override
void initState() {
super.initState();
_controller = AnimationController(duration: _kExpand, vsync: this);
_heightFactor = _controller.drive(_easeInTween);
_iconTurns = _controller.drive(_halfTween.chain(_easeInTween));
_borderColor = _controller.drive(_borderColorTween.chain(_easeOutTween));
_headerColor = _controller.drive(_headerColorTween.chain(_easeInTween));
_iconColor = _controller.drive(_iconColorTween.chain(_easeInTween));
_backgroundColor =
_controller.drive(_backgroundColorTween.chain(_easeOutTween));
_isExpanded = PageStorage.of(context)?.readState(context) as bool? ??
widget.initiallyExpanded;
if (_isExpanded) _controller.value = 1.0;
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
void expand() {
_isExpanded = true;
handleTap();
}
void collapse() {
_isExpanded = false;
handleTap();
}
#override
void didUpdateWidget(covariant AppExpansionTile oldWidget) {
if (widget.initiallyExpanded) {
expand();
} else {
collapse();
}
super.didUpdateWidget(oldWidget);
}
void handleTap() {
setState(() {
if (_isExpanded) {
_controller.forward();
} else {
_controller.reverse().then<void>((void value) {
if (!mounted) return;
setState(() {
// Rebuild without widget.children.
});
});
}
PageStorage.of(context)?.writeState(context, _isExpanded);
});
// if (widget.onExpansionChanged != null)
// widget.onExpansionChanged!(_isExpanded);
}
Widget _buildChildren(BuildContext context, Widget? child) {
final Color borderSideColor = _borderColor.value ?? Colors.transparent;
return Container(
decoration: BoxDecoration(
color: _backgroundColor.value ?? Colors.transparent,
border: Border(
top: BorderSide(color: borderSideColor),
bottom: BorderSide(color: borderSideColor),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ListTileTheme.merge(
iconColor: _iconColor.value,
textColor: _headerColor.value,
child: ListTile(
onTap: () {
if (widget.onExpansionChanged != null) {
widget.onExpansionChanged!(_isExpanded);
}
},
contentPadding: widget.tilePadding,
leading: widget.leading,
title: widget.title,
subtitle: widget.subtitle,
trailing: widget.trailing ??
RotationTransition(
turns: _iconTurns,
child: const Icon(Icons.expand_more),
),
),
),
ClipRect(
child: Align(
alignment: widget.expandedAlignment ?? Alignment.center,
heightFactor: _heightFactor.value,
child: child,
),
),
],
),
);
}
#override
void didChangeDependencies() {
final ThemeData theme = Theme.of(context);
final ColorScheme colorScheme = theme.colorScheme;
_borderColorTween.end = theme.dividerColor;
_headerColorTween
..begin = widget.collapsedTextColor ?? theme.textTheme.subtitle1!.color
..end = widget.textColor ?? colorScheme.secondary;
_iconColorTween
..begin = widget.collapsedIconColor ?? theme.unselectedWidgetColor
..end = widget.iconColor ?? colorScheme.secondary;
_backgroundColorTween
..begin = widget.collapsedBackgroundColor
..end = widget.backgroundColor;
super.didChangeDependencies();
}
#override
Widget build(BuildContext context) {
final bool closed = !_isExpanded && _controller.isDismissed;
final bool shouldRemoveChildren = closed && !widget.maintainState;
final Widget result = Offstage(
child: TickerMode(
child: Padding(
padding: widget.childrenPadding ?? EdgeInsets.zero,
child: Column(
crossAxisAlignment:
widget.expandedCrossAxisAlignment ?? CrossAxisAlignment.center,
children: widget.children,
),
),
enabled: !closed,
),
offstage: closed,
);
return AnimatedBuilder(
animation: _controller.view,
builder: _buildChildren,
child: shouldRemoveChildren ? null : result,
);
}
}
Usage
late int _tileIndex=-1;
return AppExpansionTile(
title: Text(
'Tile $index',
tilePadding: const EdgeInsets.symmetric(horizontal: 24),
initiallyExpanded: _tileIndex == index,
onExpansionChanged: (s) {
if (_tileIndex == index) {
_tileIndex = -1;
setState(() {});
} else {
setState(() {
_tileIndex = index!;
});
}
},
);
I think it is impossible with expansion tile but, there's a package named accordion and has much more comfortabilities.
Link:https://pub.dev/packages/accordion
For List of items using #simon solution
List<GlobalKey<AppExpansionTileState> > expansionTile;
instantiate your expansionTile
expansionTile=List<GlobalKey<AppExpansionTileState>>.generate(listItems.length, (index) => GlobalKey());
and use like so inside a ListView.builder()
key: expansionTile[index],
onExpansionChanged: (value) {
if (value) {
for (var tileKey in expansionTile) {
if (tileKey.currentState !=
expansionTile[index]
.currentState) {
tileKey.currentState.collapse();
} else {
tileKey.currentState.expand();
}
}
}
},

flutter: CircleAvatar with fallback text

I'm learning Flutter and would like to make a Widget just like the built-in CircleAvatar. However, I would like the behaviour to be
specify both an Image (NetworkImage) and initials (ie, BB)
while the image isn't loaded, show the initials
if the image does load, show the image and remove the initials
The following code sort of works, but when used in the Chat demo it falls apart as multiple MyAvatars are added.
Breakpointing on initState shows that it is always called with the first message text that is entered - not what I expected.
It also flickers as images "reload". It appears that the widgets are being reused in a way I don't understand.
class MyAvatar extends StatefulWidget {
NetworkImage image;
MyAvatar({this.text}) {
debugPrint("MyAvatar " + this.text);
if (text.contains('fun')) {
this.image = new NetworkImage("https://cdn3.iconfinder.com/data/icons/minicons-for-web-sites/24/minicons2-14-512.png");
}
}
final String text;
#override
MyAvatarState createState() {
return new MyAvatarState();
}
}
class MyAvatarState extends State<MyAvatar> {
bool showImage = false;
#override
initState() {
super.initState();
if (widget.image != null) {
var completer = widget.image.load(widget.image);
completer.addListener((info, sync) {
setState(() {
showImage = true;
});
});
}
}
#override
Widget build(BuildContext context) {
return !showImage ? new CircleAvatar(radius: 40.0, child: new Text(widget.text[0]))
: new CircleAvatar(radius: 40.0, backgroundImage: widget.image);
}
}
I'm still having trouble - full code
import 'package:flutter/material.dart';
// Modify the ChatScreen class definition to extend StatefulWidget.
class ChatScreen extends StatefulWidget { //modified
ChatScreen() {
debugPrint("ChatScreen - called on hot reload");
}
#override //new
State createState() {
debugPrint("NOT on hot reload");
return new ChatScreenState();
} //new
}
// Add the ChatScreenState class definition in main.dart.
class ChatScreenState extends State<ChatScreen> {
final List<ChatMessage> _messages = <ChatMessage>[];
final TextEditingController _textController = new TextEditingController(); //new
ChatScreenState() {
debugPrint("ChatScreenState - not called on hot reload");
}
#override //new
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(title: new Text("Friendlychat")),
body: new Column( //modified
children: <Widget>[ //new
new Flexible( //new
child: new ListView.builder( //new
padding: new EdgeInsets.all(8.0), //new
reverse: true, //new
itemBuilder: (_, int index) => _messages[index], //new
itemCount: _messages.length, //new
) //new
), //new
new Divider(height: 1.0), //new
new Container( //new
decoration: new BoxDecoration(
color: Theme.of(context).cardColor), //new
child: _buildTextComposer(), //modified
), //new
] //new
), //new
);
}
Widget _buildTextComposer() {
return new IconTheme(
data: new IconThemeData(color: Theme
.of(context)
.accentColor),
child:
new Container(
margin: const EdgeInsets.symmetric(horizontal: 8.0),
child: new Row(
children: <Widget>[
new Container( //new
margin: new EdgeInsets.symmetric(horizontal: 4.0), //new
child: new IconButton( //new
icon: new Icon(Icons.send),
onPressed: () =>
_handleSubmitted(_textController.text)), //new
),
new Flexible(
child: new TextField(
controller: _textController,
onSubmitted: _handleSubmitted,
decoration: new InputDecoration.collapsed(
hintText: "Send a message"),
)
),
])
)
);
}
void _handleSubmitted(String text) {
_textController.clear();
ChatMessage message = new ChatMessage(text: text);
setState(() {
_messages.insert(0, message);
});
}
}
const String _name = "Hardcoded Name";
class ChatMessage extends StatelessWidget {
ChatMessage({this.text, this.image, this.useImage});
final String text;
final NetworkImage image;
final Map useImage;
#override
Widget build(BuildContext context) {
var use = true; //useImage != null && useImage['use'];
var image = new NetworkImage("https://cdn3.iconfinder.com/data/icons/minicons-for-web-sites/24/minicons2-14-512.png");
if (text.contains('bad')) {
image = new NetworkImage("https://cdn3.iconfinder.com/data/icons/minicons-for-web-sites/24/minicons2-14-512.pngz");
}
return new Container(
margin: const EdgeInsets.symmetric(vertical: 10.0),
child: new Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
new Container(
margin: const EdgeInsets.only(right: 16.0),
child : new CustomCircleAvatar(initials: text[0], myImage: image)
),
new Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
new Text(_name, style: Theme.of(context).textTheme.subhead),
new Container(
margin: const EdgeInsets.only(top: 5.0),
child: new Text(text),
),
],
),
],
),
);
}
}
class CustomCircleAvatar extends StatefulWidget {
NetworkImage myImage;
String initials;
CustomCircleAvatar({this.myImage, this.initials}) {
debugPrint(initials);
}
#override
_CustomCircleAvatarState createState() => new _CustomCircleAvatarState();
}
class _CustomCircleAvatarState extends State<CustomCircleAvatar>{
bool _checkLoading = true;
#override
void initState() {
if (widget.myImage != null) {
widget.myImage.resolve(new ImageConfiguration()).addListener((image, sync) {
if (mounted && image != null) {
setState(() {
_checkLoading = false;
});
}
});
}
}
#override
Widget build(BuildContext context) {
return _checkLoading == true ? new CircleAvatar(child: new Text(widget.initials))
: new CircleAvatar(backgroundImage: widget.myImage);
}
}
Enter 'fun' as a message, then 'bad' as the second -
image
The idea is that depending on what you enter, different images might load (or not). In the 'failed to load' case, the initials should remain.
You can achieve this functionality by adding a listener to ImageStream that you can obtain from ImageConfiguration,
Here, I am feeding the same data to my ListView you can of course customize this yourself by adding a List of images and initials as a field in any class and use ListView.builder instead to be able to loop on them by index.
class CustomCircleAvatar extends StatefulWidget {
NetworkImage myImage;
String initials;
CustomCircleAvatar({this.myImage, this.initials});
#override
_CustomCircleAvatarState createState() => new _CustomCircleAvatarState();
}
class _CustomCircleAvatarState extends State<CustomCircleAvatar>{
bool _checkLoading = true;
#override
void initState() {
widget.myImage.resolve(new ImageConfiguration()).addListener((_, __) {
if (mounted) {
setState(() {
_checkLoading = false;
});
}
});
}
#override
Widget build(BuildContext context) {
return _checkLoading == true ? new CircleAvatar(
child: new Text(widget.initials)) : new CircleAvatar(
backgroundImage: widget.myImage,);
}
}
Now you can use it like this:
void main() {
runApp(new MaterialApp (home: new MyApp()));
}
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => new _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(title: new Text("Custom Circle Avatar"),),
body: new ListView(children: new List.generate(20, (int index) {
return new Container(
height: 100.0,
width: 100.0,
child: new CustomCircleAvatar(myImage: new NetworkImage(
"https://www.doginni.cz/front_path/images/dog_circle.png"),
initials: "Dog",
),
);
}),),
);
}
}
This works really well and easy. Use the CachetNetworkImage and build the appropriate CircleAvatar.
return CachedNetworkImage(
httpHeaders: headers,
imageUrl: general.HOST + 'api/media/v2/' + id,
imageBuilder: (context, imageProvider) => new CircleAvatar(
radius: radius,
backgroundImage: imageProvider,
backgroundColor: backgroundColor),
errorWidget: (context, url, error) => CircleAvatar(
backgroundColor: backgroundColor,
radius: radius,
child: new Text(initials, style: textStyle,)),
);
The answer from #aziza was really the only one I could find on the topic for a while and it took me a while to read it and understand. I tried implementing it and there were some issues though I did get it to work eventually. I think I have a more readable (for me at least!)/up to date answer that might help someone stumbling upon this question:
class FallBackAvatar extends StatefulWidget {
final AssetImage image;
final String initials;
final TextStyle textStyle;
final Color circleBackground;
FallBackAvatar({#required this.image, #required this.initials, #required this.circleBackground, #required this.textStyle});
#override
_FallBackAvatarState createState() => _FallBackAvatarState();
}
class _FallBackAvatarState extends State<FallBackAvatar> {
bool _checkLoading = true;
#override
initState() {
super.initState();
// Add listeners to this class
ImageStreamListener listener = ImageStreamListener(_setImage, onError: _setError);
widget.image.resolve(ImageConfiguration()).addListener(listener);
}
void _setImage(ImageInfo image, bool sync) {
setState(() => _checkLoading = false);
//DO NOT DISPOSE IF IT WILL REBUILD (e.g. Sliver/Builder ListView)
dispose();
}
void _setError(dynamic dyn, StackTrace st) {
setState(() => _checkLoading = true);
dispose();
}
#override
Widget build(BuildContext context) {
return _checkLoading == true ? new CircleAvatar(
backgroundColor: widget.circleBackground,
child: new Text(widget.initials, style: widget.textStyle)) : new CircleAvatar(
backgroundImage: widget.image,
backgroundColor: widget.circleBackground,);
}
}
A couple of points, I'm manually disposing because I know after this there should be no more rebuilds (did you get the image? good! no more rebuilds unless you are part of a sliver or something OR did the image fail to load? well that's it then - no more rebuilds). This also handles the error case where the AssetImage (in my case, its Asset image but you could use any kind of image provider) is not there for whatever reason.
Second edit, because I have personal problems best left out of this answer. So I noticed that there was a slight delay in loading the profile images (like a second). But then the images came flooding in. Didn't like that transition so here is one with an AnimatedSwitcher:
class FallBackAvatar extends StatefulWidget {
final AssetImage image;
final String initials;
final TextStyle textStyle;
final Color circleBackground;
final double radius;
final int msAnimationDuration;
FallBackAvatar({#required this.image, #required this.initials, #required this.circleBackground, #required this.textStyle, #required this.radius, this.msAnimationDuration});
#override
_FallBackAvatarState createState() => _FallBackAvatarState();
}
class _FallBackAvatarState extends State<FallBackAvatar> {
bool _imgSuccess = false;
#override
initState() {
super.initState();
// Add listeners to this class
ImageStreamListener listener = ImageStreamListener(_setImage, onError: _setError);
widget.image.resolve(ImageConfiguration()).addListener(listener);
}
void _setImage(ImageInfo image, bool sync) {
setState(() => _imgSuccess = true);
}
void _setError(dynamic dyn, StackTrace st) {
setState(() => _imgSuccess = false);
dispose();
}
Widget _fallBackAvatar() {
return Container(
height: widget.radius*2,
width: widget.radius*2,
decoration: BoxDecoration(
color: widget.circleBackground,
borderRadius: BorderRadius.all(Radius.circular(widget.radius))
),
child: Center(child: Text(widget.initials, style: widget.textStyle))
);
}
Widget _avatarImage() {
return CircleAvatar(
backgroundImage: widget.image,
backgroundColor: widget.circleBackground
);
}
#override
Widget build(BuildContext context) {
return AnimatedSwitcher(
duration: Duration(milliseconds: widget.msAnimationDuration ?? 500),
child: _imgSuccess ? _avatarImage() : _fallBackAvatar(),
);
}
}
Actually the code can be even simpler:
if you want to put a text when the image is unavailable you should simply use foregroundImage instead of backgroundImage.
The text will displayed by default, when the image is loaded it will cover the text without having to deal with image loading status etc.
If you need to know if the image had an error you can intercept it with onForegroundImageError.
Example function:
Widget CircleAvatarTest(
{String? imageUrl,
String? text,
double radius = 35,
Color? backgroundColor}) {
return CircleAvatar(
radius: radius,
child: (text != null)
? Center(
child: Text(text,
style: TextStyle(
color: Colors.white,
fontSize: radius * 2 / text.length - 10,
)),
)
: null,
foregroundImage: imageUrl == null ? null : NetworkImage(imageUrl),
backgroundColor: backgroundColor,
//onForegroundImageError: (e,trace){/*....*/},
);
}
Here is the sample with stacked architecture where fallback is person icon.
ViewBuilder and ViewModel are just extended widgets from stacked architecture alternatives. #swidget is functional widget. You can achieve the same functionality via StatefulWidget.
#swidget
Widget avatarView({String userId, double radius = 24}) =>
ViewBuilder<AvatarViewModel>(
viewModelBuilder: () => AvatarViewModel(),
builder: (model) => CircleAvatar(
radius: radius,
backgroundColor: CColors.blackThird,
backgroundImage: NetworkImage(
Config.photoUrl + userId ?? userService.id,
),
child: model.isFailed ? Icon(EvaIcons.person, size: radius) : null,
onBackgroundImageError: (e, _) => model.isFailed = e != null,
),
);
class AvatarViewModel extends ViewModel {
bool _isFailed = false;
bool get isFailed => _isFailed;
set isFailed(bool isFailed) {
_isFailed = isFailed;
notifyListeners();
}
}

How do I create an animated number counter?

I'd like to create a number counter that animates from a starting value to an end value. I've looked into using a Timer but can't seem to animate/update state properly. Including the decimal value would be great, but a simple integer animation is fine.
Number counter that needs to animate
double _mileCounter = 643.6;
_animateMileCounter() {
Duration duration = new Duration(milliseconds: 300);
return new Timer(duration, _updateMileCounter);
}
_updateMileCounter() {
setState(() {
_mileCounter += 1;
});
}
How would I increment the counter X number of times (with animation)? Similar to how a car's odometer increments.
For anyone still looking, you can use ImplicitlyAnimatedWidget.
Here is an example of an int counter. Works analogously for doubles.
class AnimatedCount extends ImplicitlyAnimatedWidget {
final int count;
AnimatedCount({
Key key,
#required this.count,
#required Duration duration,
Curve curve = Curves.linear
}) : super(duration: duration, curve: curve, key: key);
#override
ImplicitlyAnimatedWidgetState<ImplicitlyAnimatedWidget> createState() => _AnimatedCountState();
}
class _AnimatedCountState extends AnimatedWidgetBaseState<AnimatedCount> {
IntTween _count;
#override
Widget build(BuildContext context) {
return new Text(_count.evaluate(animation).toString());
}
#override
void forEachTween(TweenVisitor visitor) {
_count = visitor(_count, widget.count, (dynamic value) => new IntTween(begin: value));
}
}
Just rebuild the widget with a new value and it automatically animates there.
You should use an AnimationController with an AnimatedBuilder to rebuild your text when the controller changes. Here's an example that increments the miles when the floating action button is pressed (double.toStringAsFixed to get the decimal to show), with a curve on the animation speed:
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(primarySwatch: Colors.purple),
home: new MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key}) : super(key: key);
#override
createState() => new MyHomePageState();
}
class MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
AnimationController _controller;
Animation<double> _animation;
double _miles = 0.0;
#override initState() {
_controller = new AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1500),
);
_animation = _controller;
super.initState();
}
#override
Widget build(BuildContext context) {
TextTheme textTheme = Theme.of(context).textTheme;
return new Scaffold(
body: new Material(
color: const Color.fromRGBO(246, 251, 8, 1.0),
child: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
new AnimatedBuilder(
animation: _animation,
builder: (BuildContext context, Widget child) {
return new Text(
_animation.value.toStringAsFixed(1),
style: textTheme.display4.copyWith(fontStyle: FontStyle.italic),
);
},
),
new Text(
"MILES",
style: textTheme.display1.copyWith(fontStyle: FontStyle.italic),
)
],
),
),
),
floatingActionButton: new FloatingActionButton(
child: new Icon(Icons.directions_run),
onPressed: () {
Random rng = new Random();
setState(() {
_miles += rng.nextInt(20) + 0.3;
_animation = new Tween<double>(
begin: _animation.value,
end: _miles,
).animate(new CurvedAnimation(
curve: Curves.fastOutSlowIn,
parent: _controller,
));
});
_controller.forward(from: 0.0);
}
),
);
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
}
You can use Countup package.
Countup(
begin: 0,
end: 7500,
duration: Duration(seconds: 3),
separator: ',',
style: TextStyle(
fontSize: 36,
),
)
https://pub.dev/packages/countup
You can simply use this plugin countup: ^0.1.3
import 'package:countup/countup.dart';
Countup(
begin: 100,
end: 8000,
duration: Duration(seconds: 3),
separator: ',',
style: TextStyle(
fontSize: 36,
fontweight : Fontweight.bold,
),
),

Resources