2022-08-29 20:38:28 -06:00
|
|
|
part of '../timeline_screen.dart';
|
|
|
|
|
|
|
|
class _BottomScrubber extends StatelessWidget {
|
|
|
|
const _BottomScrubber(this.scroller,
|
|
|
|
{Key? key, required this.timelineMinSize, required this.size, required this.selectedWonder})
|
|
|
|
: super(key: key);
|
|
|
|
final ScrollController? scroller;
|
|
|
|
final double timelineMinSize;
|
|
|
|
final double size;
|
|
|
|
final WonderType? selectedWonder;
|
|
|
|
|
|
|
|
/// Calculate what fraction the scroller has travelled
|
|
|
|
double _calculateScrollFraction(ScrollPosition? pos) {
|
|
|
|
if (pos == null || pos.maxScrollExtent == 0) return 0;
|
|
|
|
return pos.pixels / pos.maxScrollExtent;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Calculates what fraction of the scroller is current visible
|
|
|
|
double _calculateViewPortFraction(ScrollPosition? pos) {
|
|
|
|
if (pos == null) return 1;
|
|
|
|
final viewportSize = pos.viewportDimension;
|
|
|
|
final result = viewportSize / (pos.maxScrollExtent + viewportSize);
|
|
|
|
return result.clamp(0, 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
final scroller = this.scroller;
|
|
|
|
|
|
|
|
/// It might take a frame until we receive a valid scroller
|
|
|
|
if (scroller == null) return SizedBox.shrink();
|
|
|
|
void handleScrubberPan(DragUpdateDetails details) {
|
|
|
|
if (!scroller.hasClients) return;
|
|
|
|
double dragMultiplier = (scroller.position.maxScrollExtent + timelineMinSize) / context.widthPx;
|
|
|
|
double newPos = scroller.position.pixels + details.delta.dx * dragMultiplier;
|
|
|
|
scroller.position.jumpTo(newPos.clamp(0, scroller.position.maxScrollExtent));
|
|
|
|
}
|
|
|
|
|
|
|
|
return SizedBox(
|
|
|
|
height: size,
|
|
|
|
child: Stack(
|
|
|
|
children: [
|
|
|
|
/// Timeline background
|
2022-11-05 02:10:11 -06:00
|
|
|
Container(
|
|
|
|
padding: EdgeInsets.all($styles.insets.md),
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
color: $styles.colors.greyStrong,
|
|
|
|
borderRadius: BorderRadius.circular($styles.corners.md),
|
|
|
|
),
|
2022-08-29 20:38:28 -06:00
|
|
|
child: WondersTimelineBuilder(
|
|
|
|
crossAxisGap: 4,
|
2022-10-25 00:27:37 -06:00
|
|
|
selectedWonders: selectedWonder != null ? [selectedWonder!] : [],
|
2022-08-29 20:38:28 -06:00
|
|
|
),
|
|
|
|
),
|
|
|
|
|
|
|
|
/// Visible area, follows the position of scroller
|
2022-11-14 14:49:00 -07:00
|
|
|
ListenableBuilder(
|
|
|
|
listenable: scroller,
|
2022-08-29 20:38:28 -06:00
|
|
|
builder: (_, __) {
|
|
|
|
ScrollPosition? pos;
|
|
|
|
if (scroller.hasClients) pos = scroller.position;
|
|
|
|
// Get current scroll offset and move the viewport to match
|
|
|
|
double scrollFraction = _calculateScrollFraction(pos);
|
|
|
|
double viewPortFraction = _calculateViewPortFraction(pos);
|
|
|
|
final scrubberAlign = Alignment(-1 + scrollFraction * 2, 0);
|
|
|
|
|
|
|
|
return Positioned.fill(
|
|
|
|
child: Semantics(
|
|
|
|
container: true,
|
|
|
|
slider: true,
|
|
|
|
label: $strings.bottomScrubberSemanticTimeline,
|
|
|
|
child: GestureDetector(
|
|
|
|
behavior: HitTestBehavior.translucent,
|
|
|
|
onPanUpdate: handleScrubberPan,
|
|
|
|
|
|
|
|
/// Scrub area
|
|
|
|
child: Align(
|
|
|
|
alignment: scrubberAlign,
|
|
|
|
child: FractionallySizedBox(
|
|
|
|
widthFactor: viewPortFraction,
|
|
|
|
heightFactor: 1,
|
|
|
|
child: _buildOutlineBox(context, scrubberAlign),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
},
|
|
|
|
)
|
|
|
|
],
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Container _buildOutlineBox(BuildContext context, Alignment alignment) {
|
|
|
|
final borderColor = $styles.colors.white;
|
|
|
|
return Container(
|
|
|
|
decoration: BoxDecoration(border: Border.all(color: borderColor)),
|
|
|
|
child: Align(alignment: alignment, child: DashedLine(vertical: true)),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|