Shake/jitter animation for TabBar Flutter - flutter

Trying to get desired output of the Animation above. Tried with AnimatedBuilder along with Transform of Matrix of z-axis for this animation. But failed to do so. Here's my code. I couldn't get the angle, shrinking or the shaking part right.
MyHomePage
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
late TabController _tabController;
late void Function() _memberCaller;
late void Function() _voucherCaller;
late void Function() _rewardCaller;
late void Function() _drinksCaller;
double degree = 0;
#override
void initState() {
super.initState();
_tabController = TabController(length: 4, vsync: this);
}
#override
void dispose() {
_tabController.dispose();
super.dispose();
}
void _callMethoCaller(int index) {
switch (index) {
case 0:
_memberCaller.call();
break;
case 1:
_voucherCaller.call();
break;
case 2:
_rewardCaller.call();
break;
case 3:
_drinksCaller.call();
break;
}
}
#override
Widget build(BuildContext context) {
List<Widget> _tabs(int index) => [
AnimationTab(
iconName: Icons.card_giftcard,
label: 'Membership',
functionCaller: (void Function() method) {
_memberCaller = method;
}),
AnimationTab(
iconName: Icons.confirmation_num_outlined,
label: 'Voucher',
functionCaller: (void Function() method) {
_voucherCaller = method;
}),
AnimationTab(
iconName: Icons.emoji_events_outlined,
label: 'Rewards',
functionCaller: (void Function() method) {
_rewardCaller = method;
}),
AnimationTab(
iconName: Icons.wine_bar_outlined,
label: 'Drinks',
functionCaller: (void Function() method) {
_drinksCaller = method;
}),
];
return Scaffold(
appBar: AppBar(
centerTitle: true,
bottom: PreferredSize(
child: Container(
child: TabBar(
onTap: _callMethoCaller,
controller: _tabController,
labelPadding: EdgeInsets.only(top: 5.0, bottom: 2.0),
indicatorColor: Colors.black,
tabs: List.generate(4, (index) => _tabs(index)[index]),
),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: const [
BoxShadow(
color: Colors.white,
spreadRadius: 5.0,
offset: Offset(0, 3))
],
),
),
preferredSize: Size.fromHeight(30),
),
),
body: TabBarView(
controller: _tabController,
children: const [
Center(child: Text('1')),
Center(child: Text('2')),
Center(child: Text('3')),
Center(child: Text('4')),
],
),
);
}
}
AnimationTab
class AnimationTab extends StatefulWidget {
final String label;
final IconData iconName;
final FunctionCaller functionCaller;
const AnimationTab({
Key? key,
required this.iconName,
required this.label,
required this.functionCaller,
}) : super(key: key);
#override
_AnimationTabState createState() => _AnimationTabState();
}
class _AnimationTabState extends State<AnimationTab>
with TickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _animation;
#override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: Duration(milliseconds: 500),
);
final _curvedAnimation = CurvedAnimation(
parent: _animationController,
curve: Curves.bounceIn,
reverseCurve: Curves.bounceOut);
_animation = TweenSequence<double>([
TweenSequenceItem<double>(tween: Tween(begin: 0, end: 12.5), weight: 1),
TweenSequenceItem<double>(tween: Tween(begin: 12.5, end: 0), weight: 1),
TweenSequenceItem<double>(tween: Tween(begin: 0, end: -12.5), weight: 1),
]).animate(_curvedAnimation)
..addListener(() {
setState(() {});
})
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
_animationController.reverse();
}
});
}
void _animationExecution() {
_animationController.forward();
}
#override
Widget build(BuildContext context) {
const _tabTextStyle = TextStyle(
fontWeight: FontWeight.w300, fontSize: 12, color: Colors.black);
widget.functionCaller.call(_animationExecution);
return AnimatedBuilder(
animation: _animationController,
builder: (ctx, _) {
return Transform(
transform: Matrix4.rotationZ(math.pi * _animation.value / 180),
alignment: Alignment.center,
child: Tab(
icon: Icon(widget.iconName, color: Colors.black),
child: Text(widget.label, style: _tabTextStyle),
),
);
},
);
}
}

you dont need those TweenSequences, "status listeners" etc, also instead of Matrix4.rotationZ use ordinary Transform.rotate, check this:
class TabTest extends StatefulWidget {
#override
_TabTestState createState() => _TabTestState();
}
class _TabTestState extends State<TabTest> with TickerProviderStateMixin {
late TabController tabController;
late List<AnimationController> animationControllers;
#override
void initState() {
super.initState();
tabController = TabController(length: 4, vsync: this)
..addListener(_listener);
animationControllers = List.generate(4, (i) => AnimationController(
vsync: this,
duration: Duration(milliseconds: 750),
reverseDuration: Duration(milliseconds: 350),
));
}
#override
Widget build(BuildContext context) {
// timeDilation = 5;
return Scaffold(
appBar: AppBar(
bottom: TabBar(
tabs: List.generate(4, (i) => AnimatedBuilder(
animation: animationControllers[i],
builder: (context, child) {
final child = Tab(
icon: Icon(Icons.cast),
child: Text('tab $i'),
);
final value = animationControllers[i].value;
if (animationControllers[i].status == AnimationStatus.forward) {
final angle = sin(4 * pi * value) * pi * 0.3;
return Transform.rotate(angle: angle, child: child);
} else {
final dy = sin(2 * pi * value) * 0.2;
return FractionalTranslation(translation: Offset(0, dy), child: child);
}
},
),
),
controller: tabController,
),
),
body: TabBarView(
children: List.generate(4, (i) =>
FittedBox(
child: Text('tab $i'),
),
),
controller: tabController,
),
);
}
void _listener() {
if (tabController.indexIsChanging) {
animationControllers[tabController.previousIndex].reverse();
} else {
animationControllers[tabController.index].forward();
}
}
#override
void dispose() {
super.dispose();
tabController
..removeListener(_listener)
..dispose();
animationControllers.forEach((ac) => ac.dispose());
}
}

Related

Trying to create a circular rotating menu with 8 radials

I have tried several packages and found the following package fulfilling the purpose to some extent. https://pub.dev/packages/circle_list
One requirement missing in this package is the click-on icon and icon rotating in the center.
I figured it out. With RotateMode.stopRotate everything is working as you wanted it to be, but if you plan to allow the user to rotate it, then the order of elements (indexes) becomes messed up, so in that case, you'll need to figure out how to track the latest position of the first element to know where the start is and where it should go (and I'm not sure if it's even possible with this package).
class Sample extends StatefulWidget {
const Sample({Key? key}) : super(key: key);
#override
State<Sample> createState() => _SampleState();
}
class _SampleState extends State<Sample> with SingleTickerProviderStateMixin {
late AnimationController animationController;
#override
void initState() {
animationController = AnimationController(
upperBound: pi * 2,
vsync: this,
duration: const Duration(seconds: 2),
);
super.initState();
}
#override
void dispose() {
animationController.dispose();
super.dispose();
}
List<int> elements = List.generate(10, (index) => index);
_center(int index) {
final angle = (pi * 2) * (index / 10);
animationController.animateTo(angle);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
drawer: const Drawer(),
body: Center(
child: AnimatedBuilder(
animation: animationController,
builder: ((context, child) {
return CircleList(
onDragEnd: () => {},
initialAngle: -animationController.value - pi / 2,
centerWidget: Text('center'),
rotateMode: RotateMode.stopRotate,
origin: Offset(0, 0),
children: elements
.map(
(index) => IconButton(
onPressed: () => _center(index),
icon: Icon(Icons.notifications),
color: Colors.blue.withOpacity(index * 0.05 + .3),
),
)
.toList(),
);
}),
),
),
);
}
}
Source Code
class RotatingSliderWidget extends StatefulWidget {
const RotatingSliderWidget({Key? key}) : super(key: key);
#override
State<RotatingSliderWidget> createState() => _RotatingSliderWidgetState();
}
class _RotatingSliderWidgetState extends State<RotatingSliderWidget>
with SingleTickerProviderStateMixin {
late AnimationController animationController;
#override
void initState() {
animationController = AnimationController(
upperBound: pi * 2,
vsync: this,
duration: const Duration(seconds: 2),
);
super.initState();
}
#override
void dispose() {
animationController.dispose();
super.dispose();
}
ContentModel? center;
final elements = [
ContentModel(index: 0, backgroundColor: const Color(0xFFFFEC38)),
ContentModel(index: 1, backgroundColor: const Color(0xFFEA1863)),
ContentModel(index: 2, backgroundColor: Colors.black),
ContentModel(index: 3, backgroundColor: const Color(0xFFF44133)),
ContentModel(index: 4, backgroundColor: const Color(0xFF1B97F3)),
ContentModel(index: 5, backgroundColor: Colors.white),
ContentModel(index: 6, backgroundColor: const Color(0xFF58BA61)),
ContentModel(index: 7, backgroundColor: const Color(0xFF9F9F9F)),
];
_center(int index) {
center = elements[index];
final angle = (pi * 2) * (index / 10);
animationController.animateTo(angle);
setState(() {});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
drawer: const Drawer(),
body: Center(
child: AnimatedBuilder(
animation: animationController,
builder: ((context, child) {
return CircleList(
centerWidget: center == null
? null
: Container(
decoration: BoxDecoration(
color: center!.backgroundColor,
shape: BoxShape.circle,
),
child: IconButton(
onPressed: () => _center(center!.index),
icon: const Icon(Icons.notifications, size: 35),
color: const Color(0xFFA6F4CE),
),
),
onDragStart: (_) {},
onDragEnd: () {},
outerCircleColor: const Color(0xFFA6F4CE),
initialAngle: -animationController.value - pi / 2,
rotateMode: RotateMode.stopRotate,
origin: const Offset(0, 0),
children: elements
.map((index) => Container(
decoration: BoxDecoration(
color: index.backgroundColor,
shape: BoxShape.circle,
),
child: IconButton(
onPressed: () => _center(index.index),
icon: const Icon(Icons.notifications, size: 35),
color: const Color(0xFFA6F4CE),
),
))
.toList());
}),
),
),
);
}
}
class ContentModel {
final int index;
final Color backgroundColor;
ContentModel({required this.index, required this.backgroundColor});
}
You can see Result in attached video

How to show Image part by part above a text in Flutter?

I want to make somthing like this in flutter
a blue iamge above a text but it's displied part by part ,how I can made somthing like this or if there is any good libery for that I on ready google it but I didnt find what excatly what I want
I try this but it's not what I want excatly
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: "App",
home: Test(),
);
}
}
class Test extends StatefulWidget {
#override
_TestState createState() => _TestState();
}
class _TestState extends State<Test> {
double rightValue = 1000;
#override
void initState() {
// TODO: implement initState
super.initState();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
backgroundColor: Colors.black,
body: SafeArea(
child: Center(
child: Stack(
alignment: Alignment.center,
children: [
AnimatedPositioned(
duration: Duration(seconds: 2),
right: rightValue,
child: Image.asset('assets/images/bg.png'),
),
Center(
child: Text('Hello world',
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold)),
),
//),
],
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
rightValue = 0;
});
},
),
),
);
}
}
the idea is using CustomClipper, animate it to reveal image :
EDIT -- as of yeasin suggestion, you can animate the text with color tween, warp it in stack.
here edited result :
code :
class Testing extends StatefulWidget {
const Testing({Key? key}) : super(key: key);
#override
State<Testing> createState() => _TestingState();
}
class _TestingState extends State<Testing> with SingleTickerProviderStateMixin {
late final AnimationController _animationController;
late final Animation<double> _animation;
late final Animation<Color?> _animationColor;
#override
void initState() {
// TODO: implement initState
super.initState();
_animationController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 1200))
..repeat(reverse: false);
_animation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _animationController, curve: Curves.ease));
_animationColor = ColorTween(begin: Colors.black, end: Colors.white)
.animate(
CurvedAnimation(parent: _animationController, curve: Curves.ease));
}
#override
void dispose() {
_animationController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
Center(
child: AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return ClipPath(
clipper: MyClipper(anim: _animation.value),
child: Container(
height: 300.0,
width: 300.0,
color: Colors.blue,
),
);
},
),
),
AnimatedBuilder(
animation: _animationController,
builder: (context, _) {
return Center(
child: Text(
"your image",
style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold,
color: _animationColor.value),
));
},
),
],
),
);
}
}
class MyClipper extends CustomClipper<Path> {
final double anim;
MyClipper({required this.anim});
#override
getClip(Size size) {
// TODO: implement getClip
var rect = Rect.fromLTWH(0.0, 0.0, size.width * anim, size.height);
var path = Path();
path.addRect(rect);
return path;
}
#override
bool shouldReclip(covariant MyClipper oldClipper) {
return oldClipper != this;
}
}

How can i reverse the animation using SizeTransition

i have this code . my animation comse from top to bottom , but How can i reverse it to other side which from bottom to top ..
as we can see it be hidden on the top then it move to down but i need to reverse it to be hidden on the bottom and it move to top
class VariableSizeContainerExample extends StatefulWidget {
VariableSizeContainerExample();
#override
_VariableSizeContainerExampleState createState() => _VariableSizeContainerExampleState();
}
class _VariableSizeContainerExampleState extends State<VariableSizeContainerExample> with TickerProviderStateMixin {
AnimationController _controller;
Animation<double> _animation;
#override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 1),
vsync: this,
);
_animation = CurvedAnimation(
parent: _controller,
curve: Curves.fastLinearToSlowEaseIn,
);
}
_toggleContainer() {
print(_animation.status);
if (_animation.status != AnimationStatus.completed) {
_controller.forward();
} else {
_controller.animateBack(0, duration: Duration(seconds: 1));
}
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: SafeArea(
child: Column(
children: [
TextButton(
onPressed: () => _toggleContainer(),
child: Text("Toggle container visibility"),
),
SizeTransition(
sizeFactor: _animation,
axis: Axis.vertical,
child: Container(
child: Text(
"This can have variable size",
style: TextStyle(fontSize: 40),
),
),
),
Text("This is below the above container"),
],
),
),
),
);
}
}
The default animation start from center then will expand.
To control this, you can use axisAlignment on SizeTransition.
A value of 1.0 indicates the bottom or end, depending upon the [axis].
A value of 0.0 (the default) indicates the center for either [axis] value.
To fixed-bottom(hide-top) use axisAlignment:1 and to fixed top(hide-bottom) axisAlignment:-1
SizeTransition(
sizeFactor: _animation,
axisAlignment: -1, //play with 1 and -1
More about SizeTransition.
test widget
void main(List<String> args) =>
runApp(MaterialApp(home: Scaffold(body: VariableSizeContainerExample())));
class VariableSizeContainerExample extends StatefulWidget {
VariableSizeContainerExample({Key? key}) : super(key: key);
#override
State<VariableSizeContainerExample> createState() =>
_VariableSizeContainerExampleState();
}
class _VariableSizeContainerExampleState
extends State<VariableSizeContainerExample>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
#override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 4),
vsync: this,
)..addListener(() {
setState(() {});
});
_animation = CurvedAnimation(
parent: _controller,
curve: Curves.fastLinearToSlowEaseIn,
);
}
#override
void dispose() {
super.dispose();
}
void _toggleContainer() {
debugPrint(_animation.status.toString());
if (_animation.status != AnimationStatus.completed) {
_controller.forward();
} else {
_controller.animateBack(0, duration: Duration(seconds: 1));
}
}
#override
Widget build(BuildContext context) {
return SafeArea(
child: Column(
children: [
TextButton(
onPressed: () => _toggleContainer(),
child: Text("Toggle container visibility"),
),
SizeTransition(
sizeFactor: _animation,
axisAlignment: 1,
/// also try -1
axis: Axis.vertical,
child: Container(
child: const Text(
"This can have variable size",
style: TextStyle(fontSize: 66),
),
),
),
const Text("This is below the above container"),
],
),
);
}
}

Triggering page controller animation from ViewModel

i'm trying to implement a View with a PageView widget, that is being controlled by the ViewModel.
In my view i have buttons that trigger the following method in the ViewModel:
#override
nextPage() {
if (_index < 4) {
_index++;
increaseProgress();
currentIndex.add(_index);
}
}
currentIndex is just a sink of my _currentIndexController, and outputCurrentIndex is a stream of the controller:
#override
Sink get currentIndex => _currentIndexController.sink;
#override
Stream<int> get outputCurrentIndex => _currentIndexController.stream.map((currentIndex) => currentIndex);
it looks like the value is added to the stream successfully, but i can't get it to trigger changing pages, i've set up this listener in the initState() method of the view:
_viewModel.outputCurrentIndex.listen((index) {
_pageController.animateToPage(index, duration: const Duration(milliseconds: 1000), curve: Curves.ease);
});
but it is not triggered for some reason.. what am i doing wrong?
here is a full code of my View:
class RegisterView extends StatefulWidget {
const RegisterView({Key? key}) : super(key: key);
#override
_RegisterViewState createState() => _RegisterViewState();
}
class _RegisterViewState extends State<RegisterView> {
final RegisterViewModel _viewModel = getIt<RegisterViewModel>();
final PageController _pageController = PageController(initialPage: 0);
final FixedExtentScrollController _weightScrollController = FixedExtentScrollController(initialItem: 80);
final FixedExtentScrollController _ageScrollController = FixedExtentScrollController(initialItem: 13);
final FixedExtentScrollController _heightScrollController = FixedExtentScrollController(initialItem: 13);
#override
void initState() {
_bind();
super.initState();
}
#override
void dispose(){
_viewModel.dispose();
super.dispose();
}
_bind() {
_viewModel.start();
_viewModel.outputCurrentIndex.listen((index) {
_pageController.animateToPage(index, duration: const Duration(milliseconds: 1000), curve: Curves.ease);
});
}
#override
Widget build(BuildContext context) {
List<Widget> pagesList = [
const SexPage(),
AgePage(
scrollController: _ageScrollController,
),
WeightPage(scrollController: _weightScrollController),
HeightPage(scrollController: _heightScrollController),
];
return MultiProvider(
providers: [
StreamProvider.value(value: _viewModel.outputProgress, initialData: 0.25),
StreamProvider.value(value: _viewModel.outputCurrentIndex, initialData: 0),
],
child: Scaffold(
backgroundColor: ColorManager.backgroundColor,
appBar: AppBar(
systemOverlayStyle: SystemUiOverlayStyle(
statusBarColor: ColorManager.backgroundColor,
statusBarBrightness: Brightness.dark,
statusBarIconBrightness: Brightness.dark,
),
centerTitle: true,
title: AppBarWidget(),
elevation: AppSize.s0,
),
body: PageView(
reverse: true,
controller: _pageController,
physics: NeverScrollableScrollPhysics(),
children: [...pagesList],
),
),
);
}
}
class AppBarWidget extends StatefulWidget {
const AppBarWidget({
Key? key,
}) : super(key: key);
#override
State<AppBarWidget> createState() => _AppBarWidgetState();
}
class _AppBarWidgetState extends State<AppBarWidget> {
final RegisterViewModel _viewModel = getIt<RegisterViewModel>();
#override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
flex: 1,
child: InkWell(
child: Text(
AppStrings.skip,
style: Theme.of(context).textTheme.labelMedium,
),
onTap: () => _viewModel.nextPage(),
),
),
Expanded(
flex: 4,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: AppPadding.p60),
child: Transform(
alignment: Alignment.center,
transform: Matrix4.rotationY(pi),
child: Progresso(
progress: Provider.of<double>(context),
progressStrokeCap: StrokeCap.round,
backgroundStrokeCap: StrokeCap.round,
progressColor: ColorManager.primary,
backgroundColor: ColorManager.progressBarBackgroundGrey,
progressStrokeWidth: 10.0,
backgroundStrokeWidth: 10.0,
),
),
),
),
Expanded(
flex: 1,
child: Provider.of<int>(context) > 1
? InkWell(
child: Row(
children: [
Text(
AppStrings.back,
style: Theme.of(context).textTheme.labelMedium,
),
Icon(
Icons.arrow_forward_ios,
color: ColorManager.subtitleGrey,
),
],
),
onTap: () => _viewModel.previousPage(),
)
: Container(),
),
],
);
}
}
and my ViewModel:
class RegisterViewModel extends BaseViewModel with RegisterViewModelInputs, RegisterViewModelOutputs {
final StreamController _progressBarController = StreamController<double>.broadcast();
final StreamController _currentIndexController = StreamController<int>.broadcast();
final StreamController _isBackEnabled = StreamController<bool>.broadcast();
double _progress = 0.25;
int _index = 0;
#override
void dispose() {
_progressBarController.close();
_currentIndexController.close();
_isBackEnabled.close();
}
#override
void start() {
// TODO: implement start
}
#override
Sink get currentIndex => _currentIndexController.sink;
#override
Stream<int> get outputCurrentIndex => _currentIndexController.stream.map((currentIndex) => currentIndex);
#override
Stream<double> get outputProgress => _progressBarController.stream.map((progress) => progress);
#override
Sink get progress => _progressBarController.sink;
#override
setCurrentIndex(int index) {
currentIndex.add(index);
}
#override
increaseProgress() {
if (_progress <= 1.0) {
_progress += 0.25;
progress.add(_progress);
}
}
#override
decreaseProgress() {
if (_progress > 0) {
_progress -= 0.25;
progress.add(_progress);
}
}
#override
Sink get isBackEnabled => _isBackEnabled.sink;
#override
Stream<bool> get outputIsBackEnabled => outputIsBackEnabled.map((isEnabled) => isEnabled);
#override
setIsBackEnabled(int index) {
_isBackEnabled.add(index > 0 ? true : false);
}
#override
nextPage() {
if (_index < 4) {
_index++;
increaseProgress();
setCurrentIndex(_index);
}
}
#override
previousPage() {
if (_index > 0) {
_index--;
decreaseProgress();
setCurrentIndex(_index);
}
}
}
abstract class RegisterViewModelInputs {
register();
increaseProgress();
decreaseProgress();
nextPage();
previousPage();
setIsBackEnabled(int index);
Sink get currentIndex;
Sink get isBackEnabled;
Sink get progress;
}
abstract class RegisterViewModelOutputs {
Stream<int> get outputCurrentIndex;
Stream<double> get outputProgress;
Stream<bool> get outputIsBackEnabled;
}
EDIT: I have moved the stream listener to the _AppBarWidgetState build method, and it seems to work now, but i don't fully understand why it hasn't worked before..
is it because the PageController wasn't assigned to a view yet? where is the correct place for the listener? it doesn't makes sense to me for it to be in a child widget.

How can I change ColorTween begin or end color with setState?

This is a widget for my quiz app. I am currently working on the quiz option button.
I want it to blink green or red whether the answer is true or not.
But it's not working. I tried different things but I couldn't succeed.
import 'package:flutter/material.dart';
class TestButton extends StatefulWidget {
TestButton({this.text, this.color, this.onPressed});
final Function onPressed;
final Color color;
final String text;
#override
_TestButtonState createState() => _TestButtonState();
}
class _TestButtonState extends State<TestButton> with TickerProviderStateMixin {
AnimationController _animationController;
Animation _animation;
void initState() {
super.initState();
_animationController =
AnimationController(vsync: this, duration: Duration(milliseconds: 400));
_animation = ColorTween(begin: Colors.transparent, end: widget.color)
.animate(CurvedAnimation(curve: Curves.decelerate, parent: _animationController))
..addListener(() {
setState(() {
});
});
_animationController.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_animationController.reverse();
} else if (status == AnimationStatus.dismissed) {
_animationController.forward();
}
});
_animationController.forward();
}
#override
void dispose() {
_animationController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
double width = MediaQuery.of(context).size.width;
return SizedBox(
width: width / 1.3,
child: InkWell(
borderRadius: BorderRadius.circular(30),
onTap: widget.onPressed,
child: Container(
child: Container(
alignment: Alignment.center,
padding: EdgeInsets.all(width / 20),
decoration: BoxDecoration(
color: _animation.value,
border: Border.all(color: Colors.white38),
borderRadius: BorderRadius.circular(20)),
child: Text(
widget.text,
style: TextStyle(fontSize: 16, fontFamily: "Fondamento"),
),
),
),
),
);
}
}
And this is the usage of the button. I am applying a setState function but it doesn't make sense. The problem is that the animation works but the setState function doesn't change the color value in the ColorTween
TestButton(
text: question.optionA,
color: colorA,//Colors.transparent
onPressed: () {
if (canTap) {
canTap = false;
if (question.answer == 1) {
setState(() {
colorA = Colors.green;
});
} else {
setState(() {
colorA = Colors.red;
});
}
}
},
),
Remove this part:
..addListener(() {
setState(() {
});
});
And use AnimatedBuilder like this:
return SizedBox(
width: width / 1.3,
child: InkWell(
borderRadius: BorderRadius.circular(30),
onTap: widget.onPressed,
child: Container(
child: AnimatedBuilder(
animation: _animationController,
builder: (_, __) => Container(
alignment: Alignment.center,
padding: EdgeInsets.all(width / 20),
decoration: BoxDecoration(
color: _animationController.value,
border: Border.all(color: Colors.white38),
borderRadius: BorderRadius.circular(20)),
child: Text(
widget.text,
style: TextStyle(fontSize: 16, fontFamily: "Fondamento"),
),
)
),
),
),
);
You can copy paste run full code below
You can use didUpdateWidget to reset _animation
In working demo, when click button, color changes from red to green
#override
void didUpdateWidget(covariant TestButton oldWidget) {
if (oldWidget.color != widget.color) {
_animation = ColorTween(begin: Colors.transparent, end: widget.color)
.animate(CurvedAnimation(
curve: Curves.decelerate, parent: _animationController))
..addListener(() {
setState(() {});
});
}
super.didUpdateWidget(oldWidget);
}
working demo
full code
import 'package:flutter/material.dart';
class TestButton extends StatefulWidget {
TestButton({this.text, this.color, this.onPressed});
final Function onPressed;
final Color color;
final String text;
#override
_TestButtonState createState() => _TestButtonState();
}
class _TestButtonState extends State<TestButton> with TickerProviderStateMixin {
AnimationController _animationController;
Animation _animation;
void initState() {
super.initState();
_animationController =
AnimationController(vsync: this, duration: Duration(milliseconds: 400));
_animation = ColorTween(begin: Colors.transparent, end: widget.color)
.animate(CurvedAnimation(
curve: Curves.decelerate, parent: _animationController))
..addListener(() {
setState(() {});
});
_animationController.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_animationController.reverse();
} else if (status == AnimationStatus.dismissed) {
_animationController.forward();
}
});
_animationController.forward();
}
#override
void didUpdateWidget(covariant TestButton oldWidget) {
if (oldWidget.color != widget.color) {
_animation = ColorTween(begin: Colors.transparent, end: widget.color)
.animate(CurvedAnimation(
curve: Curves.decelerate, parent: _animationController))
..addListener(() {
setState(() {});
});
}
super.didUpdateWidget(oldWidget);
}
#override
void dispose() {
_animationController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
double width = MediaQuery.of(context).size.width;
return SizedBox(
width: width / 1.3,
child: InkWell(
borderRadius: BorderRadius.circular(30),
onTap: widget.onPressed,
child: Container(
child: Container(
alignment: Alignment.center,
padding: EdgeInsets.all(width / 20),
decoration: BoxDecoration(
color: _animation.value,
border: Border.all(color: Colors.white38),
borderRadius: BorderRadius.circular(20)),
child: Text(
widget.text,
style: TextStyle(fontSize: 16, fontFamily: "Fondamento"),
),
),
),
),
);
}
}
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> {
Color colorA = Colors.red;
bool canTap = true;
int answer = 1;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TestButton(
text: "question.optionA",
color: colorA, //Colors.transparent
onPressed: () {
if (canTap) {
canTap = false;
if (answer == 1) {
setState(() {
colorA = Colors.green;
});
} else {
setState(() {
colorA = Colors.red;
});
}
}
},
),
],
),
),
);
}
}