#838 HDR color mode

This commit is contained in:
Thibault Deckers 2023-12-29 23:57:58 +01:00
parent 445aa2cb06
commit 49fa3eec96
10 changed files with 160 additions and 81 deletions

View file

@ -1,6 +1,7 @@
package deckers.thibault.aves.channel.calls.window package deckers.thibault.aves.channel.calls.window
import android.app.Activity import android.app.Activity
import android.content.pm.ActivityInfo
import android.os.Build import android.os.Build
import android.view.WindowManager import android.view.WindowManager
import deckers.thibault.aves.utils.getDisplayCompat import deckers.thibault.aves.utils.getDisplayCompat
@ -75,4 +76,21 @@ class ActivityWindowHandler(private val activity: Activity) : WindowHandler(acti
) )
) )
} }
override fun supportsHdr(call: MethodCall, result: MethodChannel.Result) {
result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && activity.getDisplayCompat()?.isHdr ?: false)
}
override fun setHdrColorMode(call: MethodCall, result: MethodChannel.Result) {
val on = call.argument<Boolean>("on")
if (on == null) {
result.error("setHdrColorMode-args", "missing arguments", null)
return
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
activity.window.colorMode = if (on) ActivityInfo.COLOR_MODE_HDR else ActivityInfo.COLOR_MODE_DEFAULT
}
result.success(null)
}
} }

View file

@ -28,4 +28,12 @@ class ServiceWindowHandler(service: Service) : WindowHandler(service) {
override fun getCutoutInsets(call: MethodCall, result: MethodChannel.Result) { override fun getCutoutInsets(call: MethodCall, result: MethodChannel.Result) {
result.success(HashMap<String, Any>()) result.success(HashMap<String, Any>())
} }
override fun supportsHdr(call: MethodCall, result: MethodChannel.Result) {
result.success(false)
}
override fun setHdrColorMode(call: MethodCall, result: MethodChannel.Result) {
result.success(null)
}
} }

View file

@ -18,6 +18,8 @@ abstract class WindowHandler(private val contextWrapper: ContextWrapper) : Metho
"requestOrientation" -> Coresult.safe(call, result, ::requestOrientation) "requestOrientation" -> Coresult.safe(call, result, ::requestOrientation)
"isCutoutAware" -> Coresult.safe(call, result, ::isCutoutAware) "isCutoutAware" -> Coresult.safe(call, result, ::isCutoutAware)
"getCutoutInsets" -> Coresult.safe(call, result, ::getCutoutInsets) "getCutoutInsets" -> Coresult.safe(call, result, ::getCutoutInsets)
"supportsHdr" -> Coresult.safe(call, result, ::supportsHdr)
"setHdrColorMode" -> Coresult.safe(call, result, ::setHdrColorMode)
else -> result.notImplemented() else -> result.notImplemented()
} }
} }
@ -44,6 +46,10 @@ abstract class WindowHandler(private val contextWrapper: ContextWrapper) : Metho
abstract fun getCutoutInsets(call: MethodCall, result: MethodChannel.Result) abstract fun getCutoutInsets(call: MethodCall, result: MethodChannel.Result)
abstract fun supportsHdr(call: MethodCall, result: MethodChannel.Result)
abstract fun setHdrColorMode(call: MethodCall, result: MethodChannel.Result)
companion object { companion object {
private val LOG_TAG = LogUtils.createTag<WindowHandler>() private val LOG_TAG = LogUtils.createTag<WindowHandler>()
const val CHANNEL = "deckers.thibault/aves/window" const val CHANNEL = "deckers.thibault/aves/window"

View file

@ -1,6 +1,6 @@
import 'dart:math'; import 'dart:math';
import 'package:aves/widgets/viewer/controls/controller.dart'; import 'package:aves/widgets/viewer/controls/transitions.dart';
import 'package:aves_model/aves_model.dart'; import 'package:aves_model/aves_model.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';

View file

@ -17,11 +17,17 @@ abstract class WindowService {
Future<bool> isCutoutAware(); Future<bool> isCutoutAware();
Future<EdgeInsets> getCutoutInsets(); Future<EdgeInsets> getCutoutInsets();
Future<bool> supportsHdr();
Future<void> setHdrColorMode(bool on);
} }
class PlatformWindowService implements WindowService { class PlatformWindowService implements WindowService {
static const _platform = MethodChannel('deckers.thibault/aves/window'); static const _platform = MethodChannel('deckers.thibault/aves/window');
bool? _isCutoutAware, _supportsHdr;
@override @override
Future<bool> isActivity() async { Future<bool> isActivity() async {
try { try {
@ -90,8 +96,6 @@ class PlatformWindowService implements WindowService {
} }
} }
bool? _isCutoutAware;
@override @override
Future<bool> isCutoutAware() async { Future<bool> isCutoutAware() async {
if (_isCutoutAware != null) return SynchronousFuture(_isCutoutAware!); if (_isCutoutAware != null) return SynchronousFuture(_isCutoutAware!);
@ -121,4 +125,27 @@ class PlatformWindowService implements WindowService {
} }
return EdgeInsets.zero; return EdgeInsets.zero;
} }
@override
Future<bool> supportsHdr() async {
if (_supportsHdr != null) return SynchronousFuture(_supportsHdr!);
try {
final result = await _platform.invokeMethod('supportsHdr');
_supportsHdr = result as bool?;
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
return _supportsHdr ?? false;
}
@override
Future<void> setHdrColorMode(bool on) async {
try {
await _platform.invokeMethod('setHdrColorMode', <String, dynamic>{
'on': on,
});
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
}
} }

View file

@ -154,6 +154,7 @@ class _BugReportState extends State<BugReport> with FeedbackMixin {
final androidInfo = await DeviceInfoPlugin().androidInfo; final androidInfo = await DeviceInfoPlugin().androidInfo;
final storageVolumes = await storageService.getStorageVolumes(); final storageVolumes = await storageService.getStorageVolumes();
final storageGrants = await storageService.getGrantedDirectories(); final storageGrants = await storageService.getGrantedDirectories();
final supportsHdr = await windowService.supportsHdr();
return [ return [
'Package: ${device.packageName}', 'Package: ${device.packageName}',
'Installer: ${packageInfo.installerStore}', 'Installer: ${packageInfo.installerStore}',
@ -162,7 +163,7 @@ class _BugReportState extends State<BugReport> with FeedbackMixin {
'Android version: ${androidInfo.version.release}, API ${androidInfo.version.sdkInt}', 'Android version: ${androidInfo.version.release}, API ${androidInfo.version.sdkInt}',
'Android build: ${androidInfo.display}', 'Android build: ${androidInfo.display}',
'Device: ${androidInfo.manufacturer} ${androidInfo.model}', 'Device: ${androidInfo.manufacturer} ${androidInfo.model}',
'Geocoder: ${device.hasGeocoder ? 'ready' : 'not available'}', 'Support: dynamic colors=${device.isDynamicColorAvailable}, geocoder=${device.hasGeocoder}, HDR=$supportsHdr',
'Mobile services: ${mobileServices.isServiceAvailable ? 'ready' : 'not available'}', 'Mobile services: ${mobileServices.isServiceAvailable ? 'ready' : 'not available'}',
'System locales: ${WidgetsBinding.instance.platformDispatcher.locales.join(', ')}', 'System locales: ${WidgetsBinding.instance.platformDispatcher.locales.join(', ')}',
'Storage volumes: ${storageVolumes.map((v) => v.path).join(', ')}', 'Storage volumes: ${storageVolumes.map((v) => v.path).join(', ')}',

View file

@ -96,10 +96,10 @@ class GridThemeData {
else if (entry.isAnimated) else if (entry.isAnimated)
const AnimatedImageIcon() const AnimatedImageIcon()
else ...[ else ...[
if (entry.isHdr && showHdr) const HdrIcon(),
if (entry.isRaw && showRaw) const RawIcon(), if (entry.isRaw && showRaw) const RawIcon(),
if (entry.is360) const PanoramaIcon(), if (entry.is360) const PanoramaIcon(),
], ],
if (entry.isHdr && showHdr) const HdrIcon(),
if (entry.isMotionPhoto && showMotionPhoto) const MotionPhotoIcon(), if (entry.isMotionPhoto && showMotionPhoto) const MotionPhotoIcon(),
if (entry.isMultiPage && !entry.isMotionPhoto) MultiPageIcon(entry: entry), if (entry.isMultiPage && !entry.isMotionPhoto) MultiPageIcon(entry: entry),
if (entry.isGeotiff) const GeoTiffIcon(), if (entry.isGeotiff) const GeoTiffIcon(),

View file

@ -2,6 +2,8 @@ import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/multipage.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/viewer/controls/cast.dart'; import 'package:aves/widgets/viewer/controls/cast.dart';
import 'package:aves/widgets/viewer/controls/events.dart'; import 'package:aves/widgets/viewer/controls/events.dart';
@ -57,6 +59,7 @@ class ViewerController with CastMixin {
); );
} }
_initialScale = initialScale; _initialScale = initialScale;
entryNotifier.addListener(_onEntryChanged);
_autopilotNotifier = ValueNotifier(autopilot); _autopilotNotifier = ValueNotifier(autopilot);
_autopilotNotifier.addListener(_onAutopilotChanged); _autopilotNotifier.addListener(_onAutopilotChanged);
_onAutopilotChanged(); _onAutopilotChanged();
@ -66,12 +69,21 @@ class ViewerController with CastMixin {
if (kFlutterMemoryAllocationsEnabled) { if (kFlutterMemoryAllocationsEnabled) {
MemoryAllocations.instance.dispatchObjectDisposed(object: this); MemoryAllocations.instance.dispatchObjectDisposed(object: this);
} }
entryNotifier.removeListener(_onEntryChanged);
windowService.setHdrColorMode(false);
_autopilotNotifier.dispose(); _autopilotNotifier.dispose();
_clearAutopilotAnimations(); _clearAutopilotAnimations();
_stopPlayTimer(); _stopPlayTimer();
_streamController.close(); _streamController.close();
} }
Future<void> _onEntryChanged() async {
if (await windowService.supportsHdr()) {
final enabled = entryNotifier.value?.isHdr ?? false;
await windowService.setHdrColorMode(enabled);
}
}
void _onAutopilotChanged() { void _onAutopilotChanged() {
_clearAutopilotAnimations(); _clearAutopilotAnimations();
_stopPlayTimer(); _stopPlayTimer();
@ -115,79 +127,3 @@ class ViewerController with CastMixin {
Future.delayed(ADurations.viewerHorizontalPageAnimation).then((_) => _autopilotAnimationControllers[vsync]?.forward()); Future.delayed(ADurations.viewerHorizontalPageAnimation).then((_) => _autopilotAnimationControllers[vsync]?.forward());
} }
} }
class PageTransitionEffects {
static TransitionBuilder fade(
PageController pageController,
int index, {
required bool zoomIn,
}) =>
(context, child) {
double opacity = 0;
double dx = 0;
double scale = 1;
if (pageController.hasClients && pageController.position.haveDimensions) {
final position = (pageController.page! - index).clamp(-1.0, 1.0);
final width = pageController.position.viewportDimension;
opacity = (1 - position.abs()).clamp(0, 1);
dx = position * width;
if (zoomIn) {
scale = 1 + position;
}
}
return Opacity(
opacity: opacity,
child: Transform.translate(
offset: Offset(dx, 0),
child: Transform.scale(
scale: scale,
child: child,
),
),
);
};
static TransitionBuilder slide(
PageController pageController,
int index, {
required bool parallax,
}) =>
(context, child) {
double dx = 0;
if (pageController.hasClients && pageController.position.haveDimensions) {
final position = (pageController.page! - index).clamp(-1.0, 1.0);
final width = pageController.position.viewportDimension;
if (parallax) {
dx = position * width / 2;
}
}
return ClipRect(
child: Transform.translate(
offset: Offset(dx, 0),
child: child,
),
);
};
static TransitionBuilder none(
PageController pageController,
int index,
) =>
(context, child) {
double opacity = 0;
double dx = 0;
if (pageController.hasClients && pageController.position.haveDimensions) {
final position = (pageController.page! - index).clamp(-1.0, 1.0);
final width = pageController.position.viewportDimension;
opacity = (1 - position.abs()).roundToDouble().clamp(0, 1);
dx = position * width;
}
return Opacity(
opacity: opacity,
child: Transform.translate(
offset: Offset(dx, 0),
child: child,
),
);
};
}

View file

@ -0,0 +1,77 @@
import 'package:flutter/widgets.dart';
class PageTransitionEffects {
static TransitionBuilder fade(
PageController pageController,
int index, {
required bool zoomIn,
}) =>
(context, child) {
double opacity = 0;
double dx = 0;
double scale = 1;
if (pageController.hasClients && pageController.position.haveDimensions) {
final position = (pageController.page! - index).clamp(-1.0, 1.0);
final width = pageController.position.viewportDimension;
opacity = (1 - position.abs()).clamp(0, 1);
dx = position * width;
if (zoomIn) {
scale = 1 + position;
}
}
return Opacity(
opacity: opacity,
child: Transform.translate(
offset: Offset(dx, 0),
child: Transform.scale(
scale: scale,
child: child,
),
),
);
};
static TransitionBuilder slide(
PageController pageController,
int index, {
required bool parallax,
}) =>
(context, child) {
double dx = 0;
if (pageController.hasClients && pageController.position.haveDimensions) {
final position = (pageController.page! - index).clamp(-1.0, 1.0);
final width = pageController.position.viewportDimension;
if (parallax) {
dx = position * width / 2;
}
}
return ClipRect(
child: Transform.translate(
offset: Offset(dx, 0),
child: child,
),
);
};
static TransitionBuilder none(
PageController pageController,
int index,
) =>
(context, child) {
double opacity = 0;
double dx = 0;
if (pageController.hasClients && pageController.position.haveDimensions) {
final position = (pageController.page! - index).clamp(-1.0, 1.0);
final width = pageController.position.viewportDimension;
opacity = (1 - position.abs()).roundToDouble().clamp(0, 1);
dx = position * width;
}
return Opacity(
opacity: opacity,
child: Transform.translate(
offset: Offset(dx, 0),
child: child,
),
);
};
}

View file

@ -21,4 +21,10 @@ class FakeWindowService extends Fake implements WindowService {
@override @override
Future<EdgeInsets> getCutoutInsets() => SynchronousFuture(EdgeInsets.zero); Future<EdgeInsets> getCutoutInsets() => SynchronousFuture(EdgeInsets.zero);
@override
Future<bool> supportsHdr() => SynchronousFuture(false);
@override
Future<void> setHdrColorMode(bool on) => SynchronousFuture(null);
} }