map browse prep

This commit is contained in:
Thibault Deckers 2021-08-18 18:48:18 +09:00
parent c0af01578a
commit fd33904658
19 changed files with 253 additions and 150 deletions

View file

@ -43,8 +43,8 @@ class Durations {
static const viewerVerticalPageScrollAnimation = Duration(milliseconds: 500);
static const viewerOverlayAnimation = Duration(milliseconds: 200);
static const viewerOverlayChangeAnimation = Duration(milliseconds: 150);
static const viewerOverlayPageScrollAnimation = Duration(milliseconds: 200);
static const viewerOverlayPageShadeAnimation = Duration(milliseconds: 150);
static const thumbnailScrollerScrollAnimation = Duration(milliseconds: 200);
static const thumbnailScrollerShadeAnimation = Duration(milliseconds: 150);
static const viewerVideoPlayerTransition = Duration(milliseconds: 500);
// info animations

View file

@ -13,7 +13,6 @@ import 'package:aves/widgets/collection/app_bar.dart';
import 'package:aves/widgets/collection/draggable_thumb_label.dart';
import 'package:aves/widgets/collection/grid/section_layout.dart';
import 'package:aves/widgets/collection/grid/thumbnail.dart';
import 'package:aves/widgets/collection/thumbnail/decorated.dart';
import 'package:aves/widgets/common/basic/draggable_scrollbar.dart';
import 'package:aves/widgets/common/basic/insets.dart';
import 'package:aves/widgets/common/behaviour/sloppy_scroll_physics.dart';
@ -27,6 +26,7 @@ import 'package:aves/widgets/common/identity/empty.dart';
import 'package:aves/widgets/common/identity/scroll_thumb.dart';
import 'package:aves/widgets/common/providers/tile_extent_controller_provider.dart';
import 'package:aves/widgets/common/scaling.dart';
import 'package:aves/widgets/common/thumbnail/decorated.dart';
import 'package:aves/widgets/common/tile_extent_controller.dart';
import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';

View file

@ -3,9 +3,9 @@ import 'package:aves/model/entry.dart';
import 'package:aves/model/selection.dart';
import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/services/viewer_service.dart';
import 'package:aves/widgets/collection/thumbnail/decorated.dart';
import 'package:aves/widgets/common/behaviour/routes.dart';
import 'package:aves/widgets/common/scaling.dart';
import 'package:aves/widgets/common/thumbnail/decorated.dart';
import 'package:aves/widgets/viewer/entry_viewer_page.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

View file

@ -27,6 +27,7 @@ class GeoMap extends StatefulWidget {
final double? mapHeight;
final ValueNotifier<bool> isAnimatingNotifier;
final UserZoomChangeCallback? onUserZoomChange;
final GeoEntryTapCallback? onEntryTap;
static const markerImageExtent = 48.0;
static const pointerSize = Size(8, 6);
@ -38,6 +39,7 @@ class GeoMap extends StatefulWidget {
this.mapHeight,
required this.isAnimatingNotifier,
this.onUserZoomChange,
this.onEntryTap,
}) : super(key: key);
@override
@ -130,6 +132,7 @@ class _GeoMapState extends State<GeoMap> with TickerProviderStateMixin {
markerCluster: markerCluster,
markerEntries: entries,
onUserZoomChange: widget.onUserZoomChange,
onEntryTap: widget.onEntryTap,
)
: EntryLeafletMap(
boundsNotifier: boundsNotifier,
@ -143,6 +146,7 @@ class _GeoMapState extends State<GeoMap> with TickerProviderStateMixin {
GeoMap.markerImageExtent + ImageMarker.outerBorderWidth * 2 + GeoMap.pointerSize.height,
),
onUserZoomChange: widget.onUserZoomChange,
onEntryTap: widget.onEntryTap,
);
child = Column(
@ -214,3 +218,4 @@ class MarkerKey extends LocalKey with EquatableMixin {
typedef EntryMarkerBuilder = Widget Function(MarkerKey key);
typedef UserZoomChangeCallback = void Function(double zoom);
typedef GeoEntryTapCallback = void Function(List<GeoEntry> geoEntries);

View file

@ -25,6 +25,7 @@ class EntryGoogleMap extends StatefulWidget {
final Fluster<GeoEntry> markerCluster;
final List<AvesEntry> markerEntries;
final UserZoomChangeCallback? onUserZoomChange;
final GeoEntryTapCallback? onEntryTap;
const EntryGoogleMap({
Key? key,
@ -35,6 +36,7 @@ class EntryGoogleMap extends StatefulWidget {
required this.markerCluster,
required this.markerEntries,
this.onUserZoomChange,
this.onEntryTap,
}) : super(key: key);
@override
@ -93,8 +95,8 @@ class _EntryGoogleMapState extends State<EntryGoogleMap> with WidgetsBindingObse
valueListenable: boundsNotifier,
builder: (context, visibleRegion, child) {
final allEntries = widget.markerEntries;
final clusters = visibleRegion != null ? widget.markerCluster.clusters(visibleRegion.boundingBox, visibleRegion.zoom.round()) : <GeoEntry>[];
final clusterByMarkerKey = Map.fromEntries(clusters.map((v) {
final geoEntries = visibleRegion != null ? widget.markerCluster.clusters(visibleRegion.boundingBox, visibleRegion.zoom.round()) : <GeoEntry>[];
final geoEntryByMarkerKey = Map.fromEntries(geoEntries.map((v) {
if (v.isCluster!) {
final uri = v.childMarkerId;
final entry = allEntries.firstWhere((v) => v.uri == uri);
@ -106,7 +108,7 @@ class _EntryGoogleMapState extends State<EntryGoogleMap> with WidgetsBindingObse
return Stack(
children: [
MarkerGeneratorWidget<MarkerKey>(
markers: clusterByMarkerKey.keys.map(widget.markerBuilder).toList(),
markers: geoEntryByMarkerKey.keys.map(widget.markerBuilder).toList(),
isReadyToRender: (key) => key.entry.isThumbnailReady(extent: GeoMap.markerImageExtent),
onRendered: (key, bitmap) {
_markerBitmaps[key] = bitmap;
@ -115,7 +117,7 @@ class _EntryGoogleMapState extends State<EntryGoogleMap> with WidgetsBindingObse
),
MapDecorator(
interactive: widget.interactive,
child: _buildMap(clusterByMarkerKey),
child: _buildMap(geoEntryByMarkerKey),
),
MapButtonPanel(
latLng: bounds.center,
@ -127,19 +129,26 @@ class _EntryGoogleMapState extends State<EntryGoogleMap> with WidgetsBindingObse
);
}
Widget _buildMap(Map<MarkerKey, GeoEntry> clusterByMarkerKey) {
Widget _buildMap(Map<MarkerKey, GeoEntry> geoEntryByMarkerKey) {
return AnimatedBuilder(
animation: _markerBitmapChangeNotifier,
builder: (context, child) {
final markers = <Marker>{};
clusterByMarkerKey.forEach((markerKey, cluster) {
final onTap = widget.onEntryTap;
geoEntryByMarkerKey.forEach((markerKey, geoEntry) {
final bytes = _markerBitmaps[markerKey];
if (bytes != null) {
final latLng = LatLng(cluster.latitude!, cluster.longitude!);
final latLng = LatLng(geoEntry.latitude!, geoEntry.longitude!);
markers.add(Marker(
markerId: MarkerId(cluster.markerId!),
markerId: MarkerId(geoEntry.markerId!),
icon: BitmapDescriptor.fromBytes(bytes),
position: latLng,
onTap: onTap != null
? () {
final clusterId = geoEntry.clusterId;
onTap(clusterId != null ? widget.markerCluster.points(clusterId) : [geoEntry]);
}
: null,
));
}
});

View file

@ -24,6 +24,7 @@ class EntryLeafletMap extends StatefulWidget {
final List<AvesEntry> markerEntries;
final Size markerSize;
final UserZoomChangeCallback? onUserZoomChange;
final GeoEntryTapCallback? onEntryTap;
const EntryLeafletMap({
Key? key,
@ -35,6 +36,7 @@ class EntryLeafletMap extends StatefulWidget {
required this.markerEntries,
required this.markerSize,
this.onUserZoomChange,
this.onEntryTap,
}) : super(key: key);
@override
@ -80,8 +82,8 @@ class _EntryLeafletMapState extends State<EntryLeafletMap> with TickerProviderSt
valueListenable: boundsNotifier,
builder: (context, visibleRegion, child) {
final allEntries = widget.markerEntries;
final clusters = visibleRegion != null ? widget.markerCluster.clusters(visibleRegion.boundingBox, visibleRegion.zoom.round()) : <GeoEntry>[];
final clusterByMarkerKey = Map.fromEntries(clusters.map((v) {
final geoEntries = visibleRegion != null ? widget.markerCluster.clusters(visibleRegion.boundingBox, visibleRegion.zoom.round()) : <GeoEntry>[];
final geoEntryByMarkerKey = Map.fromEntries(geoEntries.map((v) {
if (v.isCluster!) {
final uri = v.childMarkerId;
final entry = allEntries.firstWhere((v) => v.uri == uri);
@ -94,7 +96,7 @@ class _EntryLeafletMapState extends State<EntryLeafletMap> with TickerProviderSt
children: [
MapDecorator(
interactive: widget.interactive,
child: _buildMap(clusterByMarkerKey),
child: _buildMap(geoEntryByMarkerKey),
),
MapButtonPanel(
latLng: bounds.center,
@ -106,16 +108,20 @@ class _EntryLeafletMapState extends State<EntryLeafletMap> with TickerProviderSt
);
}
Widget _buildMap(Map<MarkerKey, GeoEntry> clusterByMarkerKey) {
Widget _buildMap(Map<MarkerKey, GeoEntry> geoEntryByMarkerKey) {
final markerSize = widget.markerSize;
final markers = clusterByMarkerKey.entries.map((kv) {
final markers = geoEntryByMarkerKey.entries.map((kv) {
final markerKey = kv.key;
final cluster = kv.value;
final latLng = LatLng(cluster.latitude!, cluster.longitude!);
final geoEntry = kv.value;
final latLng = LatLng(geoEntry.latitude!, geoEntry.longitude!);
return Marker(
point: latLng,
builder: (context) => GestureDetector(
onTap: () => _moveTo(latLng),
onTap: () {
final clusterId = geoEntry.clusterId;
widget.onEntryTap?.call(clusterId != null ? widget.markerCluster.points(clusterId) : [geoEntry]);
_moveTo(latLng);
},
child: widget.markerBuilder(markerKey),
),
width: markerSize.width,

View file

@ -1,5 +1,5 @@
import 'package:aves/model/entry.dart';
import 'package:aves/widgets/collection/thumbnail/image.dart';
import 'package:aves/widgets/common/thumbnail/image.dart';
import 'package:custom_rounded_rectangle_border/custom_rounded_rectangle_border.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

View file

@ -1,9 +1,9 @@
import 'package:aves/model/entry.dart';
import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/widgets/collection/thumbnail/image.dart';
import 'package:aves/widgets/collection/thumbnail/overlay.dart';
import 'package:aves/widgets/common/fx/borders.dart';
import 'package:aves/widgets/common/grid/overlay.dart';
import 'package:aves/widgets/common/thumbnail/image.dart';
import 'package:aves/widgets/common/thumbnail/overlay.dart';
import 'package:flutter/material.dart';
class DecoratedThumbnail extends StatelessWidget {

View file

@ -8,11 +8,11 @@ import 'package:aves/model/settings/entry_background.dart';
import 'package:aves/model/settings/enums.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/services/services.dart';
import 'package:aves/widgets/collection/thumbnail/error.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/fx/checkered_decoration.dart';
import 'package:aves/widgets/common/fx/transition_image.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/common/thumbnail/error.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

View file

@ -0,0 +1,152 @@
import 'dart:math';
import 'package:aves/model/entry.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/common/grid/theme.dart';
import 'package:aves/widgets/common/thumbnail/decorated.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
class ThumbnailScroller extends StatefulWidget {
final double availableWidth;
final int entryCount;
final AvesEntry? Function(int index) entryBuilder;
final int? initialIndex;
final bool Function(int page) isCurrentIndex;
final void Function(int index) onIndexChange;
const ThumbnailScroller({
Key? key,
required this.availableWidth,
required this.entryCount,
required this.entryBuilder,
required this.initialIndex,
required this.isCurrentIndex,
required this.onIndexChange,
}) : super(key: key);
@override
_ThumbnailScrollerState createState() => _ThumbnailScrollerState();
}
class _ThumbnailScrollerState extends State<ThumbnailScroller> {
final _cancellableNotifier = ValueNotifier(true);
late ScrollController _scrollController;
bool _syncScroll = true;
static const double extent = 48;
static const double separatorWidth = 2;
int get entryCount => widget.entryCount;
@override
void initState() {
super.initState();
_registerWidget();
}
@override
void didUpdateWidget(covariant ThumbnailScroller oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.initialIndex != widget.initialIndex) {
_unregisterWidget();
_registerWidget();
}
}
@override
void dispose() {
_unregisterWidget();
super.dispose();
}
void _registerWidget() {
final scrollOffset = indexToScrollOffset(widget.initialIndex ?? 0);
_scrollController = ScrollController(initialScrollOffset: scrollOffset);
_scrollController.addListener(_onScrollChange);
}
void _unregisterWidget() {
_scrollController.removeListener(_onScrollChange);
_scrollController.dispose();
}
@override
Widget build(BuildContext context) {
final marginWidth = max(0.0, (widget.availableWidth - extent) / 2 - separatorWidth);
final horizontalMargin = SizedBox(width: marginWidth);
const separator = SizedBox(width: separatorWidth);
return GridTheme(
extent: extent,
showLocation: false,
child: SizedBox(
height: extent,
child: ListView.separated(
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 == entryCount + 1) return horizontalMargin;
final page = index - 1;
final pageEntry = widget.entryBuilder(page);
if (pageEntry == null) return const SizedBox();
return Stack(
children: [
GestureDetector(
onTap: () => _goTo(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,
hero: false,
),
),
IgnorePointer(
child: AnimatedContainer(
color: widget.isCurrentIndex(page) ? Colors.transparent : Colors.black45,
width: extent,
height: extent,
duration: Durations.thumbnailScrollerShadeAnimation,
),
)
],
);
},
separatorBuilder: (context, index) => separator,
itemCount: entryCount + 2,
),
),
);
}
Future<void> _goTo(int index) async {
_syncScroll = false;
widget.onIndexChange(index);
await _scrollController.animateTo(
indexToScrollOffset(index),
duration: Durations.thumbnailScrollerScrollAnimation,
curve: Curves.easeOutCubic,
);
_syncScroll = true;
}
void _onScrollChange() {
if (_syncScroll) {
widget.onIndexChange(scrollOffsetToIndex(_scrollController.offset));
}
}
double indexToScrollOffset(int index) => index * (extent + separatorWidth);
int scrollOffsetToIndex(double offset) => (offset / (extent + separatorWidth)).round();
}

View file

@ -2,9 +2,9 @@ import 'package:aves/model/covers.dart';
import 'package:aves/model/entry.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/widgets/collection/thumbnail/image.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/common/thumbnail/image.dart';
import 'package:aves/widgets/dialogs/item_pick_dialog.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';

View file

@ -13,8 +13,8 @@ import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/utils/constants.dart';
import 'package:aves/widgets/collection/thumbnail/image.dart';
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
import 'package:aves/widgets/common/thumbnail/image.dart';
import 'package:aves/widgets/filter_grids/common/filter_grid_page.dart';
import 'package:decorated_icon/decorated_icon.dart';
import 'package:flutter/material.dart';

View file

@ -5,9 +5,11 @@ import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/map/geo_map.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/common/thumbnail/scroller.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:provider/provider.dart';
class MapPage extends StatefulWidget {
static const routeName = '/collection/map';
@ -25,6 +27,9 @@ class MapPage extends StatefulWidget {
class _MapPageState extends State<MapPage> {
late final ValueNotifier<bool> _isAnimatingNotifier;
int _selectedIndex = 0;
List<AvesEntry> get entries => widget.entries;
@override
void initState() {
@ -48,10 +53,34 @@ class _MapPageState extends State<MapPage> {
title: Text(context.l10n.mapPageTitle),
),
body: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: GeoMap(
entries: widget.entries,
entries: entries,
interactive: true,
isAnimatingNotifier: _isAnimatingNotifier,
onEntryTap: (geoEntries) {
debugPrint('TLAD count=${geoEntries.length} entry=${geoEntries.first.entry}');
},
),
),
const Divider(),
Selector<MediaQueryData, double>(
selector: (c, mq) => mq.size.width,
builder: (c, mqWidth, child) {
return ThumbnailScroller(
availableWidth: mqWidth,
entryCount: entries.length,
entryBuilder: (index) => entries[index],
initialIndex: _selectedIndex,
isCurrentIndex: (index) => _selectedIndex == index,
onIndexChange: (index) => _selectedIndex = index,
);
},
),
],
),
),
),

View file

@ -107,7 +107,7 @@ class _ViewerBottomOverlayState extends State<ViewerBottomOverlay> {
_lastDetails = snapshot.data;
_lastEntry = entry;
}
if (_lastEntry == null) return const SizedBox.shrink();
if (_lastEntry == null) return const SizedBox();
final mainEntry = _lastEntry!;
Widget _buildContent({AvesEntry? pageEntry}) => _BottomOverlayContent(
@ -261,7 +261,7 @@ class _BottomOverlayContent extends AnimatedWidget {
padding: const EdgeInsets.only(top: _interRowPadding),
child: _LocationRow(entry: pageEntry),
)
: const SizedBox.shrink(),
: const SizedBox(),
);
Widget _buildSoloShootingRow(double subRowWidth, bool hasShootingDetails) => AnimatedSwitcher(
@ -275,7 +275,7 @@ class _BottomOverlayContent extends AnimatedWidget {
width: subRowWidth,
child: _ShootingRow(details!),
)
: const SizedBox.shrink(),
: const SizedBox(),
);
Widget _buildDuoShootingRow(double subRowWidth, bool hasShootingDetails) => AnimatedSwitcher(
@ -291,7 +291,7 @@ class _BottomOverlayContent extends AnimatedWidget {
width: subRowWidth,
child: _ShootingRow(details!),
)
: const SizedBox.shrink(),
: const SizedBox(),
);
static Widget _soloTransition(Widget child, Animation<double> animation) => FadeTransition(

View file

@ -1,12 +1,7 @@
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/common/grid/theme.dart';
import 'package:aves/widgets/common/thumbnail/scroller.dart';
import 'package:aves/widgets/viewer/multipage/controller.dart';
import 'package:aves/widgets/viewer/visual/conductor.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@ -25,18 +20,10 @@ class MultiPageOverlay extends StatefulWidget {
}
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();
@ -48,23 +35,12 @@ class _MultiPageOverlayState extends State<MultiPageOverlay> {
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();
}
@ -75,79 +51,26 @@ class _MultiPageOverlayState extends State<MultiPageOverlay> {
void _correctDefaultPageScroll() async {
await controller.infoStream.first;
if (_initControllerPage == null) {
_initControllerPage = controller.page;
if (_initControllerPage != null && _initControllerPage != 0) {
WidgetsBinding.instance!.addPostFrameCallback((_) => _goToPage(_initControllerPage!));
setState(() => _initControllerPage = controller.page);
}
}
}
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);
const separator = SizedBox(width: separatorWidth);
return GridTheme(
extent: extent,
showLocation: false,
child: StreamBuilder<MultiPageInfo?>(
return StreamBuilder<MultiPageInfo?>(
stream: controller.infoStream,
builder: (context, snapshot) {
final multiPageInfo = controller.info;
final pageCount = multiPageInfo?.pageCount ?? 0;
return SizedBox(
height: extent,
child: ListView.separated(
return ThumbnailScroller(
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,
hero: false,
),
),
IgnorePointer(
child: AnimatedContainer(
color: controller.page == page ? Colors.transparent : Colors.black45,
width: extent,
height: extent,
duration: Durations.viewerOverlayPageShadeAnimation,
),
)
],
availableWidth: widget.availableWidth,
entryCount: multiPageInfo?.pageCount ?? 0,
entryBuilder: (page) => multiPageInfo?.getPageEntryByIndex(page),
initialIndex: _initControllerPage,
isCurrentIndex: (page) => controller.page == page,
onIndexChange: _setPage,
);
},
separatorBuilder: (context, index) => separator,
itemCount: pageCount + 2,
),
);
},
),
);
}
@ -159,25 +82,4 @@ class _MultiPageOverlayState extends State<MultiPageOverlay> {
controller.page = newPage;
context.read<ViewStateConductor>().reset(oldPageEntry);
}
Future<void> _goToPage(int page) async {
_syncScroll = false;
_setPage(page);
await _scrollController.animateTo(
pageToScrollOffset(page),
duration: Durations.viewerOverlayPageScrollAnimation,
curve: Curves.easeOutCubic,
);
_syncScroll = true;
}
void _onScrollChange() {
if (_syncScroll) {
_setPage(scrollOffsetToPage(_scrollController.offset));
}
}
double pageToScrollOffset(int page) => page * (extent + separatorWidth);
int scrollOffsetToPage(double offset) => (offset / (extent + separatorWidth)).round();
}

View file

@ -3,13 +3,13 @@ import 'dart:async';
import 'package:aves/model/entry.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/collection/thumbnail/image.dart';
import 'package:aves/widgets/common/magnifier/controller/controller.dart';
import 'package:aves/widgets/common/magnifier/controller/state.dart';
import 'package:aves/widgets/common/magnifier/magnifier.dart';
import 'package:aves/widgets/common/magnifier/scale/scale_boundaries.dart';
import 'package:aves/widgets/common/magnifier/scale/scale_level.dart';
import 'package:aves/widgets/common/magnifier/scale/state.dart';
import 'package:aves/widgets/common/thumbnail/image.dart';
import 'package:aves/widgets/viewer/hero.dart';
import 'package:aves/widgets/viewer/overlay/notifications.dart';
import 'package:aves/widgets/viewer/video/conductor.dart';