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.
This commit is contained in:
Tommy Chow 2022-06-28 06:27:38 -04:00
parent 23f48d3061
commit bfdfe4c8f1
4 changed files with 126 additions and 126 deletions

View file

@ -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<CategoryCard> createState() => _CategoryCardState();
}
class _CategoryCardState extends State<CategoryCard> 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<CategoryCard> 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<CategoryCard> 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();
}
}

View file

@ -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<StreamCard> createState() => _StreamCardState();
}
class _StreamCardState extends State<StreamCard> with SingleTickerProviderStateMixin {
late final _animationController = AnimationController(
vsync: this,
upperBound: 0.05,
);
class _StreamCardState extends State<StreamCard> {
@override
Widget build(BuildContext context) {
final time = DateTime.now();
@ -183,8 +179,10 @@ class _StreamCardState extends State<StreamCard> with SingleTickerProviderStateM
),
),
onTap: () async {
final category =
await context.read<TwitchApi>().getCategory(headers: context.read<AuthStore>().headersTwitch, gameId: widget.streamInfo.gameId);
final category = await context.read<TwitchApi>().getCategory(
headers: context.read<AuthStore>().headersTwitch,
gameId: widget.streamInfo.gameId,
);
if (!mounted) return;
Navigator.push(
@ -215,7 +213,7 @@ class _StreamCardState extends State<StreamCard> with SingleTickerProviderStateM
),
);
return InkWell(
return AnimateScale(
onTap: () => Navigator.push(
context,
MaterialPageRoute(
@ -240,55 +238,27 @@ class _StreamCardState extends State<StreamCard> 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();
}
}

View file

@ -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<AnimateScale> createState() => _AnimateScaleState();
}
class _AnimateScaleState extends State<AnimateScale> 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();
}
}

View file

@ -70,6 +70,8 @@ class _ButtonState extends State<Button> with SingleTickerProviderStateMixin {
label: widget.child,
);
const duration = Duration(milliseconds: 200);
return AnimatedBuilder(
animation: _animationController,
child: widget.onPressed == null
@ -77,18 +79,18 @@ class _ButtonState extends State<Button> with SingleTickerProviderStateMixin {
: Listener(
onPointerDown: (_) => _animationController.animateTo(
_animationController.upperBound,
curve: Curves.easeOutBack,
duration: const Duration(milliseconds: 200),
curve: Curves.easeInCubic,
duration: duration,
),
onPointerUp: (_) => _animationController.animateTo(
_animationController.lowerBound,
curve: Curves.easeOutBack,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOutCubic,
duration: duration,
),
onPointerCancel: (_) => _animationController.animateTo(
_animationController.lowerBound,
curve: Curves.easeOutBack,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOutCubic,
duration: duration,
),
child: button,
),