From bfdfe4c8f1cd5d01411bd70cbe6eb9228c6b9e4a Mon Sep 17 00:00:00 2001 From: Tommy Chow Date: Tue, 28 Jun 2022 06:27:38 -0400 Subject: [PATCH] Refactor with new `AnimateScale` widget (#90) Added a new `AnimateScale` widget that streamlines creating widgets that scale on tap/long-press. Also changed the curves and duration of scale to make it feel a little more realistic. --- lib/screens/home/widgets/category_card.dart | 92 ++++++--------------- lib/screens/home/widgets/stream_card.dart | 80 ++++++------------ lib/widgets/animate_scale.dart | 66 +++++++++++++++ lib/widgets/button.dart | 14 ++-- 4 files changed, 126 insertions(+), 126 deletions(-) create mode 100644 lib/widgets/animate_scale.dart diff --git a/lib/screens/home/widgets/category_card.dart b/lib/screens/home/widgets/category_card.dart index 91c99315..739a8d68 100644 --- a/lib/screens/home/widgets/category_card.dart +++ b/lib/screens/home/widgets/category_card.dart @@ -5,11 +5,12 @@ import 'package:frosty/core/auth/auth_store.dart'; import 'package:frosty/models/category.dart'; import 'package:frosty/screens/home/stores/list_store.dart'; import 'package:frosty/screens/home/top/category_streams.dart'; +import 'package:frosty/widgets/animate_scale.dart'; import 'package:frosty/widgets/loading_indicator.dart'; import 'package:provider/provider.dart'; /// A tappable card widget that displays a category's box art and name. -class CategoryCard extends StatefulWidget { +class CategoryCard extends StatelessWidget { final CategoryTwitch category; const CategoryCard({ @@ -17,16 +18,6 @@ class CategoryCard extends StatefulWidget { required this.category, }) : super(key: key); - @override - State createState() => _CategoryCardState(); -} - -class _CategoryCardState extends State with SingleTickerProviderStateMixin { - late final _animationController = AnimationController( - vsync: this, - upperBound: 0.05, - ); - @override Widget build(BuildContext context) { final size = MediaQuery.of(context).size; @@ -35,7 +26,7 @@ class _CategoryCardState extends State with SingleTickerProviderSt final artWidth = (size.width * pixelRatio) ~/ 3; final artHeight = (artWidth * (4 / 3)).toInt(); - return InkWell( + return AnimateScale( onTap: () => Navigator.push( context, MaterialPageRoute( @@ -49,65 +40,36 @@ class _CategoryCardState extends State with SingleTickerProviderSt ), ), ), - onTapDown: (_) => _animationController.animateTo( - _animationController.upperBound, - curve: Curves.easeOutBack, - duration: const Duration(milliseconds: 200), - ), - onTapUp: (_) => _animationController.animateTo( - _animationController.lowerBound, - curve: Curves.easeOutBack, - duration: const Duration(milliseconds: 300), - ), - onTapCancel: () => _animationController.animateTo( - _animationController.lowerBound, - curve: Curves.easeOutBack, - duration: const Duration(milliseconds: 300), - ), - child: AnimatedBuilder( - animation: _animationController, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 20.0), - child: Column( - children: [ - Expanded( - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(5.0)), - child: AspectRatio( - aspectRatio: 3 / 4, - child: CachedNetworkImage( - imageUrl: - widget.category.boxArtUrl.replaceRange(widget.category.boxArtUrl.lastIndexOf('-') + 1, null, '${artWidth}x$artHeight.jpg'), - placeholder: (context, url) => const LoadingIndicator(), - ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 20.0), + child: Column( + children: [ + Expanded( + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(5.0)), + child: AspectRatio( + aspectRatio: 3 / 4, + child: CachedNetworkImage( + imageUrl: category.boxArtUrl.replaceRange(category.boxArtUrl.lastIndexOf('-') + 1, null, '${artWidth}x$artHeight.jpg'), + placeholder: (context, url) => const LoadingIndicator(), ), ), ), - const SizedBox(height: 5.0), - Tooltip( - message: widget.category.name, - preferBelow: false, - padding: const EdgeInsets.all(10.0), - child: Text( - widget.category.name, - style: const TextStyle(fontWeight: FontWeight.bold), - overflow: TextOverflow.ellipsis, - ), + ), + const SizedBox(height: 5.0), + Tooltip( + message: category.name, + preferBelow: false, + padding: const EdgeInsets.all(10.0), + child: Text( + category.name, + style: const TextStyle(fontWeight: FontWeight.bold), + overflow: TextOverflow.ellipsis, ), - ], - ), - ), - builder: (context, child) => Transform.scale( - scale: 1 - _animationController.value, - child: child, + ), + ], ), ), ); } - - @override - void dispose() { - _animationController.dispose(); - super.dispose(); - } } diff --git a/lib/screens/home/widgets/stream_card.dart b/lib/screens/home/widgets/stream_card.dart index 21cad42a..dfa71058 100644 --- a/lib/screens/home/widgets/stream_card.dart +++ b/lib/screens/home/widgets/stream_card.dart @@ -10,6 +10,7 @@ import 'package:frosty/models/stream.dart'; import 'package:frosty/screens/channel/video_chat.dart'; import 'package:frosty/screens/home/stores/list_store.dart'; import 'package:frosty/screens/home/top/category_streams.dart'; +import 'package:frosty/widgets/animate_scale.dart'; import 'package:frosty/widgets/block_report_modal.dart'; import 'package:frosty/widgets/loading_indicator.dart'; import 'package:frosty/widgets/profile_picture.dart'; @@ -39,12 +40,7 @@ class StreamCard extends StatefulWidget { State createState() => _StreamCardState(); } -class _StreamCardState extends State with SingleTickerProviderStateMixin { - late final _animationController = AnimationController( - vsync: this, - upperBound: 0.05, - ); - +class _StreamCardState extends State { @override Widget build(BuildContext context) { final time = DateTime.now(); @@ -183,8 +179,10 @@ class _StreamCardState extends State with SingleTickerProviderStateM ), ), onTap: () async { - final category = - await context.read().getCategory(headers: context.read().headersTwitch, gameId: widget.streamInfo.gameId); + final category = await context.read().getCategory( + headers: context.read().headersTwitch, + gameId: widget.streamInfo.gameId, + ); if (!mounted) return; Navigator.push( @@ -215,7 +213,7 @@ class _StreamCardState extends State with SingleTickerProviderStateM ), ); - return InkWell( + return AnimateScale( onTap: () => Navigator.push( context, MaterialPageRoute( @@ -240,55 +238,27 @@ class _StreamCardState extends State with SingleTickerProviderStateM ), ); }, - onTapDown: (_) => _animationController.animateTo( - _animationController.upperBound, - curve: Curves.easeOutBack, - duration: const Duration(milliseconds: 200), - ), - onTapUp: (_) => _animationController.animateTo( - _animationController.lowerBound, - curve: Curves.easeOutBack, - duration: const Duration(milliseconds: 300), - ), - onTapCancel: () => _animationController.animateTo( - _animationController.lowerBound, - curve: Curves.easeOutBack, - duration: const Duration(milliseconds: 300), - ), - child: AnimatedBuilder( - animation: _animationController, - child: Padding( - padding: EdgeInsets.symmetric(vertical: 10.0, horizontal: widget.showThumbnail ? 15.0 : 5.0), - child: widget.large - ? Stack( - alignment: AlignmentDirectional.bottomStart, - children: [imageSection, streamInfoSection], - ) - : Row( - children: [ - if (widget.showThumbnail) - Flexible( - flex: 1, - child: imageSection, - ), + child: Padding( + padding: EdgeInsets.symmetric(vertical: 10.0, horizontal: widget.showThumbnail ? 15.0 : 5.0), + child: widget.large + ? Stack( + alignment: AlignmentDirectional.bottomStart, + children: [imageSection, streamInfoSection], + ) + : Row( + children: [ + if (widget.showThumbnail) Flexible( - flex: 2, - child: streamInfoSection, + flex: 1, + child: imageSection, ), - ], - ), - ), - builder: (context, child) => Transform.scale( - scale: 1 - _animationController.value, - child: child, - ), + Flexible( + flex: 2, + child: streamInfoSection, + ), + ], + ), ), ); } - - @override - void dispose() { - _animationController.dispose(); - super.dispose(); - } } diff --git a/lib/widgets/animate_scale.dart b/lib/widgets/animate_scale.dart new file mode 100644 index 00000000..956b55a2 --- /dev/null +++ b/lib/widgets/animate_scale.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; + +/// A widget than animates its scale when tapped/held on. +class AnimateScale extends StatefulWidget { + final void Function()? onTap; + final void Function()? onLongPress; + final Widget child; + + const AnimateScale({ + Key? key, + this.onTap, + this.onLongPress, + required this.child, + }) : super(key: key); + + @override + State createState() => _AnimateScaleState(); +} + +class _AnimateScaleState extends State with SingleTickerProviderStateMixin { + late final _animationController = AnimationController( + vsync: this, + upperBound: 0.05, + ); + + @override + Widget build(BuildContext context) { + const duration = Duration(milliseconds: 200); + const downCurve = Curves.easeInCubic; + const upCurve = Curves.easeOutCubic; + + return InkWell( + onTap: widget.onTap, + onTapDown: (_) => _animationController.animateTo( + _animationController.upperBound, + curve: downCurve, + duration: duration, + ), + onLongPress: widget.onLongPress, + onTapUp: (_) => _animationController.animateTo( + _animationController.lowerBound, + curve: upCurve, + duration: duration, + ), + onTapCancel: () => _animationController.animateTo( + _animationController.lowerBound, + curve: upCurve, + duration: duration, + ), + child: AnimatedBuilder( + animation: _animationController, + child: widget.child, + builder: (context, child) => Transform.scale( + scale: 1 - _animationController.value, + child: child, + ), + ), + ); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } +} diff --git a/lib/widgets/button.dart b/lib/widgets/button.dart index 162bb219..038a5978 100644 --- a/lib/widgets/button.dart +++ b/lib/widgets/button.dart @@ -70,6 +70,8 @@ class _ButtonState extends State