diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index 1a2b03d93a6a..8525ad1e21d8 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.7.0 + +* Added support for capture orientation locking on Android and iOS. +* Fixed camera preview not rotating correctly on Android and iOS. +* Fixed camera preview sometimes appearing stretched on Android and iOS. +* Fixed videos & photos saving with the incorrect rotation on iOS. +* BREAKING CHANGE: `CameraValue.aspectRatio` now returns `width / height` rather than `height / width`. + ## 0.6.6 * Adds auto focus support for Android and iOS implementations. @@ -20,7 +28,7 @@ ## 0.6.4+2 -* Set ImageStreamReader listener to null to prevent stale images when streaming images. +* Set ImageStreamReader listener to null to prevent stale images when streaming images. ## 0.6.4+1 diff --git a/packages/camera/camera/android/build.gradle b/packages/camera/camera/android/build.gradle index 0b88fd10fb71..0606738a0a69 100644 --- a/packages/camera/camera/android/build.gradle +++ b/packages/camera/camera/android/build.gradle @@ -27,7 +27,7 @@ project.getTasks().withType(JavaCompile){ apply plugin: 'com.android.library' android { - compileSdkVersion 29 + compileSdkVersion 30 defaultConfig { minSdkVersion 21 diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java index 3fc702a2a879..10d58f5e8792 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java @@ -4,7 +4,6 @@ package io.flutter.plugins.camera; -import static android.view.OrientationEventListener.ORIENTATION_UNKNOWN; import static io.flutter.plugins.camera.CameraUtils.computeBestPreviewSize; import android.annotation.SuppressLint; @@ -41,10 +40,10 @@ import android.util.Range; import android.util.Rational; import android.util.Size; -import android.view.OrientationEventListener; import android.view.Surface; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugin.common.EventChannel; import io.flutter.plugin.common.MethodChannel.Result; import io.flutter.plugins.camera.PictureCaptureRequest.State; @@ -76,7 +75,7 @@ public class Camera { private final SurfaceTextureEntry flutterTexture; private final CameraManager cameraManager; - private final OrientationEventListener orientationEventListener; + private final DeviceOrientationManager deviceOrientationListener; private final boolean isFrontFacing; private final int sensorOrientation; private final String cameraName; @@ -97,7 +96,6 @@ public class Camera { private MediaRecorder mediaRecorder; private boolean recordingVideo; private File videoRecordingFile; - private int currentOrientation = ORIENTATION_UNKNOWN; private FlashMode flashMode; private ExposureMode exposureMode; private FocusMode focusMode; @@ -106,6 +104,7 @@ public class Camera { private int exposureOffset; private boolean useAutoFocus = true; private Range fpsRange; + private PlatformChannel.DeviceOrientation lockedCaptureOrientation; private static final HashMap supportedImageFormats; // Current supported outputs @@ -136,18 +135,6 @@ public Camera( this.exposureMode = ExposureMode.auto; this.focusMode = FocusMode.auto; this.exposureOffset = 0; - orientationEventListener = - new OrientationEventListener(activity.getApplicationContext()) { - @Override - public void onOrientationChanged(int i) { - if (i == ORIENTATION_UNKNOWN) { - return; - } - // Convert the raw deg angle to the nearest multiple of 90. - currentOrientation = (int) Math.round(i / 90.0) * 90; - } - }; - orientationEventListener.enable(); cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraName); initFps(cameraCharacteristics); @@ -164,6 +151,10 @@ public void onOrientationChanged(int i) { new CameraZoom( cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE), cameraCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM)); + + deviceOrientationListener = + new DeviceOrientationManager(activity, dartMessenger, isFrontFacing, sensorOrientation); + deviceOrientationListener.start(); } private void initFps(CameraCharacteristics cameraCharacteristics) { @@ -195,7 +186,10 @@ private void prepareMediaRecorder(String outputFilePath) throws IOException { mediaRecorder = new MediaRecorderBuilder(recordingProfile, outputFilePath) .setEnableAudio(enableAudio) - .setMediaOrientation(getMediaOrientation()) + .setMediaOrientation( + lockedCaptureOrientation == null + ? deviceOrientationListener.getMediaOrientation() + : deviceOrientationListener.getMediaOrientation(lockedCaptureOrientation)) .build(); } @@ -545,7 +539,11 @@ private void runPictureCapture() { final CaptureRequest.Builder captureBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); captureBuilder.addTarget(pictureImageReader.getSurface()); - captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getMediaOrientation()); + captureBuilder.set( + CaptureRequest.JPEG_ORIENTATION, + lockedCaptureOrientation == null + ? deviceOrientationListener.getMediaOrientation() + : deviceOrientationListener.getMediaOrientation(lockedCaptureOrientation)); switch (flashMode) { case off: captureBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); @@ -968,6 +966,14 @@ public void setZoomLevel(@NonNull final Result result, float zoom) throws Camera result.success(null); } + public void lockCaptureOrientation(PlatformChannel.DeviceOrientation orientation) { + this.lockedCaptureOrientation = orientation; + } + + public void unlockCaptureOrientation() { + this.lockedCaptureOrientation = null; + } + private void updateFpsRange() { if (fpsRange == null) { return; @@ -1160,14 +1166,6 @@ public void close() { public void dispose() { close(); flutterTexture.release(); - orientationEventListener.disable(); - } - - private int getMediaOrientation() { - final int sensorOrientationOffset = - (currentOrientation == ORIENTATION_UNKNOWN) - ? 0 - : (isFrontFacing) ? -currentOrientation : currentOrientation; - return (sensorOrientationOffset + sensorOrientation + 360) % 360; + deviceOrientationListener.stop(); } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java index 6f04dc80e102..03993a3b51f9 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java @@ -14,6 +14,7 @@ import android.hardware.camera2.params.StreamConfigurationMap; import android.media.CamcorderProfile; import android.util.Size; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugins.camera.types.ResolutionPreset; import java.util.ArrayList; import java.util.Arrays; @@ -28,6 +29,59 @@ public final class CameraUtils { private CameraUtils() {} + static PlatformChannel.DeviceOrientation getDeviceOrientationFromDegrees(int degrees) { + // Round to the nearest 90 degrees. + degrees = (int) (Math.round(degrees / 90.0) * 90) % 360; + // Determine the corresponding device orientation. + switch (degrees) { + case 90: + return PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT; + case 180: + return PlatformChannel.DeviceOrientation.PORTRAIT_DOWN; + case 270: + return PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT; + case 0: + default: + return PlatformChannel.DeviceOrientation.PORTRAIT_UP; + } + } + + static String serializeDeviceOrientation(PlatformChannel.DeviceOrientation orientation) { + if (orientation == null) + throw new UnsupportedOperationException("Could not serialize null device orientation."); + switch (orientation) { + case PORTRAIT_UP: + return "portraitUp"; + case PORTRAIT_DOWN: + return "portraitDown"; + case LANDSCAPE_LEFT: + return "landscapeLeft"; + case LANDSCAPE_RIGHT: + return "landscapeRight"; + default: + throw new UnsupportedOperationException( + "Could not serialize device orientation: " + orientation.toString()); + } + } + + static PlatformChannel.DeviceOrientation deserializeDeviceOrientation(String orientation) { + if (orientation == null) + throw new UnsupportedOperationException("Could not deserialize null device orientation."); + switch (orientation) { + case "portraitUp": + return PlatformChannel.DeviceOrientation.PORTRAIT_UP; + case "portraitDown": + return PlatformChannel.DeviceOrientation.PORTRAIT_DOWN; + case "landscapeLeft": + return PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT; + case "landscapeRight": + return PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT; + default: + throw new UnsupportedOperationException( + "Could not deserialize device orientation: " + orientation); + } + } + static Size computeBestPreviewSize(String cameraName, ResolutionPreset preset) { if (preset.ordinal() > ResolutionPreset.high.ordinal()) { preset = ResolutionPreset.high; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java index ec68ac0acda3..5681f723ed80 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java @@ -6,6 +6,7 @@ import android.text.TextUtils; import androidx.annotation.Nullable; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugins.camera.types.ExposureMode; @@ -14,16 +15,44 @@ import java.util.Map; class DartMessenger { - @Nullable private MethodChannel channel; + @Nullable private MethodChannel cameraChannel; + @Nullable private MethodChannel deviceChannel; - enum EventType { - ERROR, - CAMERA_CLOSING, - INITIALIZED, + enum DeviceEventType { + ORIENTATION_CHANGED("orientation_changed"); + private final String method; + + DeviceEventType(String method) { + this.method = method; + } + } + + enum CameraEventType { + ERROR("error"), + CLOSING("camera_closing"), + INITIALIZED("initialized"); + + private final String method; + + CameraEventType(String method) { + this.method = method; + } } DartMessenger(BinaryMessenger messenger, long cameraId) { - channel = new MethodChannel(messenger, "flutter.io/cameraPlugin/camera" + cameraId); + cameraChannel = new MethodChannel(messenger, "flutter.io/cameraPlugin/camera" + cameraId); + deviceChannel = new MethodChannel(messenger, "flutter.io/cameraPlugin/device"); + } + + void sendDeviceOrientationChangeEvent(PlatformChannel.DeviceOrientation orientation) { + assert (orientation != null); + this.send( + DeviceEventType.ORIENTATION_CHANGED, + new HashMap() { + { + put("orientation", CameraUtils.serializeDeviceOrientation(orientation)); + } + }); } void sendCameraInitializedEvent( @@ -40,7 +69,7 @@ void sendCameraInitializedEvent( assert (exposurePointSupported != null); assert (focusPointSupported != null); this.send( - EventType.INITIALIZED, + CameraEventType.INITIALIZED, new HashMap() { { put("previewWidth", previewWidth.doubleValue()); @@ -54,12 +83,12 @@ void sendCameraInitializedEvent( } void sendCameraClosingEvent() { - send(EventType.CAMERA_CLOSING); + send(CameraEventType.CLOSING); } void sendCameraErrorEvent(@Nullable String description) { this.send( - EventType.ERROR, + CameraEventType.ERROR, new HashMap() { { if (!TextUtils.isEmpty(description)) put("description", description); @@ -67,14 +96,25 @@ void sendCameraErrorEvent(@Nullable String description) { }); } - void send(EventType eventType) { + void send(CameraEventType eventType) { + send(eventType, new HashMap<>()); + } + + void send(CameraEventType eventType, Map args) { + if (cameraChannel == null) { + return; + } + cameraChannel.invokeMethod(eventType.method, args); + } + + void send(DeviceEventType eventType) { send(eventType, new HashMap<>()); } - void send(EventType eventType, Map args) { - if (channel == null) { + void send(DeviceEventType eventType, Map args) { + if (deviceChannel == null) { return; } - channel.invokeMethod(eventType.toString().toLowerCase(), args); + deviceChannel.invokeMethod(eventType.method, args); } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java new file mode 100644 index 000000000000..d39a8da55cc8 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java @@ -0,0 +1,201 @@ +// Copyright 2019 The Chromium 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.camera; + +import android.app.Activity; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.Configuration; +import android.hardware.SensorManager; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.provider.Settings; +import android.view.Display; +import android.view.OrientationEventListener; +import android.view.Surface; +import android.view.WindowManager; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; + +class DeviceOrientationManager { + + private static final IntentFilter orientationIntentFilter = + new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED); + + private final Activity activity; + private final DartMessenger messenger; + private final boolean isFrontFacing; + private final int sensorOrientation; + private PlatformChannel.DeviceOrientation lastOrientation; + private OrientationEventListener orientationEventListener; + private BroadcastReceiver broadcastReceiver; + + public DeviceOrientationManager( + Activity activity, DartMessenger messenger, boolean isFrontFacing, int sensorOrientation) { + this.activity = activity; + this.messenger = messenger; + this.isFrontFacing = isFrontFacing; + this.sensorOrientation = sensorOrientation; + } + + public void start() { + startSensorListener(); + startUIListener(); + } + + public void stop() { + stopSensorListener(); + stopUIListener(); + } + + public int getMediaOrientation() { + return this.getMediaOrientation(this.lastOrientation); + } + + public int getMediaOrientation(PlatformChannel.DeviceOrientation orientation) { + int angle = 0; + switch (orientation) { + case PORTRAIT_UP: + angle = 0; + break; + case PORTRAIT_DOWN: + angle = 180; + break; + case LANDSCAPE_LEFT: + angle = 90; + break; + case LANDSCAPE_RIGHT: + angle = 270; + break; + } + if (isFrontFacing) angle *= -1; + return (angle + sensorOrientation + 360) % 360; + } + + private void startSensorListener() { + if (orientationEventListener != null) return; + orientationEventListener = + new OrientationEventListener(activity, SensorManager.SENSOR_DELAY_NORMAL) { + @Override + public void onOrientationChanged(int angle) { + if (!isSystemAutoRotationLocked()) { + PlatformChannel.DeviceOrientation newOrientation = calculateSensorOrientation(angle); + if (!newOrientation.equals(lastOrientation)) { + lastOrientation = newOrientation; + messenger.sendDeviceOrientationChangeEvent(newOrientation); + } + } + } + }; + if (orientationEventListener.canDetectOrientation()) { + orientationEventListener.enable(); + } + } + + private void startUIListener() { + if (broadcastReceiver != null) return; + broadcastReceiver = + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (isSystemAutoRotationLocked()) { + PlatformChannel.DeviceOrientation orientation = getUIOrientation(); + if (!orientation.equals(lastOrientation)) { + lastOrientation = orientation; + messenger.sendDeviceOrientationChangeEvent(orientation); + } + } + } + }; + activity.registerReceiver(broadcastReceiver, orientationIntentFilter); + broadcastReceiver.onReceive(activity, null); + } + + private void stopSensorListener() { + if (orientationEventListener == null) return; + orientationEventListener.disable(); + orientationEventListener = null; + } + + private void stopUIListener() { + if (broadcastReceiver == null) return; + activity.unregisterReceiver(broadcastReceiver); + broadcastReceiver = null; + } + + private boolean isSystemAutoRotationLocked() { + return android.provider.Settings.System.getInt( + activity.getContentResolver(), Settings.System.ACCELEROMETER_ROTATION, 0) + != 1; + } + + private PlatformChannel.DeviceOrientation getUIOrientation() { + final int rotation = getDisplay().getRotation(); + final int orientation = activity.getResources().getConfiguration().orientation; + + switch (orientation) { + case Configuration.ORIENTATION_PORTRAIT: + if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_90) { + return PlatformChannel.DeviceOrientation.PORTRAIT_UP; + } else { + return PlatformChannel.DeviceOrientation.PORTRAIT_DOWN; + } + case Configuration.ORIENTATION_LANDSCAPE: + if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_90) { + return PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT; + } else { + return PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT; + } + default: + return PlatformChannel.DeviceOrientation.PORTRAIT_UP; + } + } + + private PlatformChannel.DeviceOrientation calculateSensorOrientation(int angle) { + final int tolerance = 45; + angle += tolerance; + + // Orientation is 0 in the default orientation mode. This is portait-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]; + } + + private 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; + } + } + + @SuppressWarnings("deprecation") + private Display getDisplay() { + if (VERSION.SDK_INT >= VERSION_CODES.R) { + return activity.getDisplay(); + } else { + return ((WindowManager) activity.getSystemService(Context.WINDOW_SERVICE)) + .getDefaultDisplay(); + } + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java index 36048dbb5176..aa7483f55679 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java @@ -8,6 +8,7 @@ import android.hardware.camera2.CameraAccessException; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.EventChannel; import io.flutter.plugin.common.MethodCall; @@ -255,7 +256,7 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) case "stopImageStream": { try { - camera.stopImageStream(); + camera.startPreview(); result.success(null); } catch (Exception e) { handleException(e, result); @@ -305,6 +306,29 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) } break; } + case "lockCaptureOrientation": + { + PlatformChannel.DeviceOrientation orientation = + CameraUtils.deserializeDeviceOrientation(call.argument("orientation")); + + try { + camera.lockCaptureOrientation(orientation); + result.success(null); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "unlockCaptureOrientation": + { + try { + camera.unlockCaptureOrientation(); + result.success(null); + } catch (Exception e) { + handleException(e, result); + } + break; + } case "dispose": { if (camera != null) { diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java new file mode 100644 index 000000000000..8026b6349aa1 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java @@ -0,0 +1,102 @@ +// Copyright 2019 The Chromium 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.camera; + +import static org.junit.Assert.assertEquals; + +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import org.junit.Test; + +public class CameraUtilsTest { + + @Test + public void serializeDeviceOrientation_serializes_correctly() { + assertEquals( + "portraitUp", + CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.PORTRAIT_UP)); + assertEquals( + "portraitDown", + CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.PORTRAIT_DOWN)); + assertEquals( + "landscapeLeft", + CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT)); + assertEquals( + "landscapeRight", + CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT)); + } + + @Test(expected = UnsupportedOperationException.class) + public void serializeDeviceOrientation_throws_for_null() { + CameraUtils.serializeDeviceOrientation(null); + } + + @Test + public void deserializeDeviceOrientation_deserializes_correctly() { + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_UP, + CameraUtils.deserializeDeviceOrientation("portraitUp")); + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, + CameraUtils.deserializeDeviceOrientation("portraitDown")); + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, + CameraUtils.deserializeDeviceOrientation("landscapeLeft")); + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, + CameraUtils.deserializeDeviceOrientation("landscapeRight")); + } + + @Test(expected = UnsupportedOperationException.class) + public void deserializeDeviceOrientation_throws_for_null() { + CameraUtils.deserializeDeviceOrientation(null); + } + + @Test + public void getDeviceOrientationFromDegrees_converts_correctly() { + // Portrait UP + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_UP, + CameraUtils.getDeviceOrientationFromDegrees(0)); + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_UP, + CameraUtils.getDeviceOrientationFromDegrees(315)); + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_UP, + CameraUtils.getDeviceOrientationFromDegrees(44)); + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_UP, + CameraUtils.getDeviceOrientationFromDegrees(-45)); + // Portrait DOWN + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, + CameraUtils.getDeviceOrientationFromDegrees(180)); + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, + CameraUtils.getDeviceOrientationFromDegrees(135)); + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, + CameraUtils.getDeviceOrientationFromDegrees(224)); + // Landscape LEFT + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, + CameraUtils.getDeviceOrientationFromDegrees(90)); + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, + CameraUtils.getDeviceOrientationFromDegrees(45)); + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, + CameraUtils.getDeviceOrientationFromDegrees(134)); + // Landscape RIGHT + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, + CameraUtils.getDeviceOrientationFromDegrees(270)); + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, + CameraUtils.getDeviceOrientationFromDegrees(225)); + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, + CameraUtils.getDeviceOrientationFromDegrees(314)); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java index 64425b7b8283..e835b08f441a 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java @@ -8,6 +8,7 @@ import static org.junit.Assert.assertEquals; import androidx.annotation.NonNull; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.StandardMethodCodec; @@ -89,6 +90,17 @@ public void sendCameraClosingEvent() { assertNull(call.argument("description")); } + @Test + public void sendDeviceOrientationChangedEvent() { + dartMessenger.sendDeviceOrientationChangeEvent(PlatformChannel.DeviceOrientation.PORTRAIT_UP); + + List sentMessages = fakeBinaryMessenger.getMessages(); + assertEquals(1, sentMessages.size()); + MethodCall call = decodeSentMessage(sentMessages.get(0)); + assertEquals("orientation_changed", call.method); + assertEquals(call.argument("orientation"), "portraitUp"); + } + private MethodCall decodeSentMessage(ByteBuffer sentMessage) { sentMessage.position(0); diff --git a/packages/camera/camera/example/android/app/build.gradle b/packages/camera/camera/example/android/app/build.gradle index 7d0e281b74e8..c5eeb246fe30 100644 --- a/packages/camera/camera/example/android/app/build.gradle +++ b/packages/camera/camera/example/android/app/build.gradle @@ -25,7 +25,7 @@ apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 29 + compileSdkVersion 30 lintOptions { disable 'InvalidPackage' diff --git a/packages/camera/camera/example/ios/Runner/Info.plist b/packages/camera/camera/example/ios/Runner/Info.plist index f389a129e028..ff2e341a1803 100644 --- a/packages/camera/camera/example/ios/Runner/Info.plist +++ b/packages/camera/camera/example/ios/Runner/Info.plist @@ -39,6 +39,7 @@ UISupportedInterfaceOrientations UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight diff --git a/packages/camera/camera/example/lib/main.dart b/packages/camera/camera/example/lib/main.dart index 681a45172816..490cae6676d3 100644 --- a/packages/camera/camera/example/lib/main.dart +++ b/packages/camera/camera/example/lib/main.dart @@ -171,18 +171,18 @@ class _CameraExampleHomeState extends State ), ); } else { - return AspectRatio( - aspectRatio: controller.value.aspectRatio, - child: Listener( - onPointerDown: (_) => _pointers++, - onPointerUp: (_) => _pointers--, + return Listener( + onPointerDown: (_) => _pointers++, + onPointerUp: (_) => _pointers--, + child: CameraPreview( + controller, child: LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return GestureDetector( + behavior: HitTestBehavior.opaque, onScaleStart: _handleScaleStart, onScaleUpdate: _handleScaleUpdate, onTapDown: (details) => onViewFinderTap(details, constraints), - child: CameraPreview(controller), ); }), ), @@ -269,6 +269,15 @@ class _CameraExampleHomeState extends State color: Colors.blue, onPressed: controller != null ? onAudioModeButtonPressed : null, ), + IconButton( + icon: Icon(controller?.value?.isCaptureOrientationLocked ?? false + ? Icons.screen_lock_rotation + : Icons.screen_rotation), + color: Colors.blue, + onPressed: controller != null + ? onCaptureOrientationLockButtonPressed + : null, + ), ], ), _flashModeControlRowWidget(), @@ -634,6 +643,19 @@ class _CameraExampleHomeState extends State } } + void onCaptureOrientationLockButtonPressed() async { + if (controller != null) { + if (controller.value.isCaptureOrientationLocked) { + await controller.unlockCaptureOrientation(); + showInSnackBar('Capture orientation unlocked'); + } else { + await controller.lockCaptureOrientation(); + showInSnackBar( + 'Capture orientation locked to ${controller.value.lockedCaptureOrientation.toString().split('.').last}'); + } + } + } + void onSetFlashModeButtonPressed(FlashMode mode) { setFlashMode(mode).then((_) { if (mounted) setState(() {}); diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index 298b906ace7b..40d93fde7af6 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -18,8 +18,6 @@ @interface FLTSavePhotoDelegate : NSObject @property(readonly, nonatomic) NSString *path; @property(readonly, nonatomic) FlutterResult result; -@property(readonly, nonatomic) CMMotionManager *motionManager; -@property(readonly, nonatomic) AVCaptureDevicePosition cameraPosition; @end @interface FLTImageStreamHandler : NSObject @@ -45,15 +43,10 @@ @implementation FLTSavePhotoDelegate { FLTSavePhotoDelegate *selfReference; } -- initWithPath:(NSString *)path - result:(FlutterResult)result - motionManager:(CMMotionManager *)motionManager - cameraPosition:(AVCaptureDevicePosition)cameraPosition { +- initWithPath:(NSString *)path result:(FlutterResult)result { self = [super init]; NSAssert(self, @"super init cannot be nil"); _path = path; - _motionManager = motionManager; - _cameraPosition = cameraPosition; selfReference = self; _result = result; return self; @@ -70,15 +63,14 @@ - (void)captureOutput:(AVCapturePhotoOutput *)output _result(getFlutterError(error)); return; } + NSData *data = [AVCapturePhotoOutput JPEGPhotoDataRepresentationForJPEGSampleBuffer:photoSampleBuffer previewPhotoSampleBuffer:previewPhotoSampleBuffer]; - UIImage *image = [UIImage imageWithCGImage:[UIImage imageWithData:data].CGImage - scale:1.0 - orientation:[self getImageRotation]]; // TODO(sigurdm): Consider writing file asynchronously. - bool success = [UIImageJPEGRepresentation(image, 1.0) writeToFile:_path atomically:YES]; + bool success = [data writeToFile:_path atomically:YES]; + if (!success) { _result([FlutterError errorWithCode:@"IOError" message:@"Unable to write file" details:nil]); return; @@ -104,34 +96,6 @@ - (void)captureOutput:(AVCapturePhotoOutput *)output } _result(_path); } - -- (UIImageOrientation)getImageRotation { - float const threshold = 45.0; - BOOL (^isNearValue)(float value1, float value2) = ^BOOL(float value1, float value2) { - return fabsf(value1 - value2) < threshold; - }; - BOOL (^isNearValueABS)(float value1, float value2) = ^BOOL(float value1, float value2) { - return isNearValue(fabsf(value1), fabsf(value2)); - }; - float yxAtan = (atan2(_motionManager.accelerometerData.acceleration.y, - _motionManager.accelerometerData.acceleration.x)) * - 180 / M_PI; - if (isNearValue(-90.0, yxAtan)) { - return UIImageOrientationRight; - } else if (isNearValueABS(180.0, yxAtan)) { - return _cameraPosition == AVCaptureDevicePositionBack ? UIImageOrientationUp - : UIImageOrientationDown; - } else if (isNearValueABS(0.0, yxAtan)) { - return _cameraPosition == AVCaptureDevicePositionBack ? UIImageOrientationDown /*rotate 180* */ - : UIImageOrientationUp /*do not rotate*/; - } else if (isNearValue(90.0, yxAtan)) { - return UIImageOrientationLeft; - } - // If none of the above, then the device is likely facing straight down or straight up -- just - // pick something arbitrary - // TODO: Maybe use the UIInterfaceOrientation if in these scenarios - return UIImageOrientationUp; -} @end // Mirrors FlashMode in flash_mode.dart @@ -226,6 +190,42 @@ static ExposureMode getExposureModeForString(NSString *mode) { } } +static UIDeviceOrientation getUIDeviceOrientationForString(NSString *orientation) { + if ([orientation isEqualToString:@"portraitDown"]) { + return UIDeviceOrientationPortraitUpsideDown; + } else if ([orientation isEqualToString:@"landscapeLeft"]) { + return UIDeviceOrientationLandscapeRight; + } else if ([orientation isEqualToString:@"landscapeRight"]) { + return UIDeviceOrientationLandscapeLeft; + } else if ([orientation isEqualToString:@"portraitUp"]) { + return UIDeviceOrientationPortrait; + } else { + NSError *error = [NSError + errorWithDomain:NSCocoaErrorDomain + code:NSURLErrorUnknown + userInfo:@{ + NSLocalizedDescriptionKey : + [NSString stringWithFormat:@"Unknown device orientation %@", orientation] + }]; + @throw error; + } +} + +static NSString *getStringForUIDeviceOrientation(UIDeviceOrientation orientation) { + switch (orientation) { + case UIDeviceOrientationPortraitUpsideDown: + return @"portraitDown"; + case UIDeviceOrientationLandscapeRight: + return @"landscapeLeft"; + case UIDeviceOrientationLandscapeLeft: + return @"landscapeRight"; + case UIDeviceOrientationPortrait: + default: + return @"portraitUp"; + break; + }; +} + // Mirrors FocusMode in camera.dart typedef enum { FocusModeAuto, @@ -334,6 +334,7 @@ @interface FLTCam : NSObject *registry; @property(readonly, nonatomic) NSObject *messenger; @property(readonly, nonatomic) FLTCam *camera; +@property(readonly, nonatomic) FlutterMethodChannel *deviceEventMethodChannel; @end @implementation CameraPlugin { @@ -1161,9 +1236,36 @@ - (instancetype)initWithRegistry:(NSObject *)registry NSAssert(self, @"super init cannot be nil"); _registry = registry; _messenger = messenger; + [self initDeviceEventMethodChannel]; + [self startOrientationListener]; return self; } +- (void)initDeviceEventMethodChannel { + _deviceEventMethodChannel = + [FlutterMethodChannel methodChannelWithName:@"flutter.io/cameraPlugin/device" + binaryMessenger:_messenger]; +} + +- (void)startOrientationListener { + [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(orientationChanged:) + name:UIDeviceOrientationDidChangeNotification + object:[UIDevice currentDevice]]; +} + +- (void)orientationChanged:(NSNotification *)note { + UIDevice *device = note.object; + [self sendDeviceOrientation:device.orientation]; +} + +- (void)sendDeviceOrientation:(UIDeviceOrientation)orientation { + [_deviceEventMethodChannel + invokeMethod:@"orientation_changed" + arguments:@{@"orientation" : getStringForUIDeviceOrientation(orientation)}]; +} + - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { if (_dispatchQueue == nil) { _dispatchQueue = dispatch_queue_create("io.flutter.camera.dispatchqueue", NULL); @@ -1265,6 +1367,7 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FlutterResult)re @([_camera.captureDevice isExposurePointOfInterestSupported]), @"focusPointSupported" : @([_camera.captureDevice isFocusPointOfInterestSupported]), }]; + [self sendDeviceOrientation:[UIDevice currentDevice].orientation]; [_camera start]; result(nil); } else if ([@"takePicture" isEqualToString:call.method]) { @@ -1318,6 +1421,10 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FlutterResult)re } else if ([@"setExposureOffset" isEqualToString:call.method]) { [_camera setExposureOffsetWithResult:result offset:((NSNumber *)call.arguments[@"offset"]).doubleValue]; + } else if ([@"lockCaptureOrientation" isEqualToString:call.method]) { + [_camera lockCaptureOrientationWithResult:result orientation:call.arguments[@"orientation"]]; + } else if ([@"unlockCaptureOrientation" isEqualToString:call.method]) { + [_camera unlockCaptureOrientationWithResult:result]; } else if ([@"setFocusMode" isEqualToString:call.method]) { [_camera setFocusModeWithResult:result mode:call.arguments[@"mode"]]; } else if ([@"setFocusPoint" isEqualToString:call.method]) { diff --git a/packages/camera/camera/lib/src/camera_controller.dart b/packages/camera/camera/lib/src/camera_controller.dart index ae79cc4ad367..807bec367256 100644 --- a/packages/camera/camera/lib/src/camera_controller.dart +++ b/packages/camera/camera/lib/src/camera_controller.dart @@ -11,6 +11,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:pedantic/pedantic.dart'; +import 'package:quiver/core.dart'; final MethodChannel _channel = const MethodChannel('plugins.flutter.io/camera'); @@ -43,6 +44,9 @@ class CameraValue { this.focusMode, this.exposurePointSupported, this.focusPointSupported, + this.deviceOrientation, + this.lockedCaptureOrientation, + this.recordingOrientation, }) : _isRecordingPaused = isRecordingPaused; /// Creates a new camera controller state for an uninitialized controller. @@ -56,6 +60,7 @@ class CameraValue { flashMode: FlashMode.auto, exposurePointSupported: false, focusPointSupported: false, + deviceOrientation: DeviceOrientation.portraitUp, ); /// True after [CameraController.initialize] has completed successfully. @@ -86,10 +91,10 @@ class CameraValue { /// Is `null` until [isInitialized] is `true`. final Size previewSize; - /// Convenience getter for `previewSize.height / previewSize.width`. + /// Convenience getter for `previewSize.width / previewSize.height`. /// /// Can only be called when [initialize] is done. - double get aspectRatio => previewSize.height / previewSize.width; + double get aspectRatio => previewSize.width / previewSize.height; /// Whether the controller is in an error state. /// @@ -111,6 +116,18 @@ class CameraValue { /// Whether setting the focus point is supported. final bool focusPointSupported; + /// The current device orientation. + final DeviceOrientation deviceOrientation; + + /// The currently locked capture orientation. + final DeviceOrientation lockedCaptureOrientation; + + /// Whether the capture orientation is currently locked. + bool get isCaptureOrientationLocked => lockedCaptureOrientation != null; + + /// The orientation of the currently running video recording. + final DeviceOrientation recordingOrientation; + /// Creates a modified copy of the object. /// /// Explicitly specified fields get the specified value, all other fields get @@ -128,6 +145,9 @@ class CameraValue { FocusMode focusMode, bool exposurePointSupported, bool focusPointSupported, + DeviceOrientation deviceOrientation, + Optional lockedCaptureOrientation, + Optional recordingOrientation, }) { return CameraValue( isInitialized: isInitialized ?? this.isInitialized, @@ -143,6 +163,13 @@ class CameraValue { exposurePointSupported: exposurePointSupported ?? this.exposurePointSupported, focusPointSupported: focusPointSupported ?? this.focusPointSupported, + deviceOrientation: deviceOrientation ?? this.deviceOrientation, + lockedCaptureOrientation: lockedCaptureOrientation == null + ? this.lockedCaptureOrientation + : lockedCaptureOrientation.orNull, + recordingOrientation: recordingOrientation == null + ? this.recordingOrientation + : recordingOrientation.orNull, ); } @@ -158,7 +185,10 @@ class CameraValue { 'exposureMode: $exposureMode, ' 'focusMode: $focusMode, ' 'exposurePointSupported: $exposurePointSupported, ' - 'focusPointSupported: $focusPointSupported)'; + 'focusPointSupported: $focusPointSupported, ' + 'deviceOrientation: $deviceOrientation, ' + 'lockedCaptureOrientation: $lockedCaptureOrientation, ' + 'recordingOrientation: $recordingOrientation)'; } } @@ -201,6 +231,7 @@ class CameraController extends ValueNotifier { bool _isDisposed = false; StreamSubscription _imageStreamSubscription; FutureOr _initCalled; + StreamSubscription _deviceOrientationSubscription; /// Checks whether [CameraController.dispose] has completed successfully. /// @@ -225,6 +256,13 @@ class CameraController extends ValueNotifier { try { Completer _initializeCompleter = Completer(); + _deviceOrientationSubscription = + CameraPlatform.instance.onDeviceOrientationChanged().listen((event) { + value = value.copyWith( + deviceOrientation: event.orientation, + ); + }); + _cameraId = await CameraPlatform.instance.createCamera( description, resolutionPreset, @@ -431,7 +469,11 @@ class CameraController extends ValueNotifier { try { await CameraPlatform.instance.startVideoRecording(_cameraId); - value = value.copyWith(isRecordingVideo: true, isRecordingPaused: false); + value = value.copyWith( + isRecordingVideo: true, + isRecordingPaused: false, + recordingOrientation: Optional.fromNullable( + value.lockedCaptureOrientation ?? value.deviceOrientation)); } on PlatformException catch (e) { throw CameraException(e.code, e.message); } @@ -455,7 +497,10 @@ class CameraController extends ValueNotifier { } try { XFile file = await CameraPlatform.instance.stopVideoRecording(_cameraId); - value = value.copyWith(isRecordingVideo: false); + value = value.copyWith( + isRecordingVideo: false, + recordingOrientation: Optional.absent(), + ); return file; } on PlatformException catch (e) { throw CameraException(e.code, e.message); @@ -718,6 +763,21 @@ class CameraController extends ValueNotifier { } } + /// Locks the capture orientation. + /// + /// If [orientation] is omitted, the current device orientation is used. + Future lockCaptureOrientation([DeviceOrientation orientation]) async { + try { + await CameraPlatform.instance.lockCaptureOrientation( + _cameraId, orientation ?? value.deviceOrientation); + value = value.copyWith( + lockedCaptureOrientation: + Optional.fromNullable(orientation ?? value.deviceOrientation)); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + /// Sets the focus mode for taking pictures. Future setFocusMode(FocusMode mode) async { try { @@ -728,6 +788,16 @@ class CameraController extends ValueNotifier { } } + /// Unlocks the capture orientation. + Future unlockCaptureOrientation() async { + try { + await CameraPlatform.instance.unlockCaptureOrientation(_cameraId); + value = value.copyWith(lockedCaptureOrientation: Optional.absent()); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + /// Sets the focus point for automatically determining the focus value. Future setFocusPoint(Offset point) async { if (point != null && @@ -756,6 +826,7 @@ class CameraController extends ValueNotifier { if (_isDisposed) { return; } + unawaited(_deviceOrientationSubscription?.cancel()); _isDisposed = true; super.dispose(); if (_initCalled != null) { diff --git a/packages/camera/camera/lib/src/camera_preview.dart b/packages/camera/camera/lib/src/camera_preview.dart index bf7862eb9151..05e969004233 100644 --- a/packages/camera/camera/lib/src/camera_preview.dart +++ b/packages/camera/camera/lib/src/camera_preview.dart @@ -4,20 +4,63 @@ import 'package:camera/camera.dart'; import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; /// A widget showing a live camera preview. class CameraPreview extends StatelessWidget { /// Creates a preview widget for the given camera controller. - const CameraPreview(this.controller); + const CameraPreview(this.controller, {this.child}); /// The controller for the camera that the preview is shown for. final CameraController controller; + /// A widget to overlay on top of the camera preview + final Widget child; + @override Widget build(BuildContext context) { return controller.value.isInitialized - ? CameraPlatform.instance.buildPreview(controller.cameraId) + ? AspectRatio( + aspectRatio: _isLandscape() + ? controller.value.aspectRatio + : (1 / controller.value.aspectRatio), + child: Stack( + fit: StackFit.expand, + children: [ + RotatedBox( + quarterTurns: _getQuarterTurns(), + child: + CameraPlatform.instance.buildPreview(controller.cameraId), + ), + child ?? Container(), + ], + ), + ) : Container(); } + + DeviceOrientation _getApplicableOrientation() { + return controller.value.isRecordingVideo + ? controller.value.recordingOrientation + : (controller.value.lockedCaptureOrientation ?? + controller.value.deviceOrientation); + } + + bool _isLandscape() { + return [DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight] + .contains(_getApplicableOrientation()); + } + + int _getQuarterTurns() { + int platformOffset = defaultTargetPlatform == TargetPlatform.iOS ? 1 : 0; + Map turns = { + DeviceOrientation.portraitUp: 0, + DeviceOrientation.landscapeLeft: 1, + DeviceOrientation.portraitDown: 2, + DeviceOrientation.landscapeRight: 3, + }; + return turns[_getApplicableOrientation()] + platformOffset; + } } diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 0b21497b5462..b0ebb9c16361 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -2,7 +2,7 @@ name: camera description: A Flutter plugin for getting information about and controlling the camera on Android and iOS. Supports previewing the camera feed, capturing images, capturing video, and streaming image buffers to dart. -version: 0.6.6 +version: 0.7.0 homepage: https://github.com/flutter/plugins/tree/master/packages/camera/camera dependencies: @@ -10,6 +10,7 @@ dependencies: sdk: flutter camera_platform_interface: ^1.5.0 pedantic: ^1.8.0 + quiver: ^2.1.5 dev_dependencies: path_provider: ^0.5.0 diff --git a/packages/camera/camera/test/camera_test.dart b/packages/camera/camera/test/camera_test.dart index 1cea609d1741..2f53691217cb 100644 --- a/packages/camera/camera/test/camera_test.dart +++ b/packages/camera/camera/test/camera_test.dart @@ -38,6 +38,9 @@ get mockOnCameraInitializedEvent => CameraInitializedEvent( true, ); +get mockOnDeviceOrientationChangedEvent => + DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); + get mockOnCameraClosingEvent => null; get mockOnCameraErrorEvent => CameraErrorEvent(13, 'closing'); @@ -1021,6 +1024,106 @@ void main() { .setExposureOffset(cameraController.cameraId, -0.4)) .called(4); }); + + test('lockCaptureOrientation() calls $CameraPlatform', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + await cameraController.lockCaptureOrientation(); + expect(cameraController.value.lockedCaptureOrientation, + equals(DeviceOrientation.portraitUp)); + await cameraController + .lockCaptureOrientation(DeviceOrientation.landscapeRight); + expect(cameraController.value.lockedCaptureOrientation, + equals(DeviceOrientation.landscapeRight)); + + verify(CameraPlatform.instance.lockCaptureOrientation( + cameraController.cameraId, DeviceOrientation.portraitUp)) + .called(1); + verify(CameraPlatform.instance.lockCaptureOrientation( + cameraController.cameraId, DeviceOrientation.landscapeRight)) + .called(1); + }); + + test( + 'lockCaptureOrientation() throws $CameraException on $PlatformException', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + when(CameraPlatform.instance.lockCaptureOrientation( + cameraController.cameraId, DeviceOrientation.portraitUp)) + .thenThrow( + PlatformException( + code: 'TEST_ERROR', + message: 'This is a test error message', + details: null, + ), + ); + + expect( + cameraController.lockCaptureOrientation(DeviceOrientation.portraitUp), + throwsA(isA().having( + (error) => error.description, + 'TEST_ERROR', + 'This is a test error message', + ))); + }); + + test('unlockCaptureOrientation() calls $CameraPlatform', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + await cameraController.unlockCaptureOrientation(); + expect(cameraController.value.lockedCaptureOrientation, equals(null)); + + verify(CameraPlatform.instance + .unlockCaptureOrientation(cameraController.cameraId)) + .called(1); + }); + + test( + 'unlockCaptureOrientation() throws $CameraException on $PlatformException', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + when(CameraPlatform.instance + .unlockCaptureOrientation(cameraController.cameraId)) + .thenThrow( + PlatformException( + code: 'TEST_ERROR', + message: 'This is a test error message', + details: null, + ), + ); + + expect( + cameraController.unlockCaptureOrientation(), + throwsA(isA().having( + (error) => error.description, + 'TEST_ERROR', + 'This is a test error message', + ))); + }); }); } @@ -1057,6 +1160,10 @@ class MockCameraPlatform extends Mock Stream onCameraError(int cameraId) => Stream.value(mockOnCameraErrorEvent); + @override + Stream onDeviceOrientationChanged() => + Stream.value(mockOnDeviceOrientationChangedEvent); + @override Future takePicture(int cameraId) => mockPlatformException ? throw PlatformException(code: 'foo', message: 'bar') diff --git a/packages/camera/camera/test/camera_value_test.dart b/packages/camera/camera/test/camera_value_test.dart index eb7927b9eb6c..c365f6ddb9de 100644 --- a/packages/camera/camera/test/camera_value_test.dart +++ b/packages/camera/camera/test/camera_value_test.dart @@ -7,22 +7,27 @@ import 'dart:ui'; import 'package:camera/camera.dart'; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { group('camera_value', () { test('Can be created', () { var cameraValue = const CameraValue( - isInitialized: false, - errorDescription: null, - previewSize: Size(10, 10), - isRecordingPaused: false, - isRecordingVideo: false, - isTakingPicture: false, - isStreamingImages: false, - flashMode: FlashMode.auto, - exposureMode: ExposureMode.auto, - exposurePointSupported: true); + isInitialized: false, + errorDescription: null, + previewSize: Size(10, 10), + isRecordingPaused: false, + isRecordingVideo: false, + isTakingPicture: false, + isStreamingImages: false, + flashMode: FlashMode.auto, + exposureMode: ExposureMode.auto, + exposurePointSupported: true, + deviceOrientation: DeviceOrientation.portraitUp, + lockedCaptureOrientation: DeviceOrientation.portraitUp, + recordingOrientation: DeviceOrientation.portraitUp, + ); expect(cameraValue, isA()); expect(cameraValue.isInitialized, isFalse); @@ -35,6 +40,10 @@ void main() { expect(cameraValue.flashMode, FlashMode.auto); expect(cameraValue.exposureMode, ExposureMode.auto); expect(cameraValue.exposurePointSupported, true); + expect(cameraValue.deviceOrientation, DeviceOrientation.portraitUp); + expect( + cameraValue.lockedCaptureOrientation, DeviceOrientation.portraitUp); + expect(cameraValue.recordingOrientation, DeviceOrientation.portraitUp); }); test('Can be created as uninitialized', () { @@ -51,6 +60,9 @@ void main() { expect(cameraValue.flashMode, FlashMode.auto); expect(cameraValue.exposureMode, null); expect(cameraValue.exposurePointSupported, false); + expect(cameraValue.deviceOrientation, DeviceOrientation.portraitUp); + expect(cameraValue.lockedCaptureOrientation, null); + expect(cameraValue.recordingOrientation, null); }); test('Can be copied with isInitialized', () { @@ -68,6 +80,9 @@ void main() { expect(cameraValue.flashMode, FlashMode.auto); expect(cameraValue.exposureMode, null); expect(cameraValue.exposurePointSupported, false); + expect(cameraValue.deviceOrientation, DeviceOrientation.portraitUp); + expect(cameraValue.lockedCaptureOrientation, null); + expect(cameraValue.recordingOrientation, null); }); test('Has aspectRatio after setting size', () { @@ -75,7 +90,7 @@ void main() { var cameraValue = cv.copyWith(isInitialized: true, previewSize: Size(20, 10)); - expect(cameraValue.aspectRatio, 0.5); + expect(cameraValue.aspectRatio, 2.0); }); test('hasError is true after setting errorDescription', () { @@ -110,10 +125,13 @@ void main() { focusMode: FocusMode.auto, exposurePointSupported: true, focusPointSupported: true, + deviceOrientation: DeviceOrientation.portraitUp, + lockedCaptureOrientation: DeviceOrientation.portraitUp, + recordingOrientation: DeviceOrientation.portraitUp, ); expect(cameraValue.toString(), - 'CameraValue(isRecordingVideo: false, isInitialized: false, errorDescription: null, previewSize: Size(10.0, 10.0), isStreamingImages: false, flashMode: FlashMode.auto, exposureMode: ExposureMode.auto, focusMode: FocusMode.auto, exposurePointSupported: true, focusPointSupported: true)'); + 'CameraValue(isRecordingVideo: false, isInitialized: false, errorDescription: null, previewSize: Size(10.0, 10.0), isStreamingImages: false, flashMode: FlashMode.auto, exposureMode: ExposureMode.auto, focusMode: FocusMode.auto, exposurePointSupported: true, focusPointSupported: true, deviceOrientation: DeviceOrientation.portraitUp, lockedCaptureOrientation: DeviceOrientation.portraitUp, recordingOrientation: DeviceOrientation.portraitUp)'); }); }); }