145 lines
5.3 KiB
Dart
145 lines
5.3 KiB
Dart
|
import 'dart:ui' as ui;
|
||
|
|
||
|
import 'package:wonders/common_libs.dart';
|
||
|
import 'package:wonders/ui/wonder_illustrations/common/wonder_illustration_builder.dart';
|
||
|
|
||
|
/// Combines [Align], [FractionalBoxWithMinSize], [Image] and [Transform.translate]
|
||
|
/// to standardize behavior across the various wonder illustrations
|
||
|
class IllustrationPiece extends StatefulWidget {
|
||
|
const IllustrationPiece({
|
||
|
Key? key,
|
||
|
required this.fileName,
|
||
|
required this.heightFactor,
|
||
|
this.alignment = Alignment.center,
|
||
|
this.minHeight,
|
||
|
this.offset = Offset.zero,
|
||
|
this.fractionalOffset,
|
||
|
this.zoomAmt = 0,
|
||
|
this.initialOffset = Offset.zero,
|
||
|
this.boxFit = BoxFit.fitHeight,
|
||
|
this.overflow = true,
|
||
|
this.enableHero = false,
|
||
|
this.initialScale = 1,
|
||
|
this.dynamicHzOffset = 0,
|
||
|
this.top,
|
||
|
this.bottom,
|
||
|
}) : super(key: key);
|
||
|
|
||
|
final String fileName;
|
||
|
|
||
|
final Alignment alignment;
|
||
|
|
||
|
/// Will animate from this position to Offset.zero, eg is value is Offset(0, 100), the piece will slide up vertically 100px as it enters the screen
|
||
|
final Offset initialOffset;
|
||
|
|
||
|
/// Will animate from this scale to 1, eg if scale is .7, the piece will scale from .7 to 1.0 as it enters the screen.
|
||
|
final double initialScale;
|
||
|
|
||
|
/// % height, will be overridden by minHeight
|
||
|
final double heightFactor;
|
||
|
|
||
|
/// min height in pixels, piece will not be allowed to go below this height in px, unless it has to (available height is too small)
|
||
|
final double? minHeight;
|
||
|
|
||
|
/// px offset for this piece
|
||
|
final Offset offset;
|
||
|
|
||
|
/// offset based on a fraction of the piece size
|
||
|
final Offset? fractionalOffset;
|
||
|
|
||
|
/// The % amount that this object should scale up as the user drags their finger up the screen
|
||
|
final double zoomAmt;
|
||
|
|
||
|
/// Applied to the underlying image in the piece, defaults to [BoxFit.cover]
|
||
|
final BoxFit boxFit;
|
||
|
|
||
|
/// Whether or not this piece can overflow it's parent on the horizontal bounds
|
||
|
final bool overflow;
|
||
|
|
||
|
/// Adds a hero tag to this piece, made from wonderType + fileName
|
||
|
final bool enableHero;
|
||
|
|
||
|
/// Max px offset of the piece as the screen size grows horizontally
|
||
|
final double dynamicHzOffset;
|
||
|
|
||
|
final Widget Function(BuildContext context)? top;
|
||
|
final Widget Function(BuildContext context)? bottom;
|
||
|
|
||
|
@override
|
||
|
State<IllustrationPiece> createState() => _IllustrationPieceState();
|
||
|
}
|
||
|
|
||
|
class _IllustrationPieceState extends State<IllustrationPiece> {
|
||
|
double? aspectRatio;
|
||
|
ui.Image? uiImage;
|
||
|
@override
|
||
|
Widget build(BuildContext context) {
|
||
|
final wonderBuilder = context.watch<WonderIllustrationBuilderState>();
|
||
|
final type = wonderBuilder.widget.wonderType;
|
||
|
final imgPath = '${type.assetPath}/${widget.fileName}';
|
||
|
if (aspectRatio == null) {
|
||
|
aspectRatio == 0; // indicates load has started, so we don't run twice
|
||
|
rootBundle.load(imgPath).then((img) async {
|
||
|
uiImage = await decodeImageFromList(img.buffer.asUint8List());
|
||
|
if (!mounted) return;
|
||
|
setState(() => aspectRatio = uiImage!.width / uiImage!.height);
|
||
|
});
|
||
|
}
|
||
|
return Align(
|
||
|
alignment: widget.alignment,
|
||
|
child: LayoutBuilder(
|
||
|
key: ValueKey(aspectRatio),
|
||
|
builder: (_, constraints) {
|
||
|
final anim = wonderBuilder.anim;
|
||
|
final curvedAnim = Curves.easeOut.transform(anim.value);
|
||
|
final config = wonderBuilder.widget.config;
|
||
|
Widget img = Image.asset(imgPath, opacity: anim, fit: widget.boxFit);
|
||
|
if (widget.overflow) {
|
||
|
img = OverflowBox(maxWidth: 2000, child: img);
|
||
|
}
|
||
|
final double introZoom = (widget.initialScale - 1) * (1 - curvedAnim);
|
||
|
|
||
|
/// Determine target height
|
||
|
final double height = max(widget.minHeight ?? 0, constraints.maxHeight * widget.heightFactor);
|
||
|
|
||
|
/// Combine all the translations, initial + offset + dynamicHzOffset + fractionalOffset
|
||
|
Offset finalTranslation = widget.offset;
|
||
|
// Initial
|
||
|
if (widget.initialOffset != Offset.zero) {
|
||
|
finalTranslation += widget.initialOffset * (1 - curvedAnim);
|
||
|
}
|
||
|
// Dynamic
|
||
|
final dynamicOffsetAmt = min(context.widthPx / 1500, 1);
|
||
|
finalTranslation += Offset(dynamicOffsetAmt * widget.dynamicHzOffset, 0);
|
||
|
// Fractional
|
||
|
final width = height * (aspectRatio ?? 0);
|
||
|
if (widget.fractionalOffset != null) {
|
||
|
finalTranslation += Offset(
|
||
|
widget.fractionalOffset!.dx * width,
|
||
|
height * widget.fractionalOffset!.dy,
|
||
|
);
|
||
|
}
|
||
|
return Stack(
|
||
|
children: [
|
||
|
if (widget.bottom != null) Positioned.fill(child: widget.bottom!.call(context)),
|
||
|
if (uiImage != null) ...[
|
||
|
Transform.translate(
|
||
|
offset: finalTranslation,
|
||
|
child: Transform.scale(
|
||
|
scale: 1 + (widget.zoomAmt * config.zoom) + introZoom,
|
||
|
child: SizedBox(
|
||
|
height: height,
|
||
|
width: height * aspectRatio!,
|
||
|
child: !widget.enableHero ? img : Hero(tag: '$type-${widget.fileName}', child: img),
|
||
|
),
|
||
|
),
|
||
|
),
|
||
|
],
|
||
|
if (widget.top != null) Positioned.fill(child: widget.top!.call(context)),
|
||
|
],
|
||
|
);
|
||
|
}),
|
||
|
);
|
||
|
}
|
||
|
}
|