I've got a simple AnimatedWidget with one child widget.
AnimatedContainer(
duration: Duration(milliseconds: 2000),
curve: Curves.bounceOut,
decoration: BoxDecoration(
color: Colors.purple,
),
child: FlutterLogo(
size: _boxSize,
),
),
where _boxSize is being animated like so:
void _startAnimation() => setState(() {
_boxSize *= 1.7;
});
AnimatedContainer is not working for child widgets, however. You need to change direct properties of AnimatedContainer for the animation to work.
This is in compliance with documentation:
The [AnimatedContainer] will automatically animate between the old
and new values of properties when they change using the provided curve
and duration. Properties that are null are not animated.
Its child and descendants are not animated.
What is the equivalent of AnimatedContainer which is ALSO ABLE to animate its children?
There are few widgets which will animate the child. You can swap the new flutter logo widget with preferred size using AnimatedSwitcher Widget.
AnimatedSwitcher - This widget will swap the child widget with a new widget.
AnimatedPositioned - It'll change the position of the child from the stack widget whenever the given position changes.
AnimatedAlign - Animated version of align which will change the alignment of the child whenever the given alignment changes.
AnimatedCrossFade - It fades between two children and animate itself between their sizes.
There is no magic widget which would simply recursively animate all children. But I think what you want is an implicitly animated widget. ie. you change the constructor parameters of a widget, and as it changes it animates from one value to the next.
The easiest way is probably the ImplicitlyAnimatedWidget with a AnimatedWidgetBaseState. So for your example to animate a boxSize attribute this could look like:
class AnimatedFlutterLogo extends ImplicitlyAnimatedWidget {
const AnimatedFlutterLogo({Key key, #required this.boxSize, #required Duration duration})
: super(key: key, duration: duration);
final double boxSize;
#override
ImplicitlyAnimatedWidgetState<ImplicitlyAnimatedWidget> createState() => _AnimatedFlutterLogoState();
}
class _AnimatedFlutterLogoState extends AnimatedWidgetBaseState<AnimatedFlutterLogo> {
Tween<double> _boxSize;
#override
void forEachTween(visitor) {
_boxSize = visitor(_boxSize, widget.boxSize, (dynamic value) => Tween<double>(begin: value));
}
#override
Widget build(BuildContext context) {
return Container(
child: FlutterLogo(
size: _boxSize?.evaluate(animation),
),
);
}
}
which is imho already pretty concise, the only real boilerplate is basically the forEachTween(visitor) method which has to create Tween objects for all properties you'd like to animate.
Related
I'm new to Flutter and Dart. Hopes to get some guide on a tutorial exercise I'm stuck.
I'm following Flutter Codelab https://codelabs.developers.google.com/codelabs/flutter/index.html?index=..%2F..index#6 and able to do everything.
There's an exercise it ask us to do that is
Create a fade-in animation effect by wrapping the Container in a FadeTransition widget instead of a SizeTransition.
The code as below
#override
Widget build(BuildContext context) {
return new SizeTransition(
sizeFactor: new CurvedAnimation(
parent: animationController, curve: Curves.easeOut),
axisAlignment: 0.0,
child: new Container(
// ... other codes ...
),
);
}
So I change to FadeTransition, which requires opacity of type Animation<Double>
#override
Widget build(BuildContext context) {
return new FadeTransition(
opacity: animation
child: new Container(
// ... other codes ...
),
);
}
How could I create or send in the animation? (the above code will have animation unrecognizable).
You can try this
opacity: Tween<double>(
begin: 0.0,
end: 1.0,
).animate(animationController),
CurvedAnimation is used for non-linear animation.
See more detail here https://flutter.dev/docs/development/ui/animations/tutorial
Found the answer, by referring to https://proandroiddev.com/getting-your-hands-dirty-with-flutter-basic-animations-6b9f21fa7d17 and modify accordingly.
So to have FadeTransition, basically just replace
opacity: animation
with
opacity: new CurvedAnimation(parent: animationController, curve: Curves.easeIn),
This is not ideal as every time a message is inserted, it is creating a new CurveAnimation, but for the sake of concise solution, I make it so.
Is it possible in flutter to have the bottom sheet partially viewable at an initial state and then be able to either expand/dismiss?
I've included a screenshot of an example that Google Maps implements.
Use the DraggableScrollableSheet widget with the Stack widget:
Here's the gist for the entire page in this^ GIF, or try the Codepen.
Here's the structure of the entire page:
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: <Widget>[
CustomGoogleMap(),
CustomHeader(),
DraggableScrollableSheet(
initialChildSize: 0.30,
minChildSize: 0.15,
builder: (BuildContext context, ScrollController scrollController) {
return SingleChildScrollView(
controller: scrollController,
child: CustomScrollViewContent(),
);
},
),
],
),
);
}
In the Stack:
- The Google map is the lower most layer.
- The Header (search field + horizontally scrolling chips) is above the map.
- The DraggableBottomSheet is above the Header.
Some useful parameters as defined in draggable_scrollable_sheet.dart:
/// The initial fractional value of the parent container's height to use when
/// displaying the widget.
///
/// The default value is `0.5`.
final double initialChildSize;
/// The minimum fractional value of the parent container's height to use when
/// displaying the widget.
///
/// The default value is `0.25`.
final double minChildSize;
/// The maximum fractional value of the parent container's height to use when
/// displaying the widget.
///
/// The default value is `1.0`.
final double maxChildSize;
Edit: Thank you #Alejandro for pointing out the typo in the widget name :)
I will be implementing the same behaviour in the next few weeks and I will be referring to the backdrop implementation in Flutter Gallery, I was able to modify it previously to swipe to display and hide (with a peek area).
To be precise you can replicate the desired effect by changing this line of code in backdrop_demo.dart from Flutter Gallery :
void _handleDragUpdate(DragUpdateDetails details) {
if (_controller.isAnimating)// || _controller.status == AnimationStatus.completed)
return;
_controller.value -= details.primaryDelta / (_backdropHeight ?? details.primaryDelta);
}
I have just commented the controller status check to allow the panel to be swipe-able.
I know this isn't the complete implementation you are looking for, but I hope this helps you in any way.
How do i implement the swipe from the left to go back gesture in flutter? Not sure if it was already implemented automatically for iOS, but I wanted it for Android as well (as things are becoming gesture based).
Use CupertinoPageRoute to make it work on Android;
import 'package:flutter/cupertino.dart';
(as answered on How to implement swipe to previous page in Flutter?)
You could set your Theme.platform to TargetPlatform.ios. This will make use that the swipe back gesture is used on every device.
You can use CupertinoPageRoute() as Tom O'Sullivan said above.
However, if you want to customize it (eg. using custom transition duration) using PageRouteBuilders and get the same swipe to go back gesture, then you can override buildTransitions().
For iOS, the default PageTransitionBuilder is CupertinoPageTransitionsBuilder(). So we can use that in buildTransitions(). This automatically give us the swipe right to go back gesture.
Here's some sample code for the CustomPageRouteBuilder:
class CustomPageRouteBuilder<T> extends PageRoute<T> {
final RoutePageBuilder pageBuilder;
final PageTransitionsBuilder matchingBuilder = const CupertinoPageTransitionsBuilder(); // Default iOS/macOS (to get the swipe right to go back gesture)
// final PageTransitionsBuilder matchingBuilder = const FadeUpwardsPageTransitionsBuilder(); // Default Android/Linux/Windows
CustomPageRouteBuilder({this.pageBuilder});
#override
Color get barrierColor => null;
#override
String get barrierLabel => null;
#override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
return pageBuilder(context, animation, secondaryAnimation);
}
#override
bool get maintainState => true;
#override
Duration get transitionDuration => Duration(milliseconds: 900); // Can give custom Duration, unlike in MaterialPageRoute
#override
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
return matchingBuilder.buildTransitions<T>(this, context, animation, secondaryAnimation, child);
}
}
Then to go to a new page:
GestureDetector(
onTap: () => Navigator.push(
context,
CustomPageRouteBuilder(pageBuilder: (context, animation, secondaryAnimation) => NewScreen()),
),
child: ...,
)
You can set the platform of your theme (and darkTheme) to TargetPlatform.iOS, you can set the pageTransitionsTheme of your themes to,
pageTransitionsTheme: PageTransitionsTheme(
builders: {
TargetPlatform.android: CupertinoPageTransitionsBuilder(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
},
),
and you can load the new page using CupertinoPageRoute ... and none of that will work until you make sure to use Navigator.push (instead of Navigator.pushReplacement) to get to that new screen! I hope this helps anyone out there who was working with existing transitions and didn't notice this crucial detail. :)
Use this plugin:
https://pub.dev/packages/cupertino_back_gesture
A Flutter package to set custom width of iOS back swipe gesture area.
For basic use:
import 'package:cupertino_back_gesture/cupertino_back_gesture.dart';
BackGestureWidthTheme(
backGestureWidth: BackGestureWidth.fraction(1 / 2),
child: MaterialApp(
theme: ThemeData(
pageTransitionsTheme: PageTransitionsTheme(
builders: {
//this is default transition
//TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(),
//You can set iOS transition on Andoroid
TargetPlatform.android: CupertinoPageTransitionsBuilderCustomBackGestureWidth(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilderCustomBackGestureWidth(),
},
),
),
home: MainPage(),
),
)
More details on plugin's page
in my case, the solution turned out to be very simple. I just used context.push('screen') instead of context.go('/screen')
This should not be implemented on Android since it makes interactions inconsistent across the OS.
Swiping from the screens edge to go back is not something that Android wants you to implement, so you should better don't do it.
I'm trying to display a widget once I have info about the max scroll extent. I can find that number if I assign an instance of ScrollController to the controller property of a scrollable widget.
My problem is that the ScrollController gets attached to the scrollable widget during the build, so I can not use the max scroll extent number before the first build. Thus what I was trying to do is display an empty Container in the first build and then switch that empty Container with the widget I actually want. Something like this:
_scrollController.positions.length == 0 ? new Container() : new Align(
alignment: Alignment.center,
child: new Container(
width: constraints.maxWidth,
height: 50.0,
color: Colors.black,
)
)
Now this does not work of course because _scrollController.positions.length will be 0 at the beginning and nowhere do I call setState when this value changes (when the controller gets attached).
So my question: Is there a place where I can get notified whenever the ScrollController gets attached to a scrollable widget? Or is there a better approach for this?
If the scrollable is widget.child.
#override
Widget build(BuildContext context) {
return new NotificationListener<ScrollNotification>(
onNotification: _handleScrollNotification,
child: widget.child,
);
}
bool _handleScrollNotification(ScrollNotification notification) {
if (notification is ScrollUpdateNotification || notification is OverscrollNotification) {
widget.child.update(notification.metrics);
}
return false;
}
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => afterFirstLayout(context));
}
void afterFirstLayout(BuildContext context) {
applyInitialScrollPosition();
}
void applyInitialScrollPosition() {
ScrollController scrollControler = widget.child.controller;
ScrollPosition position = scrollControler.position;
ScrollNotification notification = ScrollUpdateNotification(
metrics: FixedScrollMetrics(
minScrollExtent: position.minScrollExtent,
maxScrollExtent: position.maxScrollExtent,
pixels: position.pixels,
viewportDimension: position.viewportDimension,
axisDirection: position.axisDirection),
context: null,
scrollDelta: 0.0);
_handleScrollNotification(notification);
}
The child must extends ChangeNotifier and has an update method:
void update(ScrollMetrics metrics) {
assert(metrics != null);
_lastMetrics = metrics; // Save the metrics.
notifyListeners();
}
All this only works if a scroll controller has explicitly been defined for the scrollable (widget.child).
I am trying to draw a widget whenever a user presses the screen.
Currently I am doing this by storing a list of widgets and when ontapup is fired on the gesture i am adding to a list of widgets.
Widget build(BuildContext context) {
Widget draw = new Text("A");
List<Widget> children = new List<Widget>();
return new Scaffold(
appBar: new AppBar(
title: const Text('Heading'),
leading: new Icon(Icons.question_answer),
),
body: new GestureDetector(
onTapUp: (details) {
setState(() {
children.add(new Positioned(
left: details.globalPosition.dx,
top: details.globalPosition.dy,
child: draw,
));
});
},
child: new Stack(children: children)
...
So my code is working I am drawing the widget when I click but my problem is that when adding the new Positioned() to stack the position is based on the screen which does not include the appbar offset. Is there a way to get the stacks initial x/y position? Or is there a way to get the appbars height? How do I get a widgets position or height/width?
Ok for anyone else who has the same issue I needed to create my own widget and use
context.findRenderObject()
and
globalToLocal()
Just FYI global to local did not work while in the one solution I needed to make it its own widget.
To get the offset of a widget, you must get the renderObject, cast it as a RenderBox, and then convert it's local position to a global position. Like this:
#override
Widget build(BuildContext context) {
RenderBox renderBox = context.findRenderObject();
Offset widgetOffset = renderBox.localToGlobal(Offset.zero);
print("X: ${widgetOffset.dx}");
print("Y: ${widgetOffset.dy}");
}
If you need the position of a child widget, you can wrap that child in a LayoutBuilder, like this:
Container(
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints box) {
RenderBox renderBox = context.findRenderObject();
Offset widgetOffset = renderBox.localToGlobal(Offset.zero);
print("X: ${widgetOffset.dx}");
print("Y: ${widgetOffset.dy}");
}
)
)