How to run animation after specified duration in proper way? - android

Let's say I have 3 shapes in Stack widget which needs to be moved from point A to point B. I would like to start these 3 animations after specified delay 0ms 1000ms 2000ms .. . So for that I have 3 separated AnimationController objects but I don't see constructor parameter like delay:. I tried to run forward method 3 times in loop using
int delay = 0;
for (final AnimationController currentController in controllers) {
Future.delayed(Duration(milliseconds: delay), () {
currentController.forward(from: value);
});
delay += 1000;
}
or
await Future.delayed(Duration(milliseconds: delay));
currentController.forward(from: value);
or using Timer class instead of Future but it doesn't work properly. In foreground its working good but when I move application to background and go back to foreground the gap between each shape disappearing and they are in the same position sticked together and moving like one shape.

You can make a stateful widget like below. Change the animation according to your needs.
class SlideUpWithFadeIn extends StatefulWidget {
final Widget child;
final int delay;
final Curve curve;
SlideUpWithFadeIn({#required this.child, #required this.curve, this.delay});
#override
_SlideUpWithFadeInState createState() => _SlideUpWithFadeInState();
}
class _SlideUpWithFadeInState extends State<SlideUpWithFadeIn>
with TickerProviderStateMixin {
AnimationController _animController;
Animation<Offset> _animOffset;
#override
void initState() {
super.initState();
_animController =
AnimationController(vsync: this, duration: Duration(milliseconds: 1250));
final curve =
CurvedAnimation(curve: widget.curve, parent: _animController);
_animOffset =
Tween<Offset>(begin: const Offset(0.0, 0.75), end: Offset.zero)
.animate(curve);
if (widget.delay == null) {
_animController.forward();
} else {
Timer(Duration(milliseconds: widget.delay), () {
_animController.forward();
});
}
}
#override
void dispose() {
_animController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return FadeTransition(
child: SlideTransition(
position: _animOffset,
child: widget.child,
),
opacity: _animController,
);
}
}
And use it like
SlideUpWithFadeIn(
child: ...,
delay: 0,
curve: ...,
),
SlideUpWithFadeIn(
child: ...,
delay: 1000,
curve: ...,
),
SlideUpWithFadeIn(
child: ...,
delay: 2000,
curve: ...,
),

Related

Flutter Widget Life Cycle Warning/Exception from Animation Widget

I am trying to build a simple Loader animation inside Flutter using custom animations making use of an Animation Controller and a Tween method. The animation works fine on the mobile, but I am getting an endless list of warning messages that state:-
Another exception was thrown: 'package:flutter/src/widgets/framework.dart': Failed assertion: line 4586 pos 12: '_lifecycleState != _ElementLifecycle.defunct': is not true.
I have no idea as to what this is. It would be great if I could get an explanation regarding this warning.
Here is my Loader Animation Class Code for reference :
class Loader extends StatefulWidget {
const Loader({super.key});
#override
State<Loader> createState() => _LoaderState();
}
class _LoaderState extends State<Loader> with TickerProviderStateMixin {
late AnimationController controller;
late Animation<double> animation_rotation;
late Animation<double> animation_Radius_in;
late Animation<double> animation_Radius_out;
final double idistance = 60;
double distance = 0;
#override
void initState() {
super.initState();
controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
);
animation_rotation = Tween<double>(begin: 1.0, end: 0.0).animate(
CurvedAnimation(
parent: controller,
curve: const Interval(0.0, 1.0, curve: Curves.linear),
),
);
animation_Radius_in = Tween<double>(begin: 1.0, end: 0.0).animate(
CurvedAnimation(
parent: controller,
curve: const Interval(0.75, 1.0, curve: Curves.easeInOut),
),
);
animation_Radius_out = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: controller,
curve: const Interval(0.0, 0.25, curve: Curves.easeInOut),
),
);
controller.addListener(() {
if (mounted) {
setState(() {
if (controller.value >= 0.75 && controller.value <= 1.0) {
distance = animation_Radius_in.value * idistance;
} else if (controller.value >= 0.0 && controller.value <= 0.25) {
distance = animation_Radius_out.value * idistance;
}
});
}
});
controller.repeat();
}
Thank You.
I am not totally sure that this is the reason, please try. Even if it does not solve your problem, it is highly recommended to remove listeners and dispose controllers according to the widget's lifecycle.
To do so, create a separate function for the listener (to be able to remove it), and add an override to the dispose method:
class _LoaderState extends State<Loader> with TickerProviderStateMixin {
late AnimationController controller;
late Animation<double> animation_rotation;
late Animation<double> animation_Radius_in;
late Animation<double> animation_Radius_out;
final double idistance = 60;
double distance = 0;
// move the listener logic here
void _listener() {
if (mounted) {
setState(() {
if (controller.value >= 0.75 && controller.value <= 1.0) {
distance = animation_Radius_in.value * idistance;
} else if (controller.value >= 0.0 && controller.value <= 0.25) distance = animation_Radius_out.value * idistance;
}
});
}
}
// get rid of the listener and controller
#override
void dispose() {
controller.removeListener(_listener);
controller.dispose();
super.dispose();
}
#override
void initState() {
super.initState();
controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
);
animation_rotation = Tween<double>(begin: 1.0, end: 0.0).animate(
CurvedAnimation(
parent: controller,
curve: const Interval(0.0, 1.0, curve: Curves.linear),
),
);
animation_Radius_in = Tween<double>(begin: 1.0, end: 0.0).animate(
CurvedAnimation(
parent: controller,
curve: const Interval(0.75, 1.0, curve: Curves.easeInOut),
),
);
animation_Radius_out = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: controller,
curve: const Interval(0.0, 0.25, curve: Curves.easeInOut),
),
);
controller.addListener(_listener);
controller.repeat();
}

Flutter: enable image zoom in/out on double tap using InteractiveViewer

I want to enable zoom in and out on double tap of the image, together with scaling in/out on pinch.
I saw some tutorials on YouTube where they implemented this feature using GestureDetector like this one but for some reason, it didn't work out for me.
In order to implement scaling in/out on pinch, I relied on this answer, and it really works well, but I also want to enable zoom in/out on double tapping the image. Looking up a way to do so on the internet, unfortunately, yielded nothing.
Is there any way to enable zoom in/out with both pinch and double tap using InteractiveViewer?
here is my code:
#override
Widget build(BuildContext context) {
return Center(
child: InteractiveViewer(
boundaryMargin: EdgeInsets.all(80),
panEnabled: false,
scaleEnabled: true,
minScale: 1.0,
maxScale: 2.2,
child: Image.network("https://pngimg.com/uploads/muffin/muffin_PNG123.png",
fit: BoxFit.fitWidth,
)
),
);
}
You can use a GestureDetector, that gives you the position of the click and with that you can zoom with the TransformationController at the click position:
final _transformationController = TransformationController();
TapDownDetails _doubleTapDetails;
#override
Widget build(BuildContext context) {
return GestureDetector(
onDoubleTapDown: _handleDoubleTapDown,
onDoubleTap: _handleDoubleTap,
child: Center(
child: InteractiveViewer(
transformationController: _transformationController,
/* ... */
),
),
);
}
void _handleDoubleTapDown(TapDownDetails details) {
_doubleTapDetails = details;
}
void _handleDoubleTap() {
if (_transformationController.value != Matrix4.identity()) {
_transformationController.value = Matrix4.identity();
} else {
final position = _doubleTapDetails.localPosition;
// For a 3x zoom
_transformationController.value = Matrix4.identity()
..translate(-position.dx * 2, -position.dy * 2)
..scale(3.0);
// Fox a 2x zoom
// ..translate(-position.dx, -position.dy)
// ..scale(2.0);
}
}
To animate the transition on double tap, you have to create an explicit animation on top of Till's code.
class _WidgetState extends State<Widget> with SingleTickerProviderStateMixin {
.
.
.
AnimationController _animationController;
Animation<Matrix4> _animation;
#override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: Duration(milliseconds: 400),
)..addListener(() {
_transformationController.value = _animation.value;
});
}
#override
void dispose() {
_animationController.dispose();
super.dispose();
}
.
.
.
void _handleDoubleTap() {
Matrix4 _endMatrix;
Offset _position = _doubleTapDetails.localPosition;
if (_transformationController.value != Matrix4.identity()) {
_endMatrix = Matrix4.identity();
} else {
_endMatrix = Matrix4.identity()
..translate(-_position.dx * 2, -_position.dy * 2)
..scale(3.0);
}
_animation = Matrix4Tween(
begin: _transformationController.value,
end: _endMatrix,
).animate(
CurveTween(curve: Curves.easeOut).animate(_animationController),
);
_animationController.forward(from: 0);
}
.
.
.
}
Here's a full, portable solution with included customizable animation:
class DoubleTappableInteractiveViewer extends StatefulWidget {
final double scale;
final Duration scaleDuration;
final Curve curve;
final Widget child;
const DoubleTappableInteractiveViewer({
super.key,
this.scale = 2,
this.curve = Curves.fastLinearToSlowEaseIn,
required this.scaleDuration,
required this.child,
});
#override
State<DoubleTappableInteractiveViewer> createState() => _DoubleTappableInteractiveViewerState();
}
class _DoubleTappableInteractiveViewerState extends State<DoubleTappableInteractiveViewer>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
Animation<Matrix4>? _zoomAnimation;
late TransformationController _transformationController;
TapDownDetails? _doubleTapDetails;
#override
void initState() {
super.initState();
_transformationController = TransformationController();
_animationController = AnimationController(
vsync: this,
duration: widget.scaleDuration,
)..addListener(() {
_transformationController.value = _zoomAnimation!.value;
});
}
#override
void dispose() {
_transformationController.dispose();
_animationController.dispose();
super.dispose();
}
void _handleDoubleTapDown(TapDownDetails details) {
_doubleTapDetails = details;
}
void _handleDoubleTap() {
final newValue =
_transformationController.value.isIdentity() ?
_applyZoom() : _revertZoom();
_zoomAnimation = Matrix4Tween(
begin: _transformationController.value,
end: newValue,
).animate(
CurveTween(curve: widget.curve)
.animate(_animationController)
);
_animationController.forward(from: 0);
}
Matrix4 _applyZoom() {
final tapPosition = _doubleTapDetails!.localPosition;
final translationCorrection = widget.scale - 1;
final zoomed = Matrix4.identity()
..translate(
-tapPosition.dx * translationCorrection,
-tapPosition.dy * translationCorrection,
)
..scale(widget.scale);
return zoomed;
}
Matrix4 _revertZoom() => Matrix4.identity();
#override
Widget build(BuildContext context) {
return GestureDetector(
onDoubleTapDown: _handleDoubleTapDown,
onDoubleTap: _handleDoubleTap,
child: InteractiveViewer(
transformationController: _transformationController,
child: widget.child,
),
);
}
}
Example usage:
DoubleTappableInteractiveViewer(
scaleDuration: const Duration(milliseconds: 600),
child: Image.network(imageUrl),
),
Play around with it on dartpad.

CustomPainter not painting at the correct position after device orientation changes

I'm trying to learn Flutter. I'm drawing a diagonal line using CustomPainter on a centered square container whenever the container is tapped. The CustomPainter draws the line correctly.
However the line changes its position when the device orientation changes and is no longer in the center of the container.
I'm using didChangeMetrics() to detect when device orientation changes and redrawing the line. But this time the line is not drawn in the center of the container as intended. However if the container is tapped again the line is drawn correctly. I'm calling the same function to draw the line at both the places didChangeMetrics and onTapDown.
I'm using the following code :
import 'package:flutter/material.dart';
import 'dart:ui';
void main() {
runApp(MaterialApp(home: HomePage()));
}
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
Offset start = Offset.zero;
Offset end = Offset.zero;
double winLineWidth = 0;
GlobalKey _containerKey = GlobalKey();
#override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
#override
void dispose() {
super.dispose();
WidgetsBinding.instance.removeObserver(this);
}
#override
void didChangeMetrics() {
_drawLine();
}
void _drawLine() {
RenderBox box = _containerKey.currentContext.findRenderObject();
var _boxOffset = box.getTransformTo(null).getTranslation();
RenderBox getBox = context.findRenderObject();
print(getBox.globalToLocal( Offset(_boxOffset.x, _boxOffset.y) ));
var appBarHeight = 80;
setState(() {
start = Offset(_boxOffset.x, _boxOffset.y - appBarHeight);
end = Offset(_boxOffset.x + 100, _boxOffset.y - appBarHeight + 100);
print('$start $end');
winLineWidth = 2;
});
}
#override
Widget build(BuildContext context) {
AppBar appBar = AppBar(
title: Text('Demo'),
);
return Scaffold(
appBar: appBar,
body: Stack(
children: <Widget>[
Center(
child: GestureDetector(
onTapDown: (TapDownDetails tapDownDetails) {
_drawLine();
},
child: Container(key: _containerKey,width: 100,height: 100,color: Colors.black),
),
),
CustomPaint(painter:WinLine(start: start, end: end, winLineWidth: winLineWidth),
)
],
),
);
}
}
class WinLine extends CustomPainter {
Paint _paint;
Offset start, end;
double winLineWidth;
WinLine({this.start, this.end, this.winLineWidth = 8}) {
_paint = Paint()
..color = Colors.red
..strokeWidth = winLineWidth;
}
#override
void paint(Canvas canvas, Size size) {
canvas.drawLine(start, end, _paint);
}
#override
bool shouldRepaint(WinLine oldDelegate) {
return true;
}
}
How do I get the CustomPainter to again redraw the line at the center of the container even after the device changes its orientation.
Thanks for your time.
The only thing you have to do is to put your drawLine method inside a FrameCallback :
#override
void didChangeMetrics() {
SchedulerBinding.instance.addPostFrameCallback((_) {
_drawLine();
});
}

Flutter Execute Method so long the button pressed

I want to execute a method while a user is pressing down on a button. In pseudocode:
while (button.isPressed) {
executeCallback();
}
In other words, the executeCallback method should fire repeatedly as long as the user is pressing down on the button, and stop firing when the button is released. How can I achieve this in Flutter?
Use a Listener and a stateful widget. I also introduced a slight delay after every loop:
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(brightness: Brightness.dark),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
bool _buttonPressed = false;
bool _loopActive = false;
void _increaseCounterWhilePressed() async {
// make sure that only one loop is active
if (_loopActive) return;
_loopActive = true;
while (_buttonPressed) {
// do your thing
setState(() {
_counter++;
});
// wait a bit
await Future.delayed(Duration(milliseconds: 200));
}
_loopActive = false;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Listener(
onPointerDown: (details) {
_buttonPressed = true;
_increaseCounterWhilePressed();
},
onPointerUp: (details) {
_buttonPressed = false;
},
child: Container(
decoration: BoxDecoration(color: Colors.orange, border: Border.all()),
padding: EdgeInsets.all(16.0),
child: Text('Value: $_counter'),
),
),
),
);
}
}
A simpler way, without the listener, is as follows:
GestureDetector(
child: InkWell(
child: Icon(Icons.skip_previous_rounded),
onTap: widget.onPrevious,
),
onLongPressStart: (_) async {
isPressed = true;
do {
print('long pressing'); // for testing
await Future.delayed(Duration(seconds: 1));
} while (isPressed);
},
onLongPressEnd: (_) => setState(() => isPressed = false),
);
}
Building on the solution from ThinkDigital, my observation is that InkWell contains all the events necessary to do this without an extra GestureDetector (I find that the GestureDetector interferes with the ink animation on long press). Here's a control I implemented for a pet project that fires its event with a decreasing delay when held (this is a rounded button with an icon, but anything using InkWell will do):
/// A round button with an icon that can be tapped or held
/// Tapping the button once simply calls [onUpdate], holding
/// the button will repeatedly call [onUpdate] with a
/// decreasing time interval.
class TapOrHoldButton extends StatefulWidget {
/// Update callback
final VoidCallback onUpdate;
/// Minimum delay between update events when holding the button
final int minDelay;
/// Initial delay between change events when holding the button
final int initialDelay;
/// Number of steps to go from [initialDelay] to [minDelay]
final int delaySteps;
/// Icon on the button
final IconData icon;
const TapOrHoldButton(
{Key? key,
required this.onUpdate,
this.minDelay = 80,
this.initialDelay = 300,
this.delaySteps = 5,
required this.icon})
: assert(minDelay <= initialDelay,
"The minimum delay cannot be larger than the initial delay"),
super(key: key);
#override
_TapOrHoldButtonState createState() => _TapOrHoldButtonState();
}
class _TapOrHoldButtonState extends State<TapOrHoldButton> {
/// True if the button is currently being held
bool _holding = false;
#override
Widget build(BuildContext context) {
var shape = CircleBorder();
return Material(
color: Theme.of(context).dividerColor,
shape: shape,
child: InkWell(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
widget.icon,
color:
Theme.of(context).textTheme.headline1?.color ?? Colors.white70,
size: 36,
),
),
onTap: () => _stopHolding(),
onTapDown: (_) => _startHolding(),
onTapCancel: () => _stopHolding(),
customBorder: shape,
),
);
}
void _startHolding() async {
// Make sure this isn't called more than once for
// whatever reason.
if (_holding) return;
_holding = true;
// Calculate the delay decrease per step
final step =
(widget.initialDelay - widget.minDelay).toDouble() / widget.delaySteps;
var delay = widget.initialDelay.toDouble();
while (_holding) {
widget.onUpdate();
await Future.delayed(Duration(milliseconds: delay.round()));
if (delay > widget.minDelay) delay -= step;
}
}
void _stopHolding() {
_holding = false;
}
}
Here it is in action:
To improve Elte Hupkes's solution, I fixed an issue where the number of clicks and the number of calls to the onUpdate callback did not match when tapping consecutively.
_tapDownCount variable is additionally used.
import 'package:flutter/material.dart';
/// A round button with an icon that can be tapped or held
/// Tapping the button once simply calls [onUpdate], holding
/// the button will repeatedly call [onUpdate] with a
/// decreasing time interval.
class TapOrHoldButton extends StatefulWidget {
/// Update callback
final VoidCallback onUpdate;
/// Minimum delay between update events when holding the button
final int minDelay;
/// Initial delay between change events when holding the button
final int initialDelay;
/// Number of steps to go from [initialDelay] to [minDelay]
final int delaySteps;
/// Icon on the button
final IconData icon;
const TapOrHoldButton(
{Key? key,
required this.onUpdate,
this.minDelay = 80,
this.initialDelay = 300,
this.delaySteps = 5,
required this.icon})
: assert(minDelay <= initialDelay, "The minimum delay cannot be larger than the initial delay"),
super(key: key);
#override
_TapOrHoldButtonState createState() => _TapOrHoldButtonState();
}
class _TapOrHoldButtonState extends State<TapOrHoldButton> {
/// True if the button is currently being held
bool _holding = false;
int _tapDownCount = 0;
#override
Widget build(BuildContext context) {
var shape = const CircleBorder();
return Material(
color: Theme.of(context).dividerColor,
shape: shape,
child: InkWell(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
widget.icon,
color: Theme.of(context).textTheme.headline1?.color ?? Colors.white70,
size: 36,
),
),
onTap: () => _stopHolding(),
onTapDown: (_) => _startHolding(),
onTapCancel: () => _stopHolding(),
customBorder: shape,
),
);
}
void _startHolding() async {
// Make sure this isn't called more than once for
// whatever reason.
widget.onUpdate();
_tapDownCount += 1;
final int myCount = _tapDownCount;
if (_holding) return;
_holding = true;
// Calculate the delay decrease per step
final step = (widget.initialDelay - widget.minDelay).toDouble() / widget.delaySteps;
var delay = widget.initialDelay.toDouble();
while (true) {
await Future.delayed(Duration(milliseconds: delay.round()));
if (_holding && myCount == _tapDownCount) {
widget.onUpdate();
} else {
return;
}
if (delay > widget.minDelay) delay -= step;
}
}
void _stopHolding() {
_holding = false;
}
}
USING SETSTATE
You can achieve this by simply "onLongPressStart" and "onLongPressEnd" properties of a button.
In case you can't find "onLongPressStart" / "onLongPressEnd" properties in your widget, wrap your widget with the "GestureDetector" widget.
GestureDetector(
child: ..,
onLongPressStart: (_) async {
isTap = true;
do {
await Future.delayed(Duration(seconds: 1));
} while (isTap );
},
onLongPressEnd: (_) => setState(() => isTap = false),
);
}

Flutter - Multiple gestures without lifting the finger

I'm trying to create the following effect: when the user long presses on the empty screen, a rectangle appears. Without lifting the finger, I want the user to be able to drag one of the edges of the rectangle (for example, vertically).
I am able to achieve these effects separately (long press, release, drag), but I need to have them without lifting the finger.
Currently, my code looks like this:
#override
Widget build(BuildContext context) {
return GestureDetector(
onPanStart: startDrag,
onPanUpdate: onDrag,
onPanEnd: endDrag,
child: CustomPaint(
painter: BoxPainter(
color: BOX_COLOR,
boxPosition: boxPosition,
boxPositionOnStart: boxPositionOnStart ?? boxPosition,
touchPoint: point,
),
child: Container(),
),
);
}
This achieves the dragging of the edge and is based on this tutorial.
To make the element appear on a long press, I use an Opacity widget.
#override
Widget build(BuildContext context) {
return new GestureDetector(
onLongPress: () {
setState(() {
this.opacity = 1.0;
});
},
child: new Container(
width: width,
height: height,
child: new Opacity(
opacity: opacity,
child: PhysicsBox(
boxPosition: 0.5,
),
),
),
);
}
If anyone is still interested, I was able to achieve the desired behaviour using the DelayedMultiDragGestureRecognizer class.
The code looks like this:
void onDrag(details) {
// Called on drag update
}
void onEndDrag(details) {
// Called on drag end
}
#override
Widget build(BuildContext context) {
return new RawGestureDetector(
gestures: <Type, GestureRecognizerFactory>{
DelayedMultiDragGestureRecognizer:
new GestureRecognizerFactoryWithHandlers<
DelayedMultiDragGestureRecognizer>(
() => new DelayedMultiDragGestureRecognizer(),
(DelayedMultiDragGestureRecognizer instance) {
instance
..onStart = (Offset offset) {
/* More code here */
return new ItemDrag(onDrag, endDrag);
};
},
),
},
);
}
ItemDrag is a class that extends the Flutter Drag class:
class ItemDrag extends Drag {
final GestureDragUpdateCallback onUpdate;
final GestureDragEndCallback onEnd;
ItemDrag(this.onUpdate, this.onEnd);
#override
void update(DragUpdateDetails details) {
super.update(details);
onUpdate(details);
}
#override
void end(DragEndDetails details) {
super.end(details);
onEnd(details);
}
}

Categories

Resources