Related
I want a smooth animation that switch between the previous Scale and position to current Scale and postion. But it look like that it's not the exact previous scale or position. Why the only first widget have a reverse scale => ZoomIn instead of ZoomOut. I add a isSet variable because of the first frame that show the widget far away from where it should start. I have try with AnimatedSlide but it's not the same way to calculate. I don't think I should add more code but if it's necessary for you I would. Am I far from the right way to to it? I'll be grateful. Thank you.
class ItemTransform extends ConsumerStatefulWidget {
final Widget child;
final String idItem;
const ItemTransform({required this.idItem, required this.child, Key? key})
: super(key: key);
#override
_ItemTransformState createState() => _ItemTransformState();
}
class _ItemTransformState extends ConsumerState<ItemTransform>
with SingleTickerProviderStateMixin {
late bool isSet;
late AnimationController _animationController;
late Animation<double> _animationOffset;
late Animation<double> _animationScale;
#override
void initState() {
super.initState();
isSet = false;
_animationController = AnimationController(
duration: const Duration(milliseconds: 500), vsync: this);
_animationOffset = Tween<double>(begin: 1.0, end: 0.0).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut));
_animationScale = Tween(begin: 1.0, end: 1.0).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut));
_animationController.forward();
WidgetsBinding.instance!.addPostFrameCallback((timeStamp) {
_afterLayout();
});
}
void _afterLayout() {
if (!mounted) {
return;
}
final RenderBox renderBoxRed = context.findRenderObject() as RenderBox;
final size = renderBoxRed.size;
ref.read(viewAnimationProvider).setSize({widget.idItem: size});
final position = renderBoxRed.localToGlobal(Offset.zero);
ref.read(viewAnimationProvider).setPosition({widget.idItem: position});
isSet = true;
}
#override
void dispose() {
_animationController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animationController,
builder: (context, _) {
final oldOffset =
ref.read(viewAnimationProvider).oldPositions[widget.idItem];
final newOffset =
ref.read(viewAnimationProvider).newPositions[widget.idItem];
Offset? toMove;
if (newOffset != null && oldOffset != null) {
if (isSet) {
toMove = Offset(
(oldOffset.dx - newOffset.dx) * _animationOffset.value,
(oldOffset.dy - newOffset.dy) * _animationOffset.value);
} else {
toMove = Offset(
(newOffset.dx - oldOffset.dx) * _animationOffset.value,
(newOffset.dy - oldOffset.dy) * _animationOffset.value);
}
}
final oldSizes =
ref.read(viewAnimationProvider).oldSizes[widget.idItem];
final newSizes =
ref.read(viewAnimationProvider).newSizes[widget.idItem];
if (oldSizes != null && newSizes != null) {
final oldSize = oldSizes.width / oldSizes.height;
final newSize = newSizes.width / newSizes.height;
if (isSet ) {
_animationScale = Tween(begin: newSize / oldSize, end: 1.0)
.animate(CurvedAnimation(
parent: _animationController, curve: Curves.easeInOut));
}else{
_animationScale = Tween(begin: oldSize / newSize, end: 1.0).animate(
CurvedAnimation(
parent: _animationController, curve: Curves.easeInOut));
}
}
return Transform.translate(
offset: toMove ?? Offset.zero,
child: Transform.scale(
scale: _animationScale.value,
child: widget.child,
),
);
});
}
}
final viewAnimationProvider =
ChangeNotifierProvider<ViewAnimationController>((ref) {
return ViewAnimationController();
});
class ViewAnimationController extends ChangeNotifier {
late TypeOfView oldView;
late TypeOfView newView;
late Map<String, Offset> oldPositions;
late Map<String, Offset> newPositions;
late Map<String, Size> oldSizes;
late Map<String, Size> newSizes;
ViewAnimationController() {
oldView = TypeOfView.staggered;
newView = TypeOfView.normal;
oldPositions = <String, Offset>{};
newPositions = <String, Offset>{};
oldSizes = <String, Size>{};
newSizes = <String, Size>{};
}
void setView(TypeOfView newV) {
oldView = newView;
newView = newV;
notifyListeners();
}
void setPosition(Map<String, Offset> position) {
final newDx = position.values.first.dx;
final newDy = position.values.first.dy;
final offset = newPositions[position.keys.first];
if (offset != null) {
if (newDx != newPositions[position.keys.first]!.dx ||
newDy != newPositions[position.keys.first]!.dy) {
oldPositions[position.keys.first] = newPositions[position.keys.first]!;
newPositions[position.keys.first] = position.values.first;
// newPositions.addAll(position);
}
} else {
newPositions.addAll(position);
}
}
void setSize(Map<String, Size> map) {
final width = map.values.first.width;
final height = map.values.first.height;
final size = newSizes[map.keys.first];
if (size != null) {
if (width != size.width || height != size.height) {
oldSizes[map.keys.first] = newSizes[map.keys.first]!;
newSizes[map.keys.first] = map.values.first;
}
} else {
newSizes.addAll(map);
}
}
}
switch (ref.watch(currentViewProvider)) {
case TypeOfView.normal:
return GridViewCount(
currentSize: currentSize,
listItem: listItem as List<Item>);
case TypeOfView.staggered:
return GridViewStaggered(
currentSize: currentSize,
listItem: listItem as List<Item>);
}
It looks like you want a GridView that can be zoomed?
I've made a widget in the past, called "GalleryView", to achieve a similar kind of effect, but on mobile devices with "pinch-to-zoom" gesture. Let me put it here and see if it's helpful for you. Maybe you can use it as a starting point and modify it towards your end goal.
Usage:
GalleryView.builder(
itemCount: 101, // total items
maxPerRow: 10, // the maximum zoom-out level allowed
itemBuilder: (_, index) => MyImage(),
)
Source:
class GalleryView extends StatefulWidget {
final int? itemCount;
final IndexedWidgetBuilder itemBuilder;
final int initialPerRow;
final int minPerRow;
final int maxPerRow;
final Duration duration;
final ScrollController? controller;
const GalleryView.builder({
Key? key,
this.itemCount,
required this.itemBuilder,
this.initialPerRow = 3,
this.minPerRow = 1,
this.maxPerRow = 7,
this.duration = const Duration(seconds: 1),
this.controller,
}) : super(key: key);
#override
_GalleryViewState createState() => _GalleryViewState();
}
class _GalleryViewState extends State<GalleryView> {
late final ScrollController _controller =
widget.controller ?? ScrollController();
double _maxWidth = 0.0;
late double _size; // size of each grid item
late double _prevSize;
#override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
if (constraints.maxWidth != _maxWidth) {
_maxWidth = constraints.maxWidth;
_size = _maxWidth / widget.initialPerRow;
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
_snapToGrid();
});
}
return GestureDetector(
child: _buildListView(),
onScaleStart: (_) {
_controller.jumpTo(0);
_prevSize = _size;
},
onScaleUpdate: (details) {
final maxSize = _maxWidth / widget.minPerRow;
final minSize = _maxWidth / widget.maxPerRow;
setState(() {
_size = (_prevSize * details.scale).clamp(minSize, maxSize);
});
},
onScaleEnd: (_) => _snapToGrid(),
);
},
);
}
_snapToGrid() {
final countPerRow = (_maxWidth / _size).round().clamp(
widget.minPerRow,
widget.maxPerRow,
);
setState(() => _size = _maxWidth / countPerRow);
}
ListView _buildListView() {
final countPerRow = (_maxWidth / _size).ceil();
return ListView.builder(
controller: _controller,
itemExtent: _size,
itemCount: widget.itemCount != null
? (widget.itemCount! / countPerRow).ceil()
: null,
itemBuilder: (context, int i) {
return OverflowBox(
maxWidth: double.infinity,
alignment: Alignment.centerLeft,
child: Row(
children: [
for (int j = 0; j < countPerRow; j++)
if (widget.itemCount == null ||
i * countPerRow + j < widget.itemCount!)
_buildItem(context, i * countPerRow + j),
],
),
);
},
);
}
Widget _buildItem(context, int index) {
return SizedBox(
width: _size,
height: _size,
child: AnimatedSwitcher(
duration: widget.duration,
child: KeyedSubtree(
key: ValueKey(index),
child: widget.itemBuilder(context, index),
),
),
);
}
}
I finally found a solution. I don't know if it's the best solution but it work as attended. I understand many things in my search. The "_animationScale" work badly because the with and height are same so I have to use "_animationHeight" and "_animationWidth". And I tried in a simple SizedBox and FittedBox to apply the size change but nothing change.
Concerning the offset I use the topLeft with the translate and minus to the new coordonate. It's the same resulte before but I didn't know about that translate methode. I've tried to gather all that _animation in one animation like "Animation<Rect?>" like here or here, but it's relative to a fixed size if I really understand. If someone could clearly explain me why it work as attended? Is there another way with those two previous links?
class ItemTransform extends ConsumerStatefulWidget {
final Widget child;
final String idItem;
const ItemTransform({required this.idItem, required this.child, Key? key})
: super(key: key);
#override
_ItemTransformState createState() => _ItemTransformState();
}
class _ItemTransformState extends ConsumerState<ItemTransform>
with TickerProviderStateMixin {
late bool isSet;
late AnimationController _animationController;
late Animation<Offset> _animationOffset;
late Animation<double> _animationWidth;
late Animation<double> _animationHeight;
#override
void initState() {
super.initState();
isSet = false;
_animationController = AnimationController(
duration: const Duration(milliseconds: 500), vsync: this);
_animationOffset = Tween<Offset>(begin: Offset.zero, end: Offset.zero)
.animate(CurvedAnimation(
parent: _animationController, curve: Curves.easeInOut));
_animationWidth = Tween(begin: 1.0, end: 1.0).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut));
_animationHeight = Tween(begin: 1.0, end: 1.0).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut));
WidgetsBinding.instance!.addPostFrameCallback((timeStamp) {
_afterLayout();
});
}
void _afterLayout() {
if (!mounted) {
return;
}
final RenderBox renderBox = context.findRenderObject() as RenderBox;
final size = renderBox.size;
_animationWidth = Tween(begin: size.width, end: size.width).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut));
_animationHeight = Tween(begin: size.height, end: size.height).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut));
final position = renderBox.localToGlobal(Offset.zero);
ref.read(viewAnimationProvider).setNewRect(
id: widget.idItem,
newFromWidget:
Rect.fromLTWH(position.dx, position.dy, size.width, size.height));
if (ref.read(viewAnimationProvider).newRect[widget.idItem] != null) {
_animationController.forward();
}
isSet = true;
}
#override
void dispose() {
_animationController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animationController,
builder: (context, _) {
final oldRect =
ref.read(viewAnimationProvider).oldRect[widget.idItem];
final newRect =
ref.read(viewAnimationProvider).newRect[widget.idItem];
if (oldRect != null && newRect != null) {
_animationOffset = Tween<Offset>(
begin: oldRect.topLeft
.translate(-newRect.topLeft.dx, -newRect.topLeft.dy),
end: Offset.zero)
.animate(CurvedAnimation(
parent: _animationController, curve: Curves.easeInOut));
//
_animationWidth =
Tween(begin: oldRect.size.width, end: newRect.size.width)
.animate(CurvedAnimation(
parent: _animationController, curve: Curves.easeInOut));
_animationHeight =
Tween(begin: oldRect.size.height, end: newRect.size.height)
.animate(CurvedAnimation(
parent: _animationController, curve: Curves.easeInOut));
}
return Offstage(
offstage: !isSet,
child: Transform.translate(
offset: _animationOffset.value,
child: Stack(
clipBehavior: Clip.none,
children: [
Positioned(
height: _animationHeight.value,
width: _animationWidth.value,
child: widget.child,
),
],
),
),
);
});
}
}
final viewAnimationProvider =
ChangeNotifierProvider<ViewAnimationController>((ref) {
return ViewAnimationController();
});
class ViewAnimationController extends ChangeNotifier {
late Map<String,Rect> oldRect;
late Map<String,Rect> newRect;
ViewAnimationController() {
oldRect = <String, Rect>{};
newRect = <String, Rect>{};
}
void setNewRect({required String id, required Rect newFromWidget}){
final rect = newRect[id];
if(rect != null && rect != newFromWidget){
oldRect[id] = rect;
newRect[id] = newFromWidget;
}else{
newRect.addAll({id:newFromWidget});
}
}
}
When I build the application, I encounter such an error.
lib/input_page/pacman_slider.dart:41:7: Error: A value of type 'Animation<BorderRadius?>' can't be assigned to a variable of type 'Animation' because 'BorderRadius?' is nullable and 'BorderRadius' isn't.
'Animation' is from 'package:flutter/src/animation/animation.dart' ('../../Dev/flutter/packages/flutter/lib/src/animation/animation.dart').
'BorderRadius' is from 'package:flutter/src/painting/border_radius.dart' ('../../Dev/flutter/packages/flutter/lib/src/painting/border_radius.dart').
).animate(CurvedAnimation(
^
Error Code:
void initState() {
super.initState();
pacmanMovementController =
AnimationController(vsync: this, duration: Duration(milliseconds: 400));
_bordersAnimation = BorderRadiusTween(
begin: BorderRadius.circular(8.0),
end: BorderRadius.circular(50.0),
).animate(CurvedAnimation(
parent: widget.submitAnimationController,
curve: Interval(0.0, 0.07),
));
}
Full Code:
const double _pacmanWidth = 21.0;
const double _sliderHorizontalMargin = 24.0;
const double _dotsLeftMargin = 8.0;
class PacmanSlider extends StatefulWidget {
final VoidCallback onSubmit;
final AnimationController submitAnimationController;
const PacmanSlider(
{Key? key,
required this.onSubmit,
required this.submitAnimationController})
: super(key: key);
#override
_PacmanSliderState createState() => _PacmanSliderState();
}
class _PacmanSliderState extends State<PacmanSlider>
with TickerProviderStateMixin {
double _pacmanPosition = 24.0;
late Animation<BorderRadius> _bordersAnimation;
late Animation<double> _submitWidthAnimation;
late AnimationController pacmanMovementController;
late Animation<double> pacmanAnimation;
#override
void initState() {
super.initState();
pacmanMovementController =
AnimationController(vsync: this, duration: Duration(milliseconds: 400));
_bordersAnimation = BorderRadiusTween(
begin: BorderRadius.circular(8.0),
end: BorderRadius.circular(50.0),
).animate(CurvedAnimation(
parent: widget.submitAnimationController,
curve: Interval(0.0, 0.07),
));
}
#override
void dispose() {
pacmanMovementController.dispose();
super.dispose();
}
// double get width => _submitWidthAnimation?.value ?? 0.0;
double get width => _submitWidthAnimation.value;
#override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
_submitWidthAnimation = Tween<double>(
begin: constraints.maxWidth,
end: screenAwareSize(52.0, context),
).animate(CurvedAnimation(
parent: widget.submitAnimationController,
curve: Interval(0.05, 0.15),
));
return AnimatedBuilder(
animation: widget.submitAnimationController,
builder: (context, child) {
Decoration decoration = BoxDecoration(
borderRadius: _bordersAnimation.value,
color: Theme.of(context).primaryColor,
);
return Center(
child: Container(
height: screenAwareSize(52.0, context),
width: width,
decoration: decoration,
child: _submitWidthAnimation.isDismissed
? GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => _animatePacmanToEnd(),
child: Stack(
alignment: Alignment.centerRight,
children: <Widget>[
AnimatedDots(),
_drawDotCurtain(decoration),
_drawPacman(),
],
),
)
: Container(),
),
);
},
);
},
);
}
Widget _drawDotCurtain(Decoration decoration) {
if (width == 0.0) {
return Container();
}
double marginRight =
width - _pacmanPosition - screenAwareSize(_pacmanWidth / 2, context);
return Positioned.fill(
right: marginRight,
child: Container(decoration: decoration),
);
}
Widget _drawPacman() {
pacmanAnimation = _initPacmanAnimation();
return Positioned(
left: _pacmanPosition,
child: GestureDetector(
onHorizontalDragUpdate: (details) => _onPacmanDrag(width, details),
onHorizontalDragEnd: (details) => _onPacmanEnd(width, details),
child: PacmanIcon(),
),
);
}
Animation<double> _initPacmanAnimation() {
Animation<double> animation = Tween(
begin: _pacmanMinPosition(),
end: _pacmanMaxPosition(width),
).animate(pacmanMovementController);
animation.addListener(() {
setState(() {
_pacmanPosition = animation.value;
});
if (animation.status == AnimationStatus.completed) {
_onPacmanSubmit();
}
});
return animation;
}
_onPacmanSubmit() {
widget.onSubmit();
Future.delayed(Duration(seconds: 1), () => _resetPacman());
}
_onPacmanDrag(double width, DragUpdateDetails details) {
setState(() {
_pacmanPosition += details.delta.dx;
_pacmanPosition = math.max(_pacmanMinPosition(),
math.min(_pacmanMaxPosition(width), _pacmanPosition));
});
}
_onPacmanEnd(double width, DragEndDetails details) {
bool isOverHalf =
_pacmanPosition + screenAwareSize(_pacmanWidth / 2, context) >
0.5 * width;
if (isOverHalf) {
_animatePacmanToEnd();
} else {
_resetPacman();
}
}
_animatePacmanToEnd() {
pacmanMovementController.forward(
from: _pacmanPosition / _pacmanMaxPosition(width));
}
_resetPacman() {
if (this.mounted) {
setState(() => _pacmanPosition = _pacmanMinPosition());
}
}
double _pacmanMinPosition() =>
screenAwareSize(_sliderHorizontalMargin, context);
double _pacmanMaxPosition(double sliderWidth) =>
sliderWidth -
screenAwareSize(_sliderHorizontalMargin / 2 + _pacmanWidth, context);
}
class AnimatedDots extends StatefulWidget {
#override
_AnimatedDotsState createState() => _AnimatedDotsState();
}
class _AnimatedDotsState extends State<AnimatedDots>
with TickerProviderStateMixin {
final int numberOfDots = 10;
final double minOpacity = 0.1;
final double maxOpacity = 0.5;
late AnimationController hintAnimationController;
#override
void initState() {
super.initState();
_initHintAnimationController();
hintAnimationController.forward();
}
#override
void dispose() {
hintAnimationController.dispose();
super.dispose();
}
void _initHintAnimationController() {
hintAnimationController = AnimationController(
vsync: this,
duration: Duration(milliseconds: 800),
);
hintAnimationController.addStatusListener((status) {
if (status == AnimationStatus.completed) {
Future.delayed(Duration(milliseconds: 800), () {
if (this.mounted) {
hintAnimationController.forward(from: 0.0);
}
});
}
});
}
#override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(
left: screenAwareSize(
_sliderHorizontalMargin + _pacmanWidth + _dotsLeftMargin,
context),
right: screenAwareSize(_sliderHorizontalMargin, context)),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(numberOfDots, _generateDot)
..add(Opacity(
opacity: maxOpacity,
child: Dot(size: 14.0),
)),
),
);
}
Widget _generateDot(int dotNumber) {
Animation animation = _initDotAnimation(dotNumber);
return AnimatedBuilder(
animation: animation,
builder: (context, child) => Opacity(
opacity: animation.value,
child: child,
),
child: Dot(size: 9.0),
);
}
Animation<double> _initDotAnimation(int dotNumber) {
double lastDotStartTime = 0.4;
double dotAnimationDuration = 0.5;
double begin = lastDotStartTime * dotNumber / numberOfDots;
double end = begin + dotAnimationDuration;
return SinusoidalAnimation(min: minOpacity, max: maxOpacity).animate(
CurvedAnimation(
parent: hintAnimationController,
curve: Interval(begin, end),
),
);
}
}
class SinusoidalAnimation extends Animatable<double> {
SinusoidalAnimation({required this.min, required this.max});
final double min;
final double max;
#override
double transform(double t) {
return min + (max - min) * math.sin(math.pi * t);
}
}
class Dot extends StatelessWidget {
final double size;
const Dot({Key? key, required this.size}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
height: screenAwareSize(size, context),
width: screenAwareSize(size, context),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white,
),
);
}
}
class PacmanIcon extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(
right: screenAwareSize(16.0, context),
),
child: SvgPicture.asset(
'images/pacman.svg',
height: screenAwareSize(25.0, context),
width: screenAwareSize(21.0, context),
),
);
}
}
As you see in the attached capture, BorderRadiusTween has a nullable value of type BorderRadius?
I'd suggest to either declare _bordersAnimation as Animation<BorderRadius?>or just using an Animation<double> instead
I want to create condition that if first api didn't finish load when click on bottom navigation tab it will not navigate to other tab. So I want to pass boolean that will change to true when api is loaded, the problem is I don't know how to pass this dynamic value to my custom Bottom Navigation Bar. Is there an other way instead of using global variable?
This is my code.
landingPage.dart
// This global variable is change to true when receive callback from api
bool isComplete = false;
class _NavigationItem {
_NavigationItem(this.iconFile, this.caption, this.page);
final String iconFile;
final String caption;
final Widget page;
}
class LandingPage extends StatefulWidget {
final String username;
LandingPage({Key key, this.username}) : super(key: key);
#override
_LandingPageState createState() => _LandingPageState();
}
class _LandingPageState extends State<LandingPage> {
int _selectedIndex = 0;
List<_NavigationItem> _navItems = [];
double _bottomNavBarHeight = 46;
double _iconSize = 26;
double _circleSize = 52;
int _animationDuration = 300;
double _circleStrokeWidth = 0;
BottomNavigationController _navigationController;
#override
void didChangeDependencies() {
// Context of a state is available to us from the moment the State loads its dependencies
// Since we need context object so it need to be accessed inside this overridden method.
_navItems = [
_NavigationItem(
"assets/icons/home.svg",
S.of(context).landing_nav_home,
HomePage(
username: widget.username,
callback: (value) { isComplete = value; }, // receiving callback data
)),
_NavigationItem("assets/icons/friend.svg",
S.of(context).landing_nav_friend, FriendPage()),
_NavigationItem(
"assets/icons/chat.svg", S.of(context).landing_nav_chat, ChatPage()),
_NavigationItem("assets/icons/widget.svg",
S.of(context).landing_nav_widget, WidgetPage()),
_NavigationItem("assets/icons/more.svg",
S.of(context).landing_nav_setting, SettingPage()),
];
super.didChangeDependencies();
}
#override
void initState() {
super.initState();
_navigationController = new BottomNavigationController(_selectedIndex);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
Container(
padding: EdgeInsets.only(bottom: 90),
child: _navItems.elementAt(_selectedIndex).page,
),
_createBottomNavigationBar()
],
)
);
}
void _onNavigationBarItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
}
Widget _createBottomNavigationBar() {
return Align(alignment: Alignment.bottomCenter, child: _bottomNav());
}
Widget _bottomNav() {
List<TabItem> tabItems = List.of([
new TabItem(Icons.home_outlined, "", CommonColor.accent,
asset: "assets/icons/home.svg"),
new TabItem(Icons.person, "", CommonColor.accent,
asset: "assets/icons/friends.svg"),
new TabItem(Icons.chat_bubble_outline, "", CommonColor.accent),
new TabItem(Icons.widgets_outlined, "", CommonColor.accent),
new TabItem(Icons.more_horiz, "", CommonColor.accent),
]);
return BottomNavigation(
tabItems,
controller: _navigationController,
barHeight: _bottomNavBarHeight,
iconsSize: _iconSize,
circleSize: _circleSize,
selectedIconColor: Colors.white,
normalIconColor: CommonColor.accent,
circleStrokeWidth: _circleStrokeWidth,
barBackgroundColor: Colors.white,
animationDuration: Duration(milliseconds: _animationDuration),
selectedCallback: (int selectedPos) {
print("selected: $_selectedIndex");
_onNavigationBarItemTapped(selectedPos);
},
);
}
#override
void dispose() {
super.dispose();
_navigationController.dispose();
}
}
BottomNavigation.dart
typedef BottomNavSelectedCallback = Function(int selectedPos);
class BottomNavigation extends StatefulWidget {
final List<TabItem> tabItems;
final int selectedPos;
final double barHeight;
final double padding;
final Color barBackgroundColor;
final double circleSize;
final double circleStrokeWidth;
final double iconsSize;
final Color selectedIconColor;
final Color normalIconColor;
final Duration animationDuration;
final BottomNavSelectedCallback selectedCallback;
final BottomNavigationController controller;
BottomNavigation(this.tabItems,
{this.selectedPos = 0,
this.barHeight = 60,
this.barBackgroundColor = Colors.white,
this.circleSize = 58,
this.circleStrokeWidth = 4,
this.iconsSize = 32,
this.padding = 16,
this.selectedIconColor = Colors.white,
this.normalIconColor = Colors.deepPurpleAccent,
this.animationDuration = const Duration(milliseconds: 300),
this.selectedCallback,
this.controller})
: assert(tabItems != null && tabItems.length >= 2 && tabItems.length <= 5,
"tabItems is required");
#override
State<StatefulWidget> createState() => _BottomNavigationState();
}
class _BottomNavigationState extends State<BottomNavigation>
with TickerProviderStateMixin {
Curve _animationsCurve = Cubic(0.27, 1.21, .77, 1.09);
AnimationController itemsController;
Animation<double> selectedPosAnimation;
Animation<double> itemsAnimation;
List<double> _itemsSelectedState;
int selectedPos;
int previousSelectedPos;
BottomNavigationController _controller;
#override
void initState() {
super.initState();
if (widget.controller != null) {
_controller = widget.controller;
previousSelectedPos = selectedPos = _controller.value;
} else {
previousSelectedPos = selectedPos = widget.selectedPos;
_controller = BottomNavigationController(selectedPos);
}
_controller.addListener(_newSelectedPosNotify);
_itemsSelectedState = List.generate(widget.tabItems.length, (index) {
return selectedPos == index ? 1.0 : 0.0;
});
itemsController = new AnimationController(
vsync: this, duration: widget.animationDuration);
itemsController.addListener(() {
setState(() {
_itemsSelectedState.asMap().forEach((i, value) {
if (i == previousSelectedPos) {
_itemsSelectedState[previousSelectedPos] =
1.0 - itemsAnimation.value;
} else if (i == selectedPos) {
_itemsSelectedState[selectedPos] = itemsAnimation.value;
} else {
_itemsSelectedState[i] = 0.0;
}
});
});
});
selectedPosAnimation = makeSelectedPosAnimation(
selectedPos.toDouble(), selectedPos.toDouble());
itemsAnimation = Tween(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: itemsController, curve: _animationsCurve));
}
Animation<double> makeSelectedPosAnimation(double begin, double end) {
return Tween(begin: begin, end: end).animate(
CurvedAnimation(parent: itemsController, curve: _animationsCurve));
}
void onSelectedPosAnimate() {
setState(() {});
}
void _newSelectedPosNotify() {
_setSelectedPos(widget.controller.value);
}
#override
Widget build(BuildContext context) {
double fullWidth = MediaQuery.of(context).size.width;
double fullHeight =
widget.barHeight + (widget.circleSize / 2) + widget.circleStrokeWidth;
double sectionsWidth = (fullWidth / 1.2) / widget.tabItems.length;
//Create the boxes Rect
List<Rect> boxes = List();
widget.tabItems.asMap().forEach((i, tabItem) {
double left = (i + 0.5) * sectionsWidth;
double top = fullHeight - widget.barHeight;
double right = left + sectionsWidth;
double bottom = fullHeight;
boxes.add(Rect.fromLTRB(left, top, right, bottom));
});
List<Widget> children = List();
// This is the full view transparent background (have free space for circle)
children.add(Padding(
padding: EdgeInsets.only(bottom: widget.padding),
child: Container(
width: fullWidth,
height: fullHeight,
)));
// This is the bar background (bottom section of our view)
children.add(Positioned.fill(
child: Padding(
padding: EdgeInsets.only(
left: widget.padding,
right: widget.padding,
bottom: widget.padding),
child: Container(
width: MediaQuery.of(context).size.width,
height: widget.barHeight,
decoration: BoxDecoration(
color: widget.barBackgroundColor,
borderRadius: BorderRadius.all(Radius.circular(20)),
boxShadow: [
new BoxShadow(color: Colors.black12, blurRadius: 8.0)
]),
),
),
top: fullHeight - widget.barHeight,
));
// This is the circle handle on selected
children.add(new Positioned(
child: Container(
width: widget.circleSize,
height: widget.circleSize,
child: Stack(
children: <Widget>[
Container(
decoration: BoxDecoration(
shape: BoxShape.rectangle,
borderRadius: BorderRadius.all(Radius.circular(14)),
color: widget.barBackgroundColor),
),
Container(
margin: EdgeInsets.all(widget.circleStrokeWidth),
decoration: BoxDecoration(
shape: BoxShape.rectangle,
borderRadius: BorderRadius.all(Radius.circular(14)),
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
CommonColor.primary,
widget.tabItems[selectedPos].circleColor,
CommonColor.slipBg,
])),
),
],
),
),
left: boxes[selectedPos].center.dx - (widget.circleSize / 2),
top: 0,
));
//Here are the Icons and texts of items
boxes.asMap().forEach((int pos, Rect r) {
// Icon
Color iconColor = pos == selectedPos
? widget.selectedIconColor
: widget.normalIconColor;
double scaleFactor = pos == selectedPos ? 1.2 : 1.0;
children.add(
Positioned(
child: Transform.scale(
scale: scaleFactor,
child: widget.tabItems[pos].asset != null
? SvgPicture.asset(widget.tabItems[pos].asset,
color: iconColor,
width: widget.iconsSize,
height: widget.iconsSize,
fit: BoxFit.cover)
: Icon(
widget.tabItems[pos].icon,
size: widget.iconsSize,
color: iconColor,
),
),
left: r.center.dx - (widget.iconsSize / 2),
top: r.center.dy -
(widget.iconsSize / 2) -
(_itemsSelectedState[pos] *
((widget.barHeight / 2) + widget.circleStrokeWidth)),
),
);
if (pos != selectedPos) {
children.add(Positioned.fromRect(
child: GestureDetector(
onTap: () {
_controller.value = pos;
},
),
rect: r,
));
}
});
return Stack(
children: children,
);
}
void _setSelectedPos(int pos) {
previousSelectedPos = selectedPos;
selectedPos = pos;
itemsController.forward(from: 0.0);
selectedPosAnimation = makeSelectedPosAnimation(
previousSelectedPos.toDouble(), selectedPos.toDouble());
selectedPosAnimation.addListener(onSelectedPosAnimate);
if (widget.selectedCallback != null) {
widget.selectedCallback(selectedPos);
}
}
#override
void dispose() {
super.dispose();
itemsController.dispose();
_controller.removeListener(_newSelectedPosNotify);
}
}
class BottomNavigationController extends ValueNotifier<int> {
BottomNavigationController(int value) : super(value);
}
You should use a FutureBuilder/ StreamBuilder to listen to an event after your api call is finished. Something like this:
Future<dynamic> yourApiCall() async {
// Execute your functions
return someValue;
}
Here in the UI, you can listen to the output of this function with:
FutureBuilder(
future: yourApiCall(),
builder: (context, snapshot) {
return BottomNavigation(
tabItems,
// ...other properties
selectedCallback: (int selectedPos) {
print("selected: $_selectedIndex");
if (snapshot.hasData) // Here you check if the snapshot.data is different from null. That means your api has finished and returned some value
_onNavigationBarItemTapped(selectedPos); // Only then, you notify the callback function
},
);
});
How do I make a AnimatedController that repeats, but starts up with a curve animation.
Code:
AnimationController :
var _animating = false;
AnimationController _rotationAnimationController;
Animation<double> _animation;
#override
void initState() {
super.initState();
_rotationAnimationController = AnimationController(
duration: Duration(milliseconds: 2000),
vsync: this,
);
_animation =
Tween<double>(begin: 0, end: 4 * pi ).animate(_rotationAnimationController)
..addListener(() {
setState(() {});
});
}
#override
void dispose() {
_rotationAnimationController.dispose();
super.dispose();
}
Button :
GestureDetector(
onTap: () {
Duration startStopTime = Duration(seconds: 3);
if(_animating) {
_rotationAnimationController.animateBack(1, duration: startStopTime, curve: Curves.easeOut);
setState(() {});
} else {
_rotationAnimationController.repeat() //This needs to start with a curve
}
setState(() {
_animating = !_animating;
});
},
child: [...]
),
If you could also make it so that when the repeat stops, it does that with a curve, that would be amazing too :)
Thanks for the help allready :D
The best I understood your question , you want a CurvedAnimation right ? This means your animation will repeat but follow a specific curve . So here the best I could do for you :
Define your AnimationController like this :
Animation<double> _animation;
AnimationController _animationController;
#override
void initState() {
super.initState();
_animationController =
AnimationController(vsync: this, duration: Duration(seconds: 2))
..addListener(() {
setState(() {});
});
final Animation curve =
CurvedAnimation(parent: _animationController, curve: Curves.easeOut);
_animation = Tween<double>(begin: 0, end: pi * 4).animate(curve);
}
and your GestureDetector like this :
GestureDetector(
onTap: () {
if (_animationController.isAnimating)
{
_animationController.animateBack(0,duration: Duration(seconds: 2), curve: Curves.easeIn);
}
else {
_animationController.repeat();
}
},
child: [...]
),
Edit :
I used a TweenAnimationBuilder to have the effect you want :
import 'package:flutter/material.dart';
import 'dart:math' as math;
class TweenAnimatedBuilderRotate extends StatefulWidget {
TweenAnimatedBuilderRotate({Key key}) : super(key: key);
#override
TweenAnimatedBuilderRotateState createState() =>
TweenAnimatedBuilderRotateState();
}
class TweenAnimatedBuilderRotateState
extends State<TweenAnimatedBuilderRotate> {
double _newAngle = 0;
Curve curveThatChanges = Curves.easeIn;
bool isAnimating = false;
int _millsecs = 2000;
void onCompletion() {
if (isAnimating) {
_newAngle += 4 * math.pi;
curveThatChanges = Curves.linear;
_millsecs = 1000;
setState(() {});
} else {
_newAngle = 0;
_millsecs = 2000;
}
}
void onContainerTap() {
if (isAnimating) {
isAnimating = false;
_newAngle = _newAngle;
setState(() {});
} else {
curveThatChanges = Curves.easeIn;
_newAngle += 4 * math.pi;
isAnimating = true;
setState(() {});
}
}
#override
Widget build(BuildContext context) {
return TweenAnimationBuilder(
tween: Tween<double>(begin: 0, end: _newAngle),
duration: Duration(milliseconds: _millsecs),
onEnd: () => onCompletion(),
curve: curveThatChanges,
builder: (
BuildContext ctx,
double angle,
Widget child,
) {
_newAngle = angle;
return Center(
child: Transform(
transform: Matrix4.identity()..rotateZ(_newAngle),
alignment: FractionalOffset.center,
child: GestureDetector(
child: Container(
color: Colors.blueGrey,
width: 200,
height: 200,
),
onTap: () => onContainerTap(),
),
),
);
});
}
}
You can refer to this Medium article to understand about how TweenAnimationdBuilder Works. You can also modify _millsecs variable to speed up/down the animation. Pass TweenAnimatedBuilderRotate() in the body parameter of the Scaffold(...).
I tried to create an animated progressbar where the progress is filling up animatedly and also it changes it's color depending on the final value of the progress.
The first part is working properly, but I can't figure out what is wrong with the color animation. What am I missing?
I used flutter_rounded_progress_bar for displaying the progress bar.
import 'package:flutter/material.dart';
import 'package:flutter_rounded_progress_bar/flutter_rounded_progress_bar.dart';
import 'package:flutter_rounded_progress_bar/rounded_progress_bar_style.dart';
class MyLinearProgressIndicator extends StatefulWidget {
final double currentProgress;
final double height;
final Color foregroundColor;
final int duration = 500;
MyLinearProgressIndicator({
this.currentProgress,
this.height = 5,
this.foregroundColor = const Color(0xFFde8405)});
#override
_LinearProgressIndicatorState createState() =>
_LinearProgressIndicatorState();
}
class _LinearProgressIndicatorState extends State<MyLinearProgressIndicator> with
SingleTickerProviderStateMixin {
AnimationController progressController;
Animation<double> animation;
Animation<Color> colorAnimation;
CurvedAnimation curve;
#override
void initState() {
super.initState();
progressController = AnimationController(vsync: this, duration: Duration(milliseconds: widget.duration));
curve = CurvedAnimation(parent: progressController, curve: Curves.ease);
animation =
Tween<double>(begin: 0, end: widget.currentProgress).animate(curve)
..addListener(() {
setState(() {});
});
Color endColor;
if (widget.currentProgress <= 30) {
endColor = const Color(0xFFFF0000);
} else if (widget.currentProgress <= 50) {
endColor = const Color(0xFF00FF00);
} else {
endColor =const Color(0xFF0000FF);
}
colorAnimation =
ColorTween(begin: widget.foregroundColor, end: endColor).animate(curve)
..addListener(() {
setState(() {});
});
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
progressController.forward();
});
}
#override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Padding(
padding: const EdgeInsets.only(right: 16.0),
child: Text("${widget.currentProgress.floor()}%",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
color: Color(0xFF333333))),
),
Container(
height: widget.height,
width: 100,
child: RoundedProgressBar(
milliseconds: widget.duration,
height: widget.height,
style: RoundedProgressBarStyle(
borderWidth: 0,
widthShadow: 0,
colorProgress: colorAnimation.value,
backgroundProgress: Color(0xFFEBEBEB)),
percent: animation.value,
)),
],
);
}
#override
void dispose() {
progressController.dispose();
super.dispose();
}
}
The issue here is that the current color value is being fetched from Animation<Color> with colorAnimation.value. A quick workaround here is to create a function that returns the color for ranges of values.
Color progressColor({required double value}) {
if (value > 0.60) {
return Colors.red;
} else if (value > 0.30) {
return Colors.orange;
} else {
return Colors.green;
}
}
and set the Color to the ProgressIndicator
RoundedProgressBar(
milliseconds: widget.duration,
height: widget.height,
style: RoundedProgressBarStyle(
borderWidth: 0,
widthShadow: 0,
colorProgress: progressColor(animation.value),
backgroundProgress: Color(0xFFEBEBEB)
),
percent: animation.value,
)