Flutter animation controller using GetX - flutter

I recently switch to GetX and want to init animation controller in GetxController and can access it in GetView. When app started animation gets to go without any problem but can not forward it again.
class SplashController extends GetxController with GetTickerProviderStateMixin {
var h = 0.0.obs;
late AnimationController ac;
late Animation animation;
Future<void> initAnimation() async {
ac = AnimationController(
vsync: this,
duration: const Duration(seconds: 1),
);
animation = Tween(begin: 0.0, end: 1.0).animate(ac);
}
forwardAnimationFromZero() {
ac.forward(from: 0);
}
#override
void onInit() {
super.onInit();
initAnimation();
forwardAnimationFromZero();
ac.addListener(() {
h.value = animation.value;
});
}
#override
void onReady() {
super.onReady();
forwardAnimationFromZero();
}
#override
void onClose() {
super.onClose();
ac.dispose();
}
}
As you see I extended GetxController with GetTickerProviderStateMixin but The ticker not work properly.
I define var h = 0.0.obs; as observable so can access in screen and without it animation does not tick!
class SplashPage extends GetView<SplashController> {
const SplashPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
var c = Get.put(SplashController());
return Scaffold(
body: Column(
children: [
Container(
color: Colors.amber,
width: (controller.animation.value * 100) + 100,
height: (controller.animation.value * 100) + 100,
child: Center(
child: Text(
controller.animation.value.toString(),
),
),
)
],
),
floatingActionButton: FloatingActionButton(
onPressed: () {
c.ac.forward(from: 0);
},
child: Icon(Icons.refresh),
),
appBar: AppBar(
title: const Text('Splash Page'),
),
);
}
}
in this view when started animation does not react but when I hot relaod i see it in end state.
when change the Container widget to:
Obx(
() => Container(
color: Colors.amber,
width: (controller.animation.value * 100) + 100,
height: (controller.h.value * 100) + 100,
child: Center(
child: Text(
controller.animation.value.toString(),
),
),
),
),
respet to ac.addListener(() {h.value = animation.value;}); animation play at the beginning but can't forward again from zero when I press floatingActionButton.
What I want:
Why animation does not paly at the beginning without h observable?
How can I access animation controller functions in the view?
When some animation controller complete I want to start another animation controller.

Use AnimatedContainer in this part. Because writing too long will slow down your work as the code grows.
Controller
class SplashController extends GetxController {
var h = 40.0.obs;
}
Page
class SplashPage extends GetView<SplashController> {
var click = true;
#override
Widget build(BuildContext context) {
return GetBuilder<SplashController>(
init: Get.put(SplashController()),
builder: (cnt) {
return Center(
child: PageView(
children: <Widget>[
Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
ElevatedButton(
child: Text('Animate!'),
onPressed: () {
click
? cnt.h.value = 250.0
: cnt.h.value = 50.0;
click = !click;
},
),
Obx(
() => AnimatedContainer(
duration: Duration(seconds: 1),
width: cnt.h.value,
height: 40,
color: Colors.red,
),
),
],
)
],
),
);
}
);
}
}
The following code is used to update your code when it is a StatelessWidget.
Obx(
() => AnimatedContainer(
duration: Duration(seconds: 1),
width: cnt.h.value,
height: 40,
color: Colors.red,
),
),
Each time you click the button, you will be able to zoom in and out.

Related

Getting my sidebar to transition between 2 states? Flutter

I am trying to create a sidebar that transitions between 2 different states in flutter for a web application. At the moment it immediately moves between the 2 different positions and their is no transition. I suspect I haven't used the AnimatedPositioned Class correctly.
How am I able to get it to animate between the 2 different positions correctly?
class _SideNavigationBarState extends State<SideNavigationBar> with singleTickerProviderStateMixin<SideNavigationBar> {
late AnimationController _animationController;
late StreamController<bool> isSideNavigationBarOpenedStreamController;
late Stream<bool> isSideNavigationBarOpenedStream;
late StreamSink<bool> isSideNavigationBarOpenedSink;
final _animationDuration = const Duration(milliseconds: 500);
#override
void initState() {
super.initState();
_animationController = AnimationController(vsync: this, duration: _animationDuration);
isSideNavigationBarOpenedStreamController = PublishSubject<bool>();
isSideNavigationBarOpenedStream = isSideNavigationBarOpenedStreamController.stream;
isSideNavigationBarOpenedSink = isSideNavigationBarOpenedStreamController.sink;
}
#override
void dispose() {
_animationController.dispose();
isSideNavigationBarOpenedStreamController.close();
isSideNavigationBarOpenedSink.close();
super.dispose();
}
void onIconPressed() {
final animationStatus = _animationController.status;
final isAnimationCompleted = animationStatus == AnimationStatus.completed;
if (isAnimationCompleted) {
isSideNavigationBarOpenedSink.add(false);
_animationController.reverse();
} else {
isSideNavigationBarOpenedSink.add(true);
_animationController.forward();
}
}
#override
Widget build(BuildContext context) {
double size = 52;
final screenWidth = MediaQuery.of(context).size.width;
return StreamBuilder<bool>(
initialData: false,
stream: isSideNavigationBarOpenedStream,
builder: (context, isSideNavigationBarOpenedAsync) {
final icon = isSideNavigationBarOpenedAsync.data! ? Icons.arrow_back_ios
: Icons.arrow_forward_ios;
return AnimatedPositioned(
duration: _animationDuration,
left: isSideNavigationBarOpenedAsync.data! ? 0 : -screenWidth,
right: isSideNavigationBarOpenedAsync.data! ? 0 : screenWidth-45,
child: Row(
children: <Widget>[
isSideNavigationBarOpenedAsync.data!
? Container(
width: 300,
color: Colors.grey,
):
Container(
width: 45,
color: Colors.grey,
),
Align(
alignment: Alignment(0, -0.9),
child: GestureDetector(
onTap: () {
onIconPressed();
},
child: Container(
child: Material(
color: Colors.transparent,
child: InkWell(
child: Container(
width: size,
height: size,
child: Icon(icon,
color: Colors.grey.withOpacity(0.5)
)
),
),
),
)
)
)
],
),
);
},
);
}
I recommend you instead of using some transition, use some basic simple code for it. for example you need to define a drawer in your scaffold, and it will handle everything itself.
Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
drawer: Container(color: Colors.amber),
body: const Center(child: Text('body')),
);

In Flutter, what's the easiest, simplest way to make a container flashing once without involving animation widgets?

Imagine Facebook mobile app, where you tap on the notification about someone like your comment. The app will open the appropriate screen, scroll you down to the comment, and after you arrive there, the comment row will flash yellow for a while, rapidly turn transparent, and then it's done.
I just want to make the same flashing animation to a ListView/Column element to let users know that something is happening there as a result of their action. But from what I gathered, to create just a simple animation like that needs a complex elaborate contraption with Animation widgets.
There's a widget that does a much appreciated fade animation called FadeInImage. I just need to provide destination URL, placeholder image asset, and the widget will handle the rest. I'm wondering if there's such alternative where I can just provide a key to a widget, and then call from anywhere: rowKey.currentState.flash(color: Colors.yellow). Or perhaps a way to let me tell the ListView or Column to flash certain row like listViewKey.currentState.items[5].flash(color: Colors.yellow).
There is no a Widget like you are looking for, but you can create a custom widget if you know the Flutter basics. You will be able to build from simple animations to the most advanced ones.
I made a simple example, a list of elements where you can select any element from the list (index).
When you open the screen, you will see the scroll animation, after that, the blink animation will start.
class FlashingHome extends StatelessWidget {
const FlashingHome({Key? key}) : super(key: key);
void _goToWidget(BuildContext context, int index) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => FlashingList(index: index),
),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
MaterialButton(
color: Colors.greenAccent,
child: const Text('Go to element 5'),
onPressed: () => _goToWidget(context, 5),
),
MaterialButton(
color: Colors.greenAccent,
child: const Text('Go to element 10'),
onPressed: () => _goToWidget(context, 10),
),
],
),
),
);
}
}
class FlashingList extends StatefulWidget {
const FlashingList({required this.index, Key? key}) : super(key: key);
final int index;
#override
State<FlashingList> createState() => _FlashingListState();
}
class _FlashingListState extends State<FlashingList>
with SingleTickerProviderStateMixin {
final _scrollController = ScrollController();
late final AnimationController _animationController;
final _itemSize = 150.0;
Timer? _timer;
Future<void> _startScrolling() async {
await _scrollController.animateTo(
_itemSize * widget.index,
duration: const Duration(seconds: 1),
curve: Curves.easeOut,
);
// after the scroll animation finishes, start the blinking
_animationController.repeat(reverse: true);
// the duration of the blinking
_timer = Timer(const Duration(seconds: 3), () {
setState(() {
_animationController.stop();
_timer?.cancel();
});
});
}
#override
void initState() {
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 500),
);
WidgetsBinding.instance!.addPostFrameCallback((_) => _startScrolling());
super.initState();
}
#override
void dispose() {
_timer?.cancel();
_animationController.dispose();
_scrollController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Flashing List'),
),
body: ListView.builder(
controller: _scrollController,
itemCount: 15,
itemExtent: 150,
itemBuilder: (context, index) {
final item = Padding(
padding: const EdgeInsets.all(20.0),
child: Text('My Item :$index'),
);
return Padding(
padding: const EdgeInsets.all(4.0),
child: FittedBox(
child: index == widget.index && _animationController.isDismissed
? FadeTransition(
opacity: _animationController,
child: Container(
color: Colors.yellow,
child: item,
),
)
: Container(
color: Colors.grey[200],
child: item,
),
),
);
},
),
);
}
}
Result:
Now that you know how to create an automatic scrolling list, animated item, you can customize this with a more complex animation and extract into a custom widget that you can reuse in your projects.
Reference: https://docs.flutter.dev/development/ui/animations
Try shimmer
While the data is being fetched, you can create a simple animation which will give the feel that something's loading. Here's a simple example.
I am using FAB onPress to reflect the changes.
bool isApiCallProcess = false;
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
isApiCallProcess = !isApiCallProcess;
});
},
),
backgroundColor: Colors.white,
body: (isApiCallProcess == false)
? CircleAvatar(
backgroundColor:
Colors.black12,
radius: 40,
backgroundImage: AssetImage(
'images/dosa.jpg',
),
):
Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Wrap(
children: [
Column(
children: [
const CircleAvatar(
radius: 40,
backgroundImage: AssetImage(
'',
),
),
const SizedBox(
height: 10,
),
Container(
decoration: ShapeDecoration(
color: Colors.grey[400]!,
shape: const
RoundedRectangleBorder(),
),
height: 12,
width: 60,
),
],
),
],
),
),
),
);
Here's the screenshots :

How to make custom animated Container from button of the app till half of the app screen

expected behavior
i tried this code but it give me completely difference result from left side and strange animated
double val = 0;
#override
Widget build(BuildContext context) {
return Stack(
children: [
Container(
height: 400,
color: Colors.red,
),
TweenAnimationBuilder(
duration: const Duration(milliseconds: 150),
tween: Tween<double>(begin: 0 , end: val) ,
builder: (BuildContext context, double? value, Widget? child) {
return (
Transform(
alignment: Alignment.center,
transform: Matrix4.identity()
..setEntry(3, 2, 0.001)
..setEntry(0, 3, 200 * value!)
..rotateY((pi/6)*value),
child: DefaultTabController(
length: 5,
child: Scaffold(
body: Center(
child: Container(
color: Colors.yellowAccent,
child: IconButton(
onPressed: () {
setState(() {
setState(() {
val == 0 ? val = 1 : val = 0 ;
});
});
},
icon: Text('tab me'),
),
),
)
)
)
)
);
}
)
],
);
}
also i need only the red Container the one who animated from down to up , but i don't know why the main screen is also animate .. i need it never animated ..
any suggestion most welcome guys .. thanks
Instead of custom animation, you can use AnimatedContainer().
Create a boolean like selected which will tell the animated container when to close and when to open the container. And using setState you can toggle the animation.
Align your AnimatedContainer() with Align() and give alignment: Alignment.bottomCenter. And give height:0 is not selected and when selected give height the half of screen using MediaQuery.of(context)
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({Key? key}) : super(key: key);
#override
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
bool selected = false;
#override
Widget build(BuildContext context) {
return Column(children: [
ElevatedButton(
onPressed: () {
setState(() {
selected = !selected;
});
},
child: Text("Tap Me!!"),
),
Spacer(),
GestureDetector(
onTap: () {
setState(() {
selected = !selected;
});
},
child: Align(
alignment: Alignment.bottomCenter,
child: AnimatedContainer(
width: double.infinity,
height: selected ? MediaQuery.of(context).size.height / 2 : 0,
color: selected ? Colors.red : Colors.blue,
alignment:
selected ? Alignment.center : AlignmentDirectional.topCenter,
duration: const Duration(seconds: 2),
curve: Curves.fastOutSlowIn,
child: const FlutterLogo(size: 75),
),
),
)
]);
}
}
You can try the same code in dartpad here

AnimatedContainer Rotation gives unwanted rotation

I have been struggling with Flutter with something that in my head seems to simple. I want a container that spins when pressing a button and this should be animated. I have a container in the center of my screen. I have 2 buttons, 1 with "+" and with a "-". When I press the "+" I want the container to rotate 180 degrees in a clockwise rotation, If I press the "+" again I want it to perform another clockwise rotation of 180 degrees. When pressing "-" I want it to spin counter clockwise for 180 degrees.
Currently I have it build so that it will rotate container however the axis is on the top left point of the container instead of the center. I have tried to tackle this but nothing seems to change this behaviour and I found this issue but it's since been closed
https://github.com/flutter/flutter/issues/27419
It's mind boggling to me that I cannot perform such a simple operation and was wondering if someone knows where I'm going wrong.
Some code:
import 'package:flutter/material.dart';
import 'dart:math';
void main() => runApp(Spinner());
class Spinner extends StatefulWidget {
#override
_SpinnerState createState() => _SpinnerState();
}
class _SpinnerState extends State<Spinner> {
double _angle = 0;
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: AnimatedContainer(
alignment: FractionalOffset.center,
height: 200,
width: 200,
transform: Matrix4.rotationZ(_angle),
decoration: BoxDecoration(
color: Colors.blue
),
duration: Duration(seconds: 2),
),
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
FloatingActionButton(onPressed: () {
setState(() {
_angle += 180 * pi / 180;
});
},
child: const Icon(Icons.add),
),
Container(
height: 20,
),
FloatingActionButton(onPressed: () {
setState(() {
_angle -= 180 * pi / 180;
});
},
child: const Icon(Icons.remove),
)
],
)
)
);
}
}
Edit:
I did find this post however when using it the container instantly snaps to the new position and I want this to be animted.
How can I rotate a Container widget in 2D around a specified anchor point?
I found a solution to tackle this problem, instead of using the animatedContained I decided to use an Animation Controller.
my SpinnerState class now looks like this:
class _SpinnerState extends State<RadialMenu> with SingleTickerProviderStateMixin {
AnimationController controller;
#override
void initState() {
super.initState();
controller = AnimationController(duration: Duration(milliseconds: 900), vsync: this);
}
#override
Widget build(BuildContext context) {
return RadialAnimation(controller: controller);
}
}
I then created a stateless widget which contains the animations etc.
class SpinnerAnimation extends StatelessWidget {
SpinnerAnimation({Key key, this.controller})
: rotation = Tween<double>(
begin: 0.0,
end: 180.0,
).animate(
CurvedAnimation(
parent: controller,
curve: Interval(
0.0,
0.75,
curve: Curves.linear,
),
),
),
super(key: key);
final AnimationController controller;
final Animation<double> rotation;
build(context) {
return AnimatedBuilder(
animation: controller,
builder: (context, builder) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FloatingActionButton(
child: Icon(Icons.add),
onPressed: _open,
backgroundColor: Colors.green),
FloatingActionButton(
child: Icon(Icons.remove),
onPressed: _close,
backgroundColor: Colors.red),
Transform.rotate(
angle: radians(rotation.value),
child: Column(
children: [
Container(
width: 200,
height: 200,
decoration: new BoxDecoration(
image: DecorationImage(
image: AssetImage('images/CRLogo.png'))),
)
],
))
],
);
});
}
_open() {
controller.forward(from: 0);
}
_close() {
controller.reverse(from: 1);
}
}
This has at least solved this problem for me and I hope it helps someone else in the future.

Flutter - Flip animation - Flip a card over its right or left side based on the tap's location

I've started playing with Flutter and now thinking about the best way how I can implement a card's flipping animation.
I found this flip_card package and I'm trying to adjust its source code to my needs.
Here is the app which I have now:
import 'dart:math';
import 'package:flutter/material.dart';
void main() => runApp(FlipAnimationApp());
class FlipAnimationApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text("Flip animation"),
),
body: Center(
child: Container(
width: 200,
height: 200,
child: WidgetFlipper(
frontWidget: Container(
color: Colors.green[200],
child: Center(
child: Text(
"FRONT side.",
),
),
),
backWidget: Container(
color: Colors.yellow[200],
child: Center(
child: Text(
"BACK side.",
),
),
),
),
),
),
),
);
}
}
class WidgetFlipper extends StatefulWidget {
WidgetFlipper({
Key key,
this.frontWidget,
this.backWidget,
}) : super(key: key);
final Widget frontWidget;
final Widget backWidget;
#override
_WidgetFlipperState createState() => _WidgetFlipperState();
}
class _WidgetFlipperState extends State<WidgetFlipper> with SingleTickerProviderStateMixin {
AnimationController controller;
Animation<double> _frontRotation;
Animation<double> _backRotation;
bool isFrontVisible = true;
#override
void initState() {
super.initState();
controller = AnimationController(duration: Duration(milliseconds: 500), vsync: this);
_frontRotation = TweenSequence(
<TweenSequenceItem<double>>[
TweenSequenceItem<double>(
tween: Tween(begin: 0.0, end: pi / 2).chain(CurveTween(curve: Curves.linear)),
weight: 50.0,
),
TweenSequenceItem<double>(
tween: ConstantTween<double>(pi / 2),
weight: 50.0,
),
],
).animate(controller);
_backRotation = TweenSequence(
<TweenSequenceItem<double>>[
TweenSequenceItem<double>(
tween: ConstantTween<double>(pi / 2),
weight: 50.0,
),
TweenSequenceItem<double>(
tween: Tween(begin: -pi / 2, end: 0.0).chain(CurveTween(curve: Curves.linear)),
weight: 50.0,
),
],
).animate(controller);
}
#override
void dispose() {
controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Stack(
fit: StackFit.expand,
children: [
AnimatedCard(
animation: _backRotation,
child: widget.backWidget,
),
AnimatedCard(
animation: _frontRotation,
child: widget.frontWidget,
),
_tapDetectionControls(),
],
);
}
Widget _tapDetectionControls() {
return Stack(
fit: StackFit.expand,
children: <Widget>[
GestureDetector(
onTap: _leftRotation,
child: FractionallySizedBox(
widthFactor: 0.5,
heightFactor: 1.0,
alignment: Alignment.topLeft,
child: Container(
color: Colors.transparent,
),
),
),
GestureDetector(
onTap: _rightRotation,
child: FractionallySizedBox(
widthFactor: 0.5,
heightFactor: 1.0,
alignment: Alignment.topRight,
child: Container(
color: Colors.transparent,
),
),
),
],
);
}
void _leftRotation() {
_toggleSide();
}
void _rightRotation() {
_toggleSide();
}
void _toggleSide() {
if (isFrontVisible) {
controller.forward();
isFrontVisible = false;
} else {
controller.reverse();
isFrontVisible = true;
}
}
}
class AnimatedCard extends StatelessWidget {
AnimatedCard({
this.child,
this.animation,
});
final Widget child;
final Animation<double> animation;
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: animation,
builder: (BuildContext context, Widget child) {
var transform = Matrix4.identity();
transform.setEntry(3, 2, 0.001);
transform.rotateY(animation.value);
return Transform(
transform: transform,
alignment: Alignment.center,
child: child,
);
},
child: child,
);
}
}
Here is how it looks like:
What I'd like to achieve is to make the card flip over its right side if it was tapped on its right half and over its left side if it was tapped on its left half. If it is tapped several times in a row on the same half it should flip over the same side (not back and forth as it is doing now).
So the desired animation should behave as the following one from Quizlet app.
You should know when you tap on the right or left to change the animations dynamically, for that you could use a flag isRightTap. Then, you should invert the values of the Tweens if it has to rotate to one side or to the other.
And the side you should rotate would be:
Rotate to left if the front is visible and you tapped on the left, or, because the back animation is reversed, if the back is is visible and you tapped on the right
Otherwise, rotate to right
Here are the things I changed in _WidgetFlipperState from the code in the question:
_updateRotations(bool isRightTap) {
setState(() {
bool rotateToLeft = (isFrontVisible && !isRightTap) || !isFrontVisible && isRightTap;
_frontRotation = TweenSequence(
<TweenSequenceItem<double>>[
TweenSequenceItem<double>(
tween: Tween(begin: 0.0, end: rotateToLeft ? (pi / 2) : (-pi / 2))
.chain(CurveTween(curve: Curves.linear)),
weight: 50.0,
),
TweenSequenceItem<double>(
tween: ConstantTween<double>(rotateToLeft ? (-pi / 2) : (pi / 2)),
weight: 50.0,
),
],
).animate(controller);
_backRotation = TweenSequence(
<TweenSequenceItem<double>>[
TweenSequenceItem<double>(
tween: ConstantTween<double>(rotateToLeft ? (pi / 2) : (-pi / 2)),
weight: 50.0,
),
TweenSequenceItem<double>(
tween: Tween(begin: rotateToLeft ? (-pi / 2) : (pi / 2), end: 0.0)
.chain(CurveTween(curve: Curves.linear)),
weight: 50.0,
),
],
).animate(controller);
});
}
#override
void initState() {
super.initState();
controller =
AnimationController(duration: Duration(milliseconds: 500), vsync: this);
_updateRotations(true);
}
void _leftRotation() {
_toggleSide(false);
}
void _rightRotation() {
_toggleSide(true);
}
void _toggleSide(bool isRightTap) {
_updateRotations(isRightTap);
if (isFrontVisible) {
controller.forward();
isFrontVisible = false;
} else {
controller.reverse();
isFrontVisible = true;
}
}
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp();
#override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage();
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool _toggler = true;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(actions: [
TextButton(
onPressed: _onFlipCardPressed,
child: const Text('change', style: TextStyle(color: Colors.white)),
)
]),
body: Center(
child: SizedBox.square(
dimension: 140,
child: FlipCard(
toggler: _toggler,
frontCard: AppCard(title: 'Front'),
backCard: AppCard(title: 'Back'),
),
),
),
);
}
void _onFlipCardPressed() {
setState(() {
_toggler = !_toggler;
});
}
}
class AppCard extends StatelessWidget {
final String title;
const AppCard({
required this.title,
});
#override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20.0),
color: Colors.deepPurple[400],
),
child: Center(
child: Text(
title,
style: const TextStyle(
fontSize: 40.0,
color: Colors.white,
),
textAlign: TextAlign.center,
),
),
);
}
}
class FlipCard extends StatelessWidget {
final bool toggler;
final Widget frontCard;
final Widget backCard;
const FlipCard({
required this.toggler,
required this.backCard,
required this.frontCard,
});
#override
Widget build(BuildContext context) {
return GestureDetector(
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 800),
transitionBuilder: _transitionBuilder,
layoutBuilder: (widget, list) => Stack(children: [widget!, ...list]),
switchInCurve: Curves.ease,
switchOutCurve: Curves.ease.flipped,
child: toggler
? SizedBox(key: const ValueKey('front'), child: frontCard)
: SizedBox(key: const ValueKey('back'), child: backCard),
),
);
}
Widget _transitionBuilder(Widget widget, Animation<double> animation) {
final rotateAnimation = Tween(begin: pi, end: 0.0).animate(animation);
return AnimatedBuilder(
animation: rotateAnimation,
child: widget,
builder: (context, widget) {
final isFront = ValueKey(toggler) == widget!.key;
final rotationY = isFront ? rotateAnimation.value : min(rotateAnimation.value, pi * 0.5);
return Transform(
transform: Matrix4.rotationY(rotationY)..setEntry(3, 0, 0),
alignment: Alignment.center,
child: widget,
);
},
);
}
}
Try this code I've made some changes to your code, now the GestureDetector is divided equally in width on widget so when you tap on the left side of the box it will reverse the animation and if you tap on right side part it will forward the animation.
Widget _tapDetectionControls() {
return Flex(
direction: Axis.horizontal,
children: <Widget>[
Expanded(
flex: 1,
child: GestureDetector(
onTap: _leftRotation,
),
),
Expanded(
flex: 1,
child: GestureDetector(
onTap: _rightRotation,
),
),
],
);
}
void _leftRotation() {
controller.reverse();
}
void _rightRotation() {
controller.forward();
}