Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ To have Play Services [pre-download the model in the background after the app is
| `onError` | Function | Android only. Callback when camera fails to initialize. Ex: `onError={(e) => console.log(e.nativeEvent.errorMessage)}`. |
| `shutterPhotoSound` | `boolean` | Android only. Enable or disable the shutter sound when capturing a photo. Default: `true` |
| **iOS only** |
| `ratioOverlay` | `'int:int'` | Show a guiding overlay in the camera preview for the selected ratio. Does not crop image as of v9.0. Example: `'16:9'` |
| `ratioOverlay` | `'width:height'` | Show a guiding overlay in the camera preview for the selected ratio, expressed as `width:height`. The largest area of that ratio is centred in the preview and the rest is masked, so `'4:5'` is taller than it is wide and `'16:9'` is wider than it is tall. Does not crop image as of v9.0. Examples: `'16:9'`, `'1:1'`, `'4:5'` |
| `ratioOverlayColor` | Color | Any color with alpha. Default: `'#ffffff77'` |
| `resetFocusTimeout` | `number` | Dismiss tap to focus after this many milliseconds. Default `0` (disabled). Example: `5000` is 5 seconds. |
| `resetFocusWhenMotionDetected` | Boolean | Dismiss tap to focus when focus area content changes. Native iOS feature, see documentation: https://developer.apple.com/documentation/avfoundation/avcapturedevice/1624644-subjectareachangemonitoringenabl?language=objc). Default `true`. |
Expand Down
144 changes: 68 additions & 76 deletions ios/ReactNativeCameraKit/RatioOverlayView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@

import UIKit

/*
* A `width:height` ratio, as accepted by the `ratioOverlay` prop, eg. "16:9" or "4:5".
*
* Width comes first, so "4:5" describes a window that is taller than it is wide,
* and "16:9" one that is wider than it is tall.
*/
struct RatioOverlayData: CustomStringConvertible {
let width: Float
let height: Float
Expand All @@ -14,24 +20,50 @@ struct RatioOverlayData: CustomStringConvertible {
let values = inputString.split(separator: ":")

if values.count == 2,
let inputHeight = Float(values[0]),
let inputWidth = Float(values[1]),
inputHeight != 0,
inputWidth != 0 {
height = inputHeight
let inputWidth = Float(values[0]),
let inputHeight = Float(values[1]),
inputWidth > 0,
inputHeight > 0 {
width = inputWidth
height = inputHeight
ratio = width / height
} else {
height = 0
width = 0
height = 0
ratio = 0
}
}

/*
* The largest rect of this ratio that fits inside `size`, centered.
*
* Returns nil when there is nothing to draw, ie. the ratio was unparseable
* or the container has not been laid out yet.
*/
func centeredWindow(fitting size: CGSize) -> CGRect? {
guard ratio > 0, size.width > 0, size.height > 0 else {
return nil
}

let targetRatio = CGFloat(ratio)
let containerRatio = size.width / size.height

// Aspect fit: the tighter of the two axes spans the container,
// the other one is derived from the requested ratio.
let windowSize = targetRatio > containerRatio
? CGSize(width: size.width, height: size.width / targetRatio)
: CGSize(width: size.height * targetRatio, height: size.height)

return CGRect(x: (size.width - windowSize.width) / 2.0,
y: (size.height - windowSize.height) / 2.0,
width: windowSize.width,
height: windowSize.height)
}

// MARK: CustomStringConvertible

var description: String {
return "height:\(height) width:\(width) ratio:\(ratio)"
return "width:\(width) height:\(height) ratio:\(ratio)"
}
}

Expand All @@ -41,8 +73,10 @@ struct RatioOverlayData: CustomStringConvertible {
class RatioOverlayView: UIView {
private var ratioData: RatioOverlayData?

private let topView: UIView = UIView()
private let bottomView: UIView = UIView()
// The two bars framing the ratio window: above/below it when letterboxing,
// left/right of it when pillarboxing.
private let leadingBarView: UIView = UIView()
private let trailingBarView: UIView = UIView()

// MARK: - Lifecycle

Expand All @@ -54,8 +88,8 @@ class RatioOverlayView: UIView {
let color = overlayColor ?? UIColor.black.withAlphaComponent(0.3)
setColor(color)

addSubview(topView)
addSubview(bottomView)
addSubview(leadingBarView)
addSubview(trailingBarView)

setRatio(ratioString)
}
Expand All @@ -82,83 +116,41 @@ class RatioOverlayView: UIView {
}

func setColor(_ color: UIColor) {
topView.backgroundColor = color
bottomView.backgroundColor = color
leadingBarView.backgroundColor = color
trailingBarView.backgroundColor = color
}

// MARK: - Private

// swiftlint:disable:next function_body_length
private func setOverlayParts() {
guard let ratioData, ratioData.ratio != 0 else {
guard let window = ratioData?.centeredWindow(fitting: bounds.size) else {
isHidden = true

return
}

isHidden = false

var centerSize = CGSize.zero
var sideSize = CGSize.zero
var centerFrame: CGRect

if ratioData.width < ratioData.height {
centerSize.width = frame.size.width
centerSize.height = frame.size.height * CGFloat(ratioData.ratio)

sideSize.width = centerSize.width
sideSize.height = (frame.size.height - centerSize.height) / 2.0

topView.frame = CGRect(x: 0,
y: 0,
width: sideSize.width,
height: sideSize.height)
centerFrame = CGRect(x: 0,
y: topView.frame.size.height + topView.frame.origin.y,
width: centerSize.width,
height: centerSize.height)
bottomView.frame = CGRect(x: 0,
y: centerFrame.size.height + centerFrame.origin.y,
width: sideSize.width,
height: sideSize.height)
} else if ratioData.width > ratioData.height {
centerSize.width = frame.size.width / CGFloat(ratioData.ratio)
centerSize.height = frame.size.height

sideSize.width = (frame.size.width - centerSize.width) / 2.0
sideSize.height = centerSize.height

topView.frame = CGRect(x: 0,
y: 0,
width: sideSize.width,
height: sideSize.height)
centerFrame = CGRect(x: topView.frame.size.width + topView.frame.origin.x,
y: 0,
width: centerSize.width,
height: centerSize.height)
bottomView.frame = CGRect(x: centerFrame.size.width + centerFrame.origin.x,
y: 0,
width: sideSize.width,
height: sideSize.height)
} else { // ratio is 1:1
centerSize.width = frame.size.width
centerSize.height = frame.size.width

sideSize.width = centerSize.width
sideSize.height = (frame.size.height - centerSize.height) / 2.0

topView.frame = CGRect(x: 0,
y: 0,
width: sideSize.width,
height: sideSize.height)
centerFrame = CGRect(x: 0,
y: topView.frame.size.height + topView.frame.origin.y,
width: centerSize.width,
height: centerSize.height)
bottomView.frame = CGRect(x: 0,
y: centerFrame.size.height + centerFrame.origin.y,
width: sideSize.width,
height: sideSize.height)
if window.height < bounds.height {
// Letterbox: mask the strips above and below the ratio window.
leadingBarView.frame = CGRect(x: 0,
y: 0,
width: bounds.width,
height: window.minY)
trailingBarView.frame = CGRect(x: 0,
y: window.maxY,
width: bounds.width,
height: bounds.height - window.maxY)
} else {
// Pillarbox: mask the strips left and right of the ratio window.
leadingBarView.frame = CGRect(x: 0,
y: 0,
width: window.minX,
height: bounds.height)
trailingBarView.frame = CGRect(x: window.maxX,
y: 0,
width: bounds.width - window.maxX,
height: bounds.height)
}
}
}