tiled image prep

This commit is contained in:
Thibault Deckers 2020-11-03 19:34:25 +09:00
parent 530cf241ce
commit f13fe37832
4 changed files with 123 additions and 62 deletions

View file

@ -156,7 +156,11 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
}, },
child: NotificationListener( child: NotificationListener(
onNotification: (notification) { 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; return false;
}, },
child: Stack( child: Stack(
@ -171,7 +175,7 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
onHorizontalPageChanged: _onHorizontalPageChanged, onHorizontalPageChanged: _onHorizontalPageChanged,
onImageTap: () => _overlayVisible.value = !_overlayVisible.value, onImageTap: () => _overlayVisible.value = !_overlayVisible.value,
onImagePageRequested: () => _goToVerticalPage(imagePage), onImagePageRequested: () => _goToVerticalPage(imagePage),
viewStateNotifiers: _viewStateNotifiers, onViewDisposed: (uri) => _updateViewState(uri, null),
), ),
_buildTopOverlay(), _buildTopOverlay(),
_buildBottomOverlay(), _buildBottomOverlay(),
@ -181,6 +185,11 @@ class FullscreenBodyState extends State<FullscreenBody> 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() { Widget _buildTopOverlay() {
final child = ValueListenableBuilder<ImageEntry>( final child = ValueListenableBuilder<ImageEntry>(
valueListenable: _entryNotifier, valueListenable: _entryNotifier,
@ -430,16 +439,15 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
class FullscreenVerticalPageView extends StatefulWidget { class FullscreenVerticalPageView extends StatefulWidget {
final CollectionLens collection; final CollectionLens collection;
final ValueNotifier<ImageEntry> entryNotifier; final ValueNotifier<ImageEntry> entryNotifier;
final List<Tuple2<String, ValueNotifier<ViewState>>> viewStateNotifiers;
final List<Tuple2<String, IjkMediaController>> videoControllers; final List<Tuple2<String, IjkMediaController>> videoControllers;
final PageController horizontalPager, verticalPager; final PageController horizontalPager, verticalPager;
final void Function(int page) onVerticalPageChanged, onHorizontalPageChanged; final void Function(int page) onVerticalPageChanged, onHorizontalPageChanged;
final VoidCallback onImageTap, onImagePageRequested; final VoidCallback onImageTap, onImagePageRequested;
final void Function(String uri) onViewDisposed;
const FullscreenVerticalPageView({ const FullscreenVerticalPageView({
@required this.collection, @required this.collection,
@required this.entryNotifier, @required this.entryNotifier,
@required this.viewStateNotifiers,
@required this.videoControllers, @required this.videoControllers,
@required this.verticalPager, @required this.verticalPager,
@required this.horizontalPager, @required this.horizontalPager,
@ -447,6 +455,7 @@ class FullscreenVerticalPageView extends StatefulWidget {
@required this.onHorizontalPageChanged, @required this.onHorizontalPageChanged,
@required this.onImageTap, @required this.onImageTap,
@required this.onImagePageRequested, @required this.onImagePageRequested,
@required this.onViewDisposed,
}); });
@override @override
@ -506,13 +515,12 @@ class _FullscreenVerticalPageViewState extends State<FullscreenVerticalPageView>
pageController: widget.horizontalPager, pageController: widget.horizontalPager,
onTap: widget.onImageTap, onTap: widget.onImageTap,
onPageChanged: widget.onHorizontalPageChanged, onPageChanged: widget.onHorizontalPageChanged,
viewStateNotifiers: widget.viewStateNotifiers,
videoControllers: widget.videoControllers, videoControllers: widget.videoControllers,
onViewDisposed: widget.onViewDisposed,
) )
: SingleImagePage( : SingleImagePage(
entry: entry, entry: entry,
onTap: widget.onImageTap, onTap: widget.onImageTap,
viewStateNotifiers: widget.viewStateNotifiers,
videoControllers: widget.videoControllers, videoControllers: widget.videoControllers,
), ),
NotificationListener( NotificationListener(

View file

@ -11,16 +11,16 @@ class MultiImagePage extends StatefulWidget {
final PageController pageController; final PageController pageController;
final ValueChanged<int> onPageChanged; final ValueChanged<int> onPageChanged;
final VoidCallback onTap; final VoidCallback onTap;
final List<Tuple2<String, ValueNotifier<ViewState>>> viewStateNotifiers;
final List<Tuple2<String, IjkMediaController>> videoControllers; final List<Tuple2<String, IjkMediaController>> videoControllers;
final void Function(String uri) onViewDisposed;
const MultiImagePage({ const MultiImagePage({
this.collection, this.collection,
this.pageController, this.pageController,
this.onPageChanged, this.onPageChanged,
this.onTap, this.onTap,
this.viewStateNotifiers,
this.videoControllers, this.videoControllers,
this.onViewDisposed,
}); });
@override @override
@ -50,8 +50,8 @@ class MultiImagePageState extends State<MultiImagePage> with AutomaticKeepAliveC
entry: entry, entry: entry,
heroTag: widget.collection.heroTag(entry), heroTag: widget.collection.heroTag(entry),
onTap: widget.onTap, onTap: widget.onTap,
viewStateNotifiers: widget.viewStateNotifiers,
videoControllers: widget.videoControllers, videoControllers: widget.videoControllers,
onDisposed: () => widget.onViewDisposed?.call(entry.uri),
), ),
); );
}, },
@ -67,13 +67,11 @@ class MultiImagePageState extends State<MultiImagePage> with AutomaticKeepAliveC
class SingleImagePage extends StatefulWidget { class SingleImagePage extends StatefulWidget {
final ImageEntry entry; final ImageEntry entry;
final VoidCallback onTap; final VoidCallback onTap;
final List<Tuple2<String, ValueNotifier<ViewState>>> viewStateNotifiers;
final List<Tuple2<String, IjkMediaController>> videoControllers; final List<Tuple2<String, IjkMediaController>> videoControllers;
const SingleImagePage({ const SingleImagePage({
this.entry, this.entry,
this.onTap, this.onTap,
this.viewStateNotifiers,
this.videoControllers, this.videoControllers,
}); });
@ -91,7 +89,6 @@ class SingleImagePageState extends State<SingleImagePage> with AutomaticKeepAliv
child: ImageView( child: ImageView(
entry: widget.entry, entry: widget.entry,
onTap: widget.onTap, onTap: widget.onTap,
viewStateNotifiers: widget.viewStateNotifiers,
videoControllers: widget.videoControllers, videoControllers: widget.videoControllers,
), ),
); );

View file

@ -1,5 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'dart:math';
import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_entry.dart';
import 'package:aves/model/settings/settings.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/thumbnail_provider.dart';
import 'package:aves/widgets/common/image_providers/uri_image_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/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:aves/widgets/fullscreen/video_view.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -21,16 +21,16 @@ class ImageView extends StatefulWidget {
final ImageEntry entry; final ImageEntry entry;
final Object heroTag; final Object heroTag;
final VoidCallback onTap; final VoidCallback onTap;
final List<Tuple2<String, ValueNotifier<ViewState>>> viewStateNotifiers;
final List<Tuple2<String, IjkMediaController>> videoControllers; final List<Tuple2<String, IjkMediaController>> videoControllers;
final VoidCallback onDisposed;
const ImageView({ const ImageView({
Key key, Key key,
@required this.entry, @required this.entry,
this.heroTag, this.heroTag,
@required this.onTap, @required this.onTap,
@required this.viewStateNotifiers,
@required this.videoControllers, @required this.videoControllers,
this.onDisposed,
}) : super(key: key); }) : super(key: key);
@override @override
@ -39,6 +39,7 @@ class ImageView extends StatefulWidget {
class _ImageViewState extends State<ImageView> { class _ImageViewState extends State<ImageView> {
final PhotoViewController _photoViewController = PhotoViewController(); final PhotoViewController _photoViewController = PhotoViewController();
final ValueNotifier<ViewState> _viewStateNotifier = ValueNotifier<ViewState>(ViewState.zero);
StreamSubscription<PhotoViewControllerValue> _subscription; StreamSubscription<PhotoViewControllerValue> _subscription;
static const backgroundDecoration = BoxDecoration(color: Colors.transparent); static const backgroundDecoration = BoxDecoration(color: Colors.transparent);
@ -47,8 +48,6 @@ class _ImageViewState extends State<ImageView> {
VoidCallback get onTap => widget.onTap; VoidCallback get onTap => widget.onTap;
ValueNotifier<ViewState> get viewStateNotifier => widget.viewStateNotifiers.firstWhere((kv) => kv.item1 == entry.uri, orElse: () => null)?.item2;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -59,6 +58,7 @@ class _ImageViewState extends State<ImageView> {
void dispose() { void dispose() {
_subscription.cancel(); _subscription.cancel();
_subscription = null; _subscription = null;
widget.onDisposed?.call();
super.dispose(); super.dispose();
} }
@ -71,7 +71,7 @@ class _ImageViewState extends State<ImageView> {
child = _buildSvgView(); child = _buildSvgView();
} else if (entry.canDecode) { } else if (entry.canDecode) {
if (isLargeImage) { if (isLargeImage) {
child = _buildLargeImageView(); child = _buildTiledImageView();
} else { } else {
child = _buildImageView(); child = _buildImageView();
} }
@ -97,7 +97,7 @@ class _ImageViewState extends State<ImageView> {
// the images loaded by `PhotoView` cannot have a width or height larger than 8192 // 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 // 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; bool get isLargeImage => entry.width > 4096 || entry.height > 4096;
ImageProvider get fastThumbnailProvider => ThumbnailProvider(ThumbnailProviderKey.fromEntry(entry)); ImageProvider get fastThumbnailProvider => ThumbnailProvider(ThumbnailProviderKey.fromEntry(entry));
@ -147,54 +147,19 @@ class _ImageViewState extends State<ImageView> {
); );
} }
Widget _buildLargeImageView() { Widget _buildTiledImageView() {
final uriImage = UriImage(
uri: entry.uri,
mimeType: entry.mimeType,
rotationDegrees: entry.rotationDegrees,
isFlipped: entry.isFlipped,
expectedContentLength: entry.sizeBytes,
);
return PhotoView.customChild( return PhotoView.customChild(
// key includes size and orientation to refresh when the image is rotated // key includes size and orientation to refresh when the image is rotated
key: ValueKey('${entry.rotationDegrees}_${entry.isFlipped}_${entry.width}_${entry.height}_${entry.path}'), key: ValueKey('${entry.rotationDegrees}_${entry.isFlipped}_${entry.width}_${entry.height}_${entry.path}'),
child: Selector<MediaQueryData, Size>( child: Selector<MediaQueryData, Size>(
selector: (context, mq) => mq.size, selector: (context, mq) => mq.size,
builder: (context, mqSize, child) { builder: (context, mqSize, child) {
return StreamBuilder<PhotoViewControllerValue>( return TiledImageView(
stream: _photoViewController.outputStateStream, entry: entry,
builder: (context, snapshot) { viewportSize: mqSize,
if (snapshot.hasError) return SizedBox.shrink(); viewStateNotifier: _viewStateNotifier,
final displayWidth = entry.displaySize.width; baseChild: _loadingBuilder(context, fastThumbnailProvider),
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(), errorBuilder: (context, error, stackTrace) => _buildError(),
fit: BoxFit.contain,
),
],
);
},
); );
}, },
), ),
@ -260,7 +225,9 @@ class _ImageViewState extends State<ImageView> {
); );
void _onViewChanged(PhotoViewControllerValue v) { 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}'; 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}';
}
}

View file

@ -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<ViewState> 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<TiledImageView> {
ImageEntry get entry => widget.entry;
Size get viewportSize => widget.viewportSize;
ValueNotifier<ViewState> 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
],
);
});
}
}