From d050c9a681e952f730b2472fed405a3db7c933bd Mon Sep 17 00:00:00 2001 From: Paul Kepinski Date: Fri, 2 Sep 2022 22:48:32 +0200 Subject: [PATCH 1/3] Import YaruAnimatedOkIcon --- lib/src/animations/yaru_animated_ok_icon.dart | 200 ++++++++++++++++++ pubspec.yaml | 2 +- 2 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 lib/src/animations/yaru_animated_ok_icon.dart diff --git a/lib/src/animations/yaru_animated_ok_icon.dart b/lib/src/animations/yaru_animated_ok_icon.dart new file mode 100644 index 0000000..7f4b5e8 --- /dev/null +++ b/lib/src/animations/yaru_animated_ok_icon.dart @@ -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 + with TickerProviderStateMixin { + late Animation _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; + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 684b3e1..e488ee3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ homepage: https://github.com/ubuntu/yaru_icons.dart description: Ubuntu Yaru Icon theme environment: - sdk: ">=2.13.0 <3.0.0" + sdk: ">=2.17.0 <3.0.0" flutter: ">=2.2.0" dependencies: From 0f31869571d0472fa375824b1e22496c565b9541 Mon Sep 17 00:00:00 2001 From: Paul Kepinski Date: Sat, 3 Sep 2022 13:06:20 +0200 Subject: [PATCH 2/3] Export animated icons --- lib/yaru_icons.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/yaru_icons.dart b/lib/yaru_icons.dart index cdcd054..bb5c172 100644 --- a/lib/yaru_icons.dart +++ b/lib/yaru_icons.dart @@ -1,3 +1,6 @@ library yaru_icons; export 'src/yaru_icons.dart'; + +// Animated Icons +export 'src/animations/yaru_animated_ok_icon.dart'; From fa16139c4975d9dc59fcdbe9d73dcb40ad516990 Mon Sep 17 00:00:00 2001 From: Paul Kepinski Date: Sat, 3 Sep 2022 13:06:29 +0200 Subject: [PATCH 3/3] Adapt example --- example/analysis_options.yaml | 3 +- example/lib/main.dart | 138 +++++++++-------------- example/lib/src/animated_icons_grid.dart | 32 ++++++ example/lib/src/icon_size_provider.dart | 26 +++++ example/lib/src/icons_grid.dart | 34 ++++++ example/lib/src/yaru_icon_data.dart | 10 ++ example/pubspec.yaml | 4 +- 7 files changed, 158 insertions(+), 89 deletions(-) create mode 100644 example/lib/src/animated_icons_grid.dart create mode 100644 example/lib/src/icon_size_provider.dart create mode 100644 example/lib/src/icons_grid.dart create mode 100644 example/lib/src/yaru_icon_data.dart diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index 1411db2..3dfd9fd 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -23,7 +23,8 @@ linter: # producing the lint. rules: # avoid_print: false # Uncomment to disable the `avoid_print` rule - # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + require_trailing_commas: true # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options diff --git a/example/lib/main.dart b/example/lib/main.dart index e040191..345dbfa 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,6 +1,11 @@ +import 'package:example/src/animated_icons_grid.dart'; +import 'package:example/src/icon_size_provider.dart'; +import 'package:example/src/icons_grid.dart'; +import 'package:provider/provider.dart'; +import 'package:yaru_colors/yaru_colors.dart'; import 'package:yaru_icons/yaru_icons.dart'; import 'package:flutter/material.dart'; -import 'package:yaru/yaru.dart' as yaru; +import 'package:yaru/yaru.dart'; void main() { runApp(MyApp()); @@ -11,95 +16,54 @@ class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Yaru Icons Demo', - theme: yaru.lightTheme, - darkTheme: yaru.darkTheme, debugShowCheckedModeBanner: false, - home: const YaruIconsGrid(), - ); - } -} + home: YaruTheme( + child: ChangeNotifierProvider( + create: (context) => IconSizeProvider(), + builder: (context, child) { + final iconSizeProvider = Provider.of(context); -@immutable -class YaruIconsData extends IconData { - const YaruIconsData(int codePoint) - : super( - codePoint, - fontFamily: 'YaruIcons', - fontPackage: 'yaru_icons', - ); -} - -class YaruIconsGrid extends StatefulWidget { - const YaruIconsGrid({Key? key}) : super(key: key); - - @override - _YaruIconsGridState createState() => _YaruIconsGridState(); -} - -class _YaruIconsGridState extends State - with SingleTickerProviderStateMixin { - late AnimationController _controller; - - static const _from = 0xf101; - static const _to = 0xf2bd; - - double _iconsSize = 24; - bool _isMinIconsSize() => _iconsSize <= 16 ? true : false; - - @override - void initState() { - super.initState(); - _controller = AnimationController(vsync: this); - } - - @override - void dispose() { - super.dispose(); - _controller.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - leading: Icon(YaruIcons.ubuntu_logo, color: yaru.Colors.orange), - title: Text('Flutter Yaru Icons Demo (${_iconsSize.truncate()}px)'), - actions: [ - TextButton( - onPressed: _isMinIconsSize() ? null : _decreaseIconsSize, - child: Icon(YaruIcons.minus)), - TextButton(onPressed: _increaseIconsSize, child: Icon(YaruIcons.plus)) - ], - ), - body: GridView.extent( - padding: const EdgeInsets.all(24), - maxCrossAxisExtent: _iconsSize + 48, - children: List.generate(_to - _from + 1, (index) { - final code = index + _from; - return Column( - children: [ - Icon(YaruIconsData(code), size: _iconsSize), - const SizedBox(height: 8), - Text('ex' + code.toRadixString(16), - style: TextStyle(color: Colors.grey[600])), - ], - ); - }), + return DefaultTabController( + length: 2, + child: Scaffold( + appBar: AppBar( + leading: + Icon(YaruIcons.ubuntu_logo, color: YaruColors.orange), + title: Consumer( + builder: (context, iconsSize, _) => Text( + 'Flutter Yaru Icons Demo (${iconsSize.size.truncate()}px)', + ), + ), + actions: [ + TextButton( + onPressed: iconSizeProvider.isMinSize() + ? null + : iconSizeProvider.decreaseSize, + child: Icon(YaruIcons.minus), + ), + TextButton( + onPressed: iconSizeProvider.increaseSize, + child: Icon(YaruIcons.plus), + ) + ], + bottom: TabBar( + tabs: [ + Tab(text: 'Static Icons'), + Tab(text: 'Animated Icons'), + ], + ), + ), + body: TabBarView( + children: [ + YaruIconsGrid(), + YaruAnimatedIconsGrid(), + ], + ), + ), + ); + }, + ), ), ); } - - void _increaseIconsSize() { - setState(() { - _iconsSize += 8; - }); - } - - void _decreaseIconsSize() { - setState(() { - if (!_isMinIconsSize()) { - _iconsSize -= 8; - } - }); - } } diff --git a/example/lib/src/animated_icons_grid.dart b/example/lib/src/animated_icons_grid.dart new file mode 100644 index 0000000..0a29bf0 --- /dev/null +++ b/example/lib/src/animated_icons_grid.dart @@ -0,0 +1,32 @@ +import 'package:example/src/icon_size_provider.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:yaru_colors/yaru_colors.dart'; +import 'package:yaru_icons/yaru_icons.dart'; + +@immutable +class YaruAnimatedIconsGrid extends StatelessWidget { + const YaruAnimatedIconsGrid({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, iconSizeProvider, _) => GridView.extent( + padding: const EdgeInsets.all(24), + maxCrossAxisExtent: iconSizeProvider.size + 48, + children: [ + YaruAnimatedOkIcon( + size: iconSizeProvider.size, + filled: false, + color: YaruColors.success, + ), + YaruAnimatedOkIcon( + size: iconSizeProvider.size, + filled: true, + color: YaruColors.success, + ) + ], + ), + ); + } +} diff --git a/example/lib/src/icon_size_provider.dart b/example/lib/src/icon_size_provider.dart new file mode 100644 index 0000000..4cd11e8 --- /dev/null +++ b/example/lib/src/icon_size_provider.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; + +class IconSizeProvider extends ChangeNotifier { + double _size = 24; + + double get size => _size; + + set size(double size) { + _size = size; + notifyListeners(); + } + + void increaseSize() { + _size += 8; + notifyListeners(); + } + + void decreaseSize() { + if (!isMinSize()) { + _size -= 8; + } + notifyListeners(); + } + + bool isMinSize() => _size <= 16 ? true : false; +} diff --git a/example/lib/src/icons_grid.dart b/example/lib/src/icons_grid.dart new file mode 100644 index 0000000..046ebdd --- /dev/null +++ b/example/lib/src/icons_grid.dart @@ -0,0 +1,34 @@ +import 'package:example/src/icon_size_provider.dart'; +import 'package:example/src/yaru_icon_data.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +@immutable +class YaruIconsGrid extends StatelessWidget { + const YaruIconsGrid({Key? key}) : super(key: key); + static const _from = 0xf101; + static const _to = 0xf2bd; + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, iconSizeProvider, _) => GridView.extent( + padding: const EdgeInsets.all(24), + maxCrossAxisExtent: iconSizeProvider.size + 48, + children: List.generate(_to - _from + 1, (index) { + final code = index + _from; + return Column( + children: [ + Icon(YaruIconsData(code), size: iconSizeProvider.size), + const SizedBox(height: 8), + Text( + 'ex' + code.toRadixString(16), + style: TextStyle(color: Colors.grey[600]), + ), + ], + ); + }), + ), + ); + } +} diff --git a/example/lib/src/yaru_icon_data.dart b/example/lib/src/yaru_icon_data.dart new file mode 100644 index 0000000..18756eb --- /dev/null +++ b/example/lib/src/yaru_icon_data.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class YaruIconsData extends IconData { + const YaruIconsData(int codePoint) + : super( + codePoint, + fontFamily: 'YaruIcons', + fontPackage: 'yaru_icons', + ); +} diff --git a/example/pubspec.yaml b/example/pubspec.yaml index d6d216b..90f040b 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -23,7 +23,9 @@ environment: dependencies: flutter: sdk: flutter - yaru: ^0.1.0 + provider: ^6.0.3 + yaru: ^0.3.3 + yaru_colors: ^0.1.0 yaru_icons: path: ../