Is it possible to copy the transition effect of iOS App Store using Flutter?
I tried using Hero Animation by placing two tags into the root layout of both widgets, but animation looks janky or not what I expected. But good thing about this is I am able to do iOS swipe back as I'm using MaterialPageRoute.
Source
Hero(
tag: 'heroTag_destinationScreen',
transitionOnUserGestures: true,
flightShuttleBuilder: (BuildContext flightContext,
Animation<double> animation,
HeroFlightDirection flightDirection,
BuildContext fromHeroContext,
BuildContext toHeroContext,) {
final Hero toHero = toHeroContext.widget;
return ScaleTransition(
scale: animation,
child: toHero,
);
},
child: GestureDetector(
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (BuildContext context) {
return DestinationScreen()
},
),
);
},
child: Card(
...someCardContent
),
),
)
Destination Screen
#override
Widget build(BuildContext context) {
return Hero(
tag: 'heroTag_destinationScreen',
child: Scaffold(
appBar: ...someAppBar
body: ...someMainBodyContent
),
)
}
Then I have been looking around and there is a package created by Flutter team which can simulate this effect using container transform. I implemented it, works awesome but then I'm not able to do iOS swipe from left to go back and shrink the layout to card view.
https://pub.dev/packages/animations
here is my solution.
https://imgur.com/2WYn6TX
(Sorry for my reputation, I can't post a image.)
I customize hero transition to remake App store transition as much as possible.
child: Hero(
tag: widget.product.id,
child: Image.asset(widget.product.image, fit: BoxFit.cover),
flightShuttleBuilder:
(flightContext, animation, direction, fromcontext, toContext) {
final Hero toHero = toContext.widget;
// Change push and pop animation.
return direction == HeroFlightDirection.push
? ScaleTransition(
scale: animation.drive(
Tween<double>(
begin: 0.75,
end: 1.02,
).chain(
CurveTween(
curve: Interval(0.4, 1.0, curve: Curves.easeInOut)),
),
),
child: toHero.child,
)
: SizeTransition(
sizeFactor: animation,
child: toHero.child,
);
},
),
Next, I use ScaleTransition and onVerticalDragUpdate to control pop animation.
https://imgur.com/a/xEMYOPr
double _initPoint = 0;
double _pointerDistance = 0;
GestureDetector(
onVerticalDragDown: (detail) {
_initPoint = detail.globalPosition.dy;
},
onVerticalDragUpdate: (detail) {
_pointerDistance = detail.globalPosition.dy - _initPoint;
if (_pointerDistance >= 0 && _pointerDistance < 200) {
// scroll up
double _scaleValue = double.parse((_pointerDistance / 100).toStringAsFixed(2));
if (_pointerDistance < 100) {
_closeController.animateTo(_scaleValue,
duration: Duration(milliseconds: 300),
curve: Curves.linear);
}
} else if (_pointerDistance >= 260) {
if (_pop) {
_pop = false;
_closeController.fling(velocity: 1).then((_) {
setState(() {
_heightController.reverse();
});
Timer(Duration(milliseconds: 100), () {
Navigator.of(context).pop();
});
});
}
} else {
// scroll down
}
},
onVerticalDragEnd: (detail) {
if (_pointerDistance >= 550) {
if (_pop) {
_closeController.fling(velocity: 1).then((_) {
setState(() {
_heightController.reverse();
});
Timer(Duration(milliseconds: 100), () {
Navigator.of(context).pop();
});
});
}
} else {
_closeController.fling(velocity: -1);
}
},
child: Hero(
tag: _product.id,
child: Image.asset(
_product.image,
fit: BoxFit.cover,
height: 300,
),
),
),
If use Hero as a animation, you need to customize the text section transition.
Here: https://imgur.com/a/gyD6tiZ
In my case, I control text section transition by Sizetransition.
// horizontal way and vertical way.
SizeTransition(
axis: Axis.horizontal,
sizeFactor: Tween<double>(begin: 0.5, end: 1).animate(
CurvedAnimation(
curve: Curves.easeInOut, parent: _widthController),
),
child: SizeTransition(
sizeFactor: Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
curve: Curves.easeInOut, parent: _heightController),
),
child: Container(
padding: EdgeInsets.only(
left: 20, right: 20, top: 50, bottom: 30),
width: double.infinity,
color: Colors.white,
constraints: BoxConstraints(
minHeight: 650,
),
child: Column(
// title and text
children: <Widget>[
Text('Title', style: TextStyle(fontSize: 18)),
SizedBox(height: 30),
Text(_text,
style: TextStyle(
fontSize: 15,
)),
],
),
),
),
),
Although it isn't the same as App Store, i hope it is helpful for you.
Source code: https://github.com/HelloJunWei/app_store_transition
If you have any suggestion, feel free to feedback or create a pull request. :)
Related
I'm trying to make an animation with Flutter Hooks where I have a symbol going up and back down from behind a container when I tap it, but I can't get the symbol to stay behind the container. It instead goes way down. What am I doing wrong?
#override
Widget build(BuildContext context) {
AnimationController controller = useAnimationController(
duration: const Duration(seconds: 3), initialValue: 0);
Animation<double> animation = Tween<double>(begin: 0, end: 1)
.animate(CurvedAnimation(parent: controller, curve: Curves.easeOut));
return Stack(alignment: Alignment.bottomCenter, children: [
AnimatedBuilder(
animation: animation,
builder: (context, child) {
return Transform.translate(
offset: Offset(0, cos(animation.value) * 500),
child: child,
);
},
child: const CircleAvatar(
backgroundColor: Colors.red,
radius: 30,
)),
GestureDetector(
child: Container(
color: Colors.brown,
width: 300,
height: 100,
),
onTap: () {
print("TAP : ${(controller.status != AnimationStatus.completed)}");
if (controller.status != AnimationStatus.completed) {
controller.forward().whenComplete(() => controller.reverse());
}
},
),
]);
}
I am trying to animate between widgets as follows:
AnimatedSwitcher(
duration: const Duration(seconds: 1),
transitionBuilder: (Widget child, Animation<double> animation) {
return SlideTransition(
position: Tween(
begin: Offset(1.0, 0.0),
end: Offset(0.0, 0.0),)
.animate(animation),
child: child,
);
},
child: Provider.of<UserWidgets>(context, listen: false).renderWidget(context),
),
This works fine but for two different sized widgets its not smooth because of OffSet.
Try wrapping both your child widgets inside an Align widget like this,
child: Align(
alignment: Alignment.topCenter,
child: Provider.of<UserWidgets>(context, listen: false).renderWidget(context),
)
This should ensure that both your old and new children are always aligned to the topCenter while animating.
Here is the full working example.
class Switcher extends StatefulWidget {
State<StatefulWidget> createState() => SwitcherS();
}
class SwitcherS extends State<Switcher> {
bool state = false;
buildChild (index) => Align(
alignment: Alignment.topCenter,
child: Container(
width: index == 0 ? 100 : 150,
height: index == 0 ? 200 : 150,
color:index == 0 ? Colors.deepPurple : Colors.deepOrange,
),
key: ValueKey('$index'));
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => setState(() { state = !state; }),
child: Padding(
padding: EdgeInsets.all(24),
child: AnimatedSwitcher(
duration: const Duration(seconds: 1),
transitionBuilder: (Widget child, Animation<double> animation) {
return SlideTransition(
position: Tween(
begin: Offset(1.0, 1.0),
end: Offset(0.0, 0.0),
).animate(animation),
child: child,
);
},
child: buildChild(state ? 0 : 1),
),
);
}
}
I want to play an animation when a button is clicked. On the first press, the widget rotates 180 degrees, on the second press, another 180 degrees (that is, it returns to its original position). How can I do this?
Simulated gesture detector button
Expanded(
child: GestureDetector(
onTap: () => setState(() {
if (tapValue == 0) {
tapValue++;
animController.forward();
beginValue = 0.0;
endValue = 0.5;
} else {
tapValue--;
animController.forward();
}
}),
child: Container(
child: Image.asset('assets/images/enableAsset.png'),
),
),
),
The widget I want to rotate
child: CustomPaint (
painter: SmileyPainter(),
child: RotationTransition(
turns: Tween(begin: beginValue, end: endValue,).animate(animController),
child: CustomPaint (
painter: Smile(),
),
),
)
animation controller
#override
void initState() {
animController = AnimationController(
duration: const Duration(milliseconds: 400),
vsync: this,
);
super.initState();
}
If what you want to achieve is only to rotate a widget, I would recommend avoiding a controller. Not only will this simplify your code but it will also save you the chore of disposing it.
I have come to realize that pretty much any controller can be avoided using the TweenAnimationBuilder widget.
Here is an example of how to to make it work for your case:
Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
_rotationAngle += pi;
print(_rotationAngle);
setState(() {
});
},
),
body: Center(
child: TweenAnimationBuilder(
duration: Duration(milliseconds: 300),
tween: Tween<double>(begin: 0, end: _rotationAngle),
builder: (BuildContext context, double value, Widget child) {
return Transform.rotate(
angle: value,
child: child,
);
},
child: Container(
height: 500,
width: 50,
color: Colors.redAccent,
),
),
),
);
just replace
else {tapValue--;
animController.forward();
}
to
else {tapValue--;
animController.reverce();
}
I wanted to know if it was possible to add blur on a screen with fade in and fade out.
Do you have any idea ? I'm currently using BackDropFilter to blur my screen but it doesn't fade when appear...
You can animate the sigma values for blur,
TweenAnimationBuilder<double>(
tween: Tween<double>(begin: 0.0, end: 15.0),
duration: const Duration(milliseconds: 500),
builder: (_, value, child) {
return BackdropFilter(
filter: ImageFilter.blur(sigmaX: value, sigmaY: value),
child: child,
);
},
child: DecoratedBox(
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.5),
),
),
https://api.flutter.dev/flutter/widgets/TweenAnimationBuilder-class.html
I found that I was able to animate the backDropFiter with a widget called AnimatedOpacity. You can use it as the AnimatedContainer!
Enjoy
Pretty much the same answer as #Damon's but including working example
class BackgroundBlurExample extends StatefulWidget {
#override
_BackgroundBlurExampleState createState() => _BackgroundBlurExampleState();
}
class _BackgroundBlurExampleState extends State<BackgroundBlurExample> {
double _begin = 10.0;
double _end = 0.0;
void _animateBlur() {
setState(() {
_begin == 10.0 ? _begin = 0.0 : _begin = 10.0;
_end == 0.0 ? _end = 10.0 : _end = 0.0;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
Align(
alignment: Alignment.center,
child: FlutterLogo(
size: 100,
),
),
// ... Things you want to blur above the IgnorePointer
IgnorePointer( // This is in case you want to tap things under the BackdropFilter
child: TweenAnimationBuilder<double>(
tween: Tween<double>(begin: _begin, end: _end),
duration: Duration(milliseconds: 500),
curve: Curves.easeIn,
builder: (_, value, __) {
return BackdropFilter(
filter: ImageFilter.blur(
sigmaX: value,
sigmaY: value,
),
child: Container(
width: double.maxFinite,
height: double.maxFinite,
color: Colors.transparent,
),
);
},
),
),
Align(
alignment: Alignment.bottomCenter,
child: ElevatedButton(
onPressed: _animateBlur,
child: Text('Animate'),
),
)
],
),
);
}
}
in this below code i want to animate change DraggableScrollableSheet border radius after that achieve to stick to top of screen such as AppBar, implemented animate change border radius for that, but it doesn't work and i get this error:
Error:
The following assertion was thrown building _BottomBarControllerScope:
'package:flutter/src/animation/animations.dart': Failed assertion:
line 376 pos 15: 'parent != null': is not true.
Either the assertion
indicates an error in the framework itself, or we should provide
substantially more information in this error message to help you
determine and fix the underlying cause. In either case, please report
this assertion by filing a bug on GitHub:
https://github.com/flutter/flutter/issues/new?template=BUG.md When the
exception was thrown, this was the stack:
2 new CurvedAnimation (package:flutter/src/animation/animations.dart:376:15)
3 _HomeState.initState (package:cheetah/screens/home/main/view/home.dart:45:7)
in that home.dart:45:7 is: CurvedAnimation in this part of code:
borderRadius = BorderRadiusTween(
begin: BorderRadius.circular(75.0),
end: BorderRadius.circular(0.0),
).animate(
CurvedAnimation(
parent: _borderRadiusController,
curve: Curves.ease,
),
);
my code:
class _HomeState extends State<Home> with TickerProviderStateMixin {
AnimationController _draggableBottomSheetController;
AnimationController _borderRadiusController;
Animation<BorderRadius> borderRadius;
Duration _duration = Duration(milliseconds: 500);
Tween<Offset> _draggableBottomSheetTween = Tween(begin: Offset(0, 1), end: Offset(0, 0));
#override
void initState() {
super.initState();
_draggableBottomSheetController = AnimationController(vsync: this, duration: _duration);
borderRadius = BorderRadiusTween(
begin: BorderRadius.circular(75.0),
end: BorderRadius.circular(0.0),
).animate(
CurvedAnimation(
parent: _borderRadiusController,
curve: Curves.ease,
),
);
}
#override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.rtl,
child: AnimatedBuilder(
animation: _borderRadiusController,
builder: (BuildContext context, Widget widget){
return Scaffold(
backgroundColor: Theme.of(context).canvasColor,
extendBody: true,
primary: true,
appBar: ApplicationToolbar(title: Strings.appName),
resizeToAvoidBottomInset: false,
resizeToAvoidBottomPadding: false,
body: Container(
color: applicationBackgroundColor,
child: Stack(children: <Widget>[
Container(
width: double.infinity,
height: double.infinity,
child: PageView(
children: <Widget>[
FollowersFeedsPage(),
],
),
),
SizedBox.expand(
child: SlideTransition(
position: _draggableBottomSheetTween.animate(_draggableBottomSheetController),
child: DraggableScrollableSheet(
builder: (BuildContext context, ScrollController scrollController) {
return ClipRRect(
borderRadius: borderRadius.value,
child: Container(
color: pageBackgroundColor,
child: ListView.builder(
controller: scrollController,
itemCount: 5,
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text('Item $index'));
},
),
),
);
},
),
),
),
]),
),
);
}
),
);
}
}
This should be the correct way of doing it.
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin {
AnimationController _controller;
BorderRadiusTween borderRadius;
Duration _duration = Duration(milliseconds: 500);
Tween<Offset> _tween = Tween(begin: Offset(0, 1), end: Offset(0, 0));
double _height, min = 0.1, initial = 0.3, max = 0.7;
#override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: _duration);
borderRadius = BorderRadiusTween(
begin: BorderRadius.circular(75.0),
end: BorderRadius.circular(0.0),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: GestureDetector(
child: FloatingActionButton(
child: AnimatedIcon(icon: AnimatedIcons.menu_close, progress: _controller),
elevation: 5,
backgroundColor: Colors.deepOrange,
foregroundColor: Colors.white,
onPressed: () async {
if (_controller.isDismissed)
_controller.forward();
else if (_controller.isCompleted) _controller.reverse();
},
),
),
body: SizedBox.expand(
child: Stack(
children: <Widget>[
FlutterLogo(size: 500),
SizedBox.expand(
child: SlideTransition(
position: _tween.animate(_controller),
child: DraggableScrollableSheet(
minChildSize: min, // 0.1 times of available height, sheet can't go below this on dragging
maxChildSize: max, // 0.7 times of available height, sheet can't go above this on dragging
initialChildSize: initial, // 0.1 times of available height, sheet start at this size when opened for first time
builder: (BuildContext context, ScrollController controller) {
return AnimatedBuilder(
animation: controller,
builder: (context, child) {
return ClipRRect(
borderRadius: borderRadius.evaluate(CurvedAnimation(parent: _controller, curve: Curves.ease)),
child: Container(
height: 500.0,
color: Colors.blue[800],
child: ListView.builder(
controller: controller,
itemCount: 5,
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text('Item $index'));
},
),
),
);
},
);
},
),
),
),
],
),
),
);
}
}