Flutter - ClipPath + AnimatedContainer - Path not animating properly - animation

I am looking to animate both the AnimatedContainer and the ClipPath moving on the y-axis.
This is my current code:
class Clip extends StatelessWidget {
final double height;
Clip(this.height);
#override
Widget build(BuildContext context) {
return Scaffold(
body: ClipPath(
clipper: RoundedClipper(height),
child: AnimatedContainer(
duration: Duration(seconds: 5),
height: height,
color: Colors.amber,
),
),
);
}
}
class RoundedClipper extends CustomClipper<Path> {
final double height;
RoundedClipper(this.height);
#override
Path getClip(Size size) {
var path = Path();
path.lineTo(0.0, height - 200);
path.quadraticBezierTo(
size.width / 2,
height,
size.width,
height - 200,
);
path.lineTo(size.width, 0.0);
path.close();
return path;
}
#override
bool shouldReclip(CustomClipper<Path> oldClipper) => true;
}
based on the height I pass to this class the AnimatedContainer will transition between the two with an animation, while the clipper won't animate.
This is how it looks:
I tried wrapping the clipper with an AnimatedContainer and set the animation on that but it didn't work out.
How can I make it so that the clipped path animates vertically along with the AnimatedContainer?
Thanks to anyone who's gonna want to help

Animated container uses its own animation, so, I don't know if both clip path and animated container can work with same animation together. But I tried similar approach for what you need by using custom animation. Please, have a look and see if this is what you want.
I converted clip class to stateful for use of animation.
class Clip extends StatefulWidget {
final double height;
Clip(this.height);
#override
_ClipState createState() => _ClipState();
}
class _ClipState extends State<Clip> with TickerProviderStateMixin {
AnimationController _controller;
Animation<double> animation;
final double startingHeight =20.0;
#override
void initState() {
super.initState();
_controller =
AnimationController(vsync: this, duration: Duration(seconds: 5));
animation = Tween<double>(begin: startingHeight, end: widget.height).animate(_controller);
_controller.forward(from: 0.0);
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: AnimatedBuilder(
builder: (context, anim) {
return ClipPath(
clipper: RoundedClipper(animation.value),
child: Container(
height: animation.value,
color: Colors.amber,
),
);
},
animation: _controller,
),
);
}
}
here, you can control your animation using _controller.

This will not work properly, the easiest way is to create another widget (screen) and navigate to it, and add the target container (after the animation is done) and the encapsulate both containers in both screens with the (Hero) widget with the same tag key

Related

Flutter Image with Gradient Border color

how can I achieve something like this in Flutter:
Just replace Text to Image.network
Use below code
UnicornOutlineButton(
strokeWidth: 2,
radius: 24,
gradient: LinearGradient(
colors: [Colors.black, Colors.redAccent]),
child: Text('OMG', style: TextStyle(fontSize: 16)),
onPressed: () {},
),
Make new class with the name UnicornOutlineButton
class UnicornOutlineButton extends StatelessWidget {
final _GradientPainter _painter;
final Widget _child;
final VoidCallback _callback;
final double _radius;
UnicornOutlineButton({
#required double strokeWidth,
#required double radius,
#required Gradient gradient,
#required Widget child,
#required VoidCallback onPressed,
}) : this._painter = _GradientPainter(strokeWidth: strokeWidth, radius: radius, gradient: gradient),
this._child = child,
this._callback = onPressed,
this._radius = radius;
#override
Widget build(BuildContext context) {
return CustomPaint(
painter: _painter,
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: _callback,
child: InkWell(
borderRadius: BorderRadius.circular(_radius),
onTap: _callback,
child: Container(
constraints: BoxConstraints(minWidth: 88, minHeight: 48),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
_child,
],
),
),
),
),
);
}
}
class _GradientPainter extends CustomPainter {
final Paint _paint = Paint();
final double radius;
final double strokeWidth;
final Gradient gradient;
_GradientPainter({#required double strokeWidth, #required double radius, #required Gradient gradient})
: this.strokeWidth = strokeWidth,
this.radius = radius,
this.gradient = gradient;
#override
void paint(Canvas canvas, Size size) {
// create outer rectangle equals size
Rect outerRect = Offset.zero & size;
var outerRRect = RRect.fromRectAndRadius(outerRect, Radius.circular(radius));
// create inner rectangle smaller by strokeWidth
Rect innerRect = Rect.fromLTWH(strokeWidth, strokeWidth, size.width - strokeWidth * 2, size.height - strokeWidth * 2);
var innerRRect = RRect.fromRectAndRadius(innerRect, Radius.circular(radius - strokeWidth));
// apply gradient shader
_paint.shader = gradient.createShader(outerRect);
// create difference between outer and inner paths and draw it
Path path1 = Path()..addRRect(outerRRect);
Path path2 = Path()..addRRect(innerRRect);
var path = Path.combine(PathOperation.difference, path1, path2);
canvas.drawPath(path, _paint);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => oldDelegate != this;
}

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:

Widgets stacking at the bottom when animating a PageRoute Transition

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.

Flutter code creating laggy animation. What is wrong here?

I have created a flutter widget that moves the circle with accelerometer. It is very laggy as I have to use setState to change the position of the circle when phone is moved. Is there an alternative to creating this?
I have used AnimatedBuilder here but not sure how that can change the position of circle when device is moved smoothly.
class _AnimationWidgetState extends State<AnimationWidget>
with TickerProviderStateMixin {
AnimationController _animeController;
Animation _anime;
double x = 0.0, y = 0.0;
#override
void initState() {
super.initState();
_animeController =
AnimationController(vsync: this, duration: const Duration(seconds: 2));
_anime = Tween(begin: 0.5, end: 0.5).animate(
CurvedAnimation(parent: _animeController, curve: Curves.ease));
accelerometerEvents.listen((AccelerometerEvent event) {
var a = ((event.x * 100).round() / 100).clamp(-1.0, 1.0) * -1;
var b = ((event.y * 100).round() / 100).clamp(-1.0, 1.0);
if ((x - a).abs() > 0.02 || (y - b).abs() > 0.02) {
setState(() {
x = a; y = b;
});
}
});
}
#override
void dispose() {
_animeController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
_animeController.forward();
final double width = MediaQuery.of(context).size.width;
final double height = MediaQuery.of(context).size.height;
return AnimatedBuilder(
animation: _animeController,
builder: (context, child) {
return Scaffold(
body: Transform(
transform: Matrix4.translationValues(
_anime.value * width * x, _anime.value * height * y, 0.0),
child: Center(
child: CircleAvatar(
radius: 15.0,
backgroundColor: Colors.green,
),
),
),
);
},
);
}
}
Animation is not at all smooth. This is because I have to use setState but the movement of circle is working as desired.
The whole purpose of using AnimationController is to listen to its events - that's what AnimatedBuilder does and rebuilds its subtree accordingly.
I will post here my overall recommendations on what to change in the code.
Remove setState - that's what makes your entire layout rebuild all over again, i.e. lags.
Also trigger _animeController listeners, i.e. AnimatedBuilder in your case - to rebuild itself.
accelerometerEvents.listen((AccelerometerEvent event) {
var a = ((event.x * 100).round() / 100).clamp(-1.0, 1.0) * -1;
var b = ((event.y * 100).round() / 100).clamp(-1.0, 1.0);
if ((x - a).abs() > 0.02 || (y - b).abs() > 0.02) {
x = a; y = b;
_animeController.value = _animeController.value; // Trigger controller's listeners
}
});
Start animation in initState, instead of build. This is the second thing that produces lags in your case. .forward triggers rebuild of your widgets, which leads to an infinite loop.
#override
void initState() {
super.initState();
_animeController.forward();
}
Use child property of your AnimatedBuilder to save up resources on rebuilding the avatar block every time. Also I'm not sure what Scaffold is here for - let's remove it if it's not needed.
AnimatedBuilder(
animation: _animeController,
builder: (context, child) => Transform(
transform: Matrix4.translationValues(_anime.value * width * x, _anime.value * height * y, 0.0),
child: child,
),
child: Center(
child: CircleAvatar(
radius: 15.0,
backgroundColor: Colors.green,
),
),
);
Follow Official Animations Tutorial to master Flutter animations.
Let me know if this helps.

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!

Resources