-
Notifications
You must be signed in to change notification settings - Fork 3.3k
[camerax] Implement lockCaptureOrientation
& unlockCaptureOrientation
#5285
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
dec3d69
0e0333b
bd7ac99
5c3363b
fed9621
5aabe34
2b9a352
a1173da
cbc3d6b
cae5a4c
72283db
166a77c
399780e
8d5d0e7
084d960
d2a59ac
a1422bf
bdd87a6
137a28b
bc0db5a
d04b466
a9cfe87
a32def1
4785148
7a8fc69
b02e15f
c6e5868
0c0065a
9dfe259
bfcc0df
b80cc86
4663838
33fe515
0c34fb6
b4b6347
ea4387d
5c22eb8
49b4743
87d1382
4ba04e0
dc8e8f3
3affd3a
cf4b868
5c1470e
13fd29e
6096c37
fd99476
03efa2f
b37dbbf
1307099
b4ad9c3
39ab573
85754de
2ecdcb0
e7fa2c2
6b6080e
64e07de
0e614aa
85f8190
00879d2
95daaf6
b5acca5
a410de1
f1d21e2
2be21da
6f81dbd
b28ad12
0e01e43
99c42f6
00c6ace
d2ecd9a
6e35acd
cc80fc2
06b38cf
34d074c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,6 +27,7 @@ public final class CameraAndroidCameraxPlugin implements FlutterPlugin, Activity | |
private ImageCaptureHostApiImpl imageCaptureHostApiImpl; | ||
private CameraControlHostApiImpl cameraControlHostApiImpl; | ||
public @Nullable SystemServicesHostApiImpl systemServicesHostApiImpl; | ||
public @Nullable DeviceOrientationManagerHostApiImpl deviceOrientationManagerHostApiImpl; | ||
|
||
@VisibleForTesting | ||
public @Nullable ProcessCameraProviderHostApiImpl processCameraProviderHostApiImpl; | ||
|
@@ -71,6 +72,10 @@ public void setUp( | |
systemServicesHostApiImpl = | ||
new SystemServicesHostApiImpl(binaryMessenger, instanceManager, context); | ||
GeneratedCameraXLibrary.SystemServicesHostApi.setup(binaryMessenger, systemServicesHostApiImpl); | ||
deviceOrientationManagerHostApiImpl = | ||
new DeviceOrientationManagerHostApiImpl(binaryMessenger, instanceManager); | ||
GeneratedCameraXLibrary.DeviceOrientationManagerHostApi.setup( | ||
binaryMessenger, deviceOrientationManagerHostApiImpl); | ||
GeneratedCameraXLibrary.PreviewHostApi.setup( | ||
binaryMessenger, new PreviewHostApiImpl(binaryMessenger, instanceManager, textureRegistry)); | ||
imageCaptureHostApiImpl = | ||
|
@@ -145,6 +150,7 @@ public void onAttachedToActivity(@NonNull ActivityPluginBinding activityPluginBi | |
systemServicesHostApiImpl.setActivity(activity); | ||
systemServicesHostApiImpl.setPermissionsRegistry( | ||
activityPluginBinding::addRequestPermissionsResultListener); | ||
deviceOrientationManagerHostApiImpl.setActivity(activity); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we be unsetting the activity in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think so after reading the docs so thanks for pointing this out! I don't want this change to get lost in this PR since it seems like a legitimate bug, so I filed flutter/flutter#139937 and will follow up with a PR. |
||
} | ||
|
||
@Override | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,7 +14,6 @@ | |
import android.view.Surface; | ||
import android.view.WindowManager; | ||
import androidx.annotation.NonNull; | ||
import androidx.annotation.Nullable; | ||
import androidx.annotation.VisibleForTesting; | ||
import io.flutter.embedding.engine.systemchannels.PlatformChannel; | ||
import io.flutter.embedding.engine.systemchannels.PlatformChannel.DeviceOrientation; | ||
|
@@ -85,120 +84,6 @@ public void stop() { | |
broadcastReceiver = null; | ||
} | ||
|
||
/** | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note to reviewer: I removed all unused functionality. |
||
* Returns the device's photo orientation in degrees based on the sensor orientation and the last | ||
* known UI orientation. | ||
* | ||
* <p>Returns one of 0, 90, 180 or 270. | ||
* | ||
* @return The device's photo orientation in degrees. | ||
*/ | ||
public int getPhotoOrientation() { | ||
return this.getPhotoOrientation(this.lastOrientation); | ||
} | ||
|
||
/** | ||
* Returns the device's photo orientation in degrees based on the sensor orientation and the | ||
* supplied {@link PlatformChannel.DeviceOrientation} value. | ||
* | ||
* <p>Returns one of 0, 90, 180 or 270. | ||
* | ||
* @param orientation The {@link PlatformChannel.DeviceOrientation} value that is to be converted | ||
* into degrees. | ||
* @return The device's photo orientation in degrees. | ||
*/ | ||
public int getPhotoOrientation(@Nullable PlatformChannel.DeviceOrientation orientation) { | ||
int angle = 0; | ||
// Fallback to device orientation when the orientation value is null. | ||
if (orientation == null) { | ||
orientation = getUIOrientation(); | ||
} | ||
|
||
switch (orientation) { | ||
case PORTRAIT_UP: | ||
angle = 90; | ||
break; | ||
case PORTRAIT_DOWN: | ||
angle = 270; | ||
break; | ||
case LANDSCAPE_LEFT: | ||
angle = isFrontFacing ? 180 : 0; | ||
break; | ||
case LANDSCAPE_RIGHT: | ||
angle = isFrontFacing ? 0 : 180; | ||
break; | ||
} | ||
|
||
// Sensor orientation is 90 for most devices, or 270 for some devices (eg. Nexus 5X). | ||
// This has to be taken into account so the JPEG is rotated properly. | ||
// For devices with orientation of 90, this simply returns the mapping from ORIENTATIONS. | ||
// For devices with orientation of 270, the JPEG is rotated 180 degrees instead. | ||
return (angle + sensorOrientation + 270) % 360; | ||
} | ||
|
||
/** | ||
* Returns the device's video orientation in clockwise degrees based on the sensor orientation and | ||
* the last known UI orientation. | ||
* | ||
* <p>Returns one of 0, 90, 180 or 270. | ||
* | ||
* @return The device's video orientation in clockwise degrees. | ||
*/ | ||
public int getVideoOrientation() { | ||
return this.getVideoOrientation(this.lastOrientation); | ||
} | ||
|
||
/** | ||
* Returns the device's video orientation in clockwise degrees based on the sensor orientation and | ||
* the supplied {@link PlatformChannel.DeviceOrientation} value. | ||
* | ||
* <p>Returns one of 0, 90, 180 or 270. | ||
* | ||
* <p>More details can be found in the official Android documentation: | ||
* https://developer.android.com/reference/android/media/MediaRecorder#setOrientationHint(int) | ||
* | ||
* <p>See also: | ||
* https://developer.android.com/training/camera2/camera-preview-large-screens#orientation_calculation | ||
* | ||
* @param orientation The {@link PlatformChannel.DeviceOrientation} value that is to be converted | ||
* into degrees. | ||
* @return The device's video orientation in clockwise degrees. | ||
*/ | ||
public int getVideoOrientation(@Nullable PlatformChannel.DeviceOrientation orientation) { | ||
int angle = 0; | ||
|
||
// Fallback to device orientation when the orientation value is null. | ||
if (orientation == null) { | ||
orientation = getUIOrientation(); | ||
} | ||
|
||
switch (orientation) { | ||
case PORTRAIT_UP: | ||
angle = 0; | ||
break; | ||
case PORTRAIT_DOWN: | ||
angle = 180; | ||
break; | ||
case LANDSCAPE_LEFT: | ||
angle = 270; | ||
break; | ||
case LANDSCAPE_RIGHT: | ||
angle = 90; | ||
break; | ||
} | ||
|
||
if (isFrontFacing) { | ||
angle *= -1; | ||
} | ||
|
||
return (angle + sensorOrientation + 360) % 360; | ||
} | ||
|
||
/** @return the last received UI orientation. */ | ||
public @Nullable PlatformChannel.DeviceOrientation getLastUIOrientation() { | ||
return this.lastOrientation; | ||
} | ||
|
||
/** | ||
* Handles orientation changes based on change events triggered by the OrientationIntentFilter. | ||
* | ||
|
@@ -241,7 +126,7 @@ static void handleOrientationChange( | |
@SuppressWarnings("deprecation") | ||
@VisibleForTesting | ||
PlatformChannel.DeviceOrientation getUIOrientation() { | ||
final int rotation = getDisplay().getRotation(); | ||
final int rotation = getDefaultRotation(); | ||
final int orientation = activity.getResources().getConfiguration().orientation; | ||
|
||
switch (orientation) { | ||
|
@@ -265,57 +150,18 @@ PlatformChannel.DeviceOrientation getUIOrientation() { | |
} | ||
|
||
/** | ||
* Calculates the sensor orientation based on the supplied angle. | ||
* | ||
* <p>This method is visible for testing purposes only and should never be used outside this | ||
* class. | ||
* | ||
* @param angle Orientation angle. | ||
* @return The sensor orientation based on the supplied angle. | ||
*/ | ||
@VisibleForTesting | ||
PlatformChannel.DeviceOrientation calculateSensorOrientation(int angle) { | ||
final int tolerance = 45; | ||
angle += tolerance; | ||
|
||
// Orientation is 0 in the default orientation mode. This is portrait-mode for phones | ||
// and landscape for tablets. We have to compensate for this by calculating the default | ||
// orientation, and apply an offset accordingly. | ||
int defaultDeviceOrientation = getDeviceDefaultOrientation(); | ||
if (defaultDeviceOrientation == Configuration.ORIENTATION_LANDSCAPE) { | ||
angle += 90; | ||
} | ||
// Determine the orientation | ||
angle = angle % 360; | ||
return new PlatformChannel.DeviceOrientation[] { | ||
PlatformChannel.DeviceOrientation.PORTRAIT_UP, | ||
PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, | ||
PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, | ||
PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, | ||
} | ||
[angle / 90]; | ||
} | ||
|
||
/** | ||
* Gets the default orientation of the device. | ||
* Gets default capture rotation for CameraX {@code UseCase}s. | ||
* | ||
* <p>This method is visible for testing purposes only and should never be used outside this | ||
* class. | ||
* <p>See | ||
* https://developer.android.com/reference/androidx/camera/core/ImageCapture#setTargetRotation(int), | ||
* for instance. | ||
* | ||
* @return The default orientation of the device. | ||
* @return The rotation of the screen from its "natural" orientation; one of {@code | ||
* Surface.ROTATION_0}, {@code Surface.ROTATION_90}, {@code Surface.ROTATION_180}, {@code | ||
* Surface.ROTATION_270} | ||
*/ | ||
@VisibleForTesting | ||
int getDeviceDefaultOrientation() { | ||
Configuration config = activity.getResources().getConfiguration(); | ||
int rotation = getDisplay().getRotation(); | ||
if (((rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) | ||
&& config.orientation == Configuration.ORIENTATION_LANDSCAPE) | ||
|| ((rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) | ||
&& config.orientation == Configuration.ORIENTATION_PORTRAIT)) { | ||
return Configuration.ORIENTATION_LANDSCAPE; | ||
} else { | ||
return Configuration.ORIENTATION_PORTRAIT; | ||
} | ||
int getDefaultRotation() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: I believe this method is named Basically it just feels more like a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To address this, I renamed the wrapped method to |
||
return getDisplay().getRotation(); | ||
} | ||
|
||
/** | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
// Copyright 2013 The Flutter Authors. All rights reserved. | ||
// Use of this source code is governed by a BSD-style license that can be | ||
// found in the LICENSE file. | ||
|
||
package io.flutter.plugins.camerax; | ||
|
||
import androidx.annotation.NonNull; | ||
import io.flutter.plugin.common.BinaryMessenger; | ||
import io.flutter.plugins.camerax.GeneratedCameraXLibrary.DeviceOrientationManagerFlutterApi; | ||
|
||
public class DeviceOrientationManagerFlutterApiImpl extends DeviceOrientationManagerFlutterApi { | ||
public DeviceOrientationManagerFlutterApiImpl(@NonNull BinaryMessenger binaryMessenger) { | ||
super(binaryMessenger); | ||
} | ||
|
||
public void sendDeviceOrientationChangedEvent( | ||
@NonNull String orientation, @NonNull Reply<Void> reply) { | ||
super.onDeviceOrientationChanged(orientation, reply); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
// Copyright 2013 The Flutter Authors. All rights reserved. | ||
// Use of this source code is governed by a BSD-style license that can be | ||
// found in the LICENSE file. | ||
|
||
package io.flutter.plugins.camerax; | ||
|
||
import android.app.Activity; | ||
import androidx.annotation.NonNull; | ||
import androidx.annotation.Nullable; | ||
import androidx.annotation.VisibleForTesting; | ||
import io.flutter.embedding.engine.systemchannels.PlatformChannel.DeviceOrientation; | ||
import io.flutter.plugin.common.BinaryMessenger; | ||
import io.flutter.plugins.camerax.CameraPermissionsManager.PermissionsRegistry; | ||
import io.flutter.plugins.camerax.GeneratedCameraXLibrary.DeviceOrientationManagerHostApi; | ||
|
||
public class DeviceOrientationManagerHostApiImpl implements DeviceOrientationManagerHostApi { | ||
private final BinaryMessenger binaryMessenger; | ||
private final InstanceManager instanceManager; | ||
|
||
@VisibleForTesting public @NonNull CameraXProxy cameraXProxy = new CameraXProxy(); | ||
@VisibleForTesting public @Nullable DeviceOrientationManager deviceOrientationManager; | ||
|
||
@VisibleForTesting | ||
public @NonNull DeviceOrientationManagerFlutterApiImpl deviceOrientationManagerFlutterApiImpl; | ||
|
||
private Activity activity; | ||
private PermissionsRegistry permissionsRegistry; | ||
|
||
public DeviceOrientationManagerHostApiImpl( | ||
@NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { | ||
this.binaryMessenger = binaryMessenger; | ||
this.instanceManager = instanceManager; | ||
this.deviceOrientationManagerFlutterApiImpl = | ||
new DeviceOrientationManagerFlutterApiImpl(binaryMessenger); | ||
} | ||
|
||
public void setActivity(@NonNull Activity activity) { | ||
this.activity = activity; | ||
} | ||
|
||
/** | ||
* Starts listening for device orientation changes using an instance of a {@link | ||
* DeviceOrientationManager}. | ||
* | ||
* <p>Whenever a change in device orientation is detected by the {@code DeviceOrientationManager}, | ||
* the {@link SystemServicesFlutterApi} will be used to notify the Dart side. | ||
*/ | ||
@Override | ||
public void startListeningForDeviceOrientationChange( | ||
@NonNull Boolean isFrontFacing, @NonNull Long sensorOrientation) { | ||
deviceOrientationManager = | ||
cameraXProxy.createDeviceOrientationManager( | ||
activity, | ||
isFrontFacing, | ||
sensorOrientation.intValue(), | ||
(DeviceOrientation newOrientation) -> { | ||
deviceOrientationManagerFlutterApiImpl.sendDeviceOrientationChangedEvent( | ||
serializeDeviceOrientation(newOrientation), reply -> {}); | ||
}); | ||
deviceOrientationManager.start(); | ||
} | ||
|
||
/** Serializes {@code DeviceOrientation} into a String that the Dart side is able to recognize. */ | ||
String serializeDeviceOrientation(DeviceOrientation orientation) { | ||
return orientation.toString(); | ||
} | ||
|
||
/** | ||
* Tells the {@code deviceOrientationManager} to stop listening for orientation updates. | ||
* | ||
* <p>Has no effect if the {@code deviceOrientationManager} was never created to listen for device | ||
* orientation updates. | ||
*/ | ||
@Override | ||
public void stopListeningForDeviceOrientationChange() { | ||
if (deviceOrientationManager != null) { | ||
deviceOrientationManager.stop(); | ||
} | ||
} | ||
|
||
/** | ||
* Gets default capture rotation for CameraX {@code UseCase}s. | ||
* | ||
* <p>The default capture rotation for CameraX is the rotation of default {@code Display} at the | ||
* time that a {@code UseCase} is bound, but the default {@code Display} does not change in this | ||
* plugin, so this value is {@code Display}-agnostic. | ||
* | ||
* <p>See | ||
* https://developer.android.com/reference/androidx/camera/core/ImageCapture#setTargetRotation(int) | ||
* for instance for more information on how this default value is used. | ||
*/ | ||
@Override | ||
public @NonNull Long getDefaultDisplayRotation() { | ||
int defaultRotation; | ||
try { | ||
defaultRotation = deviceOrientationManager.getDefaultRotation(); | ||
} catch (NullPointerException e) { | ||
throw new IllegalStateException( | ||
"startListeningForDeviceOrientationChange must first be called to subscribe to device orientation changes in order to retrieve default rotation."); | ||
} | ||
|
||
return Long.valueOf(defaultRotation); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note to reviewer:
Refactored
DeviceOrientationManager.java
functionality into its own class (previously a part ofSystemServices
host/Flutter API implementations) for clarity. I will mark* changes related to this throughout this PR to make it clear that these are simply a refactor and no changes in logic occurred.*Marked with comment: "Part of DeviceOrientation refactor!"