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(
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<FullscreenBody> 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<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() {
final child = ValueListenableBuilder<ImageEntry>(
valueListenable: _entryNotifier,
@ -430,16 +439,15 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
class FullscreenVerticalPageView extends StatefulWidget {
final CollectionLens collection;
final ValueNotifier<ImageEntry> entryNotifier;
final List<Tuple2<String, ValueNotifier<ViewState>>> viewStateNotifiers;
final List<Tuple2<String, IjkMediaController>> 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<FullscreenVerticalPageView>
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(

View file

@ -11,16 +11,16 @@ class MultiImagePage extends StatefulWidget {
final PageController pageController;
final ValueChanged<int> onPageChanged;
final VoidCallback onTap;
final List<Tuple2<String, ValueNotifier<ViewState>>> viewStateNotifiers;
final List<Tuple2<String, IjkMediaController>> 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<MultiImagePage> 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<MultiImagePage> with AutomaticKeepAliveC
class SingleImagePage extends StatefulWidget {
final ImageEntry entry;
final VoidCallback onTap;
final List<Tuple2<String, ValueNotifier<ViewState>>> viewStateNotifiers;
final List<Tuple2<String, IjkMediaController>> videoControllers;
const SingleImagePage({
this.entry,
this.onTap,
this.viewStateNotifiers,
this.videoControllers,
});
@ -91,7 +89,6 @@ class SingleImagePageState extends State<SingleImagePage> with AutomaticKeepAliv
child: ImageView(
entry: widget.entry,
onTap: widget.onTap,
viewStateNotifiers: widget.viewStateNotifiers,
videoControllers: widget.videoControllers,
),
);

View file

@ -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<Tuple2<String, ValueNotifier<ViewState>>> viewStateNotifiers;
final List<Tuple2<String, IjkMediaController>> 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<ImageView> {
final PhotoViewController _photoViewController = PhotoViewController();
final ValueNotifier<ViewState> _viewStateNotifier = ValueNotifier<ViewState>(ViewState.zero);
StreamSubscription<PhotoViewControllerValue> _subscription;
static const backgroundDecoration = BoxDecoration(color: Colors.transparent);
@ -47,8 +48,6 @@ class _ImageViewState extends State<ImageView> {
VoidCallback get onTap => widget.onTap;
ValueNotifier<ViewState> 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<ImageView> {
void dispose() {
_subscription.cancel();
_subscription = null;
widget.onDisposed?.call();
super.dispose();
}
@ -71,7 +71,7 @@ class _ImageViewState extends State<ImageView> {
child = _buildSvgView();
} else if (entry.canDecode) {
if (isLargeImage) {
child = _buildLargeImageView();
child = _buildTiledImageView();
} else {
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
// 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<ImageView> {
);
}
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<MediaQueryData, Size>(
selector: (context, mq) => mq.size,
builder: (context, mqSize, child) {
return StreamBuilder<PhotoViewControllerValue>(
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<ImageView> {
);
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}';
}
}

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
],
);
});
}
}