From f13fe3783284e955e30c41b336656cae0e265f3a Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Tue, 3 Nov 2020 19:34:25 +0900 Subject: [PATCH] tiled image prep --- lib/widgets/fullscreen/fullscreen_body.dart | 20 ++++-- lib/widgets/fullscreen/image_page.dart | 9 +-- lib/widgets/fullscreen/image_view.dart | 79 ++++++++------------- lib/widgets/fullscreen/tiled_view.dart | 77 ++++++++++++++++++++ 4 files changed, 123 insertions(+), 62 deletions(-) create mode 100644 lib/widgets/fullscreen/tiled_view.dart diff --git a/lib/widgets/fullscreen/fullscreen_body.dart b/lib/widgets/fullscreen/fullscreen_body.dart index 7c8a0b4d7..11b25828f 100644 --- a/lib/widgets/fullscreen/fullscreen_body.dart +++ b/lib/widgets/fullscreen/fullscreen_body.dart @@ -156,7 +156,11 @@ class FullscreenBodyState extends State with SingleTickerProvide }, child: NotificationListener( onNotification: (notification) { - if (notification is FilterNotification) _goToCollection(notification.filter); + if (notification is FilterNotification) { + _goToCollection(notification.filter); + } else if (notification is ViewStateNotification) { + _updateViewState(notification.uri, notification.viewState); + } return false; }, child: Stack( @@ -171,7 +175,7 @@ class FullscreenBodyState extends State with SingleTickerProvide onHorizontalPageChanged: _onHorizontalPageChanged, onImageTap: () => _overlayVisible.value = !_overlayVisible.value, onImagePageRequested: () => _goToVerticalPage(imagePage), - viewStateNotifiers: _viewStateNotifiers, + onViewDisposed: (uri) => _updateViewState(uri, null), ), _buildTopOverlay(), _buildBottomOverlay(), @@ -181,6 +185,11 @@ class FullscreenBodyState extends State with SingleTickerProvide ); } + void _updateViewState(String uri, ViewState viewState) { + final viewStateNotifier = _viewStateNotifiers.firstWhere((kv) => kv.item1 == uri, orElse: () => null)?.item2; + viewStateNotifier?.value = viewState ?? ViewState.zero; + } + Widget _buildTopOverlay() { final child = ValueListenableBuilder( valueListenable: _entryNotifier, @@ -430,16 +439,15 @@ class FullscreenBodyState extends State with SingleTickerProvide class FullscreenVerticalPageView extends StatefulWidget { final CollectionLens collection; final ValueNotifier entryNotifier; - final List>> viewStateNotifiers; final List> videoControllers; final PageController horizontalPager, verticalPager; final void Function(int page) onVerticalPageChanged, onHorizontalPageChanged; final VoidCallback onImageTap, onImagePageRequested; + final void Function(String uri) onViewDisposed; const FullscreenVerticalPageView({ @required this.collection, @required this.entryNotifier, - @required this.viewStateNotifiers, @required this.videoControllers, @required this.verticalPager, @required this.horizontalPager, @@ -447,6 +455,7 @@ class FullscreenVerticalPageView extends StatefulWidget { @required this.onHorizontalPageChanged, @required this.onImageTap, @required this.onImagePageRequested, + @required this.onViewDisposed, }); @override @@ -506,13 +515,12 @@ class _FullscreenVerticalPageViewState extends State pageController: widget.horizontalPager, onTap: widget.onImageTap, onPageChanged: widget.onHorizontalPageChanged, - viewStateNotifiers: widget.viewStateNotifiers, videoControllers: widget.videoControllers, + onViewDisposed: widget.onViewDisposed, ) : SingleImagePage( entry: entry, onTap: widget.onImageTap, - viewStateNotifiers: widget.viewStateNotifiers, videoControllers: widget.videoControllers, ), NotificationListener( diff --git a/lib/widgets/fullscreen/image_page.dart b/lib/widgets/fullscreen/image_page.dart index 03ab97720..a95fc1b1d 100644 --- a/lib/widgets/fullscreen/image_page.dart +++ b/lib/widgets/fullscreen/image_page.dart @@ -11,16 +11,16 @@ class MultiImagePage extends StatefulWidget { final PageController pageController; final ValueChanged onPageChanged; final VoidCallback onTap; - final List>> viewStateNotifiers; final List> videoControllers; + final void Function(String uri) onViewDisposed; const MultiImagePage({ this.collection, this.pageController, this.onPageChanged, this.onTap, - this.viewStateNotifiers, this.videoControllers, + this.onViewDisposed, }); @override @@ -50,8 +50,8 @@ class MultiImagePageState extends State with AutomaticKeepAliveC entry: entry, heroTag: widget.collection.heroTag(entry), onTap: widget.onTap, - viewStateNotifiers: widget.viewStateNotifiers, videoControllers: widget.videoControllers, + onDisposed: () => widget.onViewDisposed?.call(entry.uri), ), ); }, @@ -67,13 +67,11 @@ class MultiImagePageState extends State with AutomaticKeepAliveC class SingleImagePage extends StatefulWidget { final ImageEntry entry; final VoidCallback onTap; - final List>> viewStateNotifiers; final List> videoControllers; const SingleImagePage({ this.entry, this.onTap, - this.viewStateNotifiers, this.videoControllers, }); @@ -91,7 +89,6 @@ class SingleImagePageState extends State with AutomaticKeepAliv child: ImageView( entry: widget.entry, onTap: widget.onTap, - viewStateNotifiers: widget.viewStateNotifiers, videoControllers: widget.videoControllers, ), ); diff --git a/lib/widgets/fullscreen/image_view.dart b/lib/widgets/fullscreen/image_view.dart index d32a1eebf..0167a4b2e 100644 --- a/lib/widgets/fullscreen/image_view.dart +++ b/lib/widgets/fullscreen/image_view.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:math'; import 'package:aves/model/image_entry.dart'; import 'package:aves/model/settings/settings.dart'; @@ -8,6 +7,7 @@ import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/common/image_providers/thumbnail_provider.dart'; import 'package:aves/widgets/common/image_providers/uri_image_provider.dart'; import 'package:aves/widgets/common/image_providers/uri_picture_provider.dart'; +import 'package:aves/widgets/fullscreen/tiled_view.dart'; import 'package:aves/widgets/fullscreen/video_view.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -21,16 +21,16 @@ class ImageView extends StatefulWidget { final ImageEntry entry; final Object heroTag; final VoidCallback onTap; - final List>> viewStateNotifiers; final List> videoControllers; + final VoidCallback onDisposed; const ImageView({ Key key, @required this.entry, this.heroTag, @required this.onTap, - @required this.viewStateNotifiers, @required this.videoControllers, + this.onDisposed, }) : super(key: key); @override @@ -39,6 +39,7 @@ class ImageView extends StatefulWidget { class _ImageViewState extends State { final PhotoViewController _photoViewController = PhotoViewController(); + final ValueNotifier _viewStateNotifier = ValueNotifier(ViewState.zero); StreamSubscription _subscription; static const backgroundDecoration = BoxDecoration(color: Colors.transparent); @@ -47,8 +48,6 @@ class _ImageViewState extends State { VoidCallback get onTap => widget.onTap; - ValueNotifier get viewStateNotifier => widget.viewStateNotifiers.firstWhere((kv) => kv.item1 == entry.uri, orElse: () => null)?.item2; - @override void initState() { super.initState(); @@ -59,6 +58,7 @@ class _ImageViewState extends State { void dispose() { _subscription.cancel(); _subscription = null; + widget.onDisposed?.call(); super.dispose(); } @@ -71,7 +71,7 @@ class _ImageViewState extends State { child = _buildSvgView(); } else if (entry.canDecode) { if (isLargeImage) { - child = _buildLargeImageView(); + child = _buildTiledImageView(); } else { child = _buildImageView(); } @@ -97,7 +97,7 @@ class _ImageViewState extends State { // the images loaded by `PhotoView` cannot have a width or height larger than 8192 // so the reported offset and scale does not match expected values derived from the original dimensions - // TODO TLAD tile large images + // besides, large images should be tiled to be memory-friendly bool get isLargeImage => entry.width > 4096 || entry.height > 4096; ImageProvider get fastThumbnailProvider => ThumbnailProvider(ThumbnailProviderKey.fromEntry(entry)); @@ -147,54 +147,19 @@ class _ImageViewState extends State { ); } - Widget _buildLargeImageView() { - final uriImage = UriImage( - uri: entry.uri, - mimeType: entry.mimeType, - rotationDegrees: entry.rotationDegrees, - isFlipped: entry.isFlipped, - expectedContentLength: entry.sizeBytes, - ); + Widget _buildTiledImageView() { return PhotoView.customChild( // key includes size and orientation to refresh when the image is rotated key: ValueKey('${entry.rotationDegrees}_${entry.isFlipped}_${entry.width}_${entry.height}_${entry.path}'), child: Selector( selector: (context, mq) => mq.size, builder: (context, mqSize, child) { - return StreamBuilder( - stream: _photoViewController.outputStateStream, - builder: (context, snapshot) { - if (snapshot.hasError) return SizedBox.shrink(); - final displayWidth = entry.displaySize.width; - final displayHeight = entry.displaySize.height; - var scale = 0.0; - if (snapshot.hasData) { - scale = snapshot.data.scale; - } else { - // for initial scale as `PhotoViewComputedScale.contained` - scale = min(mqSize.width / displayWidth, mqSize.height / displayHeight); - } - return Stack( - alignment: Alignment.center, - children: [ - SizedBox( - width: displayWidth * scale, - height: displayHeight * scale, - child: _loadingBuilder( - context, - fastThumbnailProvider, - ), - ), - Image( - image: uriImage, - width: displayWidth * scale, - height: displayHeight * scale, - errorBuilder: (context, error, stackTrace) => _buildError(), - fit: BoxFit.contain, - ), - ], - ); - }, + return TiledImageView( + entry: entry, + viewportSize: mqSize, + viewStateNotifier: _viewStateNotifier, + baseChild: _loadingBuilder(context, fastThumbnailProvider), + errorBuilder: (context, error, stackTrace) => _buildError(), ); }, ), @@ -260,7 +225,9 @@ class _ImageViewState extends State { ); void _onViewChanged(PhotoViewControllerValue v) { - viewStateNotifier?.value = ViewState(v.position, v.scale); + final viewState = ViewState(v.position, v.scale); + _viewStateNotifier.value = viewState; + ViewStateNotification(entry.uri, viewState).dispatch(context); } } @@ -277,3 +244,15 @@ class ViewState { return '$runtimeType#${shortHash(this)}{position=$position, scale=$scale}'; } } + +class ViewStateNotification extends Notification { + final String uri; + final ViewState viewState; + + const ViewStateNotification(this.uri, this.viewState); + + @override + String toString() { + return '$runtimeType#${shortHash(this)}{uri=$uri, viewState=$viewState}'; + } +} diff --git a/lib/widgets/fullscreen/tiled_view.dart b/lib/widgets/fullscreen/tiled_view.dart new file mode 100644 index 000000000..94e2383ac --- /dev/null +++ b/lib/widgets/fullscreen/tiled_view.dart @@ -0,0 +1,77 @@ +import 'dart:math'; + +import 'package:aves/model/image_entry.dart'; +import 'package:aves/widgets/common/image_providers/uri_image_provider.dart'; +import 'package:aves/widgets/fullscreen/image_view.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +class TiledImageView extends StatefulWidget { + final ImageEntry entry; + final Size viewportSize; + final ValueNotifier viewStateNotifier; + final Widget baseChild; + final ImageErrorWidgetBuilder errorBuilder; + + const TiledImageView({ + @required this.entry, + @required this.viewportSize, + @required this.viewStateNotifier, + @required this.baseChild, + @required this.errorBuilder, + }); + + @override + _TiledImageViewState createState() => _TiledImageViewState(); +} + +class _TiledImageViewState extends State { + ImageEntry get entry => widget.entry; + + Size get viewportSize => widget.viewportSize; + + ValueNotifier get viewStateNotifier => widget.viewStateNotifier; + + @override + Widget build(BuildContext context) { + if (viewStateNotifier == null) return SizedBox.shrink(); + + final uriImage = UriImage( + uri: entry.uri, + mimeType: entry.mimeType, + rotationDegrees: entry.rotationDegrees, + isFlipped: entry.isFlipped, + expectedContentLength: entry.sizeBytes, + ); + + return AnimatedBuilder( + animation: viewStateNotifier, + builder: (context, child) { + final displayWidth = entry.displaySize.width; + final displayHeight = entry.displaySize.height; + var scale = viewStateNotifier.value.scale; + if (scale == 0.0) { + // for initial scale as `PhotoViewComputedScale.contained` + scale = min(viewportSize.width / displayWidth, viewportSize.height / displayHeight); + } + return Stack( + alignment: Alignment.center, + children: [ + SizedBox( + width: displayWidth * scale, + height: displayHeight * scale, + child: widget.baseChild, + ), + Image( + image: uriImage, + width: displayWidth * scale, + height: displayHeight * scale, + errorBuilder: widget.errorBuilder, + fit: BoxFit.contain, + ), + // TODO TLAD positioned tiles according to scale/sampleSize + ], + ); + }); + } +}