Skip to content

Commit

Permalink
Add animated heart icon (#155)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jupi007 authored Jul 4, 2023
1 parent 093f04d commit 13984b4
Show file tree
Hide file tree
Showing 3 changed files with 258 additions and 2 deletions.
18 changes: 18 additions & 0 deletions example/lib/src/icon_items.dart
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,24 @@ final List<IconItem> animatedIconItems = [
size: iconSize,
),
),
IconItem(
name: 'Heart',
usage: 'YaruAnimatedHeartIcon()',
iconBuilder: (context, iconSize) => YaruAnimatedIcon(
const YaruAnimatedHeartIcon(),
mode: YaruAnimationMode.once,
size: iconSize,
),
),
IconItem(
name: 'Heart filled',
usage: 'YaruAnimatedHeartIcon(filled: true)',
iconBuilder: (context, iconSize) => YaruAnimatedIcon(
const YaruAnimatedHeartIcon(filled: true),
mode: YaruAnimationMode.once,
size: iconSize,
),
),
];

final List<IconItem> widgetIconItems = [
Expand Down
239 changes: 239 additions & 0 deletions lib/src/animated_icons/yaru_animated_heart_icon.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
import 'package:flutter/material.dart';

import '../../yaru_icons.dart';

const _kAnimationCurve = Curves.easeInOutCubic;
const _kAnimationDuration = Duration(milliseconds: 600);
const _kTargetCanvasSize = 24.0;

/// An animated Yaru heart icon, similar to [YaruIcons.heart].
class YaruAnimatedHeartIcon extends YaruAnimatedIconData {
/// An animated Yaru heart icon, similar to [YaruIcons.heart].
const YaruAnimatedHeartIcon({
this.filled,
});

/// Determines if the icon uses a solid background, like [YaruIcons.heart_filled]
/// Defaults to false as the original Yaru icon.
final bool? filled;

@override
Widget build(
BuildContext context,
Animation<double> progress,
double? size,
Color? color,
) {
return YaruAnimatedHeartIconWidget(
progress: progress,
size: size,
filled: filled,
color: color,
);
}

@override
Duration get defaultDuration => _kAnimationDuration;

@override
Curve get defaultCurve => _kAnimationCurve;
}

/// An animated Yaru heart icon, similar to [YaruIcons.heart].
class YaruAnimatedHeartIconWidget extends StatelessWidget {
/// An animated Yaru heart icon, similar to [YaruIcons.heart].
const YaruAnimatedHeartIconWidget({
super.key,
this.size,
this.color,
this.filled,
required this.progress,
});

/// Determines the icon canvas size
/// To fit the original Yaru icon, the icon will be slightly smaller (20.0 on a 24.0 canvas).
/// Defaults to 24.0 as the original Yaru icon.
final double? size;

/// Determines if the icon uses a solid background, like [YaruIcons.heart_filled]
/// Defaults to false as the original Yaru icon.
final bool? filled;

/// Color used to draw the icon.
/// If null, defaults to colorScheme.onSurface.
final Color? color;

/// The animation progress for the animated icon.
/// The value is clamped to be between 0 and 1.
final Animation<double> progress;

@override
Widget build(BuildContext context) {
final size = this.size ?? _kTargetCanvasSize;
final color = this.color ?? Theme.of(context).colorScheme.onSurface;
final filled = this.filled != null ? this.filled! : false;

return RepaintBoundary(
child: SizedBox.square(
dimension: size,
child: CustomPaint(
painter: _YaruAnimatedHeartIconPainter(
size,
color,
filled,
progress.value,
),
),
),
);
}
}

class _YaruAnimatedHeartIconPainter extends CustomPainter {
const _YaruAnimatedHeartIconPainter(
this.size,
this.color,
this.filled,
this.progress,
) : assert(progress >= 0.0 && progress <= 1.0);

final double size;
final Color color;
final bool filled;
final double progress;

@override
void paint(Canvas canvas, Size size) {
if (progress < .5) {
final metric = _getHeartPath().computeMetrics().single;
final localProgress = _computeLocalProgress(0, .5);
final drawPath = metric.extractPath(
metric.length / 2 - metric.length / 2 * localProgress,
metric.length / 2 + metric.length / 2 * localProgress,
);

canvas.drawPath(
drawPath,
_getStrokePaint(),
);
} else {
final localProgress = _computeLocalProgress(.5, .5);
final scale = localProgress < .5
? 1 - .25 * _computeLocalProgress(.5, .25)
: .75 + .25 * _computeLocalProgress(.75, .25);
canvas.drawPath(
_getHeartPath(scale: scale),
_getStrokePaint(),
);

if (filled && localProgress >= .5) {
canvas.drawPath(
_getHeartPath(scale: _computeLocalProgress(.75, .25)),
_getFillPaint(),
);
}
}
}

Path _getHeartPath({double scale = 1.0}) {
assert(scale >= 0.0 && scale <= 1.0);
final localSize = size * scale;

return Path()
..moveTo(
localSize * 0.5000696 + (size - localSize) / 2,
localSize * 0.2653214 + (size - localSize) / 2,
)
..cubicTo(
localSize * 0.4252717 + (size - localSize) / 2,
localSize * 0.2038911 + (size - localSize) / 2,
localSize * 0.3562770 + (size - localSize) / 2,
localSize * 0.1784415 + (size - localSize) / 2,
localSize * 0.3001936 + (size - localSize) / 2,
localSize * 0.1904627 + (size - localSize) / 2,
)
..cubicTo(
localSize * 0.2407755 + (size - localSize) / 2,
localSize * 0.2031987 + (size - localSize) / 2,
localSize * 0.2012291 + (size - localSize) / 2,
localSize * 0.2556057 + (size - localSize) / 2,
localSize * 0.1907196 + (size - localSize) / 2,
localSize * 0.3239365 + (size - localSize) / 2,
)
..cubicTo(
localSize * 0.1697006 + (size - localSize) / 2,
localSize * 0.4605983 + (size - localSize) / 2,
localSize * 0.2522089 + (size - localSize) / 2,
localSize * 0.6683053 + (size - localSize) / 2,
localSize * 0.4883975 + (size - localSize) / 2,
localSize * 0.8454960 + (size - localSize) / 2,
)
..lineTo(
localSize * 0.5000696 + (size - localSize) / 2,
localSize * 0.8542308 + (size - localSize) / 2,
)
..lineTo(
localSize * 0.5117417 + (size - localSize) / 2,
localSize * 0.8454960 + (size - localSize) / 2,
)
..cubicTo(
localSize * 0.7479304 + (size - localSize) / 2,
localSize * 0.6683053 + (size - localSize) / 2,
localSize * 0.8304387 + (size - localSize) / 2,
localSize * 0.4605982 + (size - localSize) / 2,
localSize * 0.8094196 + (size - localSize) / 2,
localSize * 0.3239365 + (size - localSize) / 2,
)
..cubicTo(
localSize * 0.7989101 + (size - localSize) / 2,
localSize * 0.2556057 + (size - localSize) / 2,
localSize * 0.7593637 + (size - localSize) / 2,
localSize * 0.2031987 + (size - localSize) / 2,
localSize * 0.6999455 + (size - localSize) / 2,
localSize * 0.1904627 + (size - localSize) / 2,
)
..cubicTo(
localSize * 0.6438623 + (size - localSize) / 2,
localSize * 0.1784415 + (size - localSize) / 2,
localSize * 0.5748676 + (size - localSize) / 2,
localSize * 0.2038911 + (size - localSize) / 2,
localSize * 0.5000696 + (size - localSize) / 2,
localSize * 0.2653214 + (size - localSize) / 2,
)
..close();
}

Paint _getFillPaint() {
return Paint()
..style = PaintingStyle.fill
..color = color
..blendMode = BlendMode.src;
}

Paint _getStrokePaint() {
return Paint()
..style = PaintingStyle.stroke
..color = color
..strokeWidth = 1 / (_kTargetCanvasSize / size)
..blendMode = BlendMode.src;
}

double _computeLocalProgress(double start, double duration) {
assert(start >= 0.0 && start <= 1.0);
assert(duration >= 0.0 && duration <= 1.0);
assert(start + duration <= 1.0);

final localProgress =
progress >= start ? (progress - start) * (1.0 / duration) : 0.0;

return localProgress < 1.0 ? localProgress : 1.0;
}

@override
bool shouldRepaint(_YaruAnimatedHeartIconPainter oldDelegate) {
return oldDelegate.size != size ||
oldDelegate.color != color ||
oldDelegate.filled != filled ||
oldDelegate.progress != progress;
}
}
3 changes: 1 addition & 2 deletions lib/yaru_icons.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@ library yaru_icons;

// Animated Icons
export 'src/animated_icons/yaru_animated_compass_icon.dart';
export 'src/animated_icons/yaru_animated_heart_icon.dart';
export 'src/animated_icons/yaru_animated_icon.dart';
export 'src/animated_icons/yaru_animated_no_network_icon.dart';
export 'src/animated_icons/yaru_animated_ok_icon.dart';

// Widgets
export 'src/widgets/yaru_placeholder_icon.dart';

export 'src/yaru_freedesktop_icons.dart';
export 'src/yaru_icons.dart';

0 comments on commit 13984b4

Please sign in to comment.