overlay: favourite toggle highlight

This commit is contained in:
Thibault Deckers 2020-03-30 22:32:48 +09:00
parent e915f1922f
commit 459fc24856
3 changed files with 148 additions and 5 deletions

View file

@ -47,7 +47,7 @@ class ThumbnailCollection extends StatelessWidget {
child: ValueListenableBuilder( child: ValueListenableBuilder(
valueListenable: _columnCountNotifier, valueListenable: _columnCountNotifier,
builder: (context, columnCount, child) { builder: (context, columnCount, child) {
debugPrint('$runtimeType builder columnCount=$columnCount'); debugPrint('$runtimeType builder columnCount=$columnCount entries=${collection.entryCount}');
final scrollView = CustomScrollView( final scrollView = CustomScrollView(
key: _scrollableKey, key: _scrollableKey,
primary: true, primary: true,

View file

@ -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<bool> 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<Sweeper> with SingleTickerProviderStateMixin {
AnimationController _angleAnimationController;
Animation<double> _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<double>(
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<void> _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<Path> {
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<Path> oldClipper) => true;
}

View file

@ -1,4 +1,5 @@
import 'package:aves/model/image_entry.dart'; 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/common/menu_row.dart';
import 'package:aves/widgets/fullscreen/fullscreen_action_delegate.dart'; import 'package:aves/widgets/fullscreen/fullscreen_action_delegate.dart';
import 'package:aves/widgets/fullscreen/overlay/common.dart'; import 'package:aves/widgets/fullscreen/overlay/common.dart';
@ -42,10 +43,19 @@ class FullscreenTopOverlay extends StatelessWidget {
scale: scale, scale: scale,
child: ValueListenableBuilder( child: ValueListenableBuilder(
valueListenable: entry.isFavouriteNotifier, valueListenable: entry.isFavouriteNotifier,
builder: (context, isFavourite, child) => IconButton( builder: (context, isFavourite, child) => Stack(
icon: Icon(isFavourite ? Icons.favorite : Icons.favorite_border), alignment: Alignment.center,
onPressed: () => onActionSelected?.call(FullscreenAction.toggleFavourite), children: [
tooltip: isFavourite ? 'Remove favourite' : 'Add favourite', 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,
),
],
), ),
), ),
), ),