Flutter - How to flip a single card from swipe stack? - flutter

I am using swipe_stack
I need to flip the front card when I click on it. The problem currently I am facing is that the entire stacks get flipped.
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:quiero_funku/widgets/appbar.dart';
import '../../utils/swipe_stack.dart';
class SwipeDeck extends StatefulWidget {
const SwipeDeck({Key? key}) : super(key: key);
#override
State<SwipeDeck> createState() => _SwipeDeckState();
}
class _SwipeDeckState extends State<SwipeDeck>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation _animation;
AnimationStatus _status = AnimationStatus.dismissed;
List<AnimationController> dataCtrl = <AnimationController>[];
// initialize _controller, _animation
#override
void initState() {
super.initState();
// add some AnimationController object before using any index
dataCtrl.add(AnimationController(vsync: this, duration: const Duration(seconds: 1)));
for (int i = 1; i < 10; i++) {
var "_controller$i" = AnimationController(
vsync: this,
duration: const Duration(seconds: 1),
);
"_animation$i" = Tween(end: 1.0, begin: 0.0).animate(_controller)
..addListener(() {
setState(() {});
})
..addStatusListener((status) {
_status = status;
});
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomAppbar(
onBackPressed: () {},
title: '',
),
body: Container(
padding: const EdgeInsets.all(20),
height: 300,
width: double.infinity,
child: SwipeStack(
children: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((int index) {
return SwiperItem(
builder: (SwiperPosition position, double progress) {
return Transform(
alignment: FractionalOffset.center,
transform: Matrix4.identity()
..setEntry(3, 2, 0.0015)
..rotateX(pi * _animation.value),
child: GestureDetector(
onTap: () {
print("tapped");
if (_status == AnimationStatus.dismissed) {
_controller.forward();
} else {
_controller.reverse();
}
},
child: Material(
elevation: 4,
borderRadius: const BorderRadius.all(Radius.circular(6)),
child: Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(6)),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
"Item $index",
style: const TextStyle(
color: Colors.black,
fontSize: 20,
),
),
Text(
"Progress $progress",
style: const TextStyle(
color: Colors.blue,
fontSize: 12,
),
),
],
),
),
),
),
);
});
}).toList(),
visibleCount: 3,
stackFrom: StackFrom.Right,
translationInterval: 6,
scaleInterval: 0.03,
onEnd: () => debugPrint("onEnd"),
onSwipe: (int index, SwiperPosition position) =>
debugPrint("onSwipe $index $position"),
onRewind: (int index, SwiperPosition position) =>
debugPrint("onRewind $index $position"),
),
),
);
}

Related

Unsupported operation: Nan or infinity toInt for Tween Animation

I have found a code that i tried to replicate. It has an app itself and it is working. However when i tried to write the code down it shows the problem Unsupported operation NaN or infinity toInt for a function named TweenAnimation. I am still trying to learn how to use tween animation so any pros have any idea on this problem?
below is the code for the animation
import 'package:flutter/material.dart';
class AnimatedFlipCounter extends StatelessWidget {
final int value;
final Duration duration;
final double size;
final Color color;
const AnimatedFlipCounter({
Key? key,
required this.value,
required this.duration,
this.size = 72,
this.color = Colors.black,
}) : super(key: key);
#override
Widget build(BuildContext context) {
List<int> digits = value == 0 ? [0] : [];
int v = value;
if (v < 0) {
v *= -1;
}
while (v > 0) {
digits.add(v);
v = v ~/ 10;
}
return Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(digits.length, (int i) {
return _SingleDigitFlipCounter(
key: ValueKey(digits.length - i),
value: digits[digits.length - i - 1].toDouble(),
duration: duration,
height: size,
width: size / 1.8,
color: color,
);
}),
);
}
}
class _SingleDigitFlipCounter extends StatelessWidget {
final double value;
final Duration duration;
final double height;
final double width;
final Color color;
const _SingleDigitFlipCounter({
required Key key,
required this.value,
required this.duration,
required this.height,
required this.width,
required this.color,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return TweenAnimationBuilder(
tween: Tween(begin: value, end: value),
duration: duration,
builder: (context,double value, child) {
final whole = 1.0 ~/ value ;
final decimal = value - whole;
return SizedBox(
height: height,
width: width,
child: Stack(
fit: StackFit.expand,
children: <Widget>[
_buildSingleDigit(
digit: whole % 10,
offset: height * decimal,
opacity: 1.0 - decimal,
),
_buildSingleDigit(
digit: (whole + 1) % 10,
offset: height * decimal - height,
opacity: decimal,
),
],
),
);
},
);
}
Widget _buildSingleDigit({required int digit, required double offset, required double opacity}) {
return Positioned(
child: SizedBox(
width: width,
child: Opacity(
opacity: opacity,
child: Text(
"$digit",
style: TextStyle(
fontSize: height, color: color, fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
),
),
bottom: offset,
);
}
below is the main page that calls this widget
import 'package:carousel_slider/carousel_slider.dart';
import 'package:daily5/helpers/constants_tasbih.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:get_storage/get_storage.dart';
import 'package:vibration/vibration.dart';
import '../../helpers/animated_flip_counter.dart';
class Tasbih extends StatefulWidget {
const Tasbih({Key? key}) : super(key: key);
#override
_TasbihState createState() => _TasbihState();
}
class _TasbihState extends State<Tasbih> {
final PageController _controller =
PageController(viewportFraction: 0.1, initialPage: 5);
final int _numberOfCountsToCompleteRound = 33;
int _imageIndex = 1;
int _beadCounter = 0;
int _roundCounter = 0;
int _accumulatedCounter = 0;
bool _canVibrate = true;
bool _isDisposed = false;
final List<Color> _bgColour = [
Colors.teal.shade50,
Colors.lime.shade50,
Colors.lightBlue.shade50,
Colors.pink.shade50,
Colors.black12
];
final CarouselController _buttonCarouselController = CarouselController();
#override
void initState() {
super.initState();
_loadSettings();
}
#override
void dispose() {
_isDisposed = true;
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: GestureDetector(
onTap: _clicked,
onVerticalDragStart: (_) => _clicked(),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
Expanded(
flex: 2,
child: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Row(
children: [
const SizedBox(width: 45),
IconButton(
tooltip: 'Change colour',
icon: const Icon(Icons.palette),
onPressed: () {
setState(() {
_imageIndex < 5
? _imageIndex++
: _imageIndex = 1;
});
}),
IconButton(
tooltip: 'Reset counter',
icon: const Icon(Icons.refresh),
onPressed: () {
confirmReset(context, _resetEverything);
}),
],
),
const Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
textDirection: TextDirection.ltr,
children: <Widget>[
_Counter(
counter: _roundCounter, counterName: 'Round'),
_Counter(counter: _beadCounter, counterName: 'Beads'),
],
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 32),
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Text('Accumulated'),
const SizedBox(width: 10),
AnimatedFlipCounter(
value: _accumulatedCounter,
duration: const Duration(milliseconds: 730),
size: 14),
],
),
),
CarouselSlider(
carouselController: _buttonCarouselController,
options: CarouselOptions(
height: 100.0,
enlargeCenterPage: true,
),
items: [1, 2, 3, 4].map((i) {
return Builder(
builder: (BuildContext context) {
return Container(
width: MediaQuery.of(context).size.width,
margin: const EdgeInsets.symmetric(
horizontal: 5.0),
decoration: BoxDecoration(
color: _bgColour[_imageIndex - 1],
borderRadius: BorderRadius.circular(12)),
child: Image.asset('assets/images/zikr/$i.png'));
},
);
}).toList(),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
onPressed: () {
_buttonCarouselController.previousPage();
},
icon: const Icon(Icons.navigate_before)),
IconButton(
onPressed: () {
_buttonCarouselController.nextPage();
},
icon: const Icon(Icons.navigate_next)),
],
),
const Spacer()
],
),
)),
Expanded(
flex: 1,
child: PageView.builder(
reverse: true,
physics: const NeverScrollableScrollPhysics(),
controller: _controller,
scrollDirection: Axis.vertical,
itemBuilder: (_, __) {
return Image.asset(
'assets/images/beads/bead-$_imageIndex.png',
);
},
itemCount: null,
),
),
],
),
),
);
}
void _loadSettings() async {
bool? canVibrate = await Vibration.hasVibrator();
if (!_isDisposed) {
setState(() {
_canVibrate = canVibrate!;
_loadData();
});
}
}
void _loadData() {
if (!_isDisposed) {
setState(() {
_beadCounter = GetStorage().read(kBeadsCount) ?? 0;
_roundCounter = GetStorage().read(kRoundCount) ?? 0;
_accumulatedCounter =
_roundCounter * _numberOfCountsToCompleteRound + _beadCounter;
});
}
}
void _resetEverything() {
GetStorage().write(kBeadsCount, 0);
GetStorage().write(kRoundCount, 0);
_loadData();
}
void _clicked() {
if (!_isDisposed) {
setState(() {
_beadCounter++;
_accumulatedCounter++;
if (_beadCounter > _numberOfCountsToCompleteRound) {
_beadCounter = 1;
_roundCounter++;
if (_canVibrate) Vibration.vibrate(duration: 100, amplitude: 100);
}
});
}
GetStorage().write(kBeadsCount, _beadCounter);
GetStorage().write(kRoundCount, _roundCounter);
int nextPage = _controller.page!.round() + 1;
_controller.animateToPage(nextPage,
duration: const Duration(milliseconds: 200), curve: Curves.easeIn);
}
}
class _Counter extends StatelessWidget {
const _Counter(
{Key? key,
required this.counter,
this.tsCounter =
const TextStyle(fontSize: 50, fontWeight: FontWeight.bold),
required this.counterName,
this.tsCounterName = const TextStyle(
fontSize: 20,
fontStyle: FontStyle.italic,
fontWeight: FontWeight.w300)})
: super(key: key);
final int counter;
final TextStyle tsCounter;
final String counterName;
final TextStyle tsCounterName;
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
AnimatedFlipCounter(
duration: const Duration(milliseconds: 300),
value: counter,
),
Text(counterName, style: tsCounterName)
],
);
}
}
void confirmReset(BuildContext context, VoidCallback callback) {
const _confirmText = Text('Confirm', style: TextStyle(color: Colors.red));
const _cancelText = Text('Cancel');
const _dialogTitle = Text("Reset Counter?");
const _dialogContent = Text("This action can't be undone");
void _confirmResetAction() {
callback();
showSnackBar(
context: context,
label: 'Cleared',
icon: CupertinoIcons.check_mark_circled);
Navigator.of(context).pop();
}
showDialog(
context: context,
builder: (_) {
return kIsWeb
? AlertDialog(
title: _dialogTitle,
content: _dialogContent,
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: _cancelText,
),
TextButton(
onPressed: _confirmResetAction,
child: _confirmText,
),
],
)
: CupertinoAlertDialog(
title: _dialogTitle,
content: _dialogContent,
actions: [
CupertinoDialogAction(
child: _cancelText,
onPressed: () => Navigator.of(context).pop(),
),
CupertinoDialogAction(
child: _confirmText,
onPressed: _confirmResetAction,
),
],
);
},
);
}
void showSnackBar({required BuildContext context, required String label, required IconData icon}) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
behavior: SnackBarBehavior.floating,
content: Row(
children: [
Icon(
icon,
color: Colors.white60,
),
const SizedBox(width: 5),
Text(label)
],
),
),
);
}
Supposedly it should beads that can be counted down but the animation cant load.

How do you add gesture functionality to a widget that has been animated in Flutter?

I have created an animation in Flutter around selection a profile picture. When the user clicks their profile picture (or 'Add Photo' placeholder), two buttons fly out with an option to either take a photo, or select a picture from their gallery (illustrative screenshot provided)
My problem is that gesture detection on the two buttons that are animated does not seem to work. Adding a
I've removed some clutter and have pasted the relevant portions of the code below.. Can anyone see where i'm going wrong?
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import '../providers/user_deets_provider.dart';
import '../widgets/text_input.dart';
import 'package:image_picker/image_picker.dart';
class UserDetails extends StatefulWidget {
const UserDetails({Key? key}) : super(key: key);
#override
State<UserDetails> createState() => _UserDetailsState();
}
class _UserDetailsState extends State<UserDetails>
with SingleTickerProviderStateMixin {
File? image;
late AnimationController animationController;
late Animation cameraTranslationAnimation, galleryTranslationAnimation;
late Animation rotationAnimation;
double getRadiansFromDegree(double degree) {
double unitRadian = 57.295779513;
return degree / unitRadian;
}
Future pickImage() async {
try {
final image = await ImagePicker().pickImage(source: ImageSource.gallery);
if (image == null) return;
final imageTemporary = File(image.path);
setState(() {
this.image = imageTemporary;
});
} on PlatformException catch (e) {
print('Failed to pick image $e');
}
}
#override
void initState() {
animationController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 250));
cameraTranslationAnimation = TweenSequence([
TweenSequenceItem<double>(
tween: Tween(begin: 0.0, end: 1.2), weight: 75.0),
TweenSequenceItem<double>(
tween: Tween(begin: 1.2, end: 1.0), weight: 25.0)
]).animate(animationController);
galleryTranslationAnimation = TweenSequence([
TweenSequenceItem<double>(
tween: Tween(begin: 0.0, end: 1.4), weight: 55.0),
TweenSequenceItem<double>(
tween: Tween(begin: 1.4, end: 1.0), weight: 45.0)
]).animate(animationController);
rotationAnimation = Tween<double>(begin: 180.0, end: 0.0).animate(
CurvedAnimation(parent: animationController, curve: Curves.easeOut));
super.initState();
// animationController.addListener(() {
// setState(() {});
// });
}
#override
void dispose() {
animationController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
TextEditingController nameController = TextEditingController(
text: Provider.of<UserDeetsProvider>(context).name);
TextEditingController jobTitleController = TextEditingController(
text: Provider.of<UserDeetsProvider>(context).jobTitle);
TextEditingController companyController = TextEditingController(
text: Provider.of<UserDeetsProvider>(context).company);
TextEditingController emailController = TextEditingController(
text: Provider.of<UserDeetsProvider>(context).email);
TextEditingController numberController = TextEditingController(
text: Provider.of<UserDeetsProvider>(context).number);
TextEditingController locationController = TextEditingController(
text: Provider.of<UserDeetsProvider>(context).location);
TextEditingController websiteController = TextEditingController(
text: Provider.of<UserDeetsProvider>(context).website);
Size size = MediaQuery.of(context).size;
return Scaffold(
appBar: AppBar(
title: const Text('User Details'),
),
body: SingleChildScrollView(
child: Column(
children: [
SizedBox(
width: size.width,
height: 190,
child: Stack(
children: [
Positioned(
top: 30,
left: (size.width) / 2 - 70,
child: Stack(
children: [
AnimatedBuilder(
animation: animationController,
builder: (context, child) {
return Transform.translate(
offset: Offset.fromDirection(
getRadiansFromDegree(30),
cameraTranslationAnimation.value * 165),
child: Transform(
transform: Matrix4.rotationZ(
getRadiansFromDegree(
rotationAnimation.value))
..scale(cameraTranslationAnimation.value),
alignment: Alignment.center,
child: Container(
decoration: const BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle),
width: 50,
height: 50,
child: IconButton(
icon: const Icon(Icons.add_to_photos,
color: Colors.white),
onPressed: () {
print('pressed');
},
),
),
),
);
},
),
AnimatedBuilder(
animation: animationController,
builder: (_, child) {
return Positioned(
left: 10,
child: Transform.translate(
offset: Offset.fromDirection(
getRadiansFromDegree(365),
galleryTranslationAnimation.value * 135),
child: Transform(
transform: Matrix4.rotationZ(
getRadiansFromDegree(
rotationAnimation.value))
..scale(galleryTranslationAnimation.value),
alignment: Alignment.center,
child: CircularButton(
width: 50,
height: 50,
color: Colors.white,
icon: const Icon(Icons.camera_alt,
color: Colors.black87),
onClick: () {}),
),
),
);
},
),
GestureDetector(
onTap: () {
if (animationController.isCompleted) {
animationController.reverse();
} else {
animationController.forward();
}
},
child: image != null
? CircleAvatar(
radius: 70,
child: Image.file(image!),
)
: const CircleAvatar(
radius: 70,
child: Text('Add Photo'),
),
),
],
),
),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
children: [
TextButton(
onPressed: () {
print('pressed');
pickImage();
},
child: Text('pick photo')),
DeetsTextInput(
controller: nameController,
label: 'Name',
callback: () =>
Provider.of<UserDeetsProvider>(context, listen: false)
.changeName(nameController.text)),
DeetsTextInput(
controller: jobTitleController,
label: 'Job Title',
callback: () =>
Provider.of<UserDeetsProvider>(context, listen: false)
.changeJob(jobTitleController.text)),
DeetsTextInput(
controller: companyController,
label: 'Company',
callback: () =>
Provider.of<UserDeetsProvider>(context, listen: false)
.changeCompany(companyController.text)),
DeetsTextInput(
controller: emailController,
label: 'Email',
callback: () =>
Provider.of<UserDeetsProvider>(context, listen: false)
.changeEmail(emailController.text)),
DeetsTextInput(
controller: numberController,
label: 'Number',
callback: () =>
Provider.of<UserDeetsProvider>(context, listen: false)
.changePhone(numberController.text)),
DeetsTextInput(
controller: locationController,
label: 'Location',
callback: () =>
Provider.of<UserDeetsProvider>(context, listen: false)
.changeLocation(locationController.text)),
DeetsTextInput(
controller: websiteController,
label: 'Website',
callback: () =>
Provider.of<UserDeetsProvider>(context, listen: false)
.changeWebsite(websiteController.text)),
const SizedBox(height: 30),
],
),
),
],
),
),
);
}
}
class CircularButton extends StatelessWidget {
final double width;
final double height;
final Color color;
final Icon icon;
final VoidCallback onClick;
const CircularButton(
{Key? key,
required this.width,
required this.height,
required this.color,
required this.icon,
required this.onClick})
: super(key: key);
#override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
width: width,
height: height,
child: IconButton(
icon: icon,
onPressed: () => onClick(),
),
);
}
}
Your issue is that the Positioned widget that is wrapping the Stack that contains your buttons is clipping the clickable area; your buttons are working - they are just being occluded by the area you've designated to them by the Postioned widget.
You have it like this:
Stack(
children: [
Positioned(
top: 30,
left: (size.width) / 2 - 70,
// YOU DON'T HAVE ANY RIGHT POSITIONING HERE
child: Stack(
children: [
AnimatedBuilder(),
AnimatedBuilder(),
GestureDetector()
]
)
]
)
Which makes your clickable area this:
You need to, at a minimum, set the right position of the Positioned widget to 0, as in:
Stack(
children: [
Positioned(
top: 30,
left: (size.width) / 2 - 70,
right: 0,
child: Stack(
children: [
AnimatedBuilder(),
AnimatedBuilder(),
GestureDetector()
]
)
]
)
Which makes your clickable area this:
Check out this Gist I created for you as an example so you can see now that your icons become clickable.

TabBar scroll/slide making inaccuracy of displaying

I uses Container 's color to make indicator-like rather than using TabBar's indicator as I've to implement some animation to the Container.
When TabController index is changing, setState is called in the listener. Tries scroll/slide on the TabBar, the TabBar isn't properly changing the index, as listener doesn't listen to animation for TabBar.
I've tried using tabcontroller.animation.addListener method, but there isn't any workaround for me to control the scroll movement.
Attached video below demonstrates tapping and scroll/slide applied on the TabBar.
TabBar-Scroll/Slide
Code:
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) {
List<IconData> _tabIconData = [
Icons.card_giftcard,
Icons.confirmation_num_outlined,
Icons.emoji_events_outlined,
Icons.wine_bar_outlined,
];
List<String> _tabLabel = [
'Tab1',
'Tab2',
'Tab3',
'Tab4',
];
Widget _tab({
required IconData iconData,
required String label,
required bool isSelectedIndex,
// required double widthAnimation,
// required heightAnimation,
}) {
const _tabTextStyle = TextStyle(
fontWeight: FontWeight.w300, fontSize: 12, color: Colors.black);
return AnimatedContainer(
duration: Duration(milliseconds: 300),
padding: EdgeInsets.only(bottom: 2.0),
height: 55,
width: double.infinity, //_animContainerWidth - widthAnimation,
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: isSelectedIndex ? Colors.black : Colors.transparent,
width: 2.0,
),
),
),
child: Tab(
iconMargin: EdgeInsets.only(bottom: 5.0),
icon: Icon(iconData, color: Colors.black),
child: Text(label, style: _tabTextStyle),
),
);
}
List<Widget> _animationGenerator() {
return List.generate(
4,
(index) => ClipRRect(
child: AnimatedBuilder(
animation: _animationControllers[index],
builder: (ctx, _) {
final value = _animationControllers[index].value;
final angle = math.sin(value * math.pi * 2) * math.pi * 0.04;
return Transform.rotate(
angle: angle,
child: _tab(
iconData: _tabIconData[index],
label: _tabLabel[index],
isSelectedIndex: _tabController.index == index,
));
}),
),
);
}
return Scaffold(
appBar: PreferredSize(
preferredSize: Size.fromHeight(100),
child: AppBar(
iconTheme: Theme.of(context).iconTheme,
title: Text(
'Tab Bar',
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.w400,
),
),
centerTitle: true,
bottom: PreferredSize(
preferredSize: Size.fromHeight(20),
child: Container(
child: TabBar(
controller: _tabController,
labelPadding: EdgeInsets.only(top: 5.0, bottom: 2.0),
indicatorColor: Colors.transparent,
tabs: _animationGenerator(),
),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.white,
spreadRadius: 5.0,
offset: Offset(0, 3))
],
),
),
),
),
),
body: TabBarView(
controller: _tabController,
children: List.generate(
4,
(index) => FittedBox(
child: Text('Tab $index'),
)),
),
);
}
void _listener() {
if (_tabController.indexIsChanging) {
setState(() {}); // To refresh color for Container bottom Border
_animationControllers[_tabController.previousIndex].reverse();
} else {
_animationControllers[_tabController.index].forward();
}
}
#override
void dispose() {
super.dispose();
_tabController.removeListener(_listener);
}
}
this is a solution with a CustomPaint widget driven by TabController.animation:
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();
// timeDilation = 10;
_tabController = TabController(length: 4, vsync: this)
..addListener(_listener);
_animationControllers = List.generate(4, (i) => AnimationController(
vsync: this,
duration: Duration(milliseconds: 750),
));
}
#override
Widget build(BuildContext context) {
List<IconData> _tabIconData = [
Icons.card_giftcard,
Icons.confirmation_num_outlined,
Icons.emoji_events_outlined,
Icons.wine_bar_outlined,
];
List<String> _tabLabel = [
'Tab1',
'Tab2',
'Tab3',
'Tab4',
];
List<Color> _tabColor = [
Color(0xffaa0000),
Color(0xff00aa00),
Color(0xff0000aa),
Colors.black,
];
Widget _tab({
required IconData iconData,
required String label,
required Color color,
required int index,
required Animation<double>? animation,
}) {
const _tabTextStyle = TextStyle(fontWeight: FontWeight.w300, fontSize: 12, color: Colors.black);
return CustomPaint(
painter: TabPainter(
animation: animation!,
index: index,
color: color,
),
child: SizedBox(
width: double.infinity,
child: Tab(
iconMargin: EdgeInsets.only(bottom: 5.0),
icon: Icon(iconData, color: Colors.black),
child: Text(label, style: _tabTextStyle),
),
),
);
}
List<Widget> _animationGenerator() {
return List.generate(
4,
(index) => AnimatedBuilder(
animation: _animationControllers[index],
builder: (ctx, _) {
final value = _animationControllers[index].value;
final angle = sin(value * pi * 3) * pi * 0.04;
return Transform.rotate(
angle: angle,
child: _tab(
iconData: _tabIconData[index],
label: _tabLabel[index],
color: _tabColor[index],
index: index,
animation: _tabController.animation,
));
}),
);
}
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
title: Text('Tab Bar',
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.w400,
),
),
centerTitle: true,
bottom: TabBar(
controller: _tabController,
labelPadding: EdgeInsets.only(top: 5.0, bottom: 2.0),
indicatorColor: Colors.transparent,
tabs: _animationGenerator(),
),
),
body: TabBarView(
controller: _tabController,
children: List.generate(4, (index) => FittedBox(
child: Text('Tab $index'),
)),
),
);
}
void _listener() {
if (_tabController.indexIsChanging) {
_animationControllers[_tabController.previousIndex].value = 0;
} else {
_animationControllers[_tabController.index].forward();
}
}
#override
void dispose() {
super.dispose();
_tabController
..removeListener(_listener)
..dispose();
_animationControllers.forEach((ac) => ac.dispose());
}
}
class TabPainter extends CustomPainter {
final Animation<double> animation;
final int index;
final Color color;
final tabPaint = Paint();
TabPainter({
required this.animation,
required this.index,
required this.color,
});
#override
void paint(ui.Canvas canvas, ui.Size size) {
// timeDilation = 10;
if ((animation.value - index).abs() < 1) {
final rect = Offset.zero & size;
canvas.clipRect(rect);
canvas.translate(size.width * (animation.value - index), 0);
final tabRect = Alignment.bottomCenter.inscribe(Size(size.width, 3), rect);
canvas.drawRect(tabRect, tabPaint..color = color);
}
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

Flutter Slide Transition To a Specific Location

I'm making a grammar quiz app using flutter, I have a question and a couple of choices, I want to make the choice slides to the empty space part of the question with a slide animation
For example:
How is _ new School?
(You) (Your) (It)
and when I press on (Your) the choice widget slides to the _ leaving an empty container
How is (Your) new School?
(You) ( ) (It)
I Made it with Draggable and DragTarget and you can see it in these images
image 1
image 2
but I want it to slide when I press on it without dragging and dropping
here is some of the code
class QuestionsScreen extends StatefulWidget {
QuestionsScreen({Key key}) : super(key: key);
#override
_QuestionsScreenState createState() => _QuestionsScreenState();
}
class _QuestionsScreenState extends State<QuestionsScreen> {
String userAnswer = "_";
int indexOfDragPlace = QuestionBrain.getQuesitonText().indexOf("_");
#override
Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
return Scaffold(
body: SafeArea(
child: Column(
children: [
Container(
padding: EdgeInsets.all(10),
color: Colors.white,
child: Center(
child: Scrollbar(
child: ListView(
children: [
Center(
child: Wrap(
children: [
...QuestionBrain.getQuesitonText()
.substring(0, indexOfDragPlace)
.split(" ")
.map((e) => QuestionHolder(
question: e + " ",
)),
_buildDragTarget(),
...QuestionBrain.getQuesitonText()
.substring(indexOfDragPlace + 1)
.split(" ")
.map((e) => QuestionHolder(
question: e + " ",
)),
],
),
)
],
),
),
),
),
Wrap(
children: [
...QuestionBrain.choices.map((choice) {
if (choice == userAnswer) {
return ChoiceHolder(
choice: "",
backGroundColor: Colors.black12,
);
}
return DraggableChoiceBox(
choice: choice,
userAnswer: userAnswer,
onDragStarted: () {
setState(() {
dragedAnswerResult = "";
});
},
onDragCompleted: () {
setState(() {
userAnswer = choice;
setState(() {
answerColor = Colors.orange;
});
print("Called");
});
},
);
}).toList()
],
),
],
),
),
);
}
Widget _buildDragTarget() {
return DragTarget<String>(
builder: (context, icoming, rejected) {
return Material(
child: Container(
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
width: MediaQuery.of(context).size.width * 0.20,
height: MediaQuery.of(context).size.height * 0.05,
color: answerColor,
child: FittedBox(
child: Text(
userAnswer,
style: TextStyle(
fontSize: 12,
color: Colors.white,
fontWeight: FontWeight.bold),
),
),
),
);
},
onAccept: (data) {
userAnswer = data;
answerColor = Colors.orange;
},
);
}
}
class DraggableChoiceBox extends StatelessWidget {
const DraggableChoiceBox({
Key key,
this.choice,
this.userAnswer,
this.onDragCompleted,
this.onDragStarted,
}) : super(key: key);
final String choice;
final String userAnswer;
final Function onDragCompleted;
final Function onDragStarted;
#override
Widget build(BuildContext context) {
return Draggable(
onDragCompleted: onDragCompleted,
data: choice,
child: ChoiceHolder(choice: choice),
feedback: Material(
elevation: 20,
child: ChoiceHolder(
choice: choice,
margin: 0,
),
),
childWhenDragging: ChoiceHolder(
choice: "",
backGroundColor: Colors.black12,
),
onDragStarted: onDragStarted,
);
}
}
You can use overlays similar to the way Hero widgets work, here is an "unpolished" example:
import 'package:flutter/material.dart';
class SlideToPosition extends StatefulWidget {
#override
_SlideToPositionState createState() => _SlideToPositionState();
}
class _SlideToPositionState extends State<SlideToPosition> {
GlobalKey target = GlobalKey();
GlobalKey toMove = GlobalKey();
double dx = 0.0, dy = 0.0, dxStart = 0.0, dyStart = 0.0;
String choosedAnswer = '', answer = 'answer', finalAnswer = '';
OverlayEntry overlayEntry;
#override
void initState() {
overlayEntry = OverlayEntry(
builder: (context) => TweenAnimationBuilder(
duration: Duration(milliseconds: 500),
tween:
Tween<Offset>(begin: Offset(dxStart, dyStart), end: Offset(dx, dy)),
builder: (context, offset, widget) {
return Positioned(
child: Material(
child: Container(
color: Colors.transparent,
height: 29,
width: 100,
child: Center(child: Text(choosedAnswer)))),
left: offset.dx,
top: offset.dy,
);
},
),
);
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
children: [
SizedBox(
height: 20,
),
Row(
children: [
Text('text'),
Container(
key: target,
height: 30,
width: 100,
child: Center(child: Text(finalAnswer)),
decoration:
BoxDecoration(border: Border(bottom: BorderSide())),
),
Text('text')
],
),
SizedBox(
height: 20,
),
GestureDetector(
child: Container(
height: 30,
width: 100,
color: Colors.blue[200],
child: Center(child: Text(answer, key: toMove))),
onTap: () async {
setState(() {
answer = '';
});
RenderBox box1 = target.currentContext.findRenderObject();
Offset targetPosition = box1.localToGlobal(Offset.zero);
RenderBox box2 = toMove.currentContext.findRenderObject();
Offset toMovePosition = box2.localToGlobal(Offset.zero);
setState(() {
answer = '';
choosedAnswer = 'answer';
});
dxStart = toMovePosition.dx;
dyStart = toMovePosition.dy;
dx = targetPosition.dx;
dy = targetPosition.dy;
Overlay.of(context).insert(overlayEntry);
setState(() {});
await Future.delayed(Duration(milliseconds: 500));
overlayEntry.remove();
setState(() {
finalAnswer = 'answer';
});
},
),
],
),
),
);
}
}
Sorry for the poor naming of the variables :)

Animated Widget doesn't seem to call build every time the listenable value changes

I was making a custom countdown circular indicator.
Here is the code.
import 'dart:math';
import 'package:audioplayers/audio_cache.dart';
import 'package:flutter/material.dart';
import 'package:flutter_neumorphic/flutter_neumorphic.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:hive/hive.dart';
import 'package:jailhouseworkout/consts.dart';
import 'package:jailhouseworkout/prefs.dart';
import 'package:jailhouseworkout/providers/juarez_provider.dart';
import 'package:provider/provider.dart';
class JuarezScreen extends StatefulWidget {
#override
_JuarezScreenState createState() => _JuarezScreenState();
}
class _JuarezScreenState extends State<JuarezScreen>
with SingleTickerProviderStateMixin {
AnimationController controller;
#override
void initState() {
super.initState();
controller = AnimationController(vsync: this);
}
#override
Widget build(BuildContext context) {
final staticJuarez = Provider.of<JuarezProvider>(context, listen: false);
return WillPopScope(
onWillPop: () {
if (staticJuarez.isResting) {
staticJuarez.timer.cancel();
controller.stop();
}
return (Future(() => true));
},
child: SafeArea(
child: Material(
color: kMainColor,
child: Column(
children: <Widget>[
Spacer(),
Row(
children: <Widget>[
Spacer(),
NeumorphicButton(
onPressed: () {
if (staticJuarez.isResting) {
staticJuarez.timer.cancel();
controller.stop();
}
Navigator.of(context).pop();
},
boxShape: NeumorphicBoxShape.circle(),
child: Icon(Icons.arrow_back, color: kAccentColor),
style: NeumorphicStyle(color: kMainColor),
),
Spacer(),
Text(
"Juarez Valley",
style: TextStyle(fontSize: 25, color: kAccentColor),
),
Spacer(),
NeumorphicButton(
boxShape: NeumorphicBoxShape.circle(),
child: Icon(FontAwesomeIcons.cog, color: kAccentColor),
style: NeumorphicStyle(color: kMainColor),
),
Spacer(),
],
),
Spacer(),
SizedBox(
width: MediaQuery.of(context).size.width * 0.8,
height: MediaQuery.of(context).size.width * 0.8,
child: Stack(
children: [
Neumorphic(
padding: EdgeInsets.all(12),
boxShape: NeumorphicBoxShape.circle(),
style: NeumorphicStyle(
depth: -3,
intensity: 1,
shape: NeumorphicShape.concave,
),
child: Neumorphic(
padding: EdgeInsets.all(40),
boxShape: NeumorphicBoxShape.circle(),
style: NeumorphicStyle(
depth: 3,
shape: NeumorphicShape.flat,
intensity: 0.7,
color: kMainColor),
child: Neumorphic(
padding: EdgeInsets.all(6),
boxShape: NeumorphicBoxShape.circle(),
style: NeumorphicStyle(depth: -2),
child: Consumer(
builder: (BuildContext context,
JuarezProvider juarez, Widget child) {
return Neumorphic(
boxShape: NeumorphicBoxShape.circle(),
style: NeumorphicStyle(
depth: 7, color: kMainColor),
child: Center(
child: juarez.hasBegun
? juarez.isResting
? Text(
"${juarez.displayedRestingTime}")
: Text(
"REPS\n ${juarez.displayedReps}")
: Text(
"START",
)),
);
},
),
),
),
),
NeumorphicCircularIndicator(
controller: controller,
size: Size.fromHeight(
MediaQuery.of(context).size.width * 0.8),
)
],
),
),
Spacer(flex: 2),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Spacer(
flex: 1,
),
NeumorphicButton(
onPressed: () {},
boxShape: NeumorphicBoxShape.circle(),
child: Icon(FontAwesomeIcons.redo, color: kAccentColor),
style: NeumorphicStyle(color: kMainColor),
),
Spacer(
flex: 2,
),
NeumorphicButton(
onPressed: () {
if (!staticJuarez.hasBegun) {
staticJuarez.start();
} else if (staticJuarez.isResting &&
!staticJuarez.paused) {
staticJuarez.onClickPause();
controller.stop();
} else if (staticJuarez.isResting &&
staticJuarez.paused) {
staticJuarez.onClickResume();
controller.forward(from: controller.value);
}
},
boxShape: NeumorphicBoxShape.circle(),
child: Consumer(
builder: (BuildContext context, JuarezProvider juarez,
Widget child) {
return Icon(
!juarez.isResting || juarez.paused
? FontAwesomeIcons.play
: FontAwesomeIcons.pause,
color: kAccentColor);
},
),
style: NeumorphicStyle(color: kMainColor),
),
Spacer(
flex: 2,
),
NeumorphicButton(
onPressed: () {
if (!staticJuarez.isResting && staticJuarez.hasBegun) {
staticJuarez.onClickRepFinished();
controller
..duration = Duration(seconds: staticJuarez.rest + 1);
controller.forward();
}
},
boxShape: NeumorphicBoxShape.circle(),
child:
Icon(FontAwesomeIcons.stepForward, color: kAccentColor),
style: NeumorphicStyle(color: kMainColor),
),
Spacer(
flex: 1,
),
],
),
Spacer(
flex: 5,
),
],
),
),
),
);
}
}
class CircularIndicatorPainter extends CustomPainter {
final Animation<double> animation;
CircularIndicatorPainter(this.animation);
#override
void paint(Canvas canvas, Size size) {
final myPaint = Paint()
..color = Colors.greenAccent
..strokeWidth = 5
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke;
double radius = (size.width / 2 - myPaint.strokeWidth / 2) - 3;
Offset center = Offset(size.width / 2, size.height / 2);
double arcAngle = (1.0 - animation.value) * 2 * pi;
canvas.drawArc(Rect.fromCircle(center: center, radius: radius), -pi / 2,
arcAngle, false, myPaint);
}
#override
bool shouldRepaint(CircularIndicatorPainter old) {
return old.animation.value != animation.value;
}
}
class NeumorphicCircularIndicator extends AnimatedWidget {
final AnimationController controller;
final Size size;
NeumorphicCircularIndicator({this.size, Key key, this.controller})
: super(key: key, listenable: controller);
Animation<double> get _progress => listenable;
#override
Widget build(BuildContext context) {
return CustomPaint(
size: size,
foregroundPainter: CircularIndicatorPainter(_progress),
);
}
}
So the problem here is that when I click 'next button'
which means
staticJuarez.onClickRepFinished();
controller
..duration = Duration(seconds: staticJuarez.rest + 1);
controller.forward();
when this part was executed, the CustomPaint should smoothly draw the decreasing outter circle but it only updates the outter circle every second.
In the provider, there is a Timer that ticks down the time every 1 second but it seems like Consumers that listen to the Provider and my Animated Widget are independent so even though the Provider updates its value and rebuild all the build functions in Consumers, it wouldn't affect the Animated Widget.
But I might be wrong about this.
How should I make Animated Widget be rebuilt every time the animation value changes not every second following the Provider's updating values?
Thank you!