I'm trying to reproduce the same contextual menu of Pinterest, in Flutter. So I have an overlay, which displays my contextual menu with a background, and I'd like to keep also the current element in front of the overlay.
Here are 2 videos: the Pinterest menu vs My Flutter menu.
To make an overlay I'm using flutter_portal package.
Here is the code of the blue card widget, with the overlay stuff:
import 'package:flutter/material.dart';
import 'package:flutter_portal/flutter_portal.dart';
import 'package:vector_math/vector_math.dart' show radians;
class CardWidget extends StatefulWidget {
#override
_CardWidgetState createState() => _CardWidgetState();
}
class _CardWidgetState extends State<CardWidget> with SingleTickerProviderStateMixin {
late AnimationController controller;
late Animation<double> degreeAnimation;
late Animation<double> opacityAnimation;
bool isMenuOpen = false;
double menuPosX = 0;
double menuPosY = 0;
#override
void initState() {
controller = AnimationController(vsync: this, duration: Duration(milliseconds: 200))
..addListener(() {
setState(() {});
})
..addStatusListener((status) {
if (status == AnimationStatus.forward) {
isMenuOpen = true;
} else if (status == AnimationStatus.dismissed) {
isMenuOpen = false;
}
});
degreeAnimation =
Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation(parent: controller, curve: Curves.easeOutBack));
opacityAnimation =
Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation(parent: controller, curve: Curves.easeOutExpo));
super.initState();
}
#override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: 63.5 / 88.9,
child: PortalEntry(
visible: isMenuOpen,
portal: Container(
color: Colors.black.withOpacity(opacityAnimation.value / 2),
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
controller.reverse();
},
),
),
child: PortalEntry(
visible: isMenuOpen,
child: GestureDetector(
onTapDown: (details) {
if (controller.isCompleted) {
controller.reverse();
} else {
menuPosX = details.globalPosition.dx;
menuPosY = details.globalPosition.dy;
controller.forward();
}
},
// Blue card content
child: Container(
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.all(
Radius.circular(8),
),
),
child: Center(
child: Text(
'Card Widget',
style: TextStyle(color: Colors.white),
),
),
),
),
// Current contextual Menu
portal: ContextualMenuWidget(),
),
),
);
}
}
Thank you for your help!
Maybe instead of an trying to add an overlay, you can change the color of all the other cards to the same as the background. By that you would achieve the same effect.
You can check if isMenuOpen is true and save the index of the card on which it was clicked, and in your grid builder function you can change the color of all the cards to the same as the background.
Thanks to orotype, instead of making a black overlay, I finally wrapped my cards with an AnimatedOpacity, and passed a callback when the contextual menu is opened/closed to trig the animation.
Result here!
I don't know if this is the best solution, but I like the result.
Related
I am using android studio and flutter. I want to build the screen as shown below in the image:screen Image
let's say I have 4 screens. on the first screen, the bar will load up to 25%. the user will move to next screen by clicking on continue, the linearbar will load up to 50% and so on. the user will get back to previous screens by clicking on the back button in the appbar.
I tried stepper but it doesn't serve my purpose.
You can use the widget LinearProgressIndicator(value: 0.25,) for the first screen and with value: 0.5 for the second screen etc.
If you want to change the bar value within a screen, just use StatefullWidget's setState(), or any state management approaches will do.
import 'package:flutter/material.dart';
class ProgressPage extends StatefulWidget {
const ProgressPage({super.key});
#override
State<ProgressPage> createState() => _ProgressPageState();
}
class _ProgressPageState extends State<ProgressPage> {
final _pageController = PageController();
final _pageCount = 3;
int? _currentPage;
double? _screenWidth;
double? _unit;
double? _progress;
#override
void initState() {
super.initState();
_pageController.addListener(() {
_currentPage = _pageController.page?.round();
setState(() {
_progress = (_currentPage! + 1) * _unit!;
});
});
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
_screenWidth = MediaQuery.of(context).size.width;
_unit = _screenWidth! / _pageCount;
_progress ??= _unit;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('HOZEROGOLD')),
body: Column(
children: [
Align(
alignment: Alignment.topLeft,
child: Container(
color: Colors.yellow,
height: 10,
width: _progress,
),
),
Expanded(
child: PageView(
controller: _pageController,
children: _createPage(),
),
),
],
),
);
}
List<Widget> _createPage() {
return List<Widget>.generate(
_pageCount,
(index) => Container(
color: Colors.white,
child: Center(
child: ElevatedButton(
onPressed: () => _moveNextPage(),
child: Text('NEXT $index'),
),
),
),
);
}
void _moveNextPage() {
if (_pageController.page!.round() == _pageCount-1) {
_pageController.jumpToPage(0);
} else {
_pageController.nextPage(
curve: Curves.bounceIn,
duration: const Duration(milliseconds: 100));
}
}
}
HAPPY CODING! I hope it will be of help.
I have a floating action button at the right bottom of a SingleChildScrollView that I will like to disappear when scrolling down and appear when scrolling up like the attached gif file :
My code is below and will appear with any suggestion
final dataKey = new GlobalKey();
initState(){
super.initState();
_isVisible = true;
_hideButtonController = new ScrollController();
_hideButtonController.addListener((){
if(_hideButtonController.position.userScrollDirection == ScrollDirection.reverse){
if(_isVisible == true) {
/* only set when the previous state is false
* Less widget rebuilds
*/
print("**** ${_isVisible} up"); //Move IO away from setState
setState((){
_isVisible = false;
});
}
} else {
if(_hideButtonController.position.userScrollDirection == ScrollDirection.forward){
if(_isVisible == false) {
/* only set when the previous state is false
* Less widget rebuilds
*/
print("**** ${_isVisible} down"); //Move IO away from setState
setState((){
_isVisible = true;
});
}
}
}});
}
floatingActionButton: new Visibility(
visible: _isVisible,
child: new FloatingActionButton(backgroundColor: colorBlue,
onPressed: () => Scrollable.ensureVisible(dataKey.currentContext,duration: Duration(seconds: 1)),
child: Icon(Icons.arrow_upward),)),
body:SingleChildScrollView(
key: dataKey,
physics: BouncingScrollPhysics(),
controller: _hideButtonController,
)
As you can see that I have key: dataKey, that simply automatically scrolls to the top of the page when click you can see I tried using Visibility but it didn't work for me, and not sure what I did wrong but I will like the FAB to appear and disappear as shown in the attached GIF. Thanks in advance.
About the fab animation, you can use SlideTransition
Run on dartPad
class FabAnimation extends StatefulWidget {
const FabAnimation({Key? key}) : super(key: key);
#override
State<FabAnimation> createState() => _FabAnimationState();
}
class _FabAnimationState extends State<FabAnimation>
with SingleTickerProviderStateMixin {
late ScrollController _hideButtonController;
late final AnimationController _controller = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
)
..addListener(() {
setState(() {});
})
..forward();//first time load
late final Animation<Offset> _offsetAnimation = Tween<Offset>(
end: Offset.zero,
begin: const Offset(0, 2.0),
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.linear,
));
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
initState() {
super.initState();
_hideButtonController = ScrollController();
_hideButtonController.addListener(() {
//add more logic for your case
if (_hideButtonController.position.userScrollDirection ==
ScrollDirection.reverse) {
if (_offsetAnimation.isCompleted) _controller.reverse();
}
if (_hideButtonController.position.userScrollDirection ==
ScrollDirection.forward) {
_controller.forward();
}
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: SlideTransition(
position: _offsetAnimation,
child: FloatingActionButton(
backgroundColor: Colors.blue,
onPressed: () {},
child: Icon(Icons.arrow_upward),
)),
body: SingleChildScrollView(
physics: BouncingScrollPhysics(),
controller: _hideButtonController,
child: Column(
children: List.generate(
33,
(index) => ListTile(
tileColor: index.isEven ? Colors.deepPurple : Colors.blue,
),
),
),
),
);
}
}
I have made a circular menu widget that opens to show the menu options, when the user clicks on it, it goes to another screen, but if the user press the back button on android it goes back to the menu screen with the circular menu still open(also the distance of the submenus to the main circle increase):
(I cropped the video just to show the button, the downward pointing triangle is part of the next screen)
I want to close the circular menu whenever the user selects any option in the submenu, so basically I want to run the close() function before changing routes, or any other way of making the button close
import 'package:flutter/material.dart';
import 'package:frontend/utils/degrees_to_radians.dart';
import 'package:frontend/utils/indexed_iterables.dart';
class CircularMenu extends StatefulWidget {
final Widget menuIcon;
final List<Widget>? children;
CircularMenu({Key? key, this.children, required this.menuIcon})
: super(key: key);
#override
_CircularMenuState createState() => _CircularMenuState();
}
class _CircularMenuState extends State<CircularMenu>
with TickerProviderStateMixin {
bool _isOpen = false;
bool _isAnimating = false;
late AnimationController _animationController;
late AnimationController _subMenuPositionController;
late Animation<double> _sizeAnimation;
late Animation<double> _subMenuPositionAnimation;
void open() {
setState(() {
_isOpen = true;
_animationController.forward();
_subMenuPositionController.forward();
});
}
#override
void initState() {
_animationController =
AnimationController(duration: Duration(milliseconds: 400), vsync: this);
_subMenuPositionController =
AnimationController(duration: Duration(milliseconds: 500), vsync: this);
_sizeAnimation = TweenSequence([
TweenSequenceItem<double>(
tween: Tween<double>(begin: 1, end: 0.4)
.chain(CurveTween(curve: Curves.easeInOut)),
weight: 50),
TweenSequenceItem<double>(
tween: Tween<double>(begin: 0.4, end: 0.9)
.chain(CurveTween(curve: Curves.easeInOut)),
weight: 50),
TweenSequenceItem<double>(
tween: Tween<double>(begin: 0.9, end: 0.3)
.chain(CurveTween(curve: Curves.easeInOut)),
weight: 50),
]).animate(_animationController);
/* ..addStatusListener((status) {
if (status == AnimationStatus.completed) {
_subMenuPositionController.forward();
}
});
*/
_subMenuPositionAnimation = Tween<double>(begin: 0, end: 75)
.chain(CurveTween(curve: Curves.bounceIn))
.animate(_subMenuPositionController);
super.initState();
}
void close() {
setState(() {
_isOpen = false;
_animationController.reverse();
_subMenuPositionController.reverse();
});
}
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animationController,
builder: (context, _) {
return Container(
child: Stack(
alignment: Alignment.center,
children: [
if(widget.children != null)
..._renderSubMenu(
widget.children!, _subMenuPositionAnimation.value),
//Center Button
FractionallySizedBox(
widthFactor: _sizeAnimation.value,
heightFactor: _sizeAnimation.value,
child: Container(
child: RawMaterialButton(
fillColor: Colors.blue,
shape: CircleBorder(),
onPressed: () {
if (_isAnimating) return;
if (_isOpen) {
close();
} else {
open();
}
},
child: Center(child: _isOpen ? Icon(Icons.close) : widget.menuIcon),
),
),
),
],
),
);
});
}
List<Widget> _renderSubMenu(List<Widget> children, double distance) {
List<Widget> _subMenu = [];
double _angleRatio = 360 / children.length;
children.forEachIndexed((child, index) {
_subMenu.add(
Transform.translate(
offset: Offset.fromDirection(
convertDegreesToRadians(index * _angleRatio), distance),
child: GestureDetector(
child: child)),
);
});
return _subMenu;
}
}
you should just pass close callback to submenu elements and call it after navigator push, without awaiting push completion. though you should change children list to builder one, so you can adjust callback to specific button.
also you should mutate state first and after check if widget is mounted before setting its state, smth like this:
void close() {
_isOpen = false;
_animationController.reverse();
_subMenuPositionController.reverse();
if (mounted) {
setState(() {});
}
}
in this simple sample code i want to have fadeIn and fadeOut animation together, but in this code fadeIn work only and reverse not work, how can i have both of them together?
import 'package:flutter/material.dart';
void main()=>runApp(MaterialApp(home: FadeTransitionSample(),));
class FadeTransitionSample extends StatefulWidget {
#override
State<StatefulWidget> createState() => _Fade();
}
class _Fade extends State<FadeTransitionSample> with TickerProviderStateMixin {
AnimationController animation;
Animation<double> _fadeInFadeOut;
#override
void initState() {
super.initState();
animation = AnimationController(vsync: this, duration: Duration(seconds: 3),);
_fadeInFadeOut = Tween<double>(begin: 0.0, end: 0.1).animate(animation);
animation.addListener((){
if(animation.isCompleted){
animation.reverse();
}else{
animation.forward();
}
});
animation.repeat();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: Center(
child: FadeTransition(
opacity: animation,
child: Container(
color: Colors.green,
width: 100,
height: 100,
),
),
),
),
);
}
}
Alright, I am assuming, you are looking to get a FadeIn & FadeOut animation on your container.
There are a few things you need to change.
The FadeTransition class should not take the animation for opacity, rather it should be the _fadeInFadeOut variable
The animation starts, when you call the animation.forward() rather than animation.repeat() (Since in your case, you also need to reverse the animation, always start with the animation.forward() call).
Make sure to use the addStatusListener() method instead of addListener() since you get much better control over your states.
So, all of this put together, below is the working code to make your animation work.
import 'package:flutter/material.dart';
void main()=>runApp(MaterialApp(home: FadeTransitionSample(),));
class FadeTransitionSample extends StatefulWidget {
#override
State<StatefulWidget> createState() => _Fade();
}
class _Fade extends State<FadeTransitionSample> with TickerProviderStateMixin {
AnimationController animation;
Animation<double> _fadeInFadeOut;
#override
void initState() {
super.initState();
animation = AnimationController(vsync: this, duration: Duration(seconds: 3),);
_fadeInFadeOut = Tween<double>(begin: 0.0, end: 0.5).animate(animation);
animation.addStatusListener((status){
if(status == AnimationStatus.completed){
animation.reverse();
}
else if(status == AnimationStatus.dismissed){
animation.forward();
}
});
animation.forward();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: Center(
child: FadeTransition(
opacity: _fadeInFadeOut,
child: Container(
color: Colors.green,
width: 100,
height: 100,
),
),
),
),
);
}
}
Option #1
Simple solution and straight forward with less code, the idea is to combine FadeOut inside FadeIn and give the FadeOut delay amount greater than the duration of the FadeIn, just copy and paste then just change the Image.asset widget to anything you want to fade in/out
1- Add this package.
2- Import it in your page import 'package:animate_do/animate_do.dart';
3- Add this Widget:
Widget _animateLogo() {
return Container(
child: FadeIn(
animate: true,
duration: Duration(seconds: 2),
child: FadeOut(
animate: true,
delay: Duration(seconds: 2),
duration: Duration(seconds: 1),
// Just change the Image.asset widget to anything you want to fade in/out:
child: Image.asset(
"assets/images/logo.png",
height: 150,
width: 150,
fit: BoxFit.contain,
), //Image.asset
) // FadeOut
),
);
}
Option #2:
Use native class AnimatedOpacity class with setState:
// 1- Declare
bool _shouldFade = false;
// 2- Animation fucntion
Widget _animateLogo() {
return AnimatedOpacity(
duration: Duration(milliseconds: 200),
opacity: _shouldFade ? 1 : 0,
child: Container(
// Whatever you want to fadein fadeout
),
);
}
// 3- Animate whenever needed
setState(() {
// Fade in
_shouldFade = true;
});
// Fadeout after 3 seconds
Future.delayed(
Duration(seconds: 3),
() => setState(() {
_shouldFade = false;
}),
);
I was trying to add a button to this page that will (play or pause) the waves animation in the background.
code link: https://github.com/apgapg/flutter_profile/blob/master/lib/page/intro_page.dart
I've tried many things but since I still bad with flutter animations I still without result.
Thanks in advance.
Short answer:
AnimationController _controller = ...;
// To stop animation
_controller.stop();
// To start from beginning
_controller.forward();
// To start from a point other than the very beginning.
_controller.forward(from: 0.8);
Long answer:
I wasn't aware of that code, here is how I did. All you need is Controller.reset() to stop the animation and Controller.repeat() to start it.
However if you need to start the animation just once, use Controller.forward() and Controller.reverse().
void main() => runApp(MaterialApp(home: Scaffold(body: HomePage())));
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin {
AnimationController _controller;
bool _isPlaying = true;
#override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
lowerBound: 0.3,
duration: Duration(seconds: 3),
)..repeat();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Animation")),
body: Stack(
alignment: Alignment.center,
children: <Widget>[
_buildCircularContainer(200),
_buildCircularContainer(250),
_buildCircularContainer(300),
Align(child: CircleAvatar(backgroundImage: AssetImage("assets/images/profile.png"), radius: 72)),
Align(
alignment: Alignment(0, 0.5),
child: RaisedButton(
child: Text(_isPlaying ? "STOP" : "START"),
onPressed: () {
if (_isPlaying) _controller.reset();
else _controller.repeat();
setState(() => _isPlaying = !_isPlaying);
},
),
),
],
),
);
}
Widget _buildCircularContainer(double radius) {
return AnimatedBuilder(
animation: CurvedAnimation(parent: _controller, curve: Curves.fastLinearToSlowEaseIn),
builder: (context, child) {
return Container(
width: _controller.value * radius,
height: _controller.value * radius,
decoration: BoxDecoration(color: Colors.black54.withOpacity(1 - _controller.value), shape: BoxShape.circle),
);
},
);
}
}
When you directly have access to the AnimationController this snippet will start/stop the animation, wherever it left off.
animationController.isAnimating
? animationController.stop()
: animationController.forward();
Here the .isAnimating property is of type bool and is true when the animationController is animating at the moment. Depending on the result .stop()/.forward() will stop/start the animation respectively.