aves/lib/widgets/viewer/overlay/bottom/multipage.dart
2021-06-06 14:48:45 +09:00

171 lines
5.5 KiB
Dart

import 'dart:math';
import 'package:aves/model/multipage.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/collection/thumbnail/decorated.dart';
import 'package:aves/widgets/collection/thumbnail/theme.dart';
import 'package:aves/widgets/viewer/multipage/controller.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
class MultiPageOverlay extends StatefulWidget {
final MultiPageController controller;
final double availableWidth;
const MultiPageOverlay({
Key? key,
required this.controller,
required this.availableWidth,
}) : super(key: key);
@override
_MultiPageOverlayState createState() => _MultiPageOverlayState();
}
class _MultiPageOverlayState extends State<MultiPageOverlay> {
final _cancellableNotifier = ValueNotifier(true);
late ScrollController _scrollController;
bool _syncScroll = true;
int? _initControllerPage;
static const double extent = 48;
static const double separatorWidth = 2;
MultiPageController get controller => widget.controller;
double get availableWidth => widget.availableWidth;
@override
void initState() {
super.initState();
_registerWidget();
}
@override
void didUpdateWidget(covariant MultiPageOverlay oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.controller != controller) {
_unregisterWidget();
_registerWidget();
}
}
@override
void dispose() {
_unregisterWidget();
super.dispose();
}
void _registerWidget() {
_initControllerPage = controller.page;
final scrollOffset = pageToScrollOffset(_initControllerPage ?? 0);
_scrollController = ScrollController(initialScrollOffset: scrollOffset);
_scrollController.addListener(_onScrollChange);
if (_initControllerPage == null) {
_correctDefaultPageScroll();
}
}
// correct scroll offset to match default page
// if default page was unknown when the scroll controller was created
void _correctDefaultPageScroll() async {
await controller.infoStream.first;
if (_initControllerPage == null) {
_initControllerPage = controller.page;
if (_initControllerPage != null && _initControllerPage != 0) {
WidgetsBinding.instance!.addPostFrameCallback((_) => _goToPage(_initControllerPage!));
}
}
}
void _unregisterWidget() {
_scrollController.removeListener(_onScrollChange);
_scrollController.dispose();
}
@override
Widget build(BuildContext context) {
final marginWidth = max(0.0, (availableWidth - extent) / 2 - separatorWidth);
final horizontalMargin = SizedBox(width: marginWidth);
final separator = SizedBox(width: separatorWidth);
return ThumbnailTheme(
extent: extent,
showLocation: false,
child: StreamBuilder<MultiPageInfo?>(
stream: controller.infoStream,
builder: (context, snapshot) {
final multiPageInfo = controller.info;
final pageCount = multiPageInfo?.pageCount ?? 0;
return SizedBox(
height: extent,
child: ListView.separated(
key: ValueKey(multiPageInfo),
scrollDirection: Axis.horizontal,
controller: _scrollController,
// default padding in scroll direction matches `MediaQuery.viewPadding`,
// but we already accommodate for it, so make sure horizontal padding is 0
padding: EdgeInsets.zero,
itemBuilder: (context, index) {
if (index == 0 || index == pageCount + 1) return horizontalMargin;
final page = index - 1;
final pageEntry = multiPageInfo!.getPageEntryByIndex(page);
return Stack(
children: [
GestureDetector(
onTap: () => _goToPage(page),
child: DecoratedThumbnail(
entry: pageEntry,
tileExtent: extent,
// the retrieval task queue can pile up for thumbnails of heavy pages
// (e.g. thumbnails of 15MP HEIF images inside 100MB+ HEIC containers)
// so we cancel these requests when possible
cancellableNotifier: _cancellableNotifier,
selectable: false,
highlightable: false,
),
),
IgnorePointer(
child: AnimatedContainer(
color: controller.page == page ? Colors.transparent : Colors.black45,
width: extent,
height: extent,
duration: Durations.viewerOverlayPageShadeAnimation,
),
)
],
);
},
separatorBuilder: (context, index) => separator,
itemCount: pageCount + 2,
),
);
},
),
);
}
Future<void> _goToPage(int page) async {
_syncScroll = false;
controller.page = page;
await _scrollController.animateTo(
pageToScrollOffset(page),
duration: Durations.viewerOverlayPageScrollAnimation,
curve: Curves.easeOutCubic,
);
_syncScroll = true;
}
void _onScrollChange() {
if (_syncScroll) {
controller.page = scrollOffsetToPage(_scrollController.offset);
}
}
double pageToScrollOffset(int page) => page * (extent + separatorWidth);
int scrollOffsetToPage(double offset) => (offset / (extent + separatorWidth)).round();
}