Created
July 8, 2020 16:48
-
-
Save AitoApps/a72b90a331b5f47d06ede62f1085caaa to your computer and use it in GitHub Desktop.
FlutterPen from Mariano Zorrilla https://codepen.io/mkiisoft/pen/qBONONY
This file contains hidden or 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 'dart:convert'; | |
import 'dart:math'; | |
import 'dart:ui'; | |
import 'package:flutter/cupertino.dart'; | |
import 'package:flutter/material.dart'; | |
void main() => runApp(ApiCall()); | |
class ApiCall extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return CupertinoApp( | |
debugShowCheckedModeBanner: false, | |
theme: CupertinoThemeData(), | |
home: MovieScreen(), | |
); | |
} | |
} | |
class MovieScreen extends StatefulWidget { | |
@override | |
_MovieScreenState createState() => _MovieScreenState(); | |
} | |
class _MovieScreenState extends State<MovieScreen> { | |
String get movie => '[ { "title": "Mission: Impossible – Fallout", "year": 2018, "image": "https://www.gstatic' | |
'.com/tv/thumb/v22vodart/13492451/p13492451_v_v8_ad.jpg", "release": "July 27, 2018" }, { "title": "Incredibles' | |
' 2", "year": 2018, "image": "https://www.gstatic.com/tv/thumb/v22vodart/13446354/p13446354_v_v8_ay.jpg", "rele' | |
'ase": "June 15, 2018" }, { "title": "Once Upon a Time in Hollywood", "year": 2019, "image": "https://www' | |
'.gstatic.com/tv/thumb/v22vodart/15226224/p15226224_v_v8_ad.jpg", "release": "July 26, 2019" }, { "title": "Joh' | |
'n Henry", "year": 2020, "image": "https://www.gstatic.com/tv/thumb/v22vodart/17733489/p17733489_v_v8_aa.jpg", ' | |
'"release": "January 24, 2020" }, { "title": "Timmy Failure: Mistakes Were Made", "year": 2020, "image": "https' | |
'://miro.medium.com/max/500/0*7ZUeYQc4vUx_i1qQ.jpg", "release": "January 25, 2020" }, { "title": "Avengers: ' | |
'Endgame", "year": 2019, "image": "https://www.gstatic.com/tv/thumb/v22vodart/15366809/p15366809_v_v8_af.jpg", ' | |
'"release": "April 26, 2019" }, { "title": "Joker", "year": 2019, "image": "https://pbs.twimg' | |
'.com/media/EDEsh0gU4AUTO3P?format=jpg&name=900x900", "release": "October 4, 2019" }, { "title": "Spider-Man: ' | |
'Into the Spider-Verse", "year": 2018, "image": "https://www.gstatic' | |
'.com/tv/thumb/v22vodart/14939602/p14939602_v_v8_ae.jpg", "release": "December 14, 2018" }, { "title": "First ' | |
'Man", "year": 2018, "image": "https://www.gstatic.com/tv/thumb/v22vodart/15398283/p15398283_v_v8_ae.jpg", "rel' | |
'ease": "October 10, 2018" }, { "title": "Avatar", "year": 2009, "image": "https://images-na.ssl-images-amazon.com/images/I/61jFTTf9RBL._AC_SL1230_.jpg",' | |
' "release": "December 18, 2009" } ]'; | |
List<Movie> _movies = []; | |
void _getMovies() { | |
final response = jsonDecode(movie) as List; | |
setState(() => _movies = response.map((json) => Movie.toObject(json)).toList()); | |
} | |
@override | |
void initState() { | |
super.initState(); | |
_getMovies(); | |
} | |
@override | |
Widget build(context) { | |
final size = MediaQuery.of(context).size; | |
return Material( | |
color: Colors.white, | |
child: Container( | |
child: Column( | |
crossAxisAlignment: CrossAxisAlignment.start, | |
children: [ | |
Padding( | |
padding: const EdgeInsets.only(left: 10, top: 50, bottom: 5), | |
child: Text('Movies', style: TextStyle(fontSize: 28, fontWeight: FontWeight.w600)), | |
), | |
Expanded( | |
child: Stack( | |
fit: StackFit.expand, | |
children: [ | |
GridView.builder( | |
padding: const EdgeInsets.only(top: 30), | |
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( | |
crossAxisCount: size.width > 600 ? size.width > 900 ? 4 : 3 : 2, childAspectRatio: 0.7), | |
itemBuilder: (context, index) { | |
final movie = _movies[index]; | |
final color = ColorGenerator.color; | |
return InkResponse( | |
onTap: () => Navigator.of(context).push(MaterialPageRoute( | |
builder: (_) => DetailsScreen( | |
movie: movie, | |
color: color, | |
))), | |
child: Container( | |
margin: const EdgeInsets.only(left: 10, right: 10, bottom: 20), | |
decoration: BoxDecoration(borderRadius: BorderRadius.circular(15), boxShadow: [ | |
BoxShadow( | |
color: color, | |
blurRadius: 70, | |
spreadRadius: -25, | |
offset: Offset(0, 20), | |
), | |
BoxShadow( | |
color: Colors.black.withAlpha(0x80), | |
blurRadius: 30, | |
spreadRadius: -20, | |
offset: Offset(0, 50), | |
) | |
]), | |
child: Hero( | |
tag: 'image_${movie.title}', | |
child: ClipRRect( | |
borderRadius: BorderRadius.circular(15), | |
clipBehavior: Clip.antiAlias, | |
child: Stack( | |
fit: StackFit.expand, | |
children: [ | |
Opacity(opacity: 0.99, child: Image.network(movie.image, fit: BoxFit.cover)), | |
Opacity(opacity: 0.6, child: Container(color: Colors.black)), | |
Column( | |
mainAxisSize: MainAxisSize.min, | |
mainAxisAlignment: MainAxisAlignment.end, | |
crossAxisAlignment: CrossAxisAlignment.start, | |
children: [ | |
Padding( | |
padding: const EdgeInsets.all(16), | |
child: Text(movie.title, style: TextStyle(color: Colors.white, fontSize: 22)), | |
) | |
], | |
), | |
], | |
), | |
), | |
), | |
), | |
); | |
}, | |
itemCount: _movies.length, | |
), | |
Align( | |
alignment: Alignment.topCenter, | |
child: Column( | |
children: [ | |
Container( | |
height: 30, | |
decoration: BoxDecoration( | |
gradient: LinearGradient( | |
colors: [Colors.white, Colors.white.withAlpha(0x00)], | |
begin: Alignment.topCenter, | |
end: Alignment.bottomCenter, | |
)), | |
) | |
], | |
), | |
) | |
], | |
), | |
), | |
], | |
), | |
), | |
); | |
} | |
} | |
class DetailsScreen extends StatefulWidget { | |
final Movie movie; | |
final Color color; | |
const DetailsScreen({Key key, this.movie, this.color}) : super(key: key); | |
@override | |
_DetailsScreenState createState() => _DetailsScreenState(); | |
} | |
class _DetailsScreenState extends State<DetailsScreen> { | |
@override | |
Widget build(BuildContext context) { | |
final size = MediaQuery.of(context).size; | |
final aspect = size.height * 0.75; | |
return Material( | |
child: SingleChildScrollView( | |
child: Stack( | |
children: [ | |
Container( | |
width: size.width, | |
height: size.height, | |
child: BackdropFilter( | |
filter: ImageFilter.blur(sigmaX: 40, sigmaY: 40), | |
child: Container( | |
width: size.width, | |
height: size.height, | |
color: widget.color.withAlpha(0x30), | |
), | |
), | |
), | |
Container( | |
width: size.width, | |
height: size.height * 0.8, | |
child: ClipPath( | |
clipper: HeaderClipper(), | |
child: Container( | |
color: widget.color, | |
), | |
), | |
), | |
Container( | |
margin: const EdgeInsets.only(right: 40), | |
child: Row( | |
mainAxisSize: MainAxisSize.max, | |
crossAxisAlignment: CrossAxisAlignment.start, | |
children: [ | |
Container( | |
width: aspect * 0.66, | |
height: aspect, | |
margin: EdgeInsets.only(left: 30, top: 70), | |
decoration: BoxDecoration( | |
borderRadius: BorderRadius.circular(15), | |
boxShadow: [ | |
BoxShadow( | |
color: widget.color, | |
blurRadius: 300, | |
spreadRadius: 20, | |
offset: Offset(0, 100), | |
), | |
], | |
), | |
child: HoverCard( | |
builder: (context, hover) { | |
return Hero( | |
tag: 'image_${widget.movie.title}', | |
child: ClipRRect( | |
borderRadius: BorderRadius.circular(15), | |
child: Image.network(widget.movie.image, fit: BoxFit.cover), | |
), | |
); | |
}, | |
depth: 0, | |
depthColor: Colors.transparent, | |
shadow: BoxShadow( | |
color: Colors.black.withAlpha(0x80), | |
blurRadius: 30, | |
spreadRadius: -20, | |
offset: Offset(0, 50), | |
), | |
), | |
), | |
Expanded( | |
child: Container( | |
padding: const EdgeInsets.only(left: 30, top: 70), | |
child: Column( | |
mainAxisSize: MainAxisSize.min, | |
crossAxisAlignment: CrossAxisAlignment.start, | |
children: [ | |
Text(widget.movie.title, | |
style: TextStyle(color: Colors.white, fontSize: 28, fontWeight: FontWeight.w600)), | |
], | |
), | |
), | |
) | |
], | |
), | |
), | |
Padding( | |
padding: const EdgeInsets.all(15), | |
child: GestureDetector( | |
onTap: () => Navigator.of(context).pop(), | |
child: Icon(Icons.arrow_back, size: 28, color: Colors.white), | |
), | |
), | |
], | |
), | |
), | |
); | |
} | |
} | |
class HeaderClipper extends CustomClipper<Path> { | |
@override | |
Path getClip(Size size) { | |
final path = Path(); | |
path.lineTo(0, 0); | |
path.lineTo(0, size.height); | |
path.quadraticBezierTo(size.width * 0.8, size.height * 0.9, size.width, size.height * 0.4); | |
path.lineTo(size.width, 0); | |
return path; | |
} | |
@override | |
bool shouldReclip(CustomClipper<Path> oldClipper) => true; | |
} | |
class Movie { | |
final String title; | |
final String image; | |
Movie(this.title, this.image); | |
factory Movie.toObject(Map<String, dynamic> json) => Movie(json['title'], json['image']); | |
Map<String, dynamic> toMap() => {'title': this.title, 'image': this.image}; | |
} | |
class ColorGenerator { | |
static Random random = Random(); | |
static Color get color => Color.fromARGB(255, random.nextInt(255), random.nextInt(255), random.nextInt(255)); | |
} | |
class HoverCard extends StatefulWidget { | |
final Widget Function(BuildContext context, bool isHovered) builder; | |
final double depth; | |
final Color depthColor; | |
final BoxShadow shadow; | |
final GestureTapCallback onTap; | |
const HoverCard({ | |
Key key, | |
@required this.builder, | |
this.onTap, | |
this.depth = 0, | |
this.depthColor = const Color(0xFF424242), | |
this.shadow = const BoxShadow( | |
offset: Offset(0, 60), | |
color: Color.fromARGB(120, 0, 0, 0), | |
blurRadius: 22, | |
spreadRadius: -20, | |
), | |
}) : super(key: key); | |
@override | |
HoverCardState createState() => HoverCardState(); | |
} | |
class HoverCardState extends State<HoverCard> | |
with SingleTickerProviderStateMixin { | |
double localX = 0; | |
double localY = 0; | |
bool defaultPosition = true; | |
bool isHover = false; | |
AnimationController animationController; | |
Animation<FractionalOffset> animation; | |
@override | |
void initState() { | |
super.initState(); | |
_setupAnimation(); | |
} | |
@override | |
void dispose() { | |
animationController.dispose(); | |
super.dispose(); | |
} | |
void _setupAnimation() { | |
animationController = AnimationController( | |
vsync: this, | |
duration: const Duration(milliseconds: 150), | |
); | |
} | |
void _resetAnimation(Size size, Offset offset) { | |
animationController.addListener(_updatePosition); | |
animation = FractionalOffsetTween( | |
begin: FractionalOffset(offset.dx, offset.dy), | |
end: FractionalOffset((size.width) / 2, (size.height) / 2), | |
).animate(CurvedAnimation( | |
curve: Curves.easeInOut, | |
parent: animationController, | |
)); | |
animationController.addStatusListener((status) { | |
if (status == AnimationStatus.completed) { | |
setState(() => defaultPosition = true); | |
animationController.removeListener(_updatePosition); | |
animationController.reverse(); | |
} | |
}); | |
} | |
void _updatePosition() { | |
setState(() { | |
localX = animation.value.dx; | |
localY = animation.value.dy; | |
}); | |
} | |
void reset(Size size) { | |
_resetAnimation(size, Offset(0, 0)); | |
_updatePosition(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return LayoutBuilder( | |
builder: (_, dimens) { | |
final size = Size(dimens.maxWidth, dimens.maxHeight); | |
double percentageX = (localX / size.width) * 100; | |
double percentageY = (localY / size.height) * 100; | |
return Transform( | |
transform: Matrix4.identity() | |
..setEntry(3, 2, 0.001) | |
..rotateX(defaultPosition ? 0 : (0.3 * (percentageY / 50) + -0.3)) | |
..rotateY(defaultPosition ? 0 : (-0.3 * (percentageX / 50) + 0.3)), | |
alignment: FractionalOffset.center, | |
child: Container( | |
width: size.width, | |
height: size.height, | |
decoration: BoxDecoration( | |
color: widget.depthColor, | |
borderRadius: BorderRadius.circular(15), | |
boxShadow: [widget.shadow], | |
), | |
child: GestureDetector( | |
onPanUpdate: (details) { | |
setState(() { | |
defaultPosition = false; | |
if (details.localPosition.dx > 0 && | |
details.localPosition.dy > 0) { | |
if (details.localPosition.dx < size.width && | |
details.localPosition.dy < size.height) { | |
localX = details.localPosition.dx; | |
localY = details.localPosition.dy; | |
} | |
} | |
}); | |
}, | |
onPanEnd: (_) { | |
setState(() { | |
isHover = true; | |
defaultPosition = false; | |
}); | |
_resetAnimation(size, Offset(localX, localY)); | |
animationController.forward(); | |
}, | |
onPanCancel: () { | |
setState(() { | |
isHover = false; | |
}); | |
_resetAnimation(size, Offset(localX, localY)); | |
animationController.forward(); | |
}, | |
onTap: widget.onTap, | |
child: MouseRegion( | |
onEnter: (_) { | |
if (mounted) | |
setState(() { | |
isHover = true; | |
defaultPosition = false; | |
}); | |
}, | |
onExit: (_) { | |
if (mounted) | |
setState(() { | |
isHover = false; | |
}); | |
_resetAnimation(size, Offset(localX, localY)); | |
animationController.forward(); | |
}, | |
onHover: (details) { | |
RenderBox box = context.findRenderObject(); | |
final _offset = box.globalToLocal(details.localPosition); | |
if (mounted) | |
setState(() { | |
defaultPosition = false; | |
if (_offset.dx > 0 && _offset.dy > 0) { | |
if (_offset.dx < size.width * 1.5 && _offset.dy > 0) { | |
localX = _offset.dx; | |
localY = _offset.dy; | |
} | |
} | |
}); | |
}, | |
child: ClipRRect( | |
borderRadius: BorderRadius.circular(10), | |
child: Container( | |
color: widget.depthColor, | |
child: Stack( | |
children: [ | |
Positioned.fill( | |
child: Transform( | |
transform: Matrix4.identity() | |
..translate( | |
defaultPosition | |
? 0.0 | |
: (widget.depth * (percentageX / 50) + | |
-widget.depth), | |
defaultPosition | |
? 0.0 | |
: (widget.depth * (percentageY / 50) + | |
-widget.depth), | |
0.0), | |
alignment: FractionalOffset.center, | |
child: ClipRRect( | |
borderRadius: BorderRadius.circular(10), | |
child: widget.builder(context, isHover), | |
), | |
), | |
), | |
Stack( | |
children: [ | |
Transform( | |
transform: Matrix4.translationValues( | |
(size.width - 50) - localX, | |
(size.height - 50) - localY, | |
0.0, | |
), | |
child: AnimatedOpacity( | |
opacity: defaultPosition ? 0 : 0.99, | |
duration: Duration(milliseconds: 500), | |
curve: Curves.decelerate, | |
child: Container( | |
height: 100, | |
width: 100, | |
decoration: BoxDecoration(boxShadow: [ | |
BoxShadow( | |
color: Colors.white.withOpacity(0.22), | |
blurRadius: 100, | |
spreadRadius: 40, | |
) | |
]), | |
), | |
), | |
), | |
], | |
), | |
], | |
), | |
), | |
), | |
), | |
), | |
), | |
); | |
}, | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment