Mobile performance improvements (#417)

* First performance tweaks (caching and rendering improvemetns)

* Revert asset response caching

* 3-step image loading in asset viewer

* Prevent panning and zooming until full-scale version is loaded

* Loading indicator

* Adapt to gallery PR

* Cleanup

* Dart format

* Fix exif sheet

* Disable three stage loading until settings are available
This commit is contained in:
Matthias Rupp
2022-08-08 02:43:09 +02:00
committed by GitHub
parent 46f4905259
commit b46e834220
13 changed files with 159 additions and 81 deletions

View File

@@ -3,7 +3,7 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:photo_view/photo_view.dart';
enum _RemoteImageStatus { empty, thumbnail, full }
enum _RemoteImageStatus { empty, thumbnail, preview, full }
class _RemotePhotoViewState extends State<RemotePhotoView> {
late CachedNetworkImageProvider _imageProvider;
@@ -15,13 +15,16 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
@override
Widget build(BuildContext context) {
bool allowMoving = _status == _RemoteImageStatus.full;
return PhotoView(
imageProvider: _imageProvider,
minScale: PhotoViewComputedScale.contained,
maxScale: allowMoving ? 1.0 : PhotoViewComputedScale.contained,
enablePanAlways: true,
scaleStateChangedCallback: _scaleStateChanged,
onScaleEnd: _onScaleListener,
return IgnorePointer(
ignoring: !allowMoving,
child: PhotoView(
imageProvider: _imageProvider,
minScale: PhotoViewComputedScale.contained,
enablePanAlways: true,
scaleStateChangedCallback: _scaleStateChanged,
onScaleEnd: _onScaleListener,
),
);
}
@@ -52,6 +55,14 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
widget.isZoomedFunction();
}
void _fireStartLoadingEvent() {
if (widget.onLoadingStart != null) widget.onLoadingStart!();
}
void _fireFinishedLoadingEvent() {
if (widget.onLoadingCompleted != null) widget.onLoadingCompleted!();
}
CachedNetworkImageProvider _authorizedImageProvider(String url) {
return CachedNetworkImageProvider(
url,
@@ -64,14 +75,25 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
_RemoteImageStatus newStatus,
CachedNetworkImageProvider provider,
) {
// Transition to same status is forbidden
if (_status == newStatus) return;
// Transition full -> thumbnail is forbidden
if (_status == _RemoteImageStatus.full &&
newStatus == _RemoteImageStatus.thumbnail) return;
if (_status == _RemoteImageStatus.preview &&
newStatus == _RemoteImageStatus.thumbnail) return;
if (_status == _RemoteImageStatus.full &&
newStatus == _RemoteImageStatus.preview) return;
if (!mounted) return;
if (newStatus != _RemoteImageStatus.full) {
_fireStartLoadingEvent();
} else {
_fireFinishedLoadingEvent();
}
setState(() {
_status = newStatus;
_imageProvider = provider;
@@ -92,6 +114,16 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
}),
);
if (widget.previewUrl != null) {
CachedNetworkImageProvider previewProvider =
_authorizedImageProvider(widget.previewUrl!);
previewProvider.resolve(const ImageConfiguration()).addListener(
ImageStreamListener((ImageInfo imageInfo, _) {
_performStateTransition(_RemoteImageStatus.preview, previewProvider);
}),
);
}
CachedNetworkImageProvider fullProvider =
_authorizedImageProvider(widget.imageUrl);
fullProvider.resolve(const ImageConfiguration()).addListener(
@@ -109,20 +141,26 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
}
class RemotePhotoView extends StatefulWidget {
const RemotePhotoView({
Key? key,
required this.thumbnailUrl,
required this.imageUrl,
required this.authToken,
required this.isZoomedFunction,
required this.isZoomedListener,
required this.onSwipeDown,
required this.onSwipeUp,
}) : super(key: key);
const RemotePhotoView(
{Key? key,
required this.thumbnailUrl,
required this.imageUrl,
required this.authToken,
required this.isZoomedFunction,
required this.isZoomedListener,
required this.onSwipeDown,
required this.onSwipeUp,
this.previewUrl,
this.onLoadingCompleted,
this.onLoadingStart})
: super(key: key);
final String thumbnailUrl;
final String imageUrl;
final String authToken;
final String? previewUrl;
final Function? onLoadingCompleted;
final Function? onLoadingStart;
final void Function() onSwipeDown;
final void Function() onSwipeUp;

View File

@@ -11,11 +11,13 @@ class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
required this.asset,
required this.onMoreInfoPressed,
required this.onDownloadPressed,
this.loading = false
}) : super(key: key);
final AssetResponseDto asset;
final Function onMoreInfoPressed;
final Function onDownloadPressed;
final bool loading;
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -35,6 +37,14 @@ class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
),
),
actions: [
if (loading) Center(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 15.0),
width: iconSize,
height: iconSize,
child: const CircularProgressIndicator(strokeWidth: 2.0),
),
) ,
IconButton(
iconSize: iconSize,
splashRadius: iconSize,

View File

@@ -17,13 +17,13 @@ import 'package:openapi/api.dart';
class GalleryViewerPage extends HookConsumerWidget {
late List<AssetResponseDto> assetList;
final AssetResponseDto asset;
final String thumbnailRequestUrl;
static const _threeStageLoading = false;
GalleryViewerPage({
Key? key,
required this.assetList,
required this.asset,
required this.thumbnailRequestUrl,
}) : super(key: key);
AssetResponseDto? assetDetail;
@@ -32,6 +32,7 @@ class GalleryViewerPage extends HookConsumerWidget {
final Box<dynamic> box = Hive.box(userInfoBox);
int indexOfAsset = assetList.indexOf(asset);
final loading = useState(false);
@override
void initState(int index) {
@@ -74,6 +75,7 @@ class GalleryViewerPage extends HookConsumerWidget {
return Scaffold(
backgroundColor: Colors.black,
appBar: TopControlAppBar(
loading: loading.value,
asset: assetList[indexOfAsset],
onMoreInfoPressed: () {
showInfo();
@@ -98,15 +100,14 @@ class GalleryViewerPage extends HookConsumerWidget {
getAssetExif();
if (assetList[index].type == AssetTypeEnum.IMAGE) {
return ImageViewerPage(
thumbnailUrl:
'${box.get(serverEndpointKey)}/asset/thumbnail/${assetList[index].id}',
imageUrl:
'${box.get(serverEndpointKey)}/asset/file?aid=${assetList[index].deviceAssetId}&did=${assetList[index].deviceId}&isThumb=false',
authToken: 'Bearer ${box.get(accessTokenKey)}',
isZoomedFunction: isZoomedMethod,
isZoomedListener: isZoomedListener,
onLoadingCompleted: () => loading.value = false,
onLoadingStart: () => loading.value = _threeStageLoading,
asset: assetList[index],
heroTag: assetList[index].id,
threeStageLoading: _threeStageLoading
);
} else {
return SwipeDetector(

View File

@@ -8,27 +8,30 @@ import 'package:immich_mobile/modules/asset_viewer/ui/download_loading_indicator
import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/remote_photo_view.dart';
import 'package:immich_mobile/modules/home/services/asset.service.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:openapi/api.dart';
// ignore: must_be_immutable
class ImageViewerPage extends HookConsumerWidget {
final String imageUrl;
final String heroTag;
final String thumbnailUrl;
final AssetResponseDto asset;
final String authToken;
final ValueNotifier<bool> isZoomedListener;
final void Function() isZoomedFunction;
final void Function() onLoadingCompleted;
final void Function() onLoadingStart;
final bool threeStageLoading;
ImageViewerPage({
Key? key,
required this.imageUrl,
required this.heroTag,
required this.thumbnailUrl,
required this.asset,
required this.authToken,
required this.isZoomedFunction,
required this.isZoomedListener,
required this.onLoadingCompleted,
required this.onLoadingStart,
required this.threeStageLoading,
}) : super(key: key);
AssetResponseDto? assetDetail;
@@ -68,14 +71,18 @@ class ImageViewerPage extends HookConsumerWidget {
child: Hero(
tag: heroTag,
child: RemotePhotoView(
thumbnailUrl: thumbnailUrl,
imageUrl: imageUrl,
authToken: authToken,
isZoomedFunction: isZoomedFunction,
isZoomedListener: isZoomedListener,
onSwipeDown: () => AutoRouter.of(context).pop(),
onSwipeUp: () => showInfo(),
),
thumbnailUrl: getThumbnailUrl(asset),
imageUrl: getImageUrl(asset),
previewUrl: threeStageLoading
? getThumbnailUrl(asset, type: ThumbnailFormat.JPEG)
: null,
authToken: authToken,
isZoomedFunction: isZoomedFunction,
isZoomedListener: isZoomedListener,
onSwipeDown: () => AutoRouter.of(context).pop(),
onSwipeUp: () => showInfo(),
onLoadingCompleted: onLoadingCompleted,
onLoadingStart: onLoadingStart),
),
),
if (downloadAssetStatus == DownloadAssetStatus.loading)