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:
parent
23f48d3061
commit
bfdfe4c8f1
4 changed files with 126 additions and 126 deletions
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
66
lib/widgets/animate_scale.dart
Normal file
66
lib/widgets/animate_scale.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue