Skip to content

Commit bbb4134

Browse files
authored
[camerax] Implement lockCaptureOrientation & unlockCaptureOrientation (#5285)
Implements `lockCaptureOrientation` & `unlockCaptureOrientation` for all camera `UseCase`s. Also fixes small bug concerning not initially setting the target rotation of the `UseCase`s to the requested sensor orientation when `createCamera` is called. Fixes flutter/flutter#125915.
1 parent 34622db commit bbb4134

File tree

50 files changed

+2071
-810
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+2071
-810
lines changed

packages/camera/camera_android_camerax/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.5.0+25
2+
3+
* Implements `lockCaptureOrientation` and `unlockCaptureOrientation`.
4+
15
## 0.5.0+24
26

37
* Updates example app to use non-deprecated video_player method.

packages/camera/camera_android_camerax/README.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,6 @@ dependencies:
3131
and thus, the plugin will fall back to 480p if configured with a
3232
`ResolutionPreset`.
3333

34-
### Locking/Unlocking capture orientation \[[Issue #125915][125915]\]
35-
36-
`lockCaptureOrientation` & `unLockCaptureOrientation` are unimplemented.
37-
3834
### Exposure mode, point, & offset configuration \[[Issue #120468][120468]\]
3935

4036
`setExposureMode`, `setExposurePoint`, & `setExposureOffset` are unimplemented.

packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public final class CameraAndroidCameraxPlugin implements FlutterPlugin, Activity
2727
private ImageCaptureHostApiImpl imageCaptureHostApiImpl;
2828
private CameraControlHostApiImpl cameraControlHostApiImpl;
2929
public @Nullable SystemServicesHostApiImpl systemServicesHostApiImpl;
30+
public @Nullable DeviceOrientationManagerHostApiImpl deviceOrientationManagerHostApiImpl;
3031

3132
@VisibleForTesting
3233
public @Nullable ProcessCameraProviderHostApiImpl processCameraProviderHostApiImpl;
@@ -71,6 +72,10 @@ public void setUp(
7172
systemServicesHostApiImpl =
7273
new SystemServicesHostApiImpl(binaryMessenger, instanceManager, context);
7374
GeneratedCameraXLibrary.SystemServicesHostApi.setup(binaryMessenger, systemServicesHostApiImpl);
75+
deviceOrientationManagerHostApiImpl =
76+
new DeviceOrientationManagerHostApiImpl(binaryMessenger, instanceManager);
77+
GeneratedCameraXLibrary.DeviceOrientationManagerHostApi.setup(
78+
binaryMessenger, deviceOrientationManagerHostApiImpl);
7479
GeneratedCameraXLibrary.PreviewHostApi.setup(
7580
binaryMessenger, new PreviewHostApiImpl(binaryMessenger, instanceManager, textureRegistry));
7681
imageCaptureHostApiImpl =
@@ -145,6 +150,7 @@ public void onAttachedToActivity(@NonNull ActivityPluginBinding activityPluginBi
145150
systemServicesHostApiImpl.setActivity(activity);
146151
systemServicesHostApiImpl.setPermissionsRegistry(
147152
activityPluginBinding::addRequestPermissionsResultListener);
153+
deviceOrientationManagerHostApiImpl.setActivity(activity);
148154
}
149155

150156
@Override

packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/DeviceOrientationManager.java

Lines changed: 10 additions & 164 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
import android.view.Surface;
1515
import android.view.WindowManager;
1616
import androidx.annotation.NonNull;
17-
import androidx.annotation.Nullable;
1817
import androidx.annotation.VisibleForTesting;
1918
import io.flutter.embedding.engine.systemchannels.PlatformChannel;
2019
import io.flutter.embedding.engine.systemchannels.PlatformChannel.DeviceOrientation;
@@ -85,120 +84,6 @@ public void stop() {
8584
broadcastReceiver = null;
8685
}
8786

88-
/**
89-
* Returns the device's photo orientation in degrees based on the sensor orientation and the last
90-
* known UI orientation.
91-
*
92-
* <p>Returns one of 0, 90, 180 or 270.
93-
*
94-
* @return The device's photo orientation in degrees.
95-
*/
96-
public int getPhotoOrientation() {
97-
return this.getPhotoOrientation(this.lastOrientation);
98-
}
99-
100-
/**
101-
* Returns the device's photo orientation in degrees based on the sensor orientation and the
102-
* supplied {@link PlatformChannel.DeviceOrientation} value.
103-
*
104-
* <p>Returns one of 0, 90, 180 or 270.
105-
*
106-
* @param orientation The {@link PlatformChannel.DeviceOrientation} value that is to be converted
107-
* into degrees.
108-
* @return The device's photo orientation in degrees.
109-
*/
110-
public int getPhotoOrientation(@Nullable PlatformChannel.DeviceOrientation orientation) {
111-
int angle = 0;
112-
// Fallback to device orientation when the orientation value is null.
113-
if (orientation == null) {
114-
orientation = getUIOrientation();
115-
}
116-
117-
switch (orientation) {
118-
case PORTRAIT_UP:
119-
angle = 90;
120-
break;
121-
case PORTRAIT_DOWN:
122-
angle = 270;
123-
break;
124-
case LANDSCAPE_LEFT:
125-
angle = isFrontFacing ? 180 : 0;
126-
break;
127-
case LANDSCAPE_RIGHT:
128-
angle = isFrontFacing ? 0 : 180;
129-
break;
130-
}
131-
132-
// Sensor orientation is 90 for most devices, or 270 for some devices (eg. Nexus 5X).
133-
// This has to be taken into account so the JPEG is rotated properly.
134-
// For devices with orientation of 90, this simply returns the mapping from ORIENTATIONS.
135-
// For devices with orientation of 270, the JPEG is rotated 180 degrees instead.
136-
return (angle + sensorOrientation + 270) % 360;
137-
}
138-
139-
/**
140-
* Returns the device's video orientation in clockwise degrees based on the sensor orientation and
141-
* the last known UI orientation.
142-
*
143-
* <p>Returns one of 0, 90, 180 or 270.
144-
*
145-
* @return The device's video orientation in clockwise degrees.
146-
*/
147-
public int getVideoOrientation() {
148-
return this.getVideoOrientation(this.lastOrientation);
149-
}
150-
151-
/**
152-
* Returns the device's video orientation in clockwise degrees based on the sensor orientation and
153-
* the supplied {@link PlatformChannel.DeviceOrientation} value.
154-
*
155-
* <p>Returns one of 0, 90, 180 or 270.
156-
*
157-
* <p>More details can be found in the official Android documentation:
158-
* https://developer.android.com/reference/android/media/MediaRecorder#setOrientationHint(int)
159-
*
160-
* <p>See also:
161-
* https://developer.android.com/training/camera2/camera-preview-large-screens#orientation_calculation
162-
*
163-
* @param orientation The {@link PlatformChannel.DeviceOrientation} value that is to be converted
164-
* into degrees.
165-
* @return The device's video orientation in clockwise degrees.
166-
*/
167-
public int getVideoOrientation(@Nullable PlatformChannel.DeviceOrientation orientation) {
168-
int angle = 0;
169-
170-
// Fallback to device orientation when the orientation value is null.
171-
if (orientation == null) {
172-
orientation = getUIOrientation();
173-
}
174-
175-
switch (orientation) {
176-
case PORTRAIT_UP:
177-
angle = 0;
178-
break;
179-
case PORTRAIT_DOWN:
180-
angle = 180;
181-
break;
182-
case LANDSCAPE_LEFT:
183-
angle = 270;
184-
break;
185-
case LANDSCAPE_RIGHT:
186-
angle = 90;
187-
break;
188-
}
189-
190-
if (isFrontFacing) {
191-
angle *= -1;
192-
}
193-
194-
return (angle + sensorOrientation + 360) % 360;
195-
}
196-
197-
/** @return the last received UI orientation. */
198-
public @Nullable PlatformChannel.DeviceOrientation getLastUIOrientation() {
199-
return this.lastOrientation;
200-
}
201-
20287
/**
20388
* Handles orientation changes based on change events triggered by the OrientationIntentFilter.
20489
*
@@ -241,7 +126,7 @@ static void handleOrientationChange(
241126
@SuppressWarnings("deprecation")
242127
@VisibleForTesting
243128
PlatformChannel.DeviceOrientation getUIOrientation() {
244-
final int rotation = getDisplay().getRotation();
129+
final int rotation = getDefaultRotation();
245130
final int orientation = activity.getResources().getConfiguration().orientation;
246131

247132
switch (orientation) {
@@ -265,57 +150,18 @@ PlatformChannel.DeviceOrientation getUIOrientation() {
265150
}
266151

267152
/**
268-
* Calculates the sensor orientation based on the supplied angle.
269-
*
270-
* <p>This method is visible for testing purposes only and should never be used outside this
271-
* class.
272-
*
273-
* @param angle Orientation angle.
274-
* @return The sensor orientation based on the supplied angle.
275-
*/
276-
@VisibleForTesting
277-
PlatformChannel.DeviceOrientation calculateSensorOrientation(int angle) {
278-
final int tolerance = 45;
279-
angle += tolerance;
280-
281-
// Orientation is 0 in the default orientation mode. This is portrait-mode for phones
282-
// and landscape for tablets. We have to compensate for this by calculating the default
283-
// orientation, and apply an offset accordingly.
284-
int defaultDeviceOrientation = getDeviceDefaultOrientation();
285-
if (defaultDeviceOrientation == Configuration.ORIENTATION_LANDSCAPE) {
286-
angle += 90;
287-
}
288-
// Determine the orientation
289-
angle = angle % 360;
290-
return new PlatformChannel.DeviceOrientation[] {
291-
PlatformChannel.DeviceOrientation.PORTRAIT_UP,
292-
PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT,
293-
PlatformChannel.DeviceOrientation.PORTRAIT_DOWN,
294-
PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT,
295-
}
296-
[angle / 90];
297-
}
298-
299-
/**
300-
* Gets the default orientation of the device.
153+
* Gets default capture rotation for CameraX {@code UseCase}s.
301154
*
302-
* <p>This method is visible for testing purposes only and should never be used outside this
303-
* class.
155+
* <p>See
156+
* https://developer.android.com/reference/androidx/camera/core/ImageCapture#setTargetRotation(int),
157+
* for instance.
304158
*
305-
* @return The default orientation of the device.
159+
* @return The rotation of the screen from its "natural" orientation; one of {@code
160+
* Surface.ROTATION_0}, {@code Surface.ROTATION_90}, {@code Surface.ROTATION_180}, {@code
161+
* Surface.ROTATION_270}
306162
*/
307-
@VisibleForTesting
308-
int getDeviceDefaultOrientation() {
309-
Configuration config = activity.getResources().getConfiguration();
310-
int rotation = getDisplay().getRotation();
311-
if (((rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180)
312-
&& config.orientation == Configuration.ORIENTATION_LANDSCAPE)
313-
|| ((rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270)
314-
&& config.orientation == Configuration.ORIENTATION_PORTRAIT)) {
315-
return Configuration.ORIENTATION_LANDSCAPE;
316-
} else {
317-
return Configuration.ORIENTATION_PORTRAIT;
318-
}
163+
int getDefaultRotation() {
164+
return getDisplay().getRotation();
319165
}
320166

321167
/**
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
package io.flutter.plugins.camerax;
6+
7+
import androidx.annotation.NonNull;
8+
import io.flutter.plugin.common.BinaryMessenger;
9+
import io.flutter.plugins.camerax.GeneratedCameraXLibrary.DeviceOrientationManagerFlutterApi;
10+
11+
public class DeviceOrientationManagerFlutterApiImpl extends DeviceOrientationManagerFlutterApi {
12+
public DeviceOrientationManagerFlutterApiImpl(@NonNull BinaryMessenger binaryMessenger) {
13+
super(binaryMessenger);
14+
}
15+
16+
public void sendDeviceOrientationChangedEvent(
17+
@NonNull String orientation, @NonNull Reply<Void> reply) {
18+
super.onDeviceOrientationChanged(orientation, reply);
19+
}
20+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
package io.flutter.plugins.camerax;
6+
7+
import android.app.Activity;
8+
import androidx.annotation.NonNull;
9+
import androidx.annotation.Nullable;
10+
import androidx.annotation.VisibleForTesting;
11+
import io.flutter.embedding.engine.systemchannels.PlatformChannel.DeviceOrientation;
12+
import io.flutter.plugin.common.BinaryMessenger;
13+
import io.flutter.plugins.camerax.CameraPermissionsManager.PermissionsRegistry;
14+
import io.flutter.plugins.camerax.GeneratedCameraXLibrary.DeviceOrientationManagerHostApi;
15+
16+
public class DeviceOrientationManagerHostApiImpl implements DeviceOrientationManagerHostApi {
17+
private final BinaryMessenger binaryMessenger;
18+
private final InstanceManager instanceManager;
19+
20+
@VisibleForTesting public @NonNull CameraXProxy cameraXProxy = new CameraXProxy();
21+
@VisibleForTesting public @Nullable DeviceOrientationManager deviceOrientationManager;
22+
23+
@VisibleForTesting
24+
public @NonNull DeviceOrientationManagerFlutterApiImpl deviceOrientationManagerFlutterApiImpl;
25+
26+
private Activity activity;
27+
private PermissionsRegistry permissionsRegistry;
28+
29+
public DeviceOrientationManagerHostApiImpl(
30+
@NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) {
31+
this.binaryMessenger = binaryMessenger;
32+
this.instanceManager = instanceManager;
33+
this.deviceOrientationManagerFlutterApiImpl =
34+
new DeviceOrientationManagerFlutterApiImpl(binaryMessenger);
35+
}
36+
37+
public void setActivity(@NonNull Activity activity) {
38+
this.activity = activity;
39+
}
40+
41+
/**
42+
* Starts listening for device orientation changes using an instance of a {@link
43+
* DeviceOrientationManager}.
44+
*
45+
* <p>Whenever a change in device orientation is detected by the {@code DeviceOrientationManager},
46+
* the {@link SystemServicesFlutterApi} will be used to notify the Dart side.
47+
*/
48+
@Override
49+
public void startListeningForDeviceOrientationChange(
50+
@NonNull Boolean isFrontFacing, @NonNull Long sensorOrientation) {
51+
deviceOrientationManager =
52+
cameraXProxy.createDeviceOrientationManager(
53+
activity,
54+
isFrontFacing,
55+
sensorOrientation.intValue(),
56+
(DeviceOrientation newOrientation) -> {
57+
deviceOrientationManagerFlutterApiImpl.sendDeviceOrientationChangedEvent(
58+
serializeDeviceOrientation(newOrientation), reply -> {});
59+
});
60+
deviceOrientationManager.start();
61+
}
62+
63+
/** Serializes {@code DeviceOrientation} into a String that the Dart side is able to recognize. */
64+
String serializeDeviceOrientation(DeviceOrientation orientation) {
65+
return orientation.toString();
66+
}
67+
68+
/**
69+
* Tells the {@code deviceOrientationManager} to stop listening for orientation updates.
70+
*
71+
* <p>Has no effect if the {@code deviceOrientationManager} was never created to listen for device
72+
* orientation updates.
73+
*/
74+
@Override
75+
public void stopListeningForDeviceOrientationChange() {
76+
if (deviceOrientationManager != null) {
77+
deviceOrientationManager.stop();
78+
}
79+
}
80+
81+
/**
82+
* Gets default capture rotation for CameraX {@code UseCase}s.
83+
*
84+
* <p>The default capture rotation for CameraX is the rotation of default {@code Display} at the
85+
* time that a {@code UseCase} is bound, but the default {@code Display} does not change in this
86+
* plugin, so this value is {@code Display}-agnostic.
87+
*
88+
* <p>See
89+
* https://developer.android.com/reference/androidx/camera/core/ImageCapture#setTargetRotation(int)
90+
* for instance for more information on how this default value is used.
91+
*/
92+
@Override
93+
public @NonNull Long getDefaultDisplayRotation() {
94+
int defaultRotation;
95+
try {
96+
defaultRotation = deviceOrientationManager.getDefaultRotation();
97+
} catch (NullPointerException e) {
98+
throw new IllegalStateException(
99+
"startListeningForDeviceOrientationChange must first be called to subscribe to device orientation changes in order to retrieve default rotation.");
100+
}
101+
102+
return Long.valueOf(defaultRotation);
103+
}
104+
}

0 commit comments

Comments
 (0)