Skip to content

Commit

Permalink
Yaru animated ok icon (#183)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jupi007 committed Aug 23, 2022
1 parent 1b6869c commit 2ee329a
Show file tree
Hide file tree
Showing 4 changed files with 227 additions and 1 deletion.
24 changes: 24 additions & 0 deletions example/lib/example_page_items.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:yaru_colors/yaru_colors.dart';
import 'package:yaru_icons/yaru_icons.dart';
import 'package:yaru_widgets/yaru_widgets.dart';
import 'package:yaru_widgets_example/pages/banner_page.dart';
Expand Down Expand Up @@ -35,6 +36,29 @@ final examplePageItems = <YaruPageItem>[
iconData: YaruIcons.settings_filled,
builder: (_) => ExtraOptionRowPage(),
),
YaruPageItem(
titleBuilder: (context) => YaruPageItemTitle.text('YaruAnimatedIcons'),
iconData: YaruIcons.ok,
builder: (_) => YaruPage(
children: [
Padding(
padding: const EdgeInsets.only(top: 25),
child: YaruAnimatedOkIcon(
size: 96,
color: YaruColors.success,
),
),
Padding(
padding: const EdgeInsets.only(top: 25),
child: YaruAnimatedOkIcon(
size: 96,
filled: true,
color: YaruColors.success,
),
),
],
),
),
YaruPageItem(
titleBuilder: (context) => YaruPageItemTitle.text('YaruProgressIndicator'),
iconData: YaruIcons.download,
Expand Down
3 changes: 2 additions & 1 deletion example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ dependencies:
provider: ^6.0.2
yaru: ^0.3.2
yaru_icons: ^0.2.1
yaru_colors: ^0.1.0
yaru_widgets:
path: ../

Expand All @@ -27,4 +28,4 @@ flutter:
uses-material-design: true

assets:
- assets/
- assets/
200 changes: 200 additions & 0 deletions lib/src/animations/yaru_animated_ok_icon.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import 'package:flutter/material.dart';

const _kTargetCanvasSize = 24.0;
const _kTargetIconSize = 20.0;
const _kAnimationCurve = Curves.easeInCubic;
const _kAnimationDuration = 400;

/// An animated Yaru ok icon, similar to the original one
class YaruAnimatedOkIcon extends StatefulWidget {
/// Create an animated Yaru ok icon, similar to the original one
const YaruAnimatedOkIcon({
this.size = 24.0,
this.filled = false,
this.color,
this.onCompleted,
super.key,
});

/// 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
/// 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;

/// Callback called once animation completed
final Function? onCompleted;

@override
_YaruAnimatedOkIconState createState() => _YaruAnimatedOkIconState();
}

class _YaruAnimatedOkIconState extends State<YaruAnimatedOkIcon>
with TickerProviderStateMixin {
late Animation<double> _animation;
late AnimationController _controller;

@override
void initState() {
super.initState();

_controller = AnimationController(
duration: const Duration(milliseconds: _kAnimationDuration),
vsync: this,
);
_animation = Tween(begin: 0.0, end: 1.0)
.chain(CurveTween(curve: _kAnimationCurve))
.animate(_controller);

_controller.addStatusListener((status) {
if (status == AnimationStatus.completed && widget.onCompleted != null) {
widget.onCompleted!();
}
});

_controller.forward();
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return ClipRRect(
child: SizedBox.square(
dimension: widget.size,
child: AnimatedBuilder(
animation: _animation,
builder: ((context, child) {
return CustomPaint(
painter: _YaruAnimatedOkIconPainter(
widget.size,
widget.filled,
widget.color ?? Theme.of(context).colorScheme.onSurface,
_animation.value,
),
);
}),
),
),
);
}
}

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

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

@override
void paint(Canvas canvas, Size size) {
if (filled && animationPosition >= 0.5) {
canvas.drawPath(
Path.combine(
PathOperation.difference,
_createCirclePath(),
_createCheckmarkPath(),
),
_createFillPaint(),
);
} else {
canvas.drawPath(_createCheckmarkPath(), _createFillPaint());
canvas.drawPath(_createCirclePath(), _createStrokePaint());
}
}

Path _createCheckmarkPath() {
final Path checkmark = Path();
final Offset start1 = Offset(size * 0.354, size * 0.477);
final Offset start2 = Offset(size * 0.310, size * 0.521);
final Offset mid1 = Offset(size * 0.521, size * 0.643);
final Offset mid2 = Offset(size * 0.521, size * 0.732);
final Offset end1 = Offset(size * 0.865, size * 0.299);
final Offset end2 = Offset(size * 0.892, size * 0.360);

if (animationPosition < 0.5) {
final double pathT = animationPosition * 2.0;
final Offset drawMid1 = Offset.lerp(start1, mid1, pathT)!;
final Offset drawMid2 = Offset.lerp(start2, mid2, pathT)!;

checkmark.moveTo(start1.dx, start1.dy);
checkmark.lineTo(drawMid1.dx, drawMid1.dy);
checkmark.lineTo(drawMid2.dx, drawMid2.dy);
checkmark.lineTo(start2.dx, start2.dy);
checkmark.close();
} else {
final double pathT = (animationPosition - 0.5) * 2.0;
final Offset drawEnd1 = Offset.lerp(mid1, end1, pathT)!;
final Offset drawEnd2 = Offset.lerp(mid2, end2, pathT)!;

checkmark.moveTo(start1.dx, start1.dy);
checkmark.lineTo(mid1.dx, mid1.dy);
checkmark.lineTo(drawEnd1.dx, drawEnd1.dy);
checkmark.lineTo(drawEnd2.dx, drawEnd2.dy);
checkmark.lineTo(mid2.dx, mid2.dy);
checkmark.lineTo(start2.dx, start2.dy);
checkmark.close();
}

return checkmark;
}

Path _createCirclePath() {
final finalCircleRadius =
(size / 2 - 1) * _kTargetIconSize / _kTargetCanvasSize;
// From 1.0 to 0.75 to 1.0
final circleRadius = animationPosition < 0.5
? finalCircleRadius - finalCircleRadius * 0.25 * animationPosition
: finalCircleRadius * 0.75 +
finalCircleRadius * 0.25 * animationPosition;

return Path()
..addOval(
Rect.fromCircle(
center: Offset(size / 2, size / 2),
radius: circleRadius,
),
);
}

Paint _createFillPaint() {
return Paint()
..color = color
..style = PaintingStyle.fill;
}

Paint _createStrokePaint() {
return Paint()
..color = color
..style = PaintingStyle.stroke
..strokeWidth = 1 / (_kTargetCanvasSize / size);
}

@override
bool shouldRepaint(
_YaruAnimatedOkIconPainter oldDelegate,
) {
return oldDelegate.animationPosition != animationPosition ||
oldDelegate.size != size ||
oldDelegate.filled != filled ||
oldDelegate.color != color;
}
}
1 change: 1 addition & 0 deletions lib/yaru_widgets.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
library yaru_widgets;

export 'src/animations/yaru_animated_ok_icon.dart';
export 'src/utilities/ubuntu_logo.dart';
export 'src/dialogs/yaru_alert_dialog.dart';
export 'src/utilities/yaru_banner.dart';
Expand Down

0 comments on commit 2ee329a

Please sign in to comment.