tiled image prep
This commit is contained in:
parent
530cf241ce
commit
f13fe37832
4 changed files with 123 additions and 62 deletions
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
|
|
|
@ -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}';
|
||||
}
|
||||
}
|
||||
|
|
77
lib/widgets/fullscreen/tiled_view.dart
Normal file
77
lib/widgets/fullscreen/tiled_view.dart
Normal 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
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue