Flutter: Make a custom bottom bar sliding up when it appears - flutter

I am trying to design a music playlist page. So far I am able to create a listview with song cards. When I click on any song, a custom bottom bar appears and the audio starts playing. However, I just hold a state with boolean and show the bottom bar according to that. Instead, I want it to appear like sliding up and reach to the position. Let say in 0.5 seconds.
I have a custom NavBar class
class NavBar extends StatefulWidget
And I use this class in build similar to:
return Column(
children: [
SizedBox(
height: constraints.maxHeight * 0.5,
hild: SlidingBanners(),
),
Expanded(
child: Lists(),
),
NavBar()
],
);
How can I such animation?

Use a SizeTransition widget https://api.flutter.dev/flutter/widgets/SizeTransition-class.html
"SizeTransition acts as a ClipRect that animates either its width or
its height, depending upon the value of axis. The alignment of the
child along the axis is specified by the axisAlignment."
Widget build(BuildContext context) {
return SizeTransition(
sizeFactor: CurvedAnimation(
curve: Curves.fastOutSlowIn,
parent: _animationController,
),
child: Container(height: 100, color: Colors.blue)
);
}
init animation controller in stateful widgets initState()
#override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 500));
}
Make sure your stateful widget uses SingleTickerProviderStateMixin
class _NavBarState extends State<NavBar>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
then open and close with
_animationController.forward()
_animationController.reverse()
You can pass the _animationController into the NavBar widget's constructor from its parent if you want the parent to control the animation.
Alternatively you can use an AnimatedContainer widget and setState with its height 0 or 100 depending on if Nav should be shown. This becomes a problem for some widgets that cannot be squished to height of 0 though and I would not recommend for any container that contains anything but text

One solution would be to use a SnackBar widget. Since it's automatically animated, you wouldn't want to worry about manually animating the bottom bar. You can insert your Audio Player (bottom bar) widget to the child of the SizedBox.
The bottom bar is made visible by,
ScaffoldMessenger.of(context).showSnackBar(snackBar);
This bottom bar is dismissed (hidden) by dragging down or by,
ScaffoldMessenger.of(context).hideCurrentSnackBar();
There maybe many other solutions as well to this, but reading your question, I hope this is what you wanted.
return Center(
child: ElevatedButton(
onPressed: () {
final snackBar = SnackBar(
duration: Duration(days: 365),
content: SizedBox(
height: 100,
//insert your audio player widget here
child: Column(
children: [
Text("YOUR AUDIOPLAYER WIDGET HERE"),
Text("Audio Controls Here"),
ElevatedButton(
onPressed: () {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
},
child: Text("Audio Player Minimize"),
),
],
),
),
);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
},
child: Text('Open Audio Player'),
),
);

Related

Display multiple pages at the same time

I have an app with two features, that have routes such as:
/feature1
/feature1/a
/feature2
/feature2/a
/feature2/a/b
/feature2/c
I can use GoRouter and its ShellRoute to switch between these one at a time using context.goNamed('feature2'), which would replace the entire screen with feature 2 (when tapping a tab in a tab bar for example). Here's a diagram of just the top level routes using tabs:
However, I would like to have an overview style menu which displays multiple destinations at once, so the user can see where they will be going before they go there (for example the preview page tabs in a mobile web browser). Here's a diagram:
and then tapping on either of the two pages would make them full screen:
Pressing the menu button at the bottom would return you to the overview menu page.
One way I have thought about solving this would be to make static preview images out of the routes when the menu button is tapped, and just display the previews. But these won't be live, and I would like a more elegant approach that actually displays the live contents of the route if possible.
Another way I have thought about solving this would be to use a top level GoRouter and then two descendant GoRouters each containing just one branch of the routes. I'm not sure if multiple GoRouters would lead to problems with things like if I wanted to context.go() to another branch.
If the ShellRoute.builder gave me access to all of the child page's widgets, I could display them however I wanted, but it just provides a single child.
I have not worked with 'go_router' or 'ShellRoute.builder', but I like to make custom animated widgets like this for apps. It's also hard to explain how it would work in your app, but here is my take on this.
Try copy pasting this in an empty page. I have written some notes in code comments that might help explain things a little bit. And, this is not perfect but with more polishing according to the needs it could work.
class CustomPageView extends StatefulWidget {
const CustomPageView({Key? key}) : super(key: key);
#override
State<CustomPageView> createState() => _CustomPageViewState();
}
class _CustomPageViewState extends State<CustomPageView> {
// Scroll Controller required to control scroll via code.
// When user taps on the navigation buttons, we will use this controller
// to scroll to the next/previous page.
final ScrollController _scrollController = ScrollController();
// Saving screen width and height to use it for the page size and page offset.
double _screenWidth = 0;
double _screenHeight = 0;
// A bool to toggle between full screen mode and normal mode.
bool _viewFull = false;
#override
void initState() {
super.initState();
// Get the screen width and height.
// This will be used to set the page size and page offset.
// As of now, this only works when page loads, not when orientation changes
// or page is resized. That requires a bit more work.
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
_screenWidth = MediaQuery.of(context).size.width;
_screenHeight = MediaQuery.of(context).size.height;
});
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
// 'Column' to wrap the 'Body' and 'BottomNavigationBar'
body: Column(
children: [
// 'Expanded' to take up the remaining space after the 'BottomNavigationBar'
Expanded(
// A 'Container' to wrap the overall 'Body' and aligned to center.
// So when it resizes, it will be centered.
child: Container(
alignment: Alignment.center,
// 'AnimatedContainer' to animate the overall height of the 'Body'
// when user taps on the 'Full Screen' button.
child: AnimatedContainer(
duration: const Duration(milliseconds: 500),
height: _viewFull ? 200 : _screenHeight,
// A 'ListView' to display the pages.
// 'ListView' is used here because we want to scroll horizontally.
// It also enables us to use 'PageView' like functionality, but
// requires a bit more work, to make the pages snap after scrolling.
child: ListView(
controller: _scrollController,
scrollDirection: Axis.horizontal,
children: [
// A 'Container' to display the first page.
AnimatedContainer(
duration: const Duration(milliseconds: 500),
width: _viewFull ? (_screenWidth / 2) - 24 : _screenWidth,
margin: _viewFull ? const EdgeInsets.all(12) : const EdgeInsets.all(0),
color: Colors.blue,
),
// A 'Container' to display the second page.
AnimatedContainer(
duration: const Duration(milliseconds: 500),
width: _viewFull ? (_screenWidth / 2) - 24 : _screenWidth,
margin: _viewFull ? const EdgeInsets.all(12) : const EdgeInsets.all(0),
color: Colors.yellow,
),
],
),
),
),
),
// 'BottomNavigationBar' to show the navigation buttons
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// 'Feature 1' button
GestureDetector(
onTap: () {
// Scroll to the first page
_scrollController.animateTo(
0,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
);
},
child: Container(
height: 60,
alignment: Alignment.center,
color: Colors.red,
padding: const EdgeInsets.all(12),
child: const Text('Feature 1'),
),
),
// 'Feature 2' button
GestureDetector(
onTap: () {
// Scroll to the second page
_scrollController.animateTo(
_screenWidth,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
);
},
child: Container(
height: 60,
alignment: Alignment.center,
color: Colors.green,
padding: const EdgeInsets.all(12),
child: const Text('Feature 2'),
),
),
// 'Full Screen' button
GestureDetector(
onTap: () {
// Toggle between full screen mode and normal mode
setState(() {
_viewFull = !_viewFull;
});
},
child: Container(
height: 60,
alignment: Alignment.center,
color: Colors.purple,
padding: const EdgeInsets.all(12),
child: const Text('View Full'),
),
),
],
),
],
),
);
}
}

How do you animate to expand a container from 0 height to the height of its contents in Flutter?

I have a container that starts at zero height and needs to be expanded after a user interaction.
I tried using AnimatedContainer / AnimatedSize and changing the child widget's height from 0 to null, but in both cases, Flutter complains that it cant' interpolate from 0 to null.
I've also tried using BoxConstraints (with expanded using maxHeight = double.infinity) instead of explicit heights, in which case Flutter complains it can't interpolate from a finite value to an indefinite one.
I've also tried setting mainAxisSize to min/max, in which case Flutter complains that vsync is null.
How do I animate expanding a widget such that it dynamically grows big enough to wrap its contents? And if this can't be done dynamically, what's a safe way to size contents such that they make sense across screen sizes? In web dev, I know things like em are sort of relative sizing, but in the context of Flutter, I don't see how to control the size of things reliably.
Update: As suggested by #pskink, wrapping the child in an Align widget and animating Align's heightFactor param accomplishes collapsing. However, I'm still having trouble getting collapse to work when the collapsing child itself has children. For example, Column widgets don't clip at all with ClipRect (see https://github.com/flutter/flutter/issues/29357), and even if I use Wrap instead of Column, that doesn't work if the Wrap's children are Rows. Not sure how to get clipping to work consistently.
Maybe you could also solve this with a SizeTransition?
class VariableSizeContainerExample extends StatefulWidget {
VariableSizeContainerExample();
#override
_VariableSizeContainerExampleState createState() => _VariableSizeContainerExampleState();
}
class _VariableSizeContainerExampleState extends State<VariableSizeContainerExample> with TickerProviderStateMixin {
AnimationController _controller;
Animation<double> _animation;
#override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 1),
vsync: this,
);
_animation = CurvedAnimation(
parent: _controller,
curve: Curves.fastLinearToSlowEaseIn,
);
}
_toggleContainer() {
print(_animation.status);
if (_animation.status != AnimationStatus.completed) {
_controller.forward();
} else {
_controller.animateBack(0, duration: Duration(seconds: 1));
}
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: SafeArea(
child: Column(
children: [
TextButton(
onPressed: () => _toggleContainer(),
child: Text("Toggle container visibility"),
),
SizeTransition(
sizeFactor: _animation,
axis: Axis.vertical,
child: Container(
child: Text(
"This can have variable size",
style: TextStyle(fontSize: 40),
),
),
),
Text("This is below the above container"),
],
),
),
),
);
}
}
Moving #pskink's comments to an answer for posterity:
The main concept is that the Align widget has a property called heightFactor, which takes a double between 0 and 1 to scale its child's height (there's also a similar widthFactor property for width). By animating this property, we can collapse/expand the child. For example:
ClipRect(
child: Align(
alignment: alignment,
child: Align(
alignment: innerAlignment,
widthFactor: constantValue,
heightFactor: animatedValue.value,
child: builder(context, animation),
),
)
)
where animatedValue is of type Animation<double>, and ClipReact is used to clip/truncate the child widget. Note that ClipReact needs to be wrapped outside the Align widget; it doesn't work consistently when wrapping Align's child widget.
Edit: it's also necessary for the recipient of the animation to be an AnimatedWidget for things to go smoothly. See selected answer for an approach that handles this for you.

How to scroll withing a widget with a button?

I am building a WebApp in flutter and I have a SingleChildScrollView with some widgets inside. I want the buttons on the appbar to take me to the correspondent widget when I press on the buttons.
Is that possible? Here I attach the part of the code.
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: true,
appBar: CustomAppBar(),
backgroundColor: Colors.white,
body: SingleChildScrollView(
controller: widget.homeController,
child: Column(
children: [
Inicio(),
Services(),
QuienesSomos(),
ContactForm(),
BottomInfo(),
],
),
),
);
}
So I have one button on the appbar per each children in the SingleChildScrollView and I would like that when I press the correspondent button, it scrolls down to the correspondent section on the widget. I tried with Navigator.of().PushNamed but it opens a new screen instead of scrolling down. Any ideas? Thanks in advance!
To control the position, you have to manage the controller of the SingleChildScrollView .
If you want to smoothly go a section, you can attach functionality to control the SingleChildScrollView controller to the button:
widget.homeController.animateTo(
0.0, // change 0.0 {double offset} to corresponding widget position
duration: Duration(seconds: 1),
curve: Curves.easeOut,
);
If you just want to instantly jump to the position:
widget.homeController.jumpTo(0.0); // change 0.0 {double value} to corresponding widget position
Make a scroll controller:
ScrollController myController = ScrollController();
and attach it to your SingleChildScrollView widget:
SingleChildScrollView(
controller: myController,
child: ...
Now create a GlobalKey:
final anchor = GlobalKey();
Attach it to any of your widget:
Container(
key: anchor,
child: ...
),
That's it, now you can programmatically scroll to this widget using scroll controller:
myController.position.ensureVisible(
anchor.currentContext.findRenderObject(),
alignment: 0.5,
duration: const Duration(seconds: 1),
);
I could achieve my goal by using,
onPressed: () {
Scrollable.ensureVisible(servicesKey.currentContext,
duration: Duration(seconds: 1),
curve: Curves.easeOut);
},
and by asigning the corresponding key in each widget.

Animating TransformationController

So I have a picture in an interactive viewer. I want to reset the Pan and Zoom every time the user is done changing it. I am successfully able to do this using the TransformationController and onInteractionEnd callback. However, of course, the reset is very abrupt, so I would like to animate it to provide a smooth transition. I have no experience with Flutter animations and this is the best I could come up with.
class _PhotoZoom extends State<PhotoZoom> with TickerProviderStateMixin {
TransformationController transformationController =
new TransformationController();
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Center(
child: InteractiveViewer(
transformationController: transformationController,
child: Image.network(imageUrl),
onInteractionEnd: (ScaleEndDetails details) {
_resetPanZoom();
},
),
),
],
),
);
}
_resetPanZoom() {
AnimationController animationController =
AnimationController(duration: const Duration(seconds: 2), vsync: this);
Animation<Matrix4> animation = Matrix4Tween(
begin: transformationController.value, end: Matrix4.identity())
.animate(animationController);
transformationController.value = animation.value;
}
}
What I am trying to do is smoothly change the controller's changed Matrix4 variable to Matrix4.identity() which will reset the image to original Pan and Zoom. But yes this doesn't work as I am probably doing something wrong. What would be the right way to do this?

How to animate an image moving across the screen?

I have a fixed size image (of a playing card) and I'd like to write code so users see the image slide from one part of the screen to another (like a card being dealt and moving across the surface). If possible, it would be best to do so in a way that's moderately responsive for different screen sizes.
Most of what I've seen or learned about involves Hero widgets or animation where a widget changes size but stays in the same location. I'm asking about something different.
What you want is the animatedPosition widget, it will move any widget from one point of the screen to another, you can even morph the size of the widget.
https://api.flutter.dev/flutter/widgets/AnimatedPositioned-class.html
AnimatedPositioned(
width: selected ? 200.0 : 50.0,
height: selected ? 50.0 : 200.0,
top: selected ? 50.0 : 150.0,
duration: const Duration(seconds: 2),
curve: Curves.fastOutSlowIn,
child: GestureDetector(
onTap: () {
setState(() {
selected = !selected;
});
},
child: Container(
color: Colors.blue,
child: const Center(child: Text('Tap me')),
),
),
),
You can either use Flutter's built in animations Animation<double>. In Flutter, an Animation object knows nothing about what is onscreen. An Animation is an abstract class that understands its current value and its state (completed or dismissed). One of the more commonly used animation types is Animation<double>.
For example:
// lib/main.dart (AnimatedLogo)
class AnimatedLogo extends AnimatedWidget {
AnimatedLogo({Key key, Animation<double> animation})
: super(key: key, listenable: animation);
Widget build(BuildContext context) {
final animation = listenable as Animation<double>;
return Center(
child: Container(
margin: EdgeInsets.symmetric(vertical: 10),
height: animation.value,
width: animation.value,
child: FlutterLogo(),
),
);
}
}
Or you can import and use one of these packages into your project:
https://pub.dev/packages/simple_animations
https://pub.dev/packages/animator