wonders/lib/ui/screens/collectible_found/collectible_found_screen.dart

190 lines
6.7 KiB
Dart
Raw Normal View History

2022-12-08 16:24:43 -07:00
import 'package:particle_field/particle_field.dart';
2022-08-29 20:38:28 -06:00
import 'package:wonders/common_libs.dart';
import 'package:wonders/logic/data/collectible_data.dart';
import 'package:wonders/ui/common/app_backdrop.dart';
import 'package:wonders/ui/common/centered_box.dart';
import 'package:wonders/ui/common/controls/app_header.dart';
import 'package:wonders/ui/common/pop_navigator_underlay.dart';
2022-08-29 20:38:28 -06:00
part 'widgets/_animated_ribbon.dart';
part 'widgets/_celebration_particles.dart';
class CollectibleFoundScreen extends StatelessWidget {
// CollectibleItem passes in a (theoretically) pre-loaded imageProvider.
// we could check for load completion, and hold after introT, but that shouldn't be necessary in a real-world scenario.
const CollectibleFoundScreen({required this.collectible, required this.imageProvider, Key? key}) : super(key: key);
final CollectibleData collectible;
final ImageProvider imageProvider;
@override
Widget build(BuildContext context) {
return RepaintBoundary(
child: _buildIntro(context).animate().swap(
delay: $styles.times.fast * 3.5,
builder: (_, __) => _buildDetail(context),
),
);
}
Widget _buildIntro(BuildContext context) {
Duration t = $styles.times.fast;
return Stack(children: [
Animate().custom(duration: t * 5, builder: (context, ratio, _) => _buildGradient(context, ratio, 0)),
// icon is handled by Hero initially, then scales slowly:
Center(
child: FractionallySizedBox(
widthFactor: 0.33,
heightFactor: 0.33,
child: Hero(
tag: 'collectible_icon_${collectible.id}',
child: Image(
image: collectible.icon,
fit: BoxFit.contain,
),
),
).animate().scale(begin: 1.5, end: 3, curve: Curves.easeInExpo, delay: t, duration: t * 3).fadeOut(),
)
]);
}
Widget _buildDetail(BuildContext context) {
Duration t = $styles.times.fast;
return Stack(key: ValueKey('detail'), children: [
/// Background
AppBackdrop(
strength: .5,
child: Container(color: $styles.colors.greyStrong.withOpacity(.96)),
).animate().fadeIn(),
/// Particles
2022-08-29 20:38:28 -06:00
_CelebrationParticles(fadeMs: (t * 6).inMilliseconds),
/// invisible close btn
PopNavigatorUnderlay(),
/// Content
2022-08-29 20:38:28 -06:00
SafeArea(
child: CenteredBox(
width: $styles.sizes.maxContentWidth3,
child: Column(
children: [
Gap($styles.insets.lg),
Spacer(),
SizedBox(
height: context.heightPx * .35,
child: Center(child: Hero(tag: 'collectible_image_${collectible.id}', child: _buildImage(context))),
),
Gap($styles.insets.lg),
_buildRibbon(context),
Gap($styles.insets.sm),
_buildTitle(context, collectible.title, $styles.text.h2, $styles.colors.offWhite, t * 1.5),
Gap($styles.insets.xs),
_buildTitle(
context, collectible.subtitle.toUpperCase(), $styles.text.title2, $styles.colors.accent1, t * 2),
Spacer(),
Gap($styles.insets.lg),
_buildCollectionButton(context),
Gap($styles.insets.lg),
Spacer(),
],
2022-08-29 20:38:28 -06:00
),
2022-10-27 10:24:12 -06:00
),
2022-08-29 20:38:28 -06:00
),
AppHeader(isTransparent: true).animate().fade(delay: t * 4, duration: t * 2),
2022-08-29 20:38:28 -06:00
]);
}
Widget _buildGradient(BuildContext context, double ratioIn, double ratioOut) {
// used by both intro and detail animations to ensure they share a mid-point.
ratioIn = Curves.easeOutExpo.transform(ratioIn);
final double opacity = 1.0 * (ratioIn * 0.8 + ratioOut * 0.2);
final Color light = $styles.colors.offWhite;
final Color dark = $styles.colors.black;
// final state is a solid fill, so optimize that case:
if (ratioOut == 1) return Container(color: dark.withOpacity(opacity));
return Container(
decoration: BoxDecoration(
gradient: RadialGradient(
colors: [Color.lerp(light, dark, ratioOut)!.withOpacity(opacity), dark.withOpacity(opacity)],
stops: [0.2, min(1, 0.25 + ratioIn * 0.5 + ratioOut * 0.5)],
),
),
);
}
Widget _buildImage(BuildContext context) {
Duration t = $styles.times.fast;
// build an image with animated shadows and scaling
return AppImage(image: imageProvider, scale: 1.0)
.animate()
.custom(
duration: t * 6,
builder: (_, ratio, child) => Container(
padding: EdgeInsets.all($styles.insets.xxs),
margin: EdgeInsets.symmetric(horizontal: $styles.insets.xl),
decoration: BoxDecoration(color: $styles.colors.offWhite, boxShadow: [
BoxShadow(
color: $styles.colors.accent1.withOpacity(ratio * 0.75),
blurRadius: $styles.insets.xl * 2,
),
BoxShadow(
color: $styles.colors.black.withOpacity(ratio * 0.75),
offset: Offset(0, $styles.insets.xxs),
blurRadius: $styles.insets.sm,
),
]),
child: child,
),
)
.scale(begin: 0.3, duration: t * 2, curve: Curves.easeOutExpo, alignment: Alignment(0, 0.7));
}
Widget _buildRibbon(BuildContext context) {
Duration t = $styles.times.fast;
return _AnimatedRibbon($strings.collectibleFoundTitleArtifactDiscovered.toUpperCase())
.animate()
.scale(begin: 0.3, duration: t * 2, curve: Curves.easeOutExpo, alignment: Alignment(0, -1));
}
Widget _buildTitle(BuildContext context, String text, TextStyle style, Color color, Duration delay) {
Duration t = $styles.times.fast;
// because this is a performance-sensitive screen, we are fading in the text by adjusting color:
return Container(
padding: EdgeInsets.symmetric(horizontal: $styles.insets.lg),
child: Animate().custom(
delay: delay,
duration: t * 2,
builder: (_, m, __) => Text(
text,
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: style.copyWith(color: color.withOpacity(m)),
),
),
);
}
Widget _buildCollectionButton(BuildContext context) {
2022-12-03 12:36:06 -07:00
Duration t = $styles.times.med;
2022-10-27 10:24:12 -06:00
return AppBtn.from(
text: $strings.collectibleFoundButtonViewCollection,
isSecondary: true,
onPressed: () => _handleViewCollectionPressed(context),
2022-12-03 12:36:06 -07:00
).animate().fadeIn(delay: t).move(
begin: Offset(0, 50),
duration: t,
curve: Curves.easeOutCubic,
);
2022-08-29 20:38:28 -06:00
}
void _handleViewCollectionPressed(BuildContext context) {
Navigator.pop(context);
context.push(ScreenPaths.collection(collectible.id));
}
}