feat: Implement resizeMode prop for iOS (#1838)

* feat: Implement `resizeMode` prop for iOS

- `"cover"`: Keep aspect ratio, but fill entire parent view (centered).
- `"contain"`: Keep aspect ratio, but make sure the entire content is visible even if it introduces additional blank areas (centered).

* chore: Update prop docs

* Update CameraProps.ts

* Lint & Format
This commit is contained in:
Marc Rousavy 2023-09-23 10:14:27 +02:00 committed by GitHub
parent c0b80b342b
commit 3169444697
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 120 additions and 38 deletions

View File

@ -44,6 +44,7 @@ Pod::Spec.new do |s|
"ios/Extensions/*.{m,mm,swift}",
"ios/Parsers/*.{m,mm,swift}",
"ios/React Utils/*.{m,mm,swift}",
"ios/Types/*.{m,mm,swift}",
"ios/CameraBridge.h",
# Frame Processors

View File

@ -16,17 +16,17 @@ import com.facebook.react.bridge.ReadableMap
import com.mrousavy.camera.core.CameraSession
import com.mrousavy.camera.core.PreviewView
import com.mrousavy.camera.core.outputs.CameraOutputs
import com.mrousavy.camera.extensions.bigger
import com.mrousavy.camera.extensions.containsAny
import com.mrousavy.camera.extensions.getPreviewTargetSize
import com.mrousavy.camera.extensions.installHierarchyFitter
import com.mrousavy.camera.extensions.smaller
import com.mrousavy.camera.frameprocessor.FrameProcessor
import com.mrousavy.camera.parsers.Orientation
import com.mrousavy.camera.parsers.PixelFormat
import com.mrousavy.camera.parsers.ResizeMode
import com.mrousavy.camera.parsers.Torch
import com.mrousavy.camera.parsers.VideoStabilizationMode
import com.mrousavy.camera.extensions.bigger
import com.mrousavy.camera.extensions.getPreviewTargetSize
import com.mrousavy.camera.extensions.smaller
import com.mrousavy.camera.parsers.ResizeMode
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -141,9 +141,10 @@ class CameraView(context: Context) : FrameLayout(context) {
configureSession()
}
previewView.layoutParams = LayoutParams(
LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT,
Gravity.CENTER)
LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT,
Gravity.CENTER
)
addView(previewView)
this.previewView = previewView
}

View File

@ -132,8 +132,9 @@ class CameraViewManager : ViewGroupManager<CameraView>() {
@ReactProp(name = "resizeMode")
fun setResizeMode(view: CameraView, resizeMode: String) {
val newMode = ResizeMode.fromUnionValue(resizeMode)
if (view.resizeMode != newMode)
if (view.resizeMode != newMode) {
addChangedPropToTransaction(view, "resizeMode")
}
view.resizeMode = newMode
}

View File

@ -7,8 +7,6 @@ import android.util.Size
import android.view.Surface
import android.view.SurfaceHolder
import android.view.SurfaceView
import com.mrousavy.camera.extensions.bigger
import com.mrousavy.camera.extensions.smaller
import com.mrousavy.camera.parsers.ResizeMode
import kotlin.math.roundToInt
@ -17,7 +15,8 @@ class PreviewView(
context: Context,
val targetSize: Size,
private val resizeMode: ResizeMode,
private val onSurfaceChanged: (surface: Surface?) -> Unit): SurfaceView(context) {
private val onSurfaceChanged: (surface: Surface?) -> Unit
) : SurfaceView(context) {
init {
Log.i(TAG, "Using Preview Size ${targetSize.width} x ${targetSize.height}.")
@ -43,7 +42,7 @@ class PreviewView(
val contentAspectRatio = contentSize.height.toDouble() / contentSize.width
val containerAspectRatio = containerWidth.toDouble() / containerHeight
Log.d(TAG, "coverSize :: $contentSize ($contentAspectRatio), ${containerWidth}x${containerHeight} ($containerAspectRatio)")
Log.d(TAG, "coverSize :: $contentSize ($contentAspectRatio), ${containerWidth}x$containerHeight ($containerAspectRatio)")
return if (contentAspectRatio > containerAspectRatio) {
// Scale by width to cover height
@ -60,7 +59,7 @@ class PreviewView(
val contentAspectRatio = contentSize.height.toDouble() / contentSize.width
val containerAspectRatio = containerWidth.toDouble() / containerHeight
Log.d(TAG, "containSize :: $contentSize ($contentAspectRatio), ${containerWidth}x${containerHeight} ($containerAspectRatio)")
Log.d(TAG, "containSize :: $contentSize ($contentAspectRatio), ${containerWidth}x$containerHeight ($containerAspectRatio)")
return if (contentAspectRatio > containerAspectRatio) {
// Scale by height to fit within width
@ -81,8 +80,8 @@ class PreviewView(
Log.d(TAG, "onMeasure($viewWidth, $viewHeight)")
val fittedSize = when (resizeMode) {
ResizeMode.COVER -> this.coverSize(targetSize, viewWidth, viewHeight)
ResizeMode.CONTAIN -> this.containSize(targetSize, viewWidth, viewHeight)
ResizeMode.COVER -> this.coverSize(targetSize, viewWidth, viewHeight)
ResizeMode.CONTAIN -> this.containSize(targetSize, viewWidth, viewHeight)
}
Log.d(TAG, "Fitted dimensions set: $fittedSize")

View File

@ -10,11 +10,11 @@ import android.util.Size
import android.view.Surface
import com.mrousavy.camera.CameraQueues
import com.mrousavy.camera.core.VideoPipeline
import com.mrousavy.camera.extensions.bigger
import com.mrousavy.camera.extensions.closestToOrMax
import com.mrousavy.camera.extensions.getPhotoSizes
import com.mrousavy.camera.extensions.getPreviewTargetSize
import com.mrousavy.camera.extensions.getVideoSizes
import com.mrousavy.camera.extensions.bigger
import com.mrousavy.camera.extensions.smaller
import java.io.Closeable
@ -63,12 +63,12 @@ class CameraOutputs(
override fun equals(other: Any?): Boolean {
if (other !is CameraOutputs) return false
return this.cameraId == other.cameraId &&
this.preview?.surface == other.preview?.surface &&
this.preview?.targetSize == other.preview?.targetSize &&
this.photo?.targetSize == other.photo?.targetSize &&
this.photo?.format == other.photo?.format &&
this.video?.enableRecording == other.video?.enableRecording &&
return this.cameraId == other.cameraId &&
this.preview?.surface == other.preview?.surface &&
this.preview?.targetSize == other.preview?.targetSize &&
this.photo?.targetSize == other.photo?.targetSize &&
this.photo?.format == other.photo?.format &&
this.video?.enableRecording == other.video?.enableRecording &&
this.video?.targetSize == other.video?.targetSize &&
this.video?.format == other.video?.format &&
this.enableHdr == other.enableHdr
@ -104,11 +104,18 @@ class CameraOutputs(
// Preview output: Low resolution repeating images (SurfaceView)
if (preview != null) {
Log.i(TAG, "Adding native preview view output.")
val previewSizeAspectRatio = if (preview.targetSize != null) preview.targetSize.bigger.toDouble() / preview.targetSize.smaller else null
val previewSizeAspectRatio = if (preview.targetSize !=
null
) {
preview.targetSize.bigger.toDouble() / preview.targetSize.smaller
} else {
null
}
previewOutput = SurfaceOutput(
preview.surface,
characteristics.getPreviewTargetSize(previewSizeAspectRatio),
SurfaceOutput.OutputType.PREVIEW)
SurfaceOutput.OutputType.PREVIEW
)
}
// Photo output: High quality still images (takePhoto())

View File

@ -2,7 +2,6 @@ package com.mrousavy.camera.extensions
import android.content.res.Resources
import android.hardware.camera2.CameraCharacteristics
import android.util.Log
import android.util.Size
import android.view.SurfaceHolder
import kotlin.math.abs
@ -12,8 +11,10 @@ private fun getMaximumPreviewSize(): Size {
// According to the Android Developer documentation, PREVIEW streams can have a resolution
// of up to the phone's display's resolution, with a maximum of 1920x1080.
val display1080p = Size(1920, 1080)
val displaySize = Size(Resources.getSystem().displayMetrics.widthPixels,
Resources.getSystem().displayMetrics.heightPixels)
val displaySize = Size(
Resources.getSystem().displayMetrics.widthPixels,
Resources.getSystem().displayMetrics.heightPixels
)
val isHighResScreen = displaySize.bigger >= display1080p.bigger || displaySize.smaller >= display1080p.smaller
return if (isHighResScreen) display1080p else displaySize
@ -38,10 +39,9 @@ fun CameraCharacteristics.getAutomaticPreviewSize(): Size {
return outputSizes.first { it.bigger <= maximumPreviewSize.bigger && it.smaller <= maximumPreviewSize.smaller }
}
fun CameraCharacteristics.getPreviewTargetSize(aspectRatio: Double?): Size {
return if (aspectRatio != null) {
fun CameraCharacteristics.getPreviewTargetSize(aspectRatio: Double?): Size =
if (aspectRatio != null) {
getPreviewSizeFromAspectRatio(aspectRatio)
} else {
getAutomaticPreviewSize()
}
}

View File

@ -1,16 +1,15 @@
package com.mrousavy.camera.parsers
enum class ResizeMode(override val unionValue: String): JSUnionValue {
enum class ResizeMode(override val unionValue: String) : JSUnionValue {
COVER("cover"),
CONTAIN("contain");
companion object: JSUnionValue.Companion<ResizeMode> {
override fun fromUnionValue(unionValue: String?): ResizeMode {
return when (unionValue) {
companion object : JSUnionValue.Companion<ResizeMode> {
override fun fromUnionValue(unionValue: String?): ResizeMode =
when (unionValue) {
"cover" -> COVER
"contain" -> CONTAIN
else -> COVER
}
}
}
}

View File

@ -733,7 +733,7 @@ SPEC CHECKSUMS:
SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d
SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
VisionCamera: 9be9a96959550faba434896a60fc1be843fca5af
VisionCamera: 4780262974d65e89883a9b374d15459359e56ab3
Yoga: 8796b55dba14d7004f980b54bcc9833ee45b28ce
PODFILE CHECKSUM: ab9c06b18c63e741c04349c0fd630c6d3145081c

View File

@ -59,6 +59,12 @@ public final class CameraView: UIView {
@objc var zoom: NSNumber = 1.0 // in "factor"
@objc var enableFpsGraph = false
@objc var videoStabilizationMode: NSString?
@objc var resizeMode: NSString = "cover" {
didSet {
previewView.resizeMode = ResizeMode(fromTypeScriptUnion: resizeMode as String)
}
}
// events
@objc var onInitialized: RCTDirectEventBlock?
@objc var onError: RCTDirectEventBlock?

View File

@ -47,6 +47,7 @@ RCT_EXPORT_VIEW_PROPERTY(zoom, NSNumber);
RCT_EXPORT_VIEW_PROPERTY(enableZoomGesture, BOOL);
RCT_EXPORT_VIEW_PROPERTY(enableFpsGraph, BOOL);
RCT_EXPORT_VIEW_PROPERTY(orientation, NSString);
RCT_EXPORT_VIEW_PROPERTY(resizeMode, NSString);
// Camera View Events
RCT_EXPORT_VIEW_PROPERTY(onError, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onInitialized, RCTDirectEventBlock);

View File

@ -11,13 +11,29 @@ import Foundation
import UIKit
class PreviewView: UIView {
/// Convenience wrapper to get layer as its statically known type.
/**
Convenience wrapper to get layer as its statically known type.
*/
var videoPreviewLayer: AVCaptureVideoPreviewLayer {
// swiftlint:disable force_cast
return layer as! AVCaptureVideoPreviewLayer
// swiftlint:enable force_cast
}
/**
Gets or sets the resize mode of the PreviewView.
*/
var resizeMode: ResizeMode = .cover {
didSet {
switch resizeMode {
case .cover:
videoPreviewLayer.videoGravity = .resizeAspectFill
case .contain:
videoPreviewLayer.videoGravity = .resizeAspect
}
}
}
override public class var layerClass: AnyClass {
return AVCaptureVideoPreviewLayer.self
}

View File

@ -0,0 +1,35 @@
//
// ResizeMode.swift
// VisionCamera
//
// Created by Marc Rousavy on 22.09.23.
// Copyright © 2023 mrousavy. All rights reserved.
//
import Foundation
/**
A ResizeMode used for the PreviewView.
*/
enum ResizeMode {
/**
Keep aspect ratio, but fill entire parent view (centered).
*/
case cover
/**
Keep aspect ratio, but make sure the entire content is visible even if it introduces additional blank areas (centered).
*/
case contain
init(fromTypeScriptUnion union: String) {
switch union {
case "cover":
self = .cover
case "contain":
self = .contain
default:
// TODO: Use the onError event for safer error handling!
fatalError("Invalid value passed for resizeMode! (\(union))")
}
}
}

View File

@ -7,6 +7,7 @@
objects = {
/* Begin PBXBuildFile section */
B80175EC2ABDEBD000E7DE90 /* ResizeMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B80175EB2ABDEBD000E7DE90 /* ResizeMode.swift */; };
B80C0E00260BDDF7001699AB /* FrameProcessorPluginRegistry.m in Sources */ = {isa = PBXBuildFile; fileRef = B80C0DFF260BDDF7001699AB /* FrameProcessorPluginRegistry.m */; };
B80E06A0266632F000728644 /* AVAudioSession+updateCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B80E069F266632F000728644 /* AVAudioSession+updateCategory.swift */; };
B81BE1BF26B936FF002696CC /* AVCaptureDevice.Format+videoDimensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B81BE1BE26B936FF002696CC /* AVCaptureDevice.Format+videoDimensions.swift */; };
@ -80,6 +81,7 @@
/* Begin PBXFileReference section */
134814201AA4EA6300B7C361 /* libVisionCamera.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libVisionCamera.a; sourceTree = BUILT_PRODUCTS_DIR; };
B80175EB2ABDEBD000E7DE90 /* ResizeMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResizeMode.swift; sourceTree = "<group>"; };
B80C02EB2A6A954D001975E2 /* FrameProcessorPluginHostObject.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FrameProcessorPluginHostObject.mm; sourceTree = "<group>"; };
B80C02EC2A6A9552001975E2 /* FrameProcessorPluginHostObject.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FrameProcessorPluginHostObject.h; sourceTree = "<group>"; };
B80C0DFE260BDD97001699AB /* FrameProcessorPluginRegistry.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FrameProcessorPluginRegistry.h; sourceTree = "<group>"; };
@ -174,6 +176,7 @@
58B511D21A9E6C8500147676 = {
isa = PBXGroup;
children = (
B80175EA2ABDEBBB00E7DE90 /* Types */,
B8DCF2D725EA940700EA5C72 /* Frame Processor */,
B887515E25E0102000DB86D6 /* CameraBridge.h */,
B84760DE2608F57D004C3180 /* CameraQueues.swift */,
@ -201,6 +204,14 @@
);
sourceTree = "<group>";
};
B80175EA2ABDEBBB00E7DE90 /* Types */ = {
isa = PBXGroup;
children = (
B80175EB2ABDEBD000E7DE90 /* ResizeMode.swift */,
);
path = Types;
sourceTree = "<group>";
};
B887516125E0102000DB86D6 /* Extensions */ = {
isa = PBXGroup;
children = (
@ -418,6 +429,7 @@
B881D35E2ABC775E009B21C8 /* AVCaptureDevice+toDictionary.swift in Sources */,
B87B11BF2A8E63B700732EBF /* PixelFormat.swift in Sources */,
B88751A625E0102000DB86D6 /* CameraViewManager.swift in Sources */,
B80175EC2ABDEBD000E7DE90 /* ResizeMode.swift in Sources */,
B887519F25E0102000DB86D6 /* AVCaptureDevice.DeviceType+physicalDeviceDescriptor.swift in Sources */,
B8D22CDC2642DB4D00234472 /* AVAssetWriterInputPixelBufferAdaptor+initWithVideoSettings.swift in Sources */,
B84760DF2608F57D004C3180 /* CameraQueues.swift in Sources */,

View File

@ -112,7 +112,11 @@ export interface CameraProps extends ViewProps {
*/
format?: CameraDeviceFormat;
/**
* Specify how you want the preview to fit the container it's in
* Specifies the Preview's resize mode.
* * `"cover"`: Keep aspect ratio and fill entire parent view (centered).
* * `"contain"`: Keep aspect ratio and make sure the entire content is visible inside the parent view, even if it introduces additional blank areas (centered).
*
* @default "cover"
*/
resizeMode?: 'cover' | 'contain';
/**