Skip to content

Commit

Permalink
Merge pull request #1329 from navaronbracke/fix_android_impeller_orie…
Browse files Browse the repository at this point in the history
…ntation

fix: correct android preview orientation
  • Loading branch information
juliansteenbakker authored Feb 18, 2025
2 parents 7947e61 + 8586736 commit 7f47cb2
Show file tree
Hide file tree
Showing 10 changed files with 174 additions and 146 deletions.
4 changes: 2 additions & 2 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ group 'dev.steenbakker.mobile_scanner'
version '1.0-SNAPSHOT'

buildscript {
ext.kotlin_version = '1.7.22'
ext.kotlin_version = '1.8.0'
repositories {
google()
mavenCentral()
Expand All @@ -29,7 +29,7 @@ android {
namespace 'dev.steenbakker.mobile_scanner'
}

compileSdk 34
compileSdk 35

compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -638,7 +638,7 @@ class MobileScanner(
}
val paint = Paint().apply { colorFilter = ColorMatrixColorFilter(colorMatrix) }

val invertedBitmap = Bitmap.createBitmap(bitmap.width, bitmap.height, bitmap.config)
val invertedBitmap = Bitmap.createBitmap(bitmap.width, bitmap.height, bitmap.config!!)
val canvas = Canvas(invertedBitmap)
canvas.drawBitmap(bitmap, 0f, 0f, paint)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ class MobileScannerHandler(
"textureId" to it.id,
"size" to mapOf("width" to it.width, "height" to it.height),
"naturalDeviceOrientation" to it.naturalDeviceOrientation,
"isPreviewPreTransformed" to it.isPreviewPreTransformed,
"handlesCropAndRotation" to it.handlesCropAndRotation,
"sensorOrientation" to it.sensorOrientation,
"currentTorchState" to it.currentTorchState,
"numberOfCameras" to it.numberOfCameras,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ class MobileScannerStartParameters(
val height: Double,
val naturalDeviceOrientation: String,
val sensorOrientation: Int,
val isPreviewPreTransformed: Boolean,
val handlesCropAndRotation: Boolean,
val currentTorchState: Int,
val id: Long,
val numberOfCameras: Int,
Expand Down
2 changes: 2 additions & 0 deletions example/android/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ GeneratedPluginRegistrant.java
key.properties
**/*.keystore
**/*.jks

**/.cxx
5 changes: 3 additions & 2 deletions example/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ if (flutterVersionName == null) {

android {
namespace "dev.steenbakker.mobile_scanner_example"
compileSdk 34
compileSdk 35

compileOptions {
sourceCompatibility JavaVersion.VERSION_17
Expand All @@ -43,7 +43,8 @@ android {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "dev.steenbakker.mobile_scanner_example"
minSdkVersion 24
targetSdkVersion 34
targetSdkVersion 35
ndkVersion flutter.ndkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
Expand Down
2 changes: 1 addition & 1 deletion example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ dependencies:
flutter:
sdk: flutter

image_picker: ^1.0.4
image_picker: ^1.1.2
mobile_scanner:
# When depending on this package from a real application you should use:
# mobile_scanner: ^x.y.z
Expand Down
151 changes: 19 additions & 132 deletions lib/src/method_channel/android_surface_producer_delegate.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import 'dart:async';

import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:mobile_scanner/src/enums/camera_facing.dart';
import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart';
import 'package:mobile_scanner/src/mobile_scanner_exception.dart';
Expand All @@ -12,10 +9,10 @@ import 'package:mobile_scanner/src/utils/parse_device_orientation_extension.dart
class AndroidSurfaceProducerDelegate {
/// Construct a new [AndroidSurfaceProducerDelegate].
AndroidSurfaceProducerDelegate({
required this.cameraIsFrontFacing,
required this.isPreviewPreTransformed,
required this.naturalOrientation,
required this.sensorOrientation,
required this.cameraFacingDirection,
required this.handlesCropAndRotation,
required this.initialDeviceOrientation,
required this.sensorOrientationDegrees,
});

/// Construct a new [AndroidSurfaceProducerDelegate]
Expand All @@ -28,18 +25,18 @@ class AndroidSurfaceProducerDelegate {
) {
if (config
case {
'isPreviewPreTransformed': final bool isPreviewPreTransformed,
'handlesCropAndRotation': final bool handlesCropAndRotation,
'naturalDeviceOrientation': final String naturalDeviceOrientation,
'sensorOrientation': final int sensorOrientation
'sensorOrientation': final int sensorOrientation,
}) {
final DeviceOrientation naturalOrientation =
naturalDeviceOrientation.parseDeviceOrientation();

return AndroidSurfaceProducerDelegate(
cameraIsFrontFacing: cameraDirection == CameraFacing.front,
isPreviewPreTransformed: isPreviewPreTransformed,
naturalOrientation: naturalOrientation,
sensorOrientation: sensorOrientation,
cameraFacingDirection: cameraDirection,
handlesCropAndRotation: handlesCropAndRotation,
initialDeviceOrientation: naturalOrientation,
sensorOrientationDegrees: sensorOrientation.toDouble(),
);
}

Expand All @@ -51,127 +48,17 @@ class AndroidSurfaceProducerDelegate {
);
}

/// The rotation degrees corresponding to each device orientation.
static const Map<DeviceOrientation, int> _degreesForDeviceOrientation =
<DeviceOrientation, int>{
DeviceOrientation.portraitUp: 0,
DeviceOrientation.landscapeRight: 90,
DeviceOrientation.portraitDown: 180,
DeviceOrientation.landscapeLeft: 270,
};

// TODO: remove this flag once the rotation correction functions correctly on Impeller.
// See https://github.com/juliansteenbakker/mobile_scanner/pull/1283#discussion_r1927798329
static const bool _rotationCorrectionEnabled = false;

/// The subscription that listens to device orientation changes.
StreamSubscription<Object?>? _deviceOrientationSubscription;

/// Whether the current camera is a front facing camera.
///
/// This is used to determine whether the orientation correction
/// should apply an additional correction for front facing cameras.
final bool cameraIsFrontFacing;

/// The current orientation of the device.
///
/// When the orientation changes this field is updated by notifications from
/// the [_deviceOrientationSubscription].
DeviceOrientation? currentDeviceOrientation;

/// Whether the camera preview is pre-transformed,
/// and thus does not need an orientation correction.
final bool isPreviewPreTransformed;
/// The facing direction of the active camera.
final CameraFacing cameraFacingDirection;

/// The initial orientation of the device, when the camera was started.
/// Whether the underlying surface producer handles crop and rotation.
///
/// The camera preview will use this orientation as the natural orientation
/// to correct its rotation with respect to, if necessary.
final DeviceOrientation naturalOrientation;

/// The sensor orientation of the current camera, in degrees.
final int sensorOrientation;

/// Apply a rotation correction to the given [texture] widget.
Widget applyRotationCorrection(Widget texture) {
if (!_rotationCorrectionEnabled) {
return texture;
}

int naturalDeviceOrientationDegrees =
_degreesForDeviceOrientation[naturalOrientation]!;

if (isPreviewPreTransformed) {
// If the camera preview is backed by a SurfaceTexture, the transformation
// needed to correctly rotate the preview has already been applied.
//
// However, the camera preview rotation may need to be corrected if the
// device is naturally landscape-oriented.
if (naturalOrientation == DeviceOrientation.landscapeLeft ||
naturalOrientation == DeviceOrientation.landscapeRight) {
final int quarterTurns = (-naturalDeviceOrientationDegrees + 360) ~/ 4;

return RotatedBox(
quarterTurns: quarterTurns,
child: texture,
);
}

return texture;
}

// If the camera preview is not backed by a SurfaceTexture,
// the camera preview rotation needs to be manually applied,
// while also taking into account devices that are naturally landscape-oriented.
final int signForCameraDirection = cameraIsFrontFacing ? 1 : -1;

// For front-facing cameras, the preview is rotated counterclockwise,
// so determine the rotation needed to correct the camera preview with
// respect to the natural orientation of the device, based on the inverse of
// of the natural orientation.
if (signForCameraDirection == 1 &&
(currentDeviceOrientation == DeviceOrientation.landscapeLeft ||
currentDeviceOrientation == DeviceOrientation.landscapeRight)) {
naturalDeviceOrientationDegrees += 180;
}
/// If this is false, the preview needs to be manually rotated.
final bool handlesCropAndRotation;

// See https://developer.android.com/media/camera/camera2/camera-preview#orientation_calculation
final double rotation = (sensorOrientation +
naturalDeviceOrientationDegrees * signForCameraDirection +
360) %
360;
/// The initial device orientation when this [AndroidSurfaceProducerDelegate] is created.
final DeviceOrientation initialDeviceOrientation;

int quarterTurnsToCorrectPreview = rotation ~/ 90;

// Correct the camera preview rotation for devices that are naturally landscape-oriented.
if (naturalOrientation == DeviceOrientation.landscapeLeft ||
naturalOrientation == DeviceOrientation.landscapeRight) {
quarterTurnsToCorrectPreview +=
(-naturalDeviceOrientationDegrees + 360) ~/ 4;
}

return RotatedBox(
quarterTurns: quarterTurnsToCorrectPreview,
child: texture,
);
}

/// Start listening to device orientation changes,
/// which are provided by the given [stream].
void startListeningToDeviceOrientation(Stream<DeviceOrientation> stream) {
if (!_rotationCorrectionEnabled) {
return;
}

_deviceOrientationSubscription ??=
stream.listen((DeviceOrientation newOrientation) {
currentDeviceOrientation = newOrientation;
});
}

/// Dispose of this delegate.
void dispose() {
_deviceOrientationSubscription?.cancel();
_deviceOrientationSubscription = null;
}
/// The orientation of the camera sensor on the device, in degrees.
final double sensorOrientationDegrees;
}
20 changes: 14 additions & 6 deletions lib/src/method_channel/mobile_scanner_method_channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:mobile_scanner/src/enums/mobile_scanner_authorization_state.dart
import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart';
import 'package:mobile_scanner/src/enums/torch_state.dart';
import 'package:mobile_scanner/src/method_channel/android_surface_producer_delegate.dart';
import 'package:mobile_scanner/src/method_channel/rotated_preview.dart';
import 'package:mobile_scanner/src/mobile_scanner_exception.dart';
import 'package:mobile_scanner/src/mobile_scanner_platform_interface.dart';
import 'package:mobile_scanner/src/mobile_scanner_view_attributes.dart';
Expand Down Expand Up @@ -239,9 +240,20 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {

final Widget texture = Texture(textureId: _textureId!);

// If the preview needs manual orientation corrections,
// correct the preview orientation based on the currently reported device orientation.
// On Android, the underlying device orientation stream will emit the current orientation
// when the first listener is attached.
if (_surfaceProducerDelegate
case final AndroidSurfaceProducerDelegate delegate) {
return delegate.applyRotationCorrection(texture);
case final AndroidSurfaceProducerDelegate delegate
when !delegate.handlesCropAndRotation) {
return RotatedPreview.fromCameraDirection(
delegate.cameraFacingDirection,
deviceOrientationStream: deviceOrientationChangedStream,
initialDeviceOrientation: delegate.initialDeviceOrientation,
sensorOrientationDegrees: delegate.sensorOrientationDegrees,
child: texture,
);
}

return texture;
Expand Down Expand Up @@ -319,9 +331,6 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
startResult,
cameraDirection,
);
_surfaceProducerDelegate?.startListeningToDeviceOrientation(
deviceOrientationChangedStream,
);
}

final int? numberOfCameras = startResult['numberOfCameras'] as int?;
Expand Down Expand Up @@ -356,7 +365,6 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {

_textureId = null;
_pausing = false;
_surfaceProducerDelegate?.dispose();
_surfaceProducerDelegate = null;

await methodChannel.invokeMethod<void>('stop');
Expand Down
Loading

0 comments on commit 7f47cb2

Please sign in to comment.