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(
|
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(
|
||||||
|
|
|
@ -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,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -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}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
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