Skip to content

Instantly share code, notes, and snippets.

@ilovejs
Last active August 15, 2021 08:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ilovejs/a6f04c006d29642ac1d6a94aac602425 to your computer and use it in GitHub Desktop.
Save ilovejs/a6f04c006d29642ac1d6a94aac602425 to your computer and use it in GitHub Desktop.
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