Skip to content

Instantly share code, notes, and snippets.

@kliuchev
Created June 1, 2024 19:24
Show Gist options
  • Save kliuchev/7d590d37bead241e09b99117e45b1865 to your computer and use it in GitHub Desktop.
Save kliuchev/7d590d37bead241e09b99117e45b1865 to your computer and use it in GitHub Desktop.
Complex animation
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