mirror of
https://github.com/KevinMidboe/immich.git
synced 2025-12-07 19:59:07 +00:00
(fix)mobile: Improve the gallery to improve scale, double tap, and swipe gesture detection (#1502)
* photoviewgallery * stiffer scrolling to react more like google photos * adds a dx threshhold for the swipe/up down from the original dropped point * stopped wrapping imageview in gallery viewer to avoid the double photoview issue. breaks imageview page pinch-to-zoom, so i need to fix that for other callers * refactors gallery view to use remoteimage directly and breaks imageviewpage * removed image_viewer_page * adds minscale * adds photo_view to repository * double tap to zoom out with hacked commit * double tapping! * got up and down swipe gestures working * fixed wrong cache and headers in image providers * fixed image quality and added videos back in * local loading asset image fix * precaches images * fixes lint errors * deleted remote_photo_view and more linters * fixes scale * load preview and load original * precache does original / preview as well * refactored image providers to nice functions and added JPEG thumbnail format to remote image thumbnail lookup * moved photo_view to shared/ui/ * three stage loading with webp and fixes some thumbnail fits * fixed local thumbnail * fixed paging in iOS
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// A [ChangeNotifier] that has a second collection of listeners: the ignorable ones
|
||||
///
|
||||
/// Those listeners will be fired when [notifyListeners] fires and will be ignored
|
||||
/// when [notifySomeListeners] fires.
|
||||
///
|
||||
/// The common collection of listeners inherited from [ChangeNotifier] will be fired
|
||||
/// every time.
|
||||
class IgnorableChangeNotifier extends ChangeNotifier {
|
||||
ObserverList<VoidCallback>? _ignorableListeners =
|
||||
ObserverList<VoidCallback>();
|
||||
|
||||
bool _debugAssertNotDisposed() {
|
||||
assert(() {
|
||||
if (_ignorableListeners == null) {
|
||||
AssertionError([
|
||||
'A $runtimeType was used after being disposed.',
|
||||
'Once you have called dispose() on a $runtimeType, it can no longer be used.'
|
||||
]);
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
bool get hasListeners {
|
||||
return super.hasListeners || (_ignorableListeners?.isNotEmpty ?? false);
|
||||
}
|
||||
|
||||
void addIgnorableListener(listener) {
|
||||
assert(_debugAssertNotDisposed());
|
||||
_ignorableListeners!.add(listener);
|
||||
}
|
||||
|
||||
void removeIgnorableListener(listener) {
|
||||
assert(_debugAssertNotDisposed());
|
||||
_ignorableListeners!.remove(listener);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_ignorableListeners = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@protected
|
||||
@override
|
||||
@visibleForTesting
|
||||
void notifyListeners() {
|
||||
super.notifyListeners();
|
||||
if (_ignorableListeners != null) {
|
||||
final List<VoidCallback> localListeners =
|
||||
List<VoidCallback>.from(_ignorableListeners!);
|
||||
for (VoidCallback listener in localListeners) {
|
||||
try {
|
||||
if (_ignorableListeners!.contains(listener)) {
|
||||
listener();
|
||||
}
|
||||
} catch (exception, stack) {
|
||||
FlutterError.reportError(
|
||||
FlutterErrorDetails(
|
||||
exception: exception,
|
||||
stack: stack,
|
||||
library: 'Photoview library',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Ignores the ignoreables
|
||||
@protected
|
||||
void notifySomeListeners() {
|
||||
super.notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Just like [ValueNotifier] except it extends [IgnorableChangeNotifier] which has
|
||||
/// listeners that wont fire when [updateIgnoring] is called.
|
||||
class IgnorableValueNotifier<T> extends IgnorableChangeNotifier
|
||||
implements ValueListenable<T> {
|
||||
IgnorableValueNotifier(this._value);
|
||||
|
||||
@override
|
||||
T get value => _value;
|
||||
T _value;
|
||||
|
||||
set value(T newValue) {
|
||||
if (_value == newValue) {
|
||||
return;
|
||||
}
|
||||
_value = newValue;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void updateIgnoring(T newValue) {
|
||||
if (_value == newValue) {
|
||||
return;
|
||||
}
|
||||
_value = newValue;
|
||||
notifySomeListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => '${describeIdentity(this)}($value)';
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// Data class that holds the attributes that are going to be passed to
|
||||
/// [PhotoViewImageWrapper]'s [Hero].
|
||||
class PhotoViewHeroAttributes {
|
||||
const PhotoViewHeroAttributes({
|
||||
required this.tag,
|
||||
this.createRectTween,
|
||||
this.flightShuttleBuilder,
|
||||
this.placeholderBuilder,
|
||||
this.transitionOnUserGestures = false,
|
||||
});
|
||||
|
||||
/// Mirror to [Hero.tag]
|
||||
final Object tag;
|
||||
|
||||
/// Mirror to [Hero.createRectTween]
|
||||
final CreateRectTween? createRectTween;
|
||||
|
||||
/// Mirror to [Hero.flightShuttleBuilder]
|
||||
final HeroFlightShuttleBuilder? flightShuttleBuilder;
|
||||
|
||||
/// Mirror to [Hero.placeholderBuilder]
|
||||
final HeroPlaceholderBuilder? placeholderBuilder;
|
||||
|
||||
/// Mirror to [Hero.transitionOnUserGestures]
|
||||
final bool transitionOnUserGestures;
|
||||
}
|
||||
145
mobile/lib/shared/ui/photo_view/src/utils/photo_view_utils.dart
Normal file
145
mobile/lib/shared/ui/photo_view/src/utils/photo_view_utils.dart
Normal file
@@ -0,0 +1,145 @@
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui' show Size;
|
||||
|
||||
import "package:immich_mobile/shared/ui/photo_view/src/photo_view_computed_scale.dart";
|
||||
import 'package:immich_mobile/shared/ui/photo_view/src/photo_view_scale_state.dart';
|
||||
|
||||
/// Given a [PhotoViewScaleState], returns a scale value considering [scaleBoundaries].
|
||||
double getScaleForScaleState(
|
||||
PhotoViewScaleState scaleState,
|
||||
ScaleBoundaries scaleBoundaries,
|
||||
) {
|
||||
switch (scaleState) {
|
||||
case PhotoViewScaleState.initial:
|
||||
case PhotoViewScaleState.zoomedIn:
|
||||
case PhotoViewScaleState.zoomedOut:
|
||||
return _clampSize(scaleBoundaries.initialScale, scaleBoundaries);
|
||||
case PhotoViewScaleState.covering:
|
||||
return _clampSize(
|
||||
_scaleForCovering(
|
||||
scaleBoundaries.outerSize,
|
||||
scaleBoundaries.childSize,
|
||||
),
|
||||
scaleBoundaries,
|
||||
);
|
||||
case PhotoViewScaleState.originalSize:
|
||||
return _clampSize(1.0, scaleBoundaries);
|
||||
// Will never be reached
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal class to wraps custom scale boundaries (min, max and initial)
|
||||
/// Also, stores values regarding the two sizes: the container and teh child.
|
||||
class ScaleBoundaries {
|
||||
const ScaleBoundaries(
|
||||
this._minScale,
|
||||
this._maxScale,
|
||||
this._initialScale,
|
||||
this.outerSize,
|
||||
this.childSize,
|
||||
);
|
||||
|
||||
final dynamic _minScale;
|
||||
final dynamic _maxScale;
|
||||
final dynamic _initialScale;
|
||||
final Size outerSize;
|
||||
final Size childSize;
|
||||
|
||||
double get minScale {
|
||||
assert(_minScale is double || _minScale is PhotoViewComputedScale);
|
||||
if (_minScale == PhotoViewComputedScale.contained) {
|
||||
return _scaleForContained(outerSize, childSize) *
|
||||
(_minScale as PhotoViewComputedScale).multiplier; // ignore: avoid_as
|
||||
}
|
||||
if (_minScale == PhotoViewComputedScale.covered) {
|
||||
return _scaleForCovering(outerSize, childSize) *
|
||||
(_minScale as PhotoViewComputedScale).multiplier; // ignore: avoid_as
|
||||
}
|
||||
assert(_minScale >= 0.0);
|
||||
return _minScale;
|
||||
}
|
||||
|
||||
double get maxScale {
|
||||
assert(_maxScale is double || _maxScale is PhotoViewComputedScale);
|
||||
if (_maxScale == PhotoViewComputedScale.contained) {
|
||||
return (_scaleForContained(outerSize, childSize) *
|
||||
(_maxScale as PhotoViewComputedScale) // ignore: avoid_as
|
||||
.multiplier)
|
||||
.clamp(minScale, double.infinity);
|
||||
}
|
||||
if (_maxScale == PhotoViewComputedScale.covered) {
|
||||
return (_scaleForCovering(outerSize, childSize) *
|
||||
(_maxScale as PhotoViewComputedScale) // ignore: avoid_as
|
||||
.multiplier)
|
||||
.clamp(minScale, double.infinity);
|
||||
}
|
||||
return _maxScale.clamp(minScale, double.infinity);
|
||||
}
|
||||
|
||||
double get initialScale {
|
||||
assert(_initialScale is double || _initialScale is PhotoViewComputedScale);
|
||||
if (_initialScale == PhotoViewComputedScale.contained) {
|
||||
return _scaleForContained(outerSize, childSize) *
|
||||
(_initialScale as PhotoViewComputedScale) // ignore: avoid_as
|
||||
.multiplier;
|
||||
}
|
||||
if (_initialScale == PhotoViewComputedScale.covered) {
|
||||
return _scaleForCovering(outerSize, childSize) *
|
||||
(_initialScale as PhotoViewComputedScale) // ignore: avoid_as
|
||||
.multiplier;
|
||||
}
|
||||
return _initialScale.clamp(minScale, maxScale);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is ScaleBoundaries &&
|
||||
runtimeType == other.runtimeType &&
|
||||
_minScale == other._minScale &&
|
||||
_maxScale == other._maxScale &&
|
||||
_initialScale == other._initialScale &&
|
||||
outerSize == other.outerSize &&
|
||||
childSize == other.childSize;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
_minScale.hashCode ^
|
||||
_maxScale.hashCode ^
|
||||
_initialScale.hashCode ^
|
||||
outerSize.hashCode ^
|
||||
childSize.hashCode;
|
||||
}
|
||||
|
||||
double _scaleForContained(Size size, Size childSize) {
|
||||
final double imageWidth = childSize.width;
|
||||
final double imageHeight = childSize.height;
|
||||
|
||||
final double screenWidth = size.width;
|
||||
final double screenHeight = size.height;
|
||||
|
||||
return math.min(screenWidth / imageWidth, screenHeight / imageHeight);
|
||||
}
|
||||
|
||||
double _scaleForCovering(Size size, Size childSize) {
|
||||
final double imageWidth = childSize.width;
|
||||
final double imageHeight = childSize.height;
|
||||
|
||||
final double screenWidth = size.width;
|
||||
final double screenHeight = size.height;
|
||||
|
||||
return math.max(screenWidth / imageWidth, screenHeight / imageHeight);
|
||||
}
|
||||
|
||||
double _clampSize(double size, ScaleBoundaries scaleBoundaries) {
|
||||
return size.clamp(scaleBoundaries.minScale, scaleBoundaries.maxScale);
|
||||
}
|
||||
|
||||
/// Simple class to store a min and a max value
|
||||
class CornersRange {
|
||||
const CornersRange(this.min, this.max);
|
||||
final double min;
|
||||
final double max;
|
||||
}
|
||||
Reference in New Issue
Block a user