-
-
Save kliuchev/7d590d37bead241e09b99117e45b1865 to your computer and use it in GitHub Desktop.
Complex animation
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 'dart:math'; | |
void main() { | |
runApp(const MaterialApp( | |
debugShowCheckedModeBanner: false, | |
home: Screen(), | |
)); | |
} | |
class Screen extends StatelessWidget { | |
const Screen({super.key}); | |
@override | |
Widget build(BuildContext context) => Scaffold( | |
appBar: AppBar(title: const Text('Explore')), | |
backgroundColor: Colors.white, | |
body: ListView( | |
children: const [ | |
PhotoHeader(), | |
Photo(), | |
], | |
), | |
); | |
} | |
class PhotoLike extends StatefulWidget { | |
const PhotoLike( | |
{super.key, | |
required Offset position, | |
required Size size, | |
required OverlayEntry overlayEntry}) | |
: _position = position, | |
_size = size, | |
_overlayEntry = overlayEntry; | |
final Offset _position; | |
final Size _size; | |
final OverlayEntry _overlayEntry; | |
@override | |
State<PhotoLike> createState() => _PhotoLikeState(); | |
} | |
class _PhotoLikeState extends State<PhotoLike> | |
with SingleTickerProviderStateMixin { | |
late final AnimationController _controller; | |
late final Animation<double> _opacity; | |
late final Animation<double> _scale; | |
late final Animation<double> _rotation; | |
late final Tween<Offset> _positionTween; | |
late final Animation<Offset> _position; | |
void initState() { | |
super.initState(); | |
_controller = | |
AnimationController(vsync: this, duration: const Duration(seconds: 1)); | |
_controller.addListener(() { | |
if (_controller.isCompleted) { | |
widget._overlayEntry.remove(); | |
} else { | |
setState(() {}); | |
} | |
}); | |
_opacity = TweenSequence<double>([ | |
TweenSequenceItem( | |
tween: Tween(begin: 0.0, end: 1.0), | |
weight: 50, | |
), | |
TweenSequenceItem( | |
tween: Tween(begin: 1.0, end: 0.0), | |
weight: 50, | |
), | |
]).animate(_controller); | |
_scale = TweenSequence<double>([ | |
TweenSequenceItem( | |
tween: Tween(begin: 0.5, end: 1.0), | |
weight: 50, | |
), | |
TweenSequenceItem( | |
tween: Tween(begin: 1.0, end: 1.5), | |
weight: 50, | |
), | |
]).animate(_controller); | |
final beginAngle = Random.secure().nextDouble() - 0.5; | |
const endAngle = 0.0; | |
_rotation = TweenSequence<double>([ | |
TweenSequenceItem( | |
tween: Tween(begin: beginAngle, end: endAngle), | |
weight: 50, | |
), | |
TweenSequenceItem( | |
tween: ConstantTween(endAngle), | |
weight: 50, | |
), | |
]).animate(_controller); | |
_positionTween = Tween<Offset>(begin: Offset.zero); | |
_position = TweenSequence<Offset>([ | |
TweenSequenceItem( | |
tween: ConstantTween(Offset.zero), | |
weight: 50, | |
), | |
TweenSequenceItem(tween: _positionTween, weight: 50), | |
]).animate(_controller); | |
_controller.forward(); | |
} | |
void didChangeDependencies() { | |
super.didChangeDependencies(); | |
final screenWidth = MediaQuery.of(context).size.width; | |
final screenCenter = screenWidth / 2.0; | |
final distance = screenCenter - widget._position.dx; | |
final leftOffset = distance * Random.secure().nextDouble(); | |
final topOffset = -widget._position.dy; | |
_positionTween.end = Offset(leftOffset, topOffset); | |
} | |
void dispose() { | |
_controller.dispose(); | |
super.dispose(); | |
} | |
@override | |
Widget build(BuildContext context) => Transform.translate( | |
offset: _position.value, | |
child: Transform.scale( | |
scale: _scale.value, | |
child: Transform.rotate( | |
angle: _rotation.value, | |
child: Opacity( | |
opacity: _opacity.value, | |
child: Heart( | |
width: widget._size.width, | |
height: widget._size.height, | |
), | |
), | |
), | |
), | |
); | |
} | |
class Heart extends StatelessWidget { | |
const Heart({ | |
super.key, | |
double? width, | |
double? height, | |
}) : _width = width, | |
_height = height; | |
final double? _width; | |
final double? _height; | |
@override | |
Widget build(BuildContext context) => Container( | |
color: Colors.black, | |
width: _width, | |
height: _height, | |
); | |
} | |
class Photo extends StatelessWidget { | |
const Photo({super.key}); | |
@override | |
Widget build(BuildContext context) => DoubleTapDetector( | |
onDoubleTap: (details) { | |
final position = details.globalPosition; | |
const size = Size(50, 50); | |
late final OverlayEntry entry; | |
entry = OverlayEntry( | |
builder: (context) { | |
return Positioned( | |
top: position.dy - size.height / 2.0, | |
left: position.dx - size.width / 2.0, | |
child: PhotoLike( | |
size: size, | |
position: position, | |
overlayEntry: entry, | |
), | |
); | |
}, | |
); | |
Overlay.of(context).insert(entry); | |
}, | |
child: Image.network( | |
'https://i.postimg.cc/tgxsFtXt/IMG-4368.jpg', | |
fit: BoxFit.cover, | |
), | |
); | |
} | |
class PhotoHeader extends StatelessWidget { | |
const PhotoHeader({super.key}); | |
@override | |
Widget build(BuildContext context) => const Row( | |
children: [ | |
UserIcon(), | |
UserNickname(), | |
Spacer(), | |
UselessButton(), | |
], | |
); | |
} | |
class UserIcon extends StatelessWidget { | |
const UserIcon({super.key}); | |
@override | |
Widget build(BuildContext context) => const Padding( | |
padding: EdgeInsets.all(8.0), | |
child: CircleAvatar( | |
backgroundImage: | |
NetworkImage('https://i.postimg.cc/SKqYW1KL/user-pic.jpg'), | |
), | |
); | |
} | |
class UserNickname extends StatelessWidget { | |
const UserNickname({super.key}); | |
@override | |
Widget build(BuildContext context) => | |
const Text('sviat', style: TextStyle(fontWeight: FontWeight.bold)); | |
} | |
class UselessButton extends StatelessWidget { | |
const UselessButton({super.key}); | |
@override | |
Widget build(BuildContext context) => | |
IconButton(onPressed: () {}, icon: const Icon(Icons.more_horiz)); | |
} | |
class DoubleTapDetector extends StatefulWidget { | |
const DoubleTapDetector({ | |
super.key, | |
required Widget child, | |
required GestureTapDownCallback onDoubleTap, | |
}) : _child = child, | |
_onDoubleTap = onDoubleTap; | |
final Widget _child; | |
final GestureTapDownCallback _onDoubleTap; | |
@override | |
State<DoubleTapDetector> createState() => _DoubleTapDetectorState(); | |
} | |
class _DoubleTapDetectorState extends State<DoubleTapDetector> { | |
TapDownDetails? _tapDownDetails; | |
@override | |
Widget build(BuildContext context) { | |
return GestureDetector( | |
onDoubleTapCancel: () => _tapDownDetails = null, | |
onDoubleTapDown: (details) => _tapDownDetails = details, | |
onDoubleTap: () { | |
if (_tapDownDetails == null) return; | |
widget._onDoubleTap(_tapDownDetails!); | |
}, | |
child: widget._child, | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment