From 459fc2485642b52518436878a95677dfb6c989a7 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Mon, 30 Mar 2020 22:32:48 +0900 Subject: [PATCH] overlay: favourite toggle highlight --- lib/widgets/album/thumbnail_collection.dart | 2 +- lib/widgets/common/fx/sweeper.dart | 133 ++++++++++++++++++++ lib/widgets/fullscreen/overlay/top.dart | 18 ++- 3 files changed, 148 insertions(+), 5 deletions(-) create mode 100644 lib/widgets/common/fx/sweeper.dart diff --git a/lib/widgets/album/thumbnail_collection.dart b/lib/widgets/album/thumbnail_collection.dart index 80048f504..fc6d4ccfc 100644 --- a/lib/widgets/album/thumbnail_collection.dart +++ b/lib/widgets/album/thumbnail_collection.dart @@ -47,7 +47,7 @@ class ThumbnailCollection extends StatelessWidget { child: ValueListenableBuilder( valueListenable: _columnCountNotifier, builder: (context, columnCount, child) { - debugPrint('$runtimeType builder columnCount=$columnCount'); + debugPrint('$runtimeType builder columnCount=$columnCount entries=${collection.entryCount}'); final scrollView = CustomScrollView( key: _scrollableKey, primary: true, diff --git a/lib/widgets/common/fx/sweeper.dart b/lib/widgets/common/fx/sweeper.dart new file mode 100644 index 000000000..84c16fa9e --- /dev/null +++ b/lib/widgets/common/fx/sweeper.dart @@ -0,0 +1,133 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; + +class Sweeper extends StatefulWidget { + final WidgetBuilder builder; + final double startAngle; + final double sweepAngle; + final Curve curve; + final ValueNotifier toggledNotifier; + + const Sweeper({ + Key key, + @required this.builder, + this.startAngle = -pi / 2, + this.sweepAngle = pi / 4, + this.curve = Curves.easeInOutCubic, + @required this.toggledNotifier, + }) : super(key: key); + + @override + _SweeperState createState() => _SweeperState(); +} + +class _SweeperState extends State with SingleTickerProviderStateMixin { + AnimationController _angleAnimationController; + Animation _angle; + bool _isAppearing = false; + + bool get isToggled => widget.toggledNotifier.value; + + static const opacityAnimationDurationMillis = 150; + static const sweepingDurationMillis = 650; + + @override + void initState() { + super.initState(); + _angleAnimationController = AnimationController( + duration: const Duration(milliseconds: sweepingDurationMillis), + vsync: this, + ); + _angle = Tween( + begin: widget.startAngle - widget.sweepAngle / 2, + end: widget.startAngle + pi * 2 - widget.sweepAngle / 2, + ).animate(CurvedAnimation( + parent: _angleAnimationController, + curve: widget.curve, + )); + _angleAnimationController.addStatusListener(_onAnimationStatusChange); + _registerWidget(widget); + } + + @override + void didUpdateWidget(Sweeper oldWidget) { + super.didUpdateWidget(oldWidget); + _unregisterWidget(oldWidget); + _registerWidget(widget); + } + + @override + void dispose() { + _angleAnimationController.removeStatusListener(_onAnimationStatusChange); + _unregisterWidget(widget); + super.dispose(); + } + + void _registerWidget(Sweeper widget) { + widget.toggledNotifier.addListener(_onToggle); + } + + void _unregisterWidget(Sweeper widget) { + widget.toggledNotifier.removeListener(_onToggle); + } + + @override + Widget build(BuildContext context) { + return IgnorePointer( + child: AnimatedOpacity( + opacity: isToggled && (_isAppearing || _angleAnimationController.status == AnimationStatus.forward) ? 1 : 0, + duration: const Duration(milliseconds: opacityAnimationDurationMillis), + child: ValueListenableBuilder( + valueListenable: _angleAnimationController, + builder: (context, value, child) { + return ClipPath( + child: widget.builder(context), + clipper: _SweepClipPath( + startAngle: _angle.value, + sweepAngle: widget.sweepAngle, + ), + ); + }), + ), + ); + } + + void _onAnimationStatusChange(AnimationStatus status) { + setState(() {}); + } + + Future _onToggle() async { + if (isToggled) { + _isAppearing = true; + setState(() {}); + await Future.delayed(Duration(milliseconds: (opacityAnimationDurationMillis * timeDilation).toInt())); + _isAppearing = false; + _angleAnimationController.forward(); + } else { + await Future.delayed(Duration(milliseconds: (opacityAnimationDurationMillis * timeDilation).toInt())); + _angleAnimationController.reset(); + } + setState(() {}); + } +} + +class _SweepClipPath extends CustomClipper { + final double startAngle; + final double sweepAngle; + + const _SweepClipPath({@required this.startAngle, @required this.sweepAngle}); + + @override + Path getClip(Size size) { + Path path = Path(); + path.moveTo(size.width / 2, size.height / 2); + path.addArc(Rect.fromLTWH(0, 0, size.width, size.height), startAngle, sweepAngle); + path.lineTo(size.width / 2, size.height / 2); + return path; + } + + @override + bool shouldReclip(CustomClipper oldClipper) => true; +} diff --git a/lib/widgets/fullscreen/overlay/top.dart b/lib/widgets/fullscreen/overlay/top.dart index 4a06e8979..b39831bb4 100644 --- a/lib/widgets/fullscreen/overlay/top.dart +++ b/lib/widgets/fullscreen/overlay/top.dart @@ -1,4 +1,5 @@ import 'package:aves/model/image_entry.dart'; +import 'package:aves/widgets/common/fx/sweeper.dart'; import 'package:aves/widgets/common/menu_row.dart'; import 'package:aves/widgets/fullscreen/fullscreen_action_delegate.dart'; import 'package:aves/widgets/fullscreen/overlay/common.dart'; @@ -42,10 +43,19 @@ class FullscreenTopOverlay extends StatelessWidget { scale: scale, child: ValueListenableBuilder( valueListenable: entry.isFavouriteNotifier, - builder: (context, isFavourite, child) => IconButton( - icon: Icon(isFavourite ? Icons.favorite : Icons.favorite_border), - onPressed: () => onActionSelected?.call(FullscreenAction.toggleFavourite), - tooltip: isFavourite ? 'Remove favourite' : 'Add favourite', + builder: (context, isFavourite, child) => Stack( + alignment: Alignment.center, + children: [ + IconButton( + icon: Icon(isFavourite ? OMIcons.favorite : OMIcons.favoriteBorder), + onPressed: () => onActionSelected?.call(FullscreenAction.toggleFavourite), + tooltip: isFavourite ? 'Remove favourite' : 'Add favourite', + ), + Sweeper( + builder: (context) => Icon(OMIcons.favoriteBorder, color: Colors.redAccent), + toggledNotifier: entry.isFavouriteNotifier, + ), + ], ), ), ),