Last active
April 8, 2020 02:54
-
-
Save tafelito/1cfd9c75c47feb41200cac0f421ad104 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/material.dart'; | |
import 'package:flutter/cupertino.dart'; | |
import 'dart:ui' as ui; | |
import 'dart:math'; | |
final Color darkBlue = Color.fromARGB(255, 18, 32, 47); | |
void main() { | |
runApp(MyApp()); | |
} | |
class MyApp extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
const title = '3D Product Detail Zoom'; | |
return MaterialApp( | |
title: title, | |
themeMode: ThemeMode.dark, | |
debugShowCheckedModeBanner: false, | |
home: ProductDetailZoomDemo()); | |
} | |
} | |
class MyWidget extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return Text('Hello, World!', style: Theme.of(context).textTheme.headline4); | |
} | |
} | |
class ProductDetailZoomDemo extends StatefulWidget { | |
@override | |
State<StatefulWidget> createState() => _ProductDetailZoomDemoState(); | |
} | |
class _ProductDetailZoomDemoState extends State<ProductDetailZoomDemo> | |
with SingleTickerProviderStateMixin { | |
AnimationController _transitionAnimController; | |
// bool _isFirstInit; | |
Size _screenSize; | |
double _frameHeight; | |
double _frameWidth; | |
double _buttonAlpha = 0; | |
TextStyle bodyStyle = TextStyle( | |
fontFamily: 'WorkSans', | |
fontWeight: FontWeight.bold, | |
fontSize: 16, | |
letterSpacing: 2); | |
@override | |
void initState() { | |
super.initState(); | |
// _isFirstInit = true; | |
_buttonAlpha = 0; | |
_transitionAnimController = AnimationController( | |
vsync: this, duration: Duration(milliseconds: 1600)); | |
_transitionIn(); | |
} | |
void _transitionIn() async { | |
await Future.delayed(Duration(milliseconds: 1000)); | |
setState(() => _buttonAlpha = 1); | |
} | |
@override | |
void dispose() { | |
_transitionAnimController.dispose(); | |
super.dispose(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
_screenSize = MediaQuery.of(context).size; | |
_initFrameValues(); | |
return Scaffold( | |
backgroundColor: Colors.black, | |
body: SafeArea( | |
child: Stack( | |
children: <Widget>[ | |
_buildAppBar(), | |
//Create a Hero tagged to match the instance details view | |
Hero( | |
tag: 'hero-speaker', | |
//Use a custom flightShuttleBuilder to control the hero transition | |
flightShuttleBuilder: (flightContext, animation, flightDirection, | |
fromHeroContext, toHeroContext) { | |
return ProductDetailsHeroFlight( | |
animation: animation, | |
toHeroContext: toHeroContext, | |
framwWidth: _frameWidth, | |
framwHeight: _frameHeight, | |
); | |
}, | |
//The child of the hero, is the main speaker animation | |
child: Align( | |
alignment: Alignment.topCenter, | |
child: Container( | |
width: _frameWidth, | |
height: _frameHeight, | |
child: Sprite( | |
// image: AssetImage("images/speaker_sprite.png"), | |
image: NetworkImage( | |
"https://firebasestorage.googleapis.com/v0/b/vgv-flutter-vignettes.appspot.com/o/product_detail_zoom%2Fspeaker_sprite.png?alt=media&token=caea503f-8e4a-4e82-80e5-bec89eecd765"), | |
frameWidth: 360, | |
frameHeight: 500, | |
frame: 1)), | |
), | |
), | |
Align( | |
alignment: Alignment(0, 0), | |
child: Transform.translate( | |
offset: Offset(100, -70), | |
child: AnimatedOpacity( | |
duration: Duration(milliseconds: 350), | |
opacity: _buttonAlpha, | |
child: PulsingButton( | |
onPressed: _handleOnPressed, | |
icon: Icons.add, | |
), | |
), | |
), | |
), | |
Align( | |
alignment: Alignment.bottomCenter, | |
child: Container( | |
width: double.infinity, | |
height: _screenSize.height * .43, | |
margin: EdgeInsets.symmetric(horizontal: 40), | |
child: Column( | |
mainAxisAlignment: MainAxisAlignment.spaceEvenly, | |
children: <Widget>[ | |
_buildSpeakerDescription(), | |
Container( | |
padding: const EdgeInsets.only(right: 36.0, left: 36.0), | |
child: Column( | |
mainAxisSize: MainAxisSize.max, | |
mainAxisAlignment: MainAxisAlignment.spaceBetween, | |
crossAxisAlignment: CrossAxisAlignment.stretch, | |
children: <Widget>[ | |
Padding( | |
padding: const EdgeInsets.only(top: 18, bottom: 12.0), | |
child: _buildBuyNowButton(), | |
), | |
_buildLearnMoreButton() | |
], | |
), | |
) | |
], | |
), | |
), | |
), | |
], | |
)), | |
); | |
} | |
void _initFrameValues() { | |
double screenRatio = _screenSize.height / _screenSize.width; | |
double frameRatio = screenRatio < 2 ? screenRatio / 2 : .95; | |
_frameWidth = _screenSize.width * frameRatio; | |
_frameHeight = (500 * _frameWidth) / 360; | |
} | |
Widget _buildSpeakerDescription() { | |
return SlideTransition( | |
position: Tween(begin: Offset.zero, end: Offset(-.1, 0)).animate( | |
CurvedAnimation( | |
curve: Interval(.5, 1, curve: Curves.easeOut), | |
parent: _transitionAnimController)), | |
child: Column( | |
children: <Widget>[ | |
Text('Classic Speaker 2700'.toUpperCase(), | |
textAlign: TextAlign.justify, | |
style: bodyStyle.copyWith( | |
fontWeight: FontWeight.w900, | |
color: Colors.white, | |
height: 1.1, | |
fontSize: 30, | |
letterSpacing: 5)), | |
SizedBox(height: 8), | |
Text( | |
'This speaker provides a home soundscape unlike any other with its high quality sound and sleek design. You won\'t believe it until you hear it.', | |
textAlign: TextAlign.start, | |
style: bodyStyle.copyWith( | |
color: Colors.white, | |
fontSize: 12, | |
letterSpacing: 2.8, | |
height: 1.3)) | |
], | |
), | |
); | |
} | |
Widget _buildBuyNowButton() { | |
return MaterialButton( | |
height: 55, | |
onPressed: () {}, | |
color: Colors.white, | |
textColor: Colors.black, | |
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(50)), | |
child: Text( | |
'Buy Now \$349.95'.toUpperCase(), | |
style: bodyStyle, | |
), | |
); | |
} | |
Widget _buildLearnMoreButton() { | |
return MaterialButton( | |
height: 55, | |
onPressed: () {}, | |
color: Colors.transparent, | |
textColor: Colors.white, | |
shape: RoundedRectangleBorder( | |
borderRadius: BorderRadius.circular(50), | |
side: BorderSide(color: Colors.white, width: 1.5)), | |
child: Text( | |
'Learn More'.toUpperCase(), | |
style: bodyStyle, | |
), | |
); | |
} | |
Widget _buildAppBar() { | |
return AppBar( | |
// leading: Icon(CupertinoIcons.left_chevron, color: Colors.white, size: 36), | |
leading: Icon(Icons.arrow_back_ios, color: Colors.white), | |
actions: <Widget>[ | |
Padding( | |
padding: const EdgeInsets.only(right: 24.0), | |
// child: Image.asset('images/shopping_bag.png', width: 20, height: 20, fit: BoxFit.contain, package: App.pkg), | |
child: Icon(Icons.shopping_basket, color: Colors.white), | |
) | |
], | |
backgroundColor: Colors.transparent, | |
elevation: 0, | |
); | |
} | |
// bool _isTransitioning = false; | |
void _handleOnPressed() async { | |
debugPrint('_handleOnPressed'); | |
// this._isFirstInit = false; | |
//Don't accept button presses if we're currently fading the btn in or out | |
if (_buttonAlpha < 1) return; | |
//Fade button out | |
setState(() => _buttonAlpha = 0); | |
//Kick off main animation sequence | |
_transitionAnimController | |
.forward() | |
.whenComplete(() => _transitionAnimController.reset()); | |
//Wait a bit to let the btn fade out | |
await Future.delayed(Duration(milliseconds: 300)); | |
//Show new page route, which will place the Hero on top of everything | |
Navigator.push( | |
context, | |
FadeColorPageRoute( | |
color: Colors.black, | |
enterPage: ProductDetailView(), | |
)); | |
//Fade button back in, now that hero is covering it | |
setState(() => _buttonAlpha = 1); | |
} | |
} | |
class ProductDetailsHeroFlight extends StatelessWidget { | |
final Animation<double> animation; | |
final BuildContext toHeroContext; | |
final double framwWidth; | |
final double framwHeight; | |
const ProductDetailsHeroFlight( | |
{Key key, | |
@required this.animation, | |
@required this.toHeroContext, | |
this.framwWidth, | |
this.framwHeight}) | |
: super(key: key); | |
@override | |
Widget build(BuildContext context) { | |
return Stack( | |
alignment: Alignment.topCenter, | |
children: <Widget>[ | |
Container( | |
width: framwWidth, | |
height: framwHeight, | |
child: AnimatedSprite( | |
image: NetworkImage( | |
"https://firebasestorage.googleapis.com/v0/b/vgv-flutter-vignettes.appspot.com/o/product_detail_zoom%2Fspeaker_sprite.png?alt=media&token=caea503f-8e4a-4e82-80e5-bec89eecd765"), | |
frameWidth: 360, | |
frameHeight: 500, | |
animation: Tween(begin: 0.0, end: 59.0).animate( | |
CurvedAnimation(curve: Interval(0, .8), parent: animation)), | |
), | |
), | |
Container( | |
width: framwWidth, | |
height: framwHeight, | |
child: AnimatedBuilder( | |
animation: animation, | |
builder: (BuildContext context, Widget child) { | |
return DefaultTextStyle( | |
style: DefaultTextStyle.of(toHeroContext).style, | |
child: | |
ProductDetailsTransition(animationValue: animation.value), | |
); | |
}, | |
), | |
) | |
], | |
); | |
} | |
} | |
class AnimatedSprite extends AnimatedWidget { | |
final ImageProvider image; | |
final int frameWidth; | |
final int frameHeight; | |
AnimatedSprite({ | |
Key key, | |
@required this.image, | |
@required this.frameWidth, | |
this.frameHeight, | |
@required Animation<double> animation, | |
}) : super(key: key, listenable: animation); | |
@override | |
Widget build(BuildContext context) { | |
final animation = listenable as Animation<double>; | |
return Sprite( | |
image: image, | |
frameWidth: frameWidth, | |
frameHeight: frameHeight, | |
frame: animation.value, | |
); | |
} | |
} | |
class Sprite extends StatefulWidget { | |
final ImageProvider image; | |
final int frameWidth; | |
final int frameHeight; | |
final num frame; | |
Sprite( | |
{Key key, | |
@required this.image, | |
@required this.frameWidth, | |
this.frameHeight, | |
this.frame = 0}) | |
: super(key: key); | |
@override | |
_SpriteState createState() => _SpriteState(); | |
} | |
class _SpriteState extends State<Sprite> { | |
ImageStream _imageStream; | |
ImageInfo _imageInfo; | |
@override | |
void initState() { | |
super.initState(); | |
} | |
@override | |
void didChangeDependencies() { | |
super.didChangeDependencies(); | |
_getImage(); | |
} | |
@override | |
void didUpdateWidget(Sprite oldWidget) { | |
super.didUpdateWidget(oldWidget); | |
if (widget.image != oldWidget.image) { | |
_getImage(); | |
} | |
} | |
void _getImage() { | |
final ImageStream oldImageStream = _imageStream; | |
_imageStream = widget.image.resolve(createLocalImageConfiguration(context)); | |
if (_imageStream.key == oldImageStream?.key) { | |
return; | |
} | |
final ImageStreamListener listener = ImageStreamListener(_updateImage); | |
oldImageStream?.removeListener(listener); | |
_imageStream.addListener(listener); | |
} | |
void _updateImage(ImageInfo imageInfo, bool synchronousCall) { | |
setState(() { | |
_imageInfo = imageInfo; | |
}); | |
} | |
@override | |
void dispose() { | |
_imageStream?.removeListener(ImageStreamListener(_updateImage)); | |
super.dispose(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
ui.Image img = _imageInfo?.image; | |
if (img == null) { | |
return SizedBox(); | |
} | |
int w = img.width, frame = widget.frame.round(); | |
int frameW = widget.frameWidth, frameH = widget.frameHeight; | |
int cols = (w / frameW).floor(); | |
int col = frame % cols, row = (frame / cols).floor(); | |
ui.Rect rect = ui.Rect.fromLTWH( | |
col * frameW * 1.0, row * frameH * 1.0, frameW * 1.0, frameH * 1.0); | |
return CustomPaint(painter: _SpritePainter(img, rect)); | |
} | |
} | |
class _SpritePainter extends CustomPainter { | |
ui.Image image; | |
ui.Rect rect; | |
_SpritePainter(this.image, this.rect); | |
@override | |
void paint(Canvas canvas, Size size) { | |
canvas.drawImageRect(image, rect, | |
ui.Rect.fromLTWH(0.0, 0.0, size.width, size.height), Paint()); | |
} | |
@override | |
bool shouldRepaint(_SpritePainter oldPainter) { | |
return oldPainter.image != image || oldPainter.rect != rect; | |
} | |
} | |
class ProductDetailsTransition extends StatelessWidget { | |
final double animationValue; | |
final CurvedAnimation _curvedAnimation; | |
final TextStyle bodyStyle = TextStyle( | |
fontFamily: 'WorkSans', | |
fontWeight: FontWeight.bold, | |
fontSize: 16, | |
letterSpacing: 2); | |
ProductDetailsTransition({Key key, this.animationValue = 1}) | |
: _curvedAnimation = CurvedAnimation( | |
curve: Interval(0, .8, curve: Curves.easeOut), | |
parent: AlwaysStoppedAnimation(animationValue)), | |
super(key: key); | |
@override | |
Widget build(BuildContext context) { | |
return Stack( | |
fit: StackFit.expand, | |
overflow: Overflow.visible, | |
children: [ | |
Positioned( | |
top: 25, | |
left: 45, | |
child: _SpeakerAttribute( | |
attribute: 'Spectacular tonal range', | |
animation: _getAttributeAnimWithInterval(0, .85), | |
lineHeight: 270, | |
), | |
), | |
Positioned( | |
top: 75, | |
left: 95, | |
child: _SpeakerAttribute( | |
attribute: 'Superior-grade aluminum', | |
animation: _getAttributeAnimWithInterval(.35, .95), | |
lineHeight: 250, | |
), | |
), | |
Positioned( | |
top: 120, | |
left: 175, | |
child: _SpeakerAttribute( | |
attribute: 'Deep 30Hz bass', | |
animation: _getAttributeAnimWithInterval(.45, 1), | |
lineHeight: 185, | |
), | |
), | |
ScaleTransition( | |
scale: Tween<double>(begin: .6, end: 1).animate(_curvedAnimation), | |
child: SlideTransition( | |
position: Tween<Offset>(begin: Offset(.6, .7), end: Offset(.1, .95)) | |
.animate(_curvedAnimation), | |
child: FadeTransition( | |
opacity: Tween<double>(begin: 0, end: 1) | |
.animate(_getCurvedAnimWithInterval(.2, 1)), | |
child: Transform( | |
transform: Matrix4.identity() | |
..setEntry(3, 2, 0.01) | |
..rotateY(Tween<double>(begin: -.09, end: 0) | |
.transform(CurvedAnimation( | |
curve: Interval(0, .8), | |
parent: AlwaysStoppedAnimation(animationValue), | |
).value)), | |
child: Container( | |
width: 300, | |
height: 500, | |
child: Column( | |
crossAxisAlignment: CrossAxisAlignment.start, | |
children: <Widget>[ | |
Text('Perfect Sound'.toUpperCase(), | |
style: bodyStyle.copyWith( | |
color: Colors.white, | |
fontWeight: FontWeight.w900, | |
fontSize: 52, | |
height: .9)), | |
Text( | |
'This is our best speaker yet.', | |
textAlign: TextAlign.start, | |
style: bodyStyle.copyWith( | |
color: Colors.white, | |
height: 1.5, | |
fontWeight: FontWeight.w600, | |
fontSize: 20), | |
), | |
], | |
), | |
), | |
)), | |
), | |
), | |
], | |
); | |
} | |
CurvedAnimation _getCurvedAnimWithInterval(double begin, double end) { | |
return CurvedAnimation( | |
curve: Interval(begin, end), parent: _curvedAnimation); | |
} | |
CurvedAnimation _getAttributeAnimWithInterval(double begin, double end) { | |
var attributeAnim = CurvedAnimation( | |
curve: Interval(.65, 1), | |
parent: AlwaysStoppedAnimation(animationValue)); | |
return CurvedAnimation(curve: Interval(begin, end), parent: attributeAnim); | |
} | |
} | |
class _SpeakerAttribute extends StatelessWidget { | |
final double lineHeight; | |
final Animation animation; | |
final String attribute; | |
const _SpeakerAttribute( | |
{Key key, this.lineHeight = 150, this.attribute, this.animation}) | |
: super(key: key); | |
@override | |
Widget build(BuildContext context) { | |
double lineHeight = Tween<double>(begin: 0, end: this.lineHeight) | |
.transform(Curves.easeInOutQuad.transform(animation.value)); | |
return Stack( | |
overflow: Overflow.visible, | |
children: <Widget>[ | |
SlideTransition( | |
position: Tween<Offset>(begin: Offset(0, -.5), end: Offset.zero) | |
.animate(_getAnimationWithInterval(.2, 1)), | |
child: FadeTransition( | |
opacity: _getAnimationWithInterval(.15, .95), | |
child: Text( | |
attribute, | |
style: TextStyle( | |
fontFamily: 'WorkSans', | |
letterSpacing: 3, | |
color: Colors.white, | |
fontSize: 13.5), | |
), | |
), | |
), | |
Positioned( | |
top: lineHeight / 2 + 17, | |
left: -lineHeight / 2 + 5, | |
child: Transform.rotate( | |
angle: pi / 2, | |
child: Container( | |
width: lineHeight, | |
height: 1, | |
color: Colors.white, | |
), | |
), | |
), | |
Positioned( | |
top: lineHeight + 17, | |
left: 5, | |
child: FadeTransition( | |
opacity: Tween<double>(begin: 0, end: 1) | |
.animate(_getAnimationWithInterval(0, .3)), | |
child: CustomPaint( | |
painter: CirclePainter( | |
radius: 3, | |
color: Colors.white, | |
)), | |
), | |
), | |
], | |
); | |
} | |
CurvedAnimation _getAnimationWithInterval(double begin, double end) { | |
return CurvedAnimation(curve: Interval(begin, end), parent: animation); | |
} | |
} | |
class CirclePainter extends CustomPainter { | |
final Paint _paint; | |
final Color color; | |
final double radius; | |
CirclePainter({this.color, this.radius}) | |
: _paint = Paint() | |
..color = color | |
..strokeWidth = 10.0 | |
..style = PaintingStyle.fill; | |
@override | |
void paint(Canvas canvas, Size size) { | |
canvas.drawCircle(Offset(size.width / 2, size.height / 2), radius, _paint); | |
} | |
@override | |
bool shouldRepaint(CirclePainter oldDelegate) { | |
return oldDelegate.color != color || oldDelegate.radius != radius; | |
} | |
} | |
class PulsingButton extends StatefulWidget { | |
final Function onPressed; | |
final IconData icon; | |
const PulsingButton({Key key, @required this.onPressed, @required this.icon}) | |
: super(key: key); | |
@override | |
State<StatefulWidget> createState() => _PulsingButtonState(); | |
} | |
class _PulsingButtonState extends State<PulsingButton> | |
with SingleTickerProviderStateMixin { | |
AnimationController _btnAnimController; | |
CurvedAnimation _btnAnim; | |
@override | |
void initState() { | |
super.initState(); | |
_btnAnimController = | |
AnimationController(vsync: this, duration: Duration(milliseconds: 1200)) | |
..repeat(); | |
_btnAnim = | |
CurvedAnimation(curve: Curves.linear, parent: _btnAnimController); | |
} | |
@override | |
void dispose() { | |
_btnAnimController.dispose(); | |
super.dispose(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Stack( | |
alignment: Alignment.center, | |
children: <Widget>[ | |
FadeTransition( | |
opacity: Tween<double>(begin: .7, end: 0).animate(_btnAnim), | |
child: ScaleTransition( | |
scale: Tween<double>(begin: .5, end: 1).animate(_btnAnim), | |
child: CustomPaint( | |
painter: CirclePainter( | |
radius: 28, | |
color: Color(0xff27aeef), | |
), | |
//Add a sizedbox child to the CustomPaint, to give the button more hit area | |
child: SizedBox( | |
width: 70, | |
height: 70, | |
), | |
), | |
), | |
), | |
AnimatedBuilder( | |
animation: _btnAnimController, | |
builder: (BuildContext context, Widget child) { | |
double opacity = | |
Tween<double>(begin: .7, end: .9).transform(_btnAnim.value); | |
return MaterialButton( | |
height: 28, | |
splashColor: Color(0xff0f668f), | |
hoverColor: Color(0xff0f668f), | |
color: Color(0xff27aeef).withOpacity(opacity), | |
textColor: Colors.white, | |
child: Icon(widget.icon), | |
shape: CircleBorder(), | |
onPressed: widget.onPressed, | |
); | |
}, | |
) | |
], | |
); | |
} | |
} | |
class FadeColorPageRoute extends PageRouteBuilder { | |
final Widget enterPage; | |
final Color color; | |
FadeColorPageRoute({this.enterPage, @required this.color}) | |
: super( | |
transitionDuration: Duration(seconds: 3), | |
pageBuilder: ( | |
BuildContext context, | |
Animation<double> animation, | |
Animation<double> secondaryAnimation, | |
) => | |
enterPage, | |
transitionsBuilder: ( | |
BuildContext context, | |
Animation<double> animation, | |
Animation<double> secondaryAnimation, | |
Widget child, | |
) { | |
Animation fadeOut = Tween<double>(begin: 0, end: 1).animate( | |
CurvedAnimation(curve: Interval(0, .2), parent: animation)); | |
Animation fadeIn = Tween<double>(begin: 0, end: 1).animate( | |
CurvedAnimation(curve: Interval(.8, 1), parent: animation)); | |
return Stack(children: <Widget>[ | |
FadeTransition( | |
opacity: fadeOut, | |
child: Container( | |
width: double.infinity, | |
height: double.infinity, | |
color: color, | |
), | |
), | |
FadeTransition( | |
opacity: fadeIn, | |
child: child, | |
) | |
]); | |
}, | |
); | |
} | |
class ProductDetailView extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
Size screenSize = MediaQuery.of(context).size; | |
double screenRatio = screenSize.height / screenSize.width; | |
double frameRatio = screenRatio < 2 ? screenRatio / 2 : .95; | |
double frameWidth = screenSize.width * frameRatio; | |
double frameHeight = (500 * frameWidth) / 360; | |
return Scaffold( | |
backgroundColor: Colors.black, | |
body: SafeArea( | |
child: Stack( | |
children: <Widget>[ | |
Align( | |
alignment: Alignment.topCenter, | |
child: Hero( | |
tag: 'hero-speaker', | |
child: Container( | |
width: frameWidth, | |
height: frameHeight, | |
child: Sprite( | |
// image: AssetImage("images/speaker_sprite.png"), | |
image: NetworkImage( | |
"https://firebasestorage.googleapis.com/v0/b/vgv-flutter-vignettes.appspot.com/o/product_detail_zoom%2Fspeaker_sprite.png?alt=media&token=caea503f-8e4a-4e82-80e5-bec89eecd765"), | |
frameWidth: 360, | |
frameHeight: 500, | |
frame: 59)), | |
), | |
), | |
Align( | |
alignment: Alignment.topCenter, | |
child: Container( | |
width: frameWidth, | |
height: frameHeight, | |
child: ProductDetailsTransition())), | |
Align( | |
alignment: Alignment.bottomCenter, | |
child: Padding( | |
padding: | |
const EdgeInsets.only(bottom: 18.0, left: 76, right: 76), | |
child: MaterialButton( | |
height: 55, | |
minWidth: double.infinity, | |
onPressed: () {}, | |
color: Colors.white, | |
textColor: Colors.black, | |
shape: RoundedRectangleBorder( | |
borderRadius: BorderRadius.circular(50)), | |
child: Text( | |
'Buy Now \$349.95'.toUpperCase(), | |
style: TextStyle( | |
fontFamily: 'WorkSans', | |
fontWeight: FontWeight.bold, | |
fontSize: 16, | |
letterSpacing: 2), | |
), | |
), | |
), | |
), | |
Align( | |
alignment: Alignment(.6, 0), | |
child: DelayedFadeIn( | |
delay: Duration(seconds: 1), | |
child: PulsingButton( | |
onPressed: () => Navigator.of(context).pop(), | |
icon: Icons.remove, | |
), | |
), | |
) | |
], | |
), | |
), | |
); | |
} | |
} | |
class DelayedFadeIn extends StatefulWidget { | |
final Widget child; | |
final Duration duration; | |
final Duration delay; | |
const DelayedFadeIn( | |
{Key key, | |
this.child, | |
@required this.delay, | |
this.duration = const Duration(milliseconds: 700)}) | |
: super(key: key); | |
@override | |
State<StatefulWidget> createState() => _DelayedFadeInState(); | |
} | |
class _DelayedFadeInState extends State<DelayedFadeIn> | |
with SingleTickerProviderStateMixin { | |
AnimationController _animationController; | |
@override | |
void initState() { | |
super.initState(); | |
_animationController = AnimationController(vsync: this); | |
_animationController.duration = widget.duration + widget.delay; | |
_animationController.forward(from: 0); | |
} | |
@override | |
void dispose() { | |
_animationController.dispose(); | |
super.dispose(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return FadeTransition( | |
opacity: CurvedAnimation( | |
curve: Interval(.9, 1), parent: _animationController), | |
child: widget.child); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment