Widgets stacking at the bottom when animating a PageRoute Transition - animation

I'm trying to animate a screen sliding in from the right and the current screen sliding out to the left at the same time as if the screens are joined together.
I made a custom PageRouteBuilder and transition as follows:
class SlideInRightRoute extends PageRouteBuilder {
final Widget exitPage;
final Widget enterPage;
SlideInRightRoute({this.exitPage, this.enterPage})
: super(
pageBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) =>
enterPage,
transitionsBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
var exitBegin = Offset.zero;
var exitEnd = Offset(-1.0, 0.0);
var exitTween = Tween(begin: exitBegin, end: exitEnd);
var enterBegin = Offset(1.0, 0.0);
var enterEnd = Offset.zero;
var enterTween = Tween(begin: enterBegin, end: enterEnd);
return Stack(
children: <Widget>[
SlideTransition(
position: exitTween.animate(animation),
child: exitPage,
),
SlideTransition(
position: enterTween.animate(animation),
child: enterPage,
)
],
);
});
}
The exiting screen is
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: kWhite,
body: Column(
children: <Widget>[
Expanded(
flex: 38,
child: /* stack with containers, image, and icon */ ,
),
Expanded(
flex: 50,
child: /* column with text widgets */,
),
Expanded(
flex: 12,
child: /* container with text widget */,
),
],
),
);
}
}
The entering screen is just a scaffold with text.
When I trigger the page route, it's almost perfect. The exiting screen slides off to the left, and the new screen slides in from the right. The only problem is that the widgets in the exiting screen all quickly drop to the bottom of the screen. When I navigate back to this screen, the widgets are briefly in the bottom, then get redrawn in the correct locations.
Why is this happening and how can I have the widgets stay where they are as the new page transitions in?
I have a feeling it is because of the expanded box widgets perhaps not knowing the size of the screen or something during the transition.
TLDR: trying an animation of a screen coming in from the left and pushing out the current screen. exiting screen widgets are redrawn at the bottom as the screen exits.
Thank you for your help.

Figured it out.
I was using the wrong widget when calling SlideInRightRoute.

Related

Change the value of a Text widget by animating the opacity down to 0 and back to 1 in Flutter

How can I do this in Flutter:
I want to update the value of a Text widget when a FloatingActionButton is pressed:
Animate the opacity of the Text widget down to 0 for a duration of 1 second (hide the old value)
Change the value of the Text widget (set a new value - setState?)
Animate the opacity of the Text widget up to 1 for a duration of 1 second (show the new value)
Can I do this using AnimatedOpacity?
Yes you can, don't forget to await before calling the second setState to reanimate the text back.
String text = 'Text Initial';
double opacity = 1.0;
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () async {
setState(() {
opacity = 0.0;
});
await Future.delayed(const Duration(seconds: 1));
setState(() {
text = 'New Text';
opacity = 1.0;
});
},
),
body: Center(
child: AnimatedOpacity(
duration: const Duration(seconds: 1),
opacity: opacity,
child: Text(text),
),
),
),
);
}

Flutter LinearGradient Animation for Draggable

I have a Draggable Container with the following decoration.
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [ThemeColors.red, ThemeColors.yellow, ThemeColors.green])
I would like to have it animated that my frame is getting greener or redder, further I drag left or right.
Here's a simple example to detect horizontal drag and change between colors inside your gradient:
class GradientScreen extends StatefulWidget {
#override
_GradientScreenState createState() => _GradientScreenState();
}
class _GradientScreenState extends State<GradientScreen> {
var percentage = 0.0;
#override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
return Scaffold(
appBar: AppBar(
title: Text('Animated Drag Gradient'),
centerTitle: true,
),
body: GestureDetector(
onHorizontalDragUpdate: (details) {
setState(() => percentage = (details.localPosition.dx - 0) / (width - 0));
},
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
_colorTween(Colors.green[900], Colors.red[900]),
Colors.yellow,
_colorTween(Colors.green[900], Colors.red[900])
],
)
),
),
),
);
}
Color _colorTween(Color begin, Color end) {
return ColorTween(begin: begin, end: end).transform(percentage);
}
}
The result of this easy implementation is the following:

Flutter zoomable image with overlays

I'm trying to make a custom widget that the user can zoom and pan an image and tap to overlay points on the image. I think I'm close but can't quite get the points to line up under the tap.
I copied the relevant zoom-pan from here:
https://github.com/flutter/flutter/blob/master/examples/layers/widgets/gestures.dart
I also have my GestureDetector pulled out into its own widget as per this post to get the correct RenderBox for global to local offset conversion
flutter : Get Local position of Gesture Detector
import 'package:flutter/material.dart';
class ImageWithOverlays extends StatefulWidget {
final List<FractionalOffset> fractionalOffsets;
ImageWithOverlays(this.fractionalOffsets);
#override
_ImageWithOverlaysState createState() => _ImageWithOverlaysState();
}
class _ImageWithOverlaysState extends State<ImageWithOverlays> {
Offset _startingFocalPoint;
Offset _previousOffset;
Offset _offset = Offset.zero;
double _previousZoom;
double _zoom = 1.0;
#override
Widget build(BuildContext context) {
return GestureDetector(
onScaleStart: _handleScaleStart,
onScaleUpdate:_handleScaleUpdate,
onTapDown: (details){
setState(() {
widget.fractionalOffsets.add(_getFractionalOffset(context, details.globalPosition));
});
},
onDoubleTap: _handleScaleReset,
child: Transform(
transform: Matrix4.diagonal3Values(_zoom, _zoom, 1.0) + Matrix4.translationValues(_offset.dx, _offset.dy, 0.0),
child: Center(
child: Container(
child:Stack(
children: <Widget>[
Image.network(
'https://picsum.photos/250?image=9',
), ]
..addAll(_getOverlays(context)),
)),
),
));
}
void _handleScaleStart(ScaleStartDetails details) {
setState(() {
_startingFocalPoint = details.focalPoint;
_previousOffset = _offset;
_previousZoom = _zoom;
});
}
void _handleScaleUpdate(ScaleUpdateDetails details) {
setState(() {
_zoom = _previousZoom * details.scale;
// Ensure that item under the focal point stays in the same place despite zooming
final Offset normalizedOffset = (_startingFocalPoint - _previousOffset) / _previousZoom;
_offset = details.focalPoint - normalizedOffset * _zoom;
});
}
void _handleScaleReset() {
setState(() {
_zoom = 1.0;
_offset = Offset.zero;
widget.fractionalOffsets.clear();
});
}
List<Widget> _getOverlays(BuildContext context) {
return widget.fractionalOffsets
.asMap()
.map((i, fo) => MapEntry(
i,
Align(
alignment: fo,
child: _buildIcon((i + 1).toString(), context)
)
))
.values
.toList();
}
Widget _buildIcon(String indexText, BuildContext context) {
return FlatButton.icon(
icon: Icon(Icons.location_on, color: Colors.red),
label: Text(
indexText,
style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
),
);
}
Widget _buildCircleIcon(String indexText) {
return Container(
margin: const EdgeInsets.all(8.0),
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
child: Text(
indexText,
style: TextStyle(
fontSize: 16,
color: Colors.white,
fontWeight: FontWeight.bold
),
));
}
FractionalOffset _getFractionalOffset(BuildContext context, Offset globalPosition) {
var renderbox = context.findRenderObject() as RenderBox;
var localOffset = renderbox.globalToLocal(globalPosition);
var width = renderbox.size.width;
var height = renderbox.size.height;
return FractionalOffset(localOffset.dx/width,localOffset.dy/height);
}
}
But I'm having a rough time getting the layout right. I think I have two issues:
The image likes to jump to the upper left. I'm not sure how to get it to stay centered.
When I zoom in, my _getFractionalOffset function is not returning the correct fraction. The icons in this screenshot are not showing up under my tap
My matrix math is poor, as is my understanding of how Flutter does its transforms, so I would appreciate any insight.
Figured it out, I had a couple issues with my code that when compounded made debugging annoying.
First, I should have been multiplying the scale/translate matrices, not adding them:
vector.Matrix4 get _transformationMatrix {
var scale = vector.Matrix4.diagonal3Values(_zoom, _zoom, 1.0);
var translation = vector.Matrix4.translationValues(_offset.dx, _offset.dy, 0.0);
var transform = translation * scale;
return transform;
}
Second, I was trying to fuss around with doing the translating from local offset to the image frame instead of letting vector_math do it for me. The new overlay offset calculation:
Offset _getOffset(BuildContext context, Offset globalPosition) {
var renderbox = context.findRenderObject() as RenderBox;
var localOffset = renderbox.globalToLocal(globalPosition);
var localVector = vector.Vector3(localOffset.dx, localOffset.dy,0);
var transformed = Matrix4.inverted(_transformationMatrix).transform3(localVector);
return Offset(transformed.x, transformed.y);
}
Since _transformationmatrix takes a point from the image frame to the screen frame, I needed to left multiply the inverse to get from the screen frame (localOffset) to the image frame (returned value).
Finally, I'm using the Positioned widget instead of Align so I can set the positioning in pixels:
double _width = 100;
double _height = 50;
List<Widget> _getOverlays(BuildContext context) {
return widget.points
.asMap()
.map((i, offset) => MapEntry(
i,
Positioned(
left: offset.dx-_width/2,
width:_width,
top: offset.dy -_height/2,
height:_height,
child: _buildIcon((i + 1).toString(), context)
)
))
.values
.toList();
}
where widget.points is a List<Offset> of offsets returned by _getOffset above, and width/height are the width and height of my icon widget.
Hope this helps someone!

Container height Animation starts from the middle

The Flutter Container height animation starts from the middle, but I need it to start from the bottom here is my code
import 'dart:math';
import 'package:flutter/material.dart';
class CorrectWrongOverlay extends StatefulWidget {
final bool _isCorrect;
final VoidCallback _onTap;
final double percentR;
final double percentW;
CorrectWrongOverlay(
this._isCorrect, this.percentR, this.percentW, this._onTap);
#override
State createState() => CorrectWrongOverlayState();
}
class CorrectWrongOverlayState extends State<CorrectWrongOverlay>
with SingleTickerProviderStateMixin {
Animation<double> _iconAnimation;
AnimationController _iconAnimationController;
#override
void initState() {
super.initState();
_iconAnimationController = AnimationController(
duration: Duration(seconds: 3), vsync: this);
_iconAnimation = CurvedAnimation(
parent: _iconAnimationController, curve: Curves.fastOutSlowIn);
_iconAnimation.addListener(() => this.setState(() {}));
_iconAnimationController.forward();
}
#override
void dispose() {
_iconAnimationController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Material(
color: Colors.black54,
child: InkWell(
onTap: () => widget._onTap(),
child: Padding(
padding: const EdgeInsets.all(18.0),
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
width: 80.0,
height: 200.0 * _iconAnimation.value,
color: Colors.green,
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
width: 80.0,
height: 200.0,
color: Colors.green,
),
)
],
),
),
),
),
);
}
}
I am trying to achieve this kind of UI with height grow animations in Flutter I want the animations to start from the bottom but it starts from the center of the container and animated it both side.
to start scale animation from certain points. You can wrap your Container with Align widget and give certain start positions with simplicty.
define your controller and animation variables;
AnimationController animationController;
Animation<double> tween;
init them in initState method;
#override
void initState() {
super.initState();
animationController = AnimationController(vsync: this, duration: Duration(milliseconds: 800));
tween = Tween<double>(begin:0,end:1).animate(CurvedAnimation(parent: animationController, curve: Curves.easeIn));
animationController.addListener(() {
setState(() {});
});
}
then in build method or where ever you add use return Widget like below;
Widget animatedContainer(){
return Align(
alignment: Alignment.topRight,// .topCenter, .topLeft, .bottomLeft, bottomCenter...etc
child: Container(
width: (size.width * tween.value).abs(),
height: (200 *tween.value).abs(),
color:Colors.red
),
);
}
You could use TweenMax for Flutter package: https://pub.dartlang.org/packages/tweenmax
Here is an example: https://github.com/mrgoonie/flutter_tweenmax/blob/master/lib/screens/animated_column_chart.dart
Click on the bottom button to animate those bars.
Cheers,
If you're animating it once and don't require advanced animation control, then you don't even need to keep track of AnimationController yourself. Just use TweenAnimationBuilder, pass to it your tween, duration and curve, and let it animate widget for you:
Widget _animatedBarWidget(Widget barWidget) => TweenAnimationBuilder<double>(
tween: Tween<double>(begin: 0.0, end: 1.0),
duration: const Duration(seconds: 3),
curve: Curves.fastOutSlowIn,
builder: (BuildContext context, double value, Widget? child) {
return Align(
alignment: Alignment.bottomCenter,
heightFactor: value,
child: child!,
);
},
child: barWidget,
);
Then, just call
_animatedBarWidget(Container(width: 80.0, height: 200.0, color:Colors.red));

How do I animate a large circle resizing down to a small circle on Long Press in Flutter?

I want to animate a large circle/image resizing down into a small circle/image using Flutter.
The small circle would be resized directly onto the pointer/my finger, and follow the pointer as it moves.
My first instinct is to use a GestureDetector's onLongPress but I'm not really sure how to tell the circle to adjust its radius and have it follow the pointer.
You can use a LongPressDraggable widget. Here is a sample (assume currentHeight/currentWidth/circkeDiameter have already been defined):
import 'dart:ui' show lerpDouble;
Animation animation = new AnimationController();
new AnimatedBuilder(
controller: animation,
child: new Container(),
builder: (_, Widget child) {
return new Container(
width: lerpDouble(currentWidth, circleDiameter, animation.value),
height: lerpDouble(currentHeight, circleDiameter, animation.value),
child: new ClipRRect(
borderRadius: new BorderRadius.circular(
(circleDiameter / 2.0) * animation.value,
),
child: child,
),
);
});
and here is an example: https://github.com/fuchsia-mirror/modules-video/blob/master/modules/video/lib/src/widgets/screen.dart#L87
If you didn't care about the small circle following your pointer as you drag (i.e. you just want to see the resizing animation), you could maybe use an AnimatedCrossFade widget, whose crossFadeState uses a flag that is set by a GestureDetector's onLongPress. E.g. something like:
bool didLongPress = false;
return new GestureDetector(
onLongPress: setDidLongPressToTrue(),
child: new AnimatedCrossFade(
duration: someAnimationTime,
firstChild: largeCircle,
secondChild: smallCircle,
crossFadeState: didLongPressed
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
),
);

Resources