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:
parent
c0b80b342b
commit
3169444697
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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())
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -733,7 +733,7 @@ SPEC CHECKSUMS:
|
||||
SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d
|
||||
SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d
|
||||
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
|
||||
VisionCamera: 9be9a96959550faba434896a60fc1be843fca5af
|
||||
VisionCamera: 4780262974d65e89883a9b374d15459359e56ab3
|
||||
Yoga: 8796b55dba14d7004f980b54bcc9833ee45b28ce
|
||||
|
||||
PODFILE CHECKSUM: ab9c06b18c63e741c04349c0fd630c6d3145081c
|
||||
|
@ -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?
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
}
|
||||
|
35
package/ios/Types/ResizeMode.swift
Normal file
35
package/ios/Types/ResizeMode.swift
Normal 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))")
|
||||
}
|
||||
}
|
||||
}
|
@ -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 */,
|
||||
|
@ -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';
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user