Merge pull request #330 from aarongrider/android-rewrite

Rewrite Android implementation, update api and docs, convert code to TypeScript
This commit is contained in:
Aaron Grider 2020-12-06 22:47:35 -08:00 committed by GitHub
commit affcdb4571
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 1112 additions and 2517 deletions

View File

@ -50,7 +50,7 @@ module.exports = {
'import/ignore': ['node_modules/react-native/index\\.js$'], // https://github.com/facebook/react-native/issues/28549
'import/resolver': {
node: {
extensions: ['.js', '.ts', '.android.js', '.ios.js'],
extensions: ['.js', '.ts', '.android.js', '.ios.js', '.android.tsx', '.ios.tsx'],
},
},
},

View File

@ -48,19 +48,17 @@ cd ios && pod install && cd ..
## APIs
### CameraKitCamera - Camera component
### Camera - Base Camera component
```js
import { CameraKitCamera } from 'react-native-camera-kit';
import { Camera } from 'react-native-camera-kit';
```
```jsx
<CameraKitCamera
ref={(cam) => (this.camera = cam)}
style={{
flex: 1,
backgroundColor: 'white',
}}
<Camera
ref={(ref) => this.camera = ref}
type={CameraType.Back} // front/back(default)
style={{ flex: 1 }}
cameraOptions={{
flashMode: 'auto', // on/off/auto(default)
focusMode: 'on', // off/on(default)
@ -68,11 +66,18 @@ import { CameraKitCamera } from 'react-native-camera-kit';
ratioOverlay: '1:1', // optional
ratioOverlayColor: '#00000077', // optional
}}
resetFocusTimeout={0} // optional
resetFocusWhenMotionDetected={true} // optional
saveToCameraRole={false} // optional
// Barcode Scanner Props
scanBarcode={false} // optional
showFrame={false} // Barcode only, optional
laserColor='red' // Barcode only, optional
frameColor='yellow' // Barcode only, optional
surfaceColor='blue' // Barcode only, optional
onReadCode={(
event, // optional
) => console.log(event.nativeEvent.codeStringValue)}
resetFocusTimeout={0} // optional
resetFocusWhenMotionDetected={true} // optional
/>
```
@ -94,12 +99,20 @@ import { CameraKitCamera } from 'react-native-camera-kit';
| `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', '1:1', '3:4']` |
| `ratioOverlayColor` | Color | Any color with alpha (default is `'#ffffff77'`) |
### CameraKitCamera API
### Camera API
#### checkDeviceCameraAuthorizationStatus
#### capture({ ... }) - must have the wanted camera capture reference
Capture image (`{ saveToCameraRoll: boolean }`). Using the camera roll is slower than using regular files stored in your app. On an iPhone X in debug mode, on a real phone, we measured around 100-150ms processing time to save to the camera roll.
```js
const isCameraAuthorized = await CameraKitCamera.checkDeviceCameraAuthorizationStatus();
const image = await this.camera.capture();
```
#### checkDeviceCameraAuthorizationStatus (iOS only)
```js
const isCameraAuthorized = await Camera.checkDeviceCameraAuthorizationStatus();
```
return values:
@ -110,54 +123,22 @@ return values:
otherwise, returns `false`
#### requestDeviceCameraAuthorization
#### requestDeviceCameraAuthorization (iOS only)
```js
const isUserAuthorizedCamera = await CameraKitCamera.requestDeviceCameraAuthorization();
const isUserAuthorizedCamera = await Camera.requestDeviceCameraAuthorization();
```
`AVAuthorizationStatusAuthorized` returns `true`
otherwise, returns `false`
#### capture({ ... }) - must have the wanted camera capture reference
Capture image (`{ saveToCameraRoll: boolean }`). Using the camera roll is slower than using regular files stored in your app. On an iPhone X in debug mode, on a real phone, we measured around 100-150ms processing time to save to the camera roll.
```js
const image = await this.camera.capture();
```
#### setFlashMode - must have the wanted camera capture reference
Set flash mode (`auto`/`on`/`off`)
```js
const success = await this.camera.setFlashMode(newFlashData.mode);
```
#### setTorchMode - must have the wanted camera capture reference
Set Torch mode (`on`/`off`)
```js
const success = await this.camera.setTorchMode(newTorchMode);
```
#### changeCamera - must have the wanted camera capture reference
Change to front/rear camera
```js
const success = await this.camera.changeCamera();
```
## QR Code
```js
import { CameraKitCameraScreen } from 'react-native-camera-kit';
import { CameraScreen } from 'react-native-camera-kit';
<CameraKitCameraScreen
<CameraScreen
actions={{ rightButtonText: 'Done', leftButtonText: 'Cancel' }}
onBottomButtonPressed={(event) => this.onBottomButtonPressed(event)}
scanBarcode={true}

View File

@ -1,4 +1,5 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
compileSdkVersion 28
@ -21,4 +22,16 @@ dependencies {
implementation 'com.facebook.react:react-native:+'
implementation 'com.google.zxing:core:3.3.3'
implementation group: 'com.drewnoakes', name: 'metadata-extractor', version: '2.12.0'
implementation "androidx.core:core-ktx:+"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "androidx.camera:camera-core:1.0.0-beta08"
implementation "androidx.camera:camera-camera2:1.0.0-beta08"
implementation "androidx.camera:camera-lifecycle:1.0.0-beta08"
implementation "androidx.camera:camera-view:1.0.0-alpha15"
implementation 'com.google.mlkit:barcode-scanning:16.0.3'
}
repositories {
mavenCentral()
}

View File

@ -0,0 +1,420 @@
package com.rncamerakit
import android.Manifest
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.annotation.SuppressLint
import android.app.Activity
import android.content.ContentValues
import android.content.Context
import android.content.pm.PackageManager
import android.graphics.Color
import android.hardware.SensorManager
import android.media.AudioManager
import android.media.MediaActionSound
import android.provider.MediaStore
import android.util.Log
import android.view.*
import android.widget.FrameLayout
import android.widget.LinearLayout
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.WritableMap
import com.facebook.react.common.ReactConstants.TAG
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.events.RCTEventEmitter
import java.io.File
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
@SuppressLint("ViewConstructor") // Extra constructors unused. Not using visual layout tools
class CKCamera(context: ThemedReactContext) : FrameLayout(context), LifecycleObserver {
private val currentContext: ThemedReactContext = context
private var preview: Preview? = null
private var imageCapture: ImageCapture? = null
private var qrCodeAnalyzer: ImageAnalysis? = null
private var cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
private var camera: Camera? = null
private var orientationListener: OrientationEventListener? = null
private var viewFinder: PreviewView = PreviewView(context)
private var cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor()
private var scanBarcode: Boolean = false
private var lensType = CameraSelector.LENS_FACING_BACK
private var autoFocus = "on"
private var cameraProvider: ProcessCameraProvider? = null
private var outputPath: String? = null
private var shutterAnimationDuration: Int = 50
private var effectLayer = View(context)
private var counter = 0
private fun getActivity() : Activity {
return currentContext.currentActivity!!
}
init {
viewFinder.layoutParams = LinearLayout.LayoutParams(
LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT
)
installHierarchyFitter(viewFinder)
addView(viewFinder)
effectLayer.alpha = 0F
effectLayer.setBackgroundColor(Color.BLACK)
addView(effectLayer)
if (hasPermissions()) {
viewFinder.post { startCamera() }
}
(getActivity() as AppCompatActivity).lifecycle.addObserver(this)
}
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun onResume() {
Log.d(TAG, "onResume")
viewFinder.post { startCamera() }
}
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
fun onPause() {
Log.d(TAG, "onPause")
stopCamera()
}
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroy() {
Log.d(TAG, "onDestroy")
stopCamera()
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
stopCamera()
}
private fun stopCamera() {
cameraExecutor.shutdown()
orientationListener?.disable()
cameraProvider?.unbindAll()
}
// If this is not called correctly, view finder will be black/blank
// https://github.com/facebook/react-native/issues/17968#issuecomment-633308615
private fun installHierarchyFitter(view: ViewGroup) {
Log.d(TAG, "CameraView looking for ThemedReactContext")
if (context is ThemedReactContext) { // only react-native setup
Log.d(TAG, "CameraView found ThemedReactContext")
view.setOnHierarchyChangeListener(object : OnHierarchyChangeListener {
override fun onChildViewRemoved(parent: View?, child: View?) = Unit
override fun onChildViewAdded(parent: View?, child: View?) {
parent?.measure(
MeasureSpec.makeMeasureSpec(measuredWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY)
)
parent?.layout(0, 0, parent.measuredWidth, parent.measuredHeight)
}
})
}
}
@SuppressLint("ClickableViewAccessibility")
private fun startCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(getActivity())
val onScaleGestureListener = object: ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: ScaleGestureDetector?): Boolean {
val cameraControl = camera?.cameraControl ?: return true
val zoom = camera?.cameraInfo?.zoomState?.value?.zoomRatio ?: return true
val scaleFactor = detector?.scaleFactor ?: return true
val scale = zoom * scaleFactor
cameraControl.setZoomRatio(scale)
return true
}
}
cameraProviderFuture.addListener(Runnable {
// Used to bind the lifecycle of cameras to the lifecycle owner
cameraProvider = cameraProviderFuture.get()
val scaleDetector = ScaleGestureDetector(context, onScaleGestureListener)
cameraSelector = CameraSelector.Builder().requireLensFacing(lensType).build()
preview = Preview.Builder().build().also {
it.setSurfaceProvider(viewFinder.createSurfaceProvider())
}
imageCapture = ImageCapture.Builder().build()
// Rotate the image according to device orientation, even when UI orientation is locked
orientationListener = object : OrientationEventListener(context, SensorManager.SENSOR_DELAY_UI) {
override fun onOrientationChanged(orientation: Int) {
val imageCapture = imageCapture ?: return
var newOrientation: Int = imageCapture.targetRotation
if (orientation >= 315 || orientation < 45) {
newOrientation = Surface.ROTATION_0
} else if (orientation in 225..314) {
newOrientation = Surface.ROTATION_90
} else if (orientation in 135..224) {
newOrientation = Surface.ROTATION_180
} else if (orientation in 45..134) {
newOrientation = Surface.ROTATION_270
}
if (newOrientation != imageCapture.targetRotation) {
imageCapture.targetRotation = newOrientation
onOrientationChange(newOrientation)
}
}
}
orientationListener!!.enable()
// Contain camera feed image within component bounds, centered
viewFinder.scaleType = PreviewView.ScaleType.FIT_CENTER
// Tap to focus
viewFinder.setOnTouchListener { _, event ->
if (event.action != MotionEvent.ACTION_UP) {
return@setOnTouchListener scaleDetector.onTouchEvent(event)
}
focusOnPoint(event.x, event.y)
return@setOnTouchListener true
}
qrCodeAnalyzer = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
val analyzer = QRCodeAnalyzer { barcodes ->
if (barcodes.isNotEmpty()) {
onBarcodeRead(barcodes)
}
}
qrCodeAnalyzer!!.setAnalyzer(cameraExecutor, analyzer)
val useCases = mutableListOf(preview, imageCapture)
if (scanBarcode) useCases.add(qrCodeAnalyzer!!)
try {
// Unbind use cases before rebinding
cameraProvider!!.unbindAll()
// Bind use cases to camera
camera = cameraProvider!!.bindToLifecycle(
getActivity() as AppCompatActivity,
cameraSelector,
*useCases.toTypedArray()
)
preview!!.setSurfaceProvider(viewFinder.createSurfaceProvider())
Log.d(TAG, "CameraView: Use cases bound")
} catch (exc: Exception) {
Log.e(TAG, "CameraView: Use cases binding failed", exc)
}
}, ContextCompat.getMainExecutor(getActivity()))
}
private fun flashViewFinder() {
if (shutterAnimationDuration == 0) return
effectLayer
.animate()
.alpha(1F)
.setDuration(shutterAnimationDuration.toLong())
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
effectLayer.animate().alpha(0F).duration = shutterAnimationDuration.toLong()
}
}).start()
}
fun setShutterAnimationDuration(duration: Int) {
shutterAnimationDuration = duration
}
fun capture(options: Map<String, Any>, promise: Promise) {
// Create output options object which contains file + metadata
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, "Untitled")
put(MediaStore.MediaColumns.MIME_TYPE, "image/jpg")
}
// Create the output file option to store the captured image in MediaStore
val outputOptions = when (outputPath) {
null -> ImageCapture.OutputFileOptions
.Builder(
context.contentResolver,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues
)
.build()
else -> ImageCapture.OutputFileOptions
.Builder(File(outputPath))
.build()
}
flashViewFinder()
val audio = getActivity().getSystemService(Context.AUDIO_SERVICE) as AudioManager
if (audio.ringerMode == AudioManager.RINGER_MODE_NORMAL) {
MediaActionSound().play(MediaActionSound.SHUTTER_CLICK);
}
// Setup image capture listener which is triggered after photo has
// been taken
imageCapture?.takePicture(
outputOptions, ContextCompat.getMainExecutor(getActivity()), object : ImageCapture.OnImageSavedCallback {
override fun onError(ex: ImageCaptureException) {
Log.e(TAG, "CameraView: Photo capture failed: ${ex.message}", ex)
promise.reject("E_CAPTURE_FAILED", "takePicture failed: ${ex.message}")
}
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
try {
val savedUri = output.savedUri.toString()
onPictureTaken(savedUri)
Log.d(TAG, "CameraView: Photo capture succeeded: $savedUri")
val imageInfo = Arguments.createMap()
imageInfo.putString("uri", savedUri)
imageInfo.putString("id", output.savedUri?.path)
imageInfo.putString("name", output.savedUri?.lastPathSegment)
// imageInfo.putInt("size", null)
imageInfo.putInt("width", width)
imageInfo.putInt("height", height)
imageInfo.putString("path", output.savedUri?.path)
promise.resolve(imageInfo)
} catch (ex: Exception) {
Log.e(TAG, "Error while saving or decoding saved photo: ${ex.message}", ex)
promise.reject("E_ON_IMG_SAVED", "Error while reading saved photo: ${ex.message}")
}
}
})
}
private fun focusOnPoint(x: Float?, y: Float?) {
if (x === null || y === null) {
camera?.cameraControl?.cancelFocusAndMetering()
return
}
val factory = viewFinder.meteringPointFactory
val builder = FocusMeteringAction.Builder(factory.createPoint(x, y))
// Auto-cancel will clear focus points (and engage AF) after a duration
if (autoFocus == "off") builder.disableAutoCancel()
camera?.cameraControl?.startFocusAndMetering(builder.build())
}
private fun onBarcodeRead(barcodes: List<String>) {
val event: WritableMap = Arguments.createMap()
event.putArray("barcodes", Arguments.makeNativeArray(barcodes))
currentContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(
id,
"onBarcodeRead",
event
)
}
private fun onOrientationChange(orientation: Int) {
val remappedOrientation = when (orientation) {
Surface.ROTATION_0 -> RNCameraKitModule.PORTRAIT
Surface.ROTATION_90 -> RNCameraKitModule.LANDSCAPE_LEFT
Surface.ROTATION_180 -> RNCameraKitModule.PORTRAIT_UPSIDE_DOWN
Surface.ROTATION_270 -> RNCameraKitModule.LANDSCAPE_RIGHT
else -> {
Log.e(TAG, "CameraView: Unknown device orientation detected: $orientation")
return
}
}
val event: WritableMap = Arguments.createMap()
event.putInt("orientation", remappedOrientation)
currentContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(
id,
"onOrientationChange",
event
)
}
private fun onPictureTaken(uri: String) {
val event: WritableMap = Arguments.createMap()
event.putString("uri", uri)
currentContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(
id,
"onPictureTaken",
event
)
}
fun setFlashMode(mode: String?) {
val imageCapture = imageCapture ?: return
val camera = camera ?: return
when (mode) {
"torch" -> camera.cameraControl.enableTorch(true)
"on" -> {
camera.cameraControl.enableTorch(false)
imageCapture.flashMode = ImageCapture.FLASH_MODE_ON
}
"off" -> {
camera.cameraControl.enableTorch(false)
imageCapture.flashMode = ImageCapture.FLASH_MODE_OFF
}
else -> { // 'auto' and any wrong values
imageCapture.flashMode = ImageCapture.FLASH_MODE_AUTO
camera.cameraControl.enableTorch(false)
}
}
}
fun setAutoFocus(mode: String = "on") {
autoFocus = mode
when(mode) {
// "cancel" clear AF points and engages continuous auto-focus
"on" -> camera?.cameraControl?.cancelFocusAndMetering()
// 'off': Handled when you tap to focus
}
}
fun setScanBarcode(enabled: Boolean) {
val restartCamera = enabled != scanBarcode
scanBarcode = enabled
if (restartCamera) startCamera()
}
fun setType(type: String = "back") {
val newLensType = when (type) {
"front" -> CameraSelector.LENS_FACING_FRONT
else -> CameraSelector.LENS_FACING_BACK
}
val restartCamera = lensType != newLensType
lensType = newLensType
if (restartCamera) startCamera()
}
fun setOutputPath(path: String) {
outputPath = path
}
private fun hasPermissions(): Boolean {
val requiredPermissions = arrayOf(Manifest.permission.CAMERA)
if (requiredPermissions.all {
ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
}) {
return true
}
ActivityCompat.requestPermissions(
getActivity(),
requiredPermissions,
42 // random callback identifier
)
return false
}
}

View File

@ -0,0 +1,79 @@
package com.rncamerakit
import android.util.Log
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableType
import com.facebook.react.common.MapBuilder
import com.facebook.react.common.ReactConstants.TAG
import com.facebook.react.uimanager.SimpleViewManager
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.annotations.ReactProp
class CKCameraManager : SimpleViewManager<CKCamera>() {
override fun getName() : String {
return "CKCameraManager"
}
override fun createViewInstance(context: ThemedReactContext): CKCamera {
return CKCamera(context)
}
override fun receiveCommand(view: CKCamera, commandId: String?, args: ReadableArray?) {
var logCommand = "CameraManager received command $commandId("
for (i in 0..(args?.size() ?: 0)) {
if (i > 0) {
logCommand += ", "
}
logCommand += when (args?.getType(0)) {
ReadableType.Null -> "Null"
ReadableType.Array -> "Array"
ReadableType.Boolean -> "Boolean"
ReadableType.Map -> "Map"
ReadableType.Number -> "Number"
ReadableType.String -> "String"
else -> ""
}
}
logCommand += ")"
Log.d(TAG, logCommand)
}
override fun getExportedCustomDirectEventTypeConstants(): Map<String, Any> {
return MapBuilder.of(
"onOrientationChange", MapBuilder.of("registrationName", "onOrientationChange"),
"onBarcodeRead", MapBuilder.of("registrationName", "onBarcodeRead"),
"onPictureTaken", MapBuilder.of("registrationName", "onPictureTaken")
)
}
@ReactProp(name = "flashMode")
fun setFlashMode(view: CKCamera, mode: String?) {
view.setFlashMode(mode)
}
@ReactProp(name = "scanBarcode")
fun setScanBarcode(view: CKCamera, enabled: Boolean) {
view.setScanBarcode(enabled)
}
@ReactProp(name = "type")
fun setType(view: CKCamera, type: String) {
view.setType(type)
}
@ReactProp(name = "autoFocus")
fun setAutoFocus(view: CKCamera, mode: String) {
view.setAutoFocus(mode)
}
@ReactProp(name = "outputPath")
fun setOutputPath(view: CKCamera, path: String) {
view.setOutputPath(path)
}
@ReactProp(name = "shutterAnimationDuration")
fun setShutterAnimationDuration(view: CKCamera, duration: Int) {
view.setShutterAnimationDuration(duration)
}
}

View File

@ -1,10 +0,0 @@
package com.rncamerakit;
import android.os.Build;
public class DeviceUtils {
public static boolean isGoogleDevice() {
return (Build.MANUFACTURER.toLowerCase().contains("google") && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) ||
(Build.MANUFACTURER.toLowerCase().contains("lge") && Build.BRAND.toLowerCase().equals("google"));
}
}

View File

@ -0,0 +1,29 @@
package com.rncamerakit
import android.annotation.SuppressLint
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.mlkit.vision.common.InputImage
class QRCodeAnalyzer (
private val onQRCodesDetected: (qrCodes: List<String>) -> Unit
) : ImageAnalysis.Analyzer {
@SuppressLint("UnsafeExperimentalUsageError")
override fun analyze(image: ImageProxy) {
val inputImage = InputImage.fromMediaImage(image.image!!, image.imageInfo.rotationDegrees)
val scanner = BarcodeScanning.getClient()
scanner.process(inputImage)
.addOnSuccessListener { barcodes ->
val strBarcodes = mutableListOf<String>()
barcodes.forEach { barcode ->
strBarcodes.add(barcode.rawValue ?: return@forEach)
}
onQRCodesDetected(strBarcodes)
}
.addOnCompleteListener{
image.close()
}
}
}

View File

@ -0,0 +1,40 @@
package com.rncamerakit
import com.facebook.react.bridge.*
import com.facebook.react.uimanager.UIManagerModule
class RNCameraKitModule(private val reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
companion object {
// 0-indexed, rotates counter-clockwise
// Values map to CameraX's Surface.ROTATION_* constants
const val PORTRAIT = 0 // ⬆️
const val LANDSCAPE_LEFT = 1 // ⬅️
const val PORTRAIT_UPSIDE_DOWN = 2 // ⬇️
const val LANDSCAPE_RIGHT = 3 // ➡️
}
override fun getName(): String {
return "RNCameraKitModule"
}
override fun getConstants(): Map<String, Any> {
return hashMapOf(
"PORTRAIT" to PORTRAIT,
"PORTRAIT_UPSIDE_DOWN" to PORTRAIT_UPSIDE_DOWN,
"LANDSCAPE_LEFT" to LANDSCAPE_LEFT,
"LANDSCAPE_RIGHT" to LANDSCAPE_RIGHT
)
}
@ReactMethod
fun capture(options: ReadableMap, viewTag: Int, promise: Promise) {
// CameraManager does not allow us to return values
val context = reactContext
val uiManager = context.getNativeModule(UIManagerModule::class.java)
context.runOnUiQueueThread {
val view = uiManager.resolveView(viewTag) as CKCamera
view.capture(options.toHashMap(), promise)
}
}
}

View File

@ -1,55 +0,0 @@
package com.rncamerakit;
import androidx.annotation.Nullable;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import com.rncamerakit.camera.CameraModule;
import com.rncamerakit.camera.CameraViewManager;
import com.rncamerakit.camera.permission.CameraPermissionRequestCallback;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class RNCameraKitPackage implements ReactPackage {
@Nullable private CameraPermissionRequestCallback cameraPermissionRequestCallback;
public RNCameraKitPackage() {
}
public RNCameraKitPackage(CameraPermissionRequestCallback cameraPermissionRequestCallback) {
this.cameraPermissionRequestCallback = cameraPermissionRequestCallback;
}
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
CameraModule cameraModule = new CameraModule(reactContext);
if (cameraPermissionRequestCallback != null) {
cameraPermissionRequestCallback.setCameraModule(cameraModule);
}
modules.add(cameraModule);
return modules;
}
// Deprecated RN 0.47
public List<Class<? extends JavaScriptModule>> createJSModules() {
return Collections.emptyList();
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
List<ViewManager> viewManagers = new ArrayList<>();
viewManagers.add(new CameraViewManager());
return viewManagers;
}
}

View File

@ -0,0 +1,22 @@
package com.rncamerakit
import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager
import java.util.*
class RNCameraKitPackage : ReactPackage {
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
val modules: MutableList<NativeModule> = ArrayList()
val cameraModule = RNCameraKitModule(reactContext)
modules.add(cameraModule)
return modules
}
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
val viewManagers: MutableList<ViewManager<*, *>> = ArrayList()
viewManagers.add(CKCameraManager())
return viewManagers
}
}

View File

@ -1,261 +0,0 @@
package com.rncamerakit;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.hardware.Camera;
import android.net.Uri;
import android.os.AsyncTask;
import android.provider.MediaStore;
import android.util.Log;
import android.util.Patterns;
import androidx.annotation.Nullable;
import com.drew.imaging.ImageMetadataReader;
import com.drew.imaging.ImageProcessingException;
import com.drew.metadata.Metadata;
import com.drew.metadata.MetadataException;
import com.drew.metadata.exif.ExifIFD0Directory;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.WritableMap;
import com.rncamerakit.camera.CameraViewManager;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import static com.facebook.react.common.ReactConstants.TAG;
public class SaveImageTask extends AsyncTask<byte[], Void, Void> {
private final Context context;
private final Promise promise;
private boolean saveToCameraRoll;
private String bitmapUrl = null;
public SaveImageTask(Context context, Promise promise, boolean saveToCameraRoll) {
this.context = context;
this.promise = promise;
this.saveToCameraRoll = saveToCameraRoll;
}
public SaveImageTask(String bitmapUrl, Context context, Promise promise, boolean saveToCameraRoll) {
this(context, promise, saveToCameraRoll);
this.bitmapUrl = bitmapUrl;
if (this.bitmapUrl != null) {
this.bitmapUrl = this.bitmapUrl.replace("file://","");
}
}
private Bitmap getImageBitmapFromRemoteImageFile() {
Bitmap image;
try {
URL url = new URL(bitmapUrl);
image = BitmapFactory.decodeStream(url.openStream());
} catch (IOException e) {
image = null;
}
return image;
}
private Bitmap getImageBitmapFromLocalImageFile() {
Bitmap image;
FileInputStream fis;
File imageFile = new File(bitmapUrl);
try {
fis = new FileInputStream(imageFile);
image = BitmapFactory.decodeStream(fis);
fis.close();
} catch (IOException e) {
e.printStackTrace();
image = null;
}
if (imageFile.exists()) {
imageFile.delete();
}
return image;
}
private Bitmap getImageBitmap(byte[]... data) {
Bitmap image;
if (bitmapUrl != null) {
if (Patterns.WEB_URL.matcher(bitmapUrl.toLowerCase()).matches()) {
image = getImageBitmapFromRemoteImageFile();
} else {
image = getImageBitmapFromLocalImageFile();
}
}
else {
byte[] rawImageData = data[0];
image = decodeAndRotateIfNeeded(rawImageData);
}
return image;
}
@Override
protected Void doInBackground(byte[]... data) {
Bitmap image = getImageBitmap(data);
if (image == null) {
promise.reject("CameraKit", "failed to get Bitmap image");
return null;
}
WritableMap imageInfo = saveToCameraRoll ? saveToMediaStore(image) : saveTempImageFile(image);
if (imageInfo == null)
promise.reject("CameraKit", "failed to save image to MediaStore");
else {
promise.resolve(imageInfo);
CameraViewManager.reconnect();
}
return null;
}
private WritableMap createImageInfo(String fileUri, String id, String fileName, long fileSize, int width, int height) {
WritableMap imageInfo = Arguments.createMap();
imageInfo.putString("uri", fileUri);
imageInfo.putString("id", id);
imageInfo.putString("name", fileName);
imageInfo.putInt("size", (int) fileSize);
imageInfo.putInt("width", width);
imageInfo.putInt("height", height);
return imageInfo;
}
private WritableMap saveToMediaStore(Bitmap image) {
try {
String fileUri = MediaStore.Images.Media.insertImage(context.getContentResolver(), image, System.currentTimeMillis() + "", "");
Cursor cursor = context.getContentResolver().query(Uri.parse(fileUri), new String[]{
MediaStore.Images.ImageColumns.DATA,
MediaStore.Images.ImageColumns.DISPLAY_NAME
}, null, null, null);
cursor.moveToFirst();
int pathIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATA);
int nameIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DISPLAY_NAME);
String filePath = cursor.getString(pathIndex);
String fileName = cursor.getString(nameIndex);
long fileSize = new File(filePath).length();
cursor.close();
return createImageInfo(fileUri, filePath, fileName, fileSize, image.getWidth(), image.getHeight());
} catch (Exception e) {
return null;
}
}
private Bitmap decodeAndRotateIfNeeded(byte[] rawImageData) {
Matrix bitmapMatrix = getRotationMatrix(rawImageData);
Bitmap image = BitmapFactory.decodeByteArray(rawImageData, 0, rawImageData.length);
if (bitmapMatrix.isIdentity())
return image;
else
return rotateImage(image, bitmapMatrix);
}
private Bitmap rotateImage(Bitmap image, Matrix bitmapMatrix) {
return Bitmap.createBitmap(image, 0, 0, image.getWidth(), image.getHeight(), bitmapMatrix, false);
}
private Matrix getRotationMatrix(byte[] rawImageData) {
try {
return tryGetRotationMatrix(rawImageData);
} catch (Exception e) {
return new Matrix();
}
}
private Matrix tryGetRotationMatrix(byte[] rawImageData) throws ImageProcessingException, IOException, MetadataException {
Matrix matrix = new Matrix();
Metadata metadata = readMetadata(rawImageData);
final ExifIFD0Directory exifIFD0Directory = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
boolean hasOrientation = exifIFD0Directory.containsTag(ExifIFD0Directory.TAG_ORIENTATION);
if (hasOrientation) {
final int exifOrientation = exifIFD0Directory.getInt(ExifIFD0Directory.TAG_ORIENTATION);
boolean isFacingFront = CameraViewManager.getCameraInfo().facing == Camera.CameraInfo.CAMERA_FACING_FRONT;
convertExifOrientationToMatrix(matrix, exifOrientation, isFacingFront);
}
return matrix;
}
private void convertExifOrientationToMatrix(Matrix matrix, int exifOrientation, boolean isCameraFacingFront) {
switch (exifOrientation) {
case 1:
break; // top left
case 2:
matrix.postScale(-1, 1);
break; // top right
case 3:
matrix.postRotate(180);
break; // bottom right
case 4:
matrix.postRotate(180);
matrix.postScale(-1, 1);
break; // bottom left
case 5:
matrix.postRotate(90);
matrix.postScale(-1, 1);
break; // left top
case 6:
matrix.postRotate(90);
break; // right top
case 7:
matrix.postRotate(270);
matrix.postScale(-1, 1);
break; // right bottom
case 8:
matrix.postRotate(270);
break; // left bottom
default:
break; // Unknown
}
if (isCameraFacingFront) {
matrix.postRotate(180);
}
}
private Metadata readMetadata(byte[] rawImageData) throws ImageProcessingException, IOException {
Metadata metadata = null;
ByteArrayInputStream inputStream = null;
BufferedInputStream bufferedInputStream = null;
try {
inputStream = new ByteArrayInputStream(rawImageData);
bufferedInputStream = new BufferedInputStream(inputStream);
metadata = ImageMetadataReader.readMetadata(bufferedInputStream, rawImageData.length);
} finally {
if (bufferedInputStream != null) bufferedInputStream.close();
if (inputStream != null) inputStream.close();
}
return metadata;
}
@Nullable
private WritableMap saveTempImageFile(Bitmap image) {
File imageFile;
FileOutputStream outputStream;
Long tsLong = System.currentTimeMillis()/1000;
String fileName = "temp_Image_" + tsLong.toString() + ".jpg";
try {
imageFile = new File(context.getCacheDir(), fileName);
if (imageFile.exists()) {
imageFile.delete();
}
outputStream = new FileOutputStream(imageFile);
image.compress(Bitmap.CompressFormat.JPEG, 100, outputStream);
outputStream.close();
} catch (IOException e) {
Log.d(TAG, "Error accessing file: " + e.getMessage());
imageFile = null;
}
return (imageFile != null) ? createImageInfo(Uri.fromFile(imageFile).toString(), imageFile.getAbsolutePath(), fileName, imageFile.length(), image.getWidth(), image.getHeight()) : null;
}
}

View File

@ -1,22 +0,0 @@
package com.rncamerakit;
import android.content.Context;
import android.content.SharedPreferences;
// We're saving in shared preferences if a permission was requested since for some unknown reason,
// activitycompat.shouldshowrequestpermissionrationale always returned false
public class SharedPrefs {
public static boolean getBoolean(Context context, String key) {
return prefs(context).getBoolean(key, false);
}
public static void putBoolean(Context context, String key, boolean value) {
prefs(context).edit().putBoolean(key, value).apply();
}
private static SharedPreferences prefs(Context context) {
return context.getSharedPreferences("RN_CAMERA_KIT", Context.MODE_PRIVATE);
}
}

View File

@ -1,224 +0,0 @@
package com.rncamerakit;
import android.content.ContentResolver;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import androidx.annotation.NonNull;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Date;
import javax.annotation.Nullable;
public class Utils {
private final static String CONTENT_PREFIX = "content://";
public final static String FILE_PREFIX = "file://";
private static final int MAX_SAMPLE_SIZE = 8;
private static final float MAX_SCREEN_RATIO = 16 / 9f;
@Nullable
public static String getStringSafe(ReadableMap map, String key) {
if (map.hasKey(key)) {
return map.getString(key);
}
return null;
}
public static @Nullable Integer getIntSafe(ReadableMap map, String key) {
if (map.hasKey(key)) {
return map.getInt(key);
}
return null;
}
public static @Nullable Boolean getBooleanSafe(ReadableMap map, String key) {
if (map.hasKey(key)) {
return map.getBoolean(key);
}
return null;
}
public static @NonNull ArrayList<String> readableArrayToList(ReadableArray items) {
ArrayList<String> list = new ArrayList<>();
for(int i = 0; i < items.size(); i++) {
list.add(items.getString(i));
}
return list;
}
@NonNull
public static WritableMap resizeImage(Context context, ReadableMap image, String imageUrlString, int maxResolution, int compressionQuality) throws IOException {
Bitmap sourceImage;
sourceImage = Utils.loadBitmapFromFile(context, imageUrlString, maxResolution, maxResolution);
if (sourceImage == null) {
throw new IOException("Unable to load source image from path");
}
Bitmap scaledImage = Utils.resizeImage(sourceImage, maxResolution, maxResolution);
if (sourceImage != scaledImage) {
sourceImage.recycle();
}
// Save the resulting image
File path = context.getCacheDir();
String resizedImagePath = Utils.saveImage(scaledImage, path, Long.toString(new Date().getTime()), Bitmap.CompressFormat.JPEG, compressionQuality);
// Clean up remaining image
scaledImage.recycle();
WritableMap ans = Arguments.createMap();
ans.merge(image);
ans.putString("uri", FILE_PREFIX+resizedImagePath);
ans.putInt("size", (int)new File(resizedImagePath).length());
ans.putInt("width", scaledImage.getWidth());
ans.putInt("height", scaledImage.getHeight());
return ans;
}
/**
* Resize the specified bitmap, keeping its aspect ratio.
*/
private static Bitmap resizeImage(Bitmap image, int maxWidth, int maxHeight) {
Bitmap newImage = null;
if (image == null) {
return null; // Can't load the image from the given path.
}
if (maxHeight > 0 && maxWidth > 0) {
float width = image.getWidth();
float height = image.getHeight();
float ratio = Math.min((float)maxWidth / width, (float)maxHeight / height);
int finalWidth = (int) (width * ratio);
int finalHeight = (int) (height * ratio);
newImage = Bitmap.createScaledBitmap(image, finalWidth, finalHeight, true);
}
return newImage;
}
/**
* Compute the inSampleSize value to use to load a bitmap.
* Adapted from https://developer.android.com/training/displaying-bitmaps/load-bitmap.html
*/
public static int calculateInSampleSize(int width, int height, int reqWidth, int reqHeight) {
if (reqHeight == 0 || reqWidth == 0) {
return 1;
}
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
while (inSampleSize <= MAX_SAMPLE_SIZE
&& (halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
private static Bitmap loadBitmap(Context context, String imagePath, BitmapFactory.Options options) throws IOException {
Bitmap sourceImage = null;
if (!imagePath.startsWith(CONTENT_PREFIX)) {
try {
sourceImage = BitmapFactory.decodeFile(imagePath, options);
} catch (Exception e) {
e.printStackTrace();
throw new IOException("Error decoding image file");
}
} else {
ContentResolver cr = context.getContentResolver();
InputStream input = cr.openInputStream(Uri.parse(imagePath));
if (input != null) {
sourceImage = BitmapFactory.decodeStream(input, null, options);
input.close();
}
}
return sourceImage;
}
/**
* Loads the bitmap resource from the file specified in imagePath.
*/
private static Bitmap loadBitmapFromFile(Context context, String imagePath, int newWidth,
int newHeight) throws IOException {
// Decode the image bounds to find the size of the source image.
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
loadBitmap(context, imagePath, options);
// Set a sample size according to the image size to lower memory usage.
options.inSampleSize = calculateInSampleSize(options.outWidth, options.outHeight , newWidth, newHeight);
options.inJustDecodeBounds = false;
return loadBitmap(context, imagePath, options);
}
/**
* Save the given bitmap in a directory. Extension is automatically generated using the bitmap format.
*/
private static String saveImage(Bitmap bitmap, File saveDirectory, String fileName,
Bitmap.CompressFormat compressFormat, int quality)
throws IOException {
if (bitmap == null) {
throw new IOException("The bitmap couldn't be resized");
}
File newFile = new File(saveDirectory, fileName + "." + compressFormat.name());
if(!newFile.createNewFile()) {
throw new IOException("The file already exists");
}
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
bitmap.compress(compressFormat, quality, outputStream);
byte[] bitmapData = outputStream.toByteArray();
outputStream.flush();
outputStream.close();
FileOutputStream fos = new FileOutputStream(newFile);
fos.write(bitmapData);
fos.flush();
fos.close();
return newFile.getAbsolutePath();
}
/**
* Since Camera API 1 doesn't support the new 18:9 and 18.5:9 screen aspect ratio, we convert to the
* max supported aspect ratio - 16:9
*/
public static int convertDeviceHeightToSupportedAspectRatio(float actualWidth, float actualHeight) {
return (int) (actualHeight / actualWidth > MAX_SCREEN_RATIO ? actualWidth * MAX_SCREEN_RATIO : actualHeight);
}
public static void runOnWorkerThread(Runnable runnable) {
new Thread(runnable).start();
}
}

View File

@ -1,116 +0,0 @@
package com.rncamerakit.camera;
import android.hardware.Camera;
import com.facebook.react.bridge.LifecycleEventListener;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.rncamerakit.camera.commands.Capture;
import com.rncamerakit.camera.permission.CameraPermission;
public class CameraModule extends ReactContextBaseJavaModule {
private final CameraPermission cameraPermission;
private Promise checkPermissionStatusPromise;
public CameraModule(ReactApplicationContext reactContext) {
super(reactContext);
cameraPermission = new CameraPermission();
checkPermissionWhenActivityIsAvailable();
}
private void checkPermissionWhenActivityIsAvailable() {
getReactApplicationContext().addLifecycleEventListener(new LifecycleEventListener() {
@Override
public void onHostResume() {
if (checkPermissionStatusPromise != null && getCurrentActivity() != null) {
getCurrentActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
checkPermissionStatusPromise.resolve(cameraPermission.checkAuthorizationStatus(getCurrentActivity()));
checkPermissionStatusPromise = null;
}
});
}
}
@Override
public void onHostPause() {
}
@Override
public void onHostDestroy() {
}
});
}
@Override
public String getName() {
return "RNKitCameraModule";
}
@ReactMethod
public void checkDeviceCameraAuthorizationStatus(Promise promise) {
if (getCurrentActivity() == null) {
checkPermissionStatusPromise = promise;
} else {
promise.resolve(cameraPermission.checkAuthorizationStatus(getCurrentActivity()));
}
}
@ReactMethod
public void requestDeviceCameraAuthorization(Promise promise) {
cameraPermission.requestAccess(getCurrentActivity(), promise);
}
@ReactMethod
public void hasFrontCamera(Promise promise) {
int numCameras = Camera.getNumberOfCameras();
for (int i = 0; i < numCameras; i++) {
Camera.CameraInfo info = new Camera.CameraInfo();
Camera.getCameraInfo(i, info);
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
promise.resolve(true);
return;
}
}
promise.resolve(false);
}
@ReactMethod
public void hasFlashForCurrentCamera(Promise promise) {
Camera camera = CameraViewManager.getCamera();
promise.resolve(camera.getParameters().getSupportedFlashModes() != null);
}
@ReactMethod
public void changeCamera(Promise promise) {
promise.resolve(CameraViewManager.changeCamera());
}
@ReactMethod
public void setFlashMode(String mode, Promise promise) {
promise.resolve(CameraViewManager.setFlashMode(mode));
}
@ReactMethod
public void getFlashMode(Promise promise) {
Camera camera = CameraViewManager.getCamera();
promise.resolve(camera.getParameters().getFlashMode());
}
@ReactMethod
public void capture(boolean saveToCameraRoll, final Promise promise) {
new Capture(getReactApplicationContext(), saveToCameraRoll).execute(promise);
}
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
cameraPermission.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}

View File

@ -1,141 +0,0 @@
package com.rncamerakit.camera;
import android.graphics.Color;
import android.graphics.Rect;
import androidx.annotation.ColorInt;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.widget.FrameLayout;
import com.facebook.react.uimanager.ThemedReactContext;
import com.rncamerakit.Utils;
import com.rncamerakit.camera.barcode.BarcodeFrame;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
public class CameraView extends FrameLayout implements SurfaceHolder.Callback {
private SurfaceView surface;
private boolean showFrame;
private Rect frameRect;
private BarcodeFrame barcodeFrame;
@ColorInt private int frameColor = Color.GREEN;
@ColorInt private int laserColor = Color.RED;
public CameraView(ThemedReactContext context) {
super(context);
surface = new SurfaceView(context);
setBackgroundColor(Color.BLACK);
addView(surface, MATCH_PARENT, MATCH_PARENT);
surface.getHolder().addCallback(this);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
int actualPreviewWidth = getResources().getDisplayMetrics().widthPixels;
int actualPreviewHeight = getResources().getDisplayMetrics().heightPixels;
int height = Utils.convertDeviceHeightToSupportedAspectRatio(actualPreviewWidth, actualPreviewHeight);
surface.layout(0, 0, actualPreviewWidth, height);
if (barcodeFrame != null) {
((View) barcodeFrame).layout(0, 0, actualPreviewWidth, height);
}
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
CameraViewManager.setCameraView(this);
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
CameraViewManager.setCameraView(this);
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
CameraViewManager.removeCameraView();
}
public SurfaceHolder getHolder() {
return surface.getHolder();
}
private final Runnable measureAndLayout = new Runnable() {
@Override
public void run() {
measure(
MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.EXACTLY));
layout(getLeft(), getTop(), getRight(), getBottom());
}
};
@Override
public void requestLayout() {
super.requestLayout();
post(measureAndLayout);
}
public void setShowFrame(boolean showFrame) {
this.showFrame = showFrame;
}
public void showFrame() {
if (showFrame) {
barcodeFrame = new BarcodeFrame(getContext());
barcodeFrame.setFrameColor(frameColor);
barcodeFrame.setLaserColor(laserColor);
addView(barcodeFrame);
requestLayout();
}
}
public Rect getFramingRectInPreview(int previewWidth, int previewHeight) {
if (frameRect == null) {
if (barcodeFrame != null) {
Rect framingRect = new Rect(barcodeFrame.getFrameRect());
int frameWidth = barcodeFrame.getWidth();
int frameHeight = barcodeFrame.getHeight();
if (previewWidth < frameWidth) {
framingRect.left = framingRect.left * previewWidth / frameWidth;
framingRect.right = framingRect.right * previewWidth / frameWidth;
}
if (previewHeight < frameHeight) {
framingRect.top = framingRect.top * previewHeight / frameHeight;
framingRect.bottom = framingRect.bottom * previewHeight / frameHeight;
}
frameRect = framingRect;
} else {
frameRect = new Rect(0, 0, previewWidth, previewHeight);
}
}
return frameRect;
}
public void setFrameColor(@ColorInt int color) {
this.frameColor = color;
if (barcodeFrame != null) {
barcodeFrame.setFrameColor(color);
}
}
public void setLaserColor(@ColorInt int color) {
this.laserColor = color;
if (barcodeFrame != null) {
barcodeFrame.setLaserColor(laserColor);
}
}
/**
* Set background color for Surface view on the period, while camera is not loaded yet.
* Provides opportunity for user to hide period while camera is loading
* @param color - color of the surfaceview
*/
public void setSurfaceBgColor(@ColorInt int color) {
surface.setBackgroundColor(color);
}
}

View File

@ -1,339 +0,0 @@
package com.rncamerakit.camera;
import android.content.Context;
import android.graphics.Color;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.graphics.Rect;
import android.hardware.Camera;
import android.hardware.SensorManager;
import androidx.annotation.ColorInt;
import androidx.annotation.IntRange;
import android.view.Display;
import android.view.OrientationEventListener;
import android.view.WindowManager;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.uimanager.SimpleViewManager;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.uimanager.events.RCTEventEmitter;
import com.google.zxing.Result;
import com.rncamerakit.Utils;
import com.rncamerakit.camera.barcode.BarcodeScanner;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.annotation.Nullable;
import static com.rncamerakit.camera.Orientation.getSupportedRotation;
@SuppressWarnings("MagicNumber deprecation")
// We're still using Camera API 1, everything is deprecated
public class CameraViewManager extends SimpleViewManager<CameraView> {
private static Camera camera = null;
private static int currentCamera = 0;
private static String flashMode = Camera.Parameters.FLASH_MODE_AUTO;
private static Stack<CameraView> cameraViews = new Stack<>();
private static ThemedReactContext reactContext;
private static OrientationEventListener orientationListener;
private static int currentRotation = 0;
private static AtomicBoolean cameraReleased = new AtomicBoolean(false);
private static boolean shouldScan = false;
private static BarcodeScanner scanner;
private static Camera.PreviewCallback previewCallback = new Camera.PreviewCallback() {
@Override
public void onPreviewFrame(final byte[] data, final Camera camera) {
Utils.runOnWorkerThread(new Runnable() {
@Override
public void run() {
if (scanner != null) {
scanner.onPreviewFrame(data, camera);
}
}
});
}
};
public static Camera getCamera() {
return camera;
}
@Override
public String getName() {
return "CameraView";
}
@Override
protected CameraView createViewInstance(ThemedReactContext reactContext) {
CameraViewManager.reactContext = reactContext;
return new CameraView(reactContext);
}
static void setCameraView(CameraView cameraView) {
if (!cameraViews.isEmpty() && cameraViews.peek() == cameraView) return;
CameraViewManager.cameraViews.push(cameraView);
connectHolder();
createOrientationListener();
}
private static void createOrientationListener() {
if (orientationListener != null) return;
orientationListener = new OrientationEventListener(reactContext, SensorManager.SENSOR_DELAY_NORMAL) {
@Override
public void onOrientationChanged(@IntRange(from = -1, to = 359) int angle) {
if (angle == OrientationEventListener.ORIENTATION_UNKNOWN) return;
setCameraRotation(359 - angle, false);
}
};
orientationListener.enable();
}
static boolean setFlashMode(String mode) {
if (camera == null) {
return false;
}
Camera.Parameters parameters = camera.getParameters();
List supportedModes = parameters.getSupportedFlashModes();
if (supportedModes != null && supportedModes.contains(mode)) {
flashMode = mode;
parameters.setFlashMode(flashMode);
camera.setParameters(parameters);
camera.startPreview();
return true;
} else {
return false;
}
}
static boolean changeCamera() {
if (Camera.getNumberOfCameras() == 1) {
return false;
}
currentCamera++;
currentCamera = currentCamera % Camera.getNumberOfCameras();
initCamera();
connectHolder();
return true;
}
private static void initCamera() {
if (camera != null) {
releaseCamera();
}
try {
camera = Camera.open(currentCamera);
updateCameraSize();
cameraReleased.set(false);
setCameraRotation(currentRotation, true);
} catch (RuntimeException e) {
e.printStackTrace();
}
setBarcodeScanner();
}
private static void releaseCamera() {
camera.setOneShotPreviewCallback(null);
cameraReleased.set(true);
camera.release();
}
private static void connectHolder() {
if (cameraViews.isEmpty() || cameraViews.peek().getHolder() == null) return;
new Thread(new Runnable() {
@Override
public void run() {
if (camera == null) {
initCamera();
}
if (cameraViews.isEmpty()) {
return;
}
cameraViews.peek().post(new Runnable() {
@Override
public void run() {
try {
camera.stopPreview();
camera.setPreviewDisplay(cameraViews.peek().getHolder());
camera.startPreview();
if (shouldScan) {
camera.setOneShotPreviewCallback(previewCallback);
}
cameraViews.peek().setSurfaceBgColor(Color.TRANSPARENT);
cameraViews.peek().showFrame();
} catch (IOException | RuntimeException e) {
e.printStackTrace();
}
}
});
}
}).start();
}
static void removeCameraView() {
if (!cameraViews.isEmpty()) {
cameraViews.pop();
}
if (!cameraViews.isEmpty()) {
connectHolder();
} else if (camera != null) {
releaseCamera();
camera = null;
}
if (cameraViews.isEmpty()) {
clearOrientationListener();
}
}
private static void clearOrientationListener() {
if (orientationListener != null) {
orientationListener.disable();
orientationListener = null;
}
}
private static void setCameraRotation(int rotation, boolean force) {
if (camera == null) return;
int supportedRotation = getSupportedRotation(rotation);
if (supportedRotation == currentRotation && !force) return;
currentRotation = supportedRotation;
if (cameraReleased.get()) return;
Camera.Parameters parameters = camera.getParameters();
parameters.setRotation(supportedRotation);
parameters.setPictureFormat(PixelFormat.JPEG);
camera.setDisplayOrientation(Orientation.getDeviceOrientation(reactContext.getCurrentActivity()));
camera.setParameters(parameters);
}
public static Camera.CameraInfo getCameraInfo() {
Camera.CameraInfo info = new Camera.CameraInfo();
Camera.getCameraInfo(currentCamera, info);
return info;
}
private static Camera.Size getOptimalPreviewSize(List<Camera.Size> sizes, int w, int h) {
final double ASPECT_TOLERANCE = 0.15;
double targetRatio = (double) h / w;
if (sizes == null) return null;
Camera.Size optimalSize = null;
double minDiff = Double.MAX_VALUE;
for (Camera.Size size : sizes) {
double ratio = (double) size.width / size.height;
if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE) continue;
if (Math.abs(size.height - h) < minDiff) {
optimalSize = size;
minDiff = Math.abs(size.height - h);
}
}
if (optimalSize == null) {
minDiff = Double.MAX_VALUE;
for (Camera.Size size : sizes) {
if (Math.abs(size.height - h) < minDiff) {
optimalSize = size;
minDiff = Math.abs(size.height - h);
}
}
}
return optimalSize;
}
private static void updateCameraSize() {
try {
Camera camera = CameraViewManager.getCamera();
WindowManager wm = (WindowManager) reactContext.getSystemService(Context.WINDOW_SERVICE);
Display display = wm.getDefaultDisplay();
Point size = new Point();
display.getSize(size);
size.y = Utils.convertDeviceHeightToSupportedAspectRatio(size.x, size.y);
if (camera == null) return;
List<Camera.Size> supportedPreviewSizes = camera.getParameters().getSupportedPreviewSizes();
List<Camera.Size> supportedPictureSizes = camera.getParameters().getSupportedPictureSizes();
Camera.Size optimalSize = getOptimalPreviewSize(supportedPreviewSizes, size.x, size.y);
Camera.Size optimalPictureSize = getOptimalPreviewSize(supportedPictureSizes, size.x, size.y);
Camera.Parameters parameters = camera.getParameters();
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
parameters.setPreviewSize(optimalSize.width, optimalSize.height);
parameters.setPictureSize(optimalPictureSize.width, optimalPictureSize.height);
parameters.setFlashMode(flashMode);
camera.setParameters(parameters);
} catch (RuntimeException ignored) {
}
}
public static void reconnect() {
connectHolder();
}
public static int getRotationCount() {
return currentRotation / 90;
}
public static void setBarcodeScanner() {
scanner = new BarcodeScanner(previewCallback, new BarcodeScanner.ResultHandler() {
@Override
public void handleResult(Result result) {
WritableMap event = Arguments.createMap();
event.putString("codeStringValue", result.getText());
if (!cameraViews.empty())
reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(cameraViews.peek().getId(), "onReadCode", event);
}
});
}
@Nullable
@Override
public Map<String, Object> getExportedCustomDirectEventTypeConstants() {
return MapBuilder.<String, Object>builder()
.put("onReadCode",
MapBuilder.of("registrationName", "onReadCode"))
.build();
}
@ReactProp(name = "scanBarcode")
public void setShouldScan(CameraView view, boolean scanBarcode) {
shouldScan = scanBarcode;
if (shouldScan && camera != null) {
camera.setOneShotPreviewCallback(previewCallback);
}
}
@ReactProp(name = "showFrame", defaultBoolean = false)
public void setFrame(CameraView view, boolean show) {
view.setShowFrame(show);
}
@ReactProp(name = "frameColor", defaultInt = Color.GREEN)
public void setFrameColor(CameraView view, @ColorInt int color) {
view.setFrameColor(color);
}
@ReactProp(name = "laserColor", defaultInt = Color.RED)
public void setLaserColor(CameraView view, @ColorInt int color) {
view.setLaserColor(color);
}
@ReactProp(name = "surfaceColor")
public void setSurfaceBackground(CameraView view, @ColorInt int color) {
view.setSurfaceBgColor(color);
}
public static synchronized Rect getFramingRectInPreview(int previewWidth, int previewHeight) {
return cameraViews.peek().getFramingRectInPreview(previewWidth, previewHeight);
}
}

View File

@ -1,74 +0,0 @@
package com.rncamerakit.camera;
import android.app.Activity;
import android.hardware.Camera;
import android.view.Surface;
import com.rncamerakit.DeviceUtils;
import static com.rncamerakit.camera.CameraViewManager.getCameraInfo;
@SuppressWarnings({"MagicNumber", "deprecation"})
class Orientation {
private static final int PORTRAIT_ROTATION = 90;
static int getDeviceOrientation(Activity activity) {
if (activity == null) return PORTRAIT_ROTATION;
int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
Camera.CameraInfo info = getCameraInfo();
int degrees = 0;
switch (rotation) {
case Surface.ROTATION_0: degrees = 0; break;
case Surface.ROTATION_90: degrees = 90; break;
case Surface.ROTATION_180: degrees = 180; break;
case Surface.ROTATION_270: degrees = 270; break;
}
int result;
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
result = (info.orientation + degrees) % 360;
result = (360 - result) % 360; // compensate the mirror
} else { // back-facing
result = (info.orientation - degrees + 360) % 360;
}
return result;
}
static int getSupportedRotation(int rotation) {
int degrees = convertRotationToSupportedAxis(rotation);
return isFrontFacingCamera() ? adaptFrontCamera(degrees) : adaptBackCamera(degrees);
}
private static int convertRotationToSupportedAxis(int rotation) {
if (rotation < 45) {
return 0;
} else if (rotation < 135) {
return 90;
} else if (rotation < 225) {
return 180;
} else if (rotation < 315){
return 270;
}
return 0;
}
private static boolean isFrontFacingCamera() {
return getCameraInfo().facing == Camera.CameraInfo.CAMERA_FACING_FRONT;
}
private static int adaptBackCamera(int degrees) {
return (getCameraInfo().orientation - degrees + 360) % 360;
}
private static int adaptFrontCamera(int degrees) {
if (DeviceUtils.isGoogleDevice()) {
int result = (getCameraInfo().orientation + degrees) % 360;
result = (result) % 360; // compensate the mirror
return result;
} else {
int result = (getCameraInfo().orientation + degrees + 180) % 360;
result = (result) % 360; // compensate the mirror
return result;
}
}
}

View File

@ -1,111 +0,0 @@
package com.rncamerakit.camera.barcode;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import androidx.annotation.ColorInt;
import android.view.View;
import com.rncamerakit.R;
public class BarcodeFrame extends View {
private static final int STROKE_WIDTH = 5;
private static final int ANIMATION_SPEED = 8;
private static final int WIDTH_SCALE = 7;
private static final double HEIGHT_SCALE = 2.75;
private Paint dimPaint;
private Paint framePaint;
private Paint borderPaint;
private Paint laserPaint;
private Rect frameRect;
private int width;
private int height;
private int borderMargin;
private long previousFrameTime = System.currentTimeMillis();
private int laserY;
public BarcodeFrame(Context context) {
super(context);
init(context);
}
private void init(Context context) {
framePaint = new Paint();
framePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
dimPaint = new Paint();
dimPaint.setStyle(Paint.Style.FILL);
dimPaint.setColor(context.getResources().getColor(R.color.bg_dark));
borderPaint = new Paint();
borderPaint.setStyle(Paint.Style.STROKE);
borderPaint.setStrokeWidth(STROKE_WIDTH);
laserPaint = new Paint();
laserPaint.setStyle(Paint.Style.STROKE);
laserPaint.setStrokeWidth(STROKE_WIDTH);
frameRect = new Rect();
borderMargin = context.getResources().getDimensionPixelSize(R.dimen.border_length);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
width = getMeasuredWidth();
height = getMeasuredHeight();
int marginWidth = width / WIDTH_SCALE;
int marginHeight = (int) (height / HEIGHT_SCALE);
frameRect.left = marginWidth;
frameRect.right = width - marginWidth;
frameRect.top = marginHeight;
frameRect.bottom = height - marginHeight;
}
@Override
protected void onDraw(Canvas canvas) {
long timeElapsed = (System.currentTimeMillis() - previousFrameTime);
super.onDraw(canvas);
canvas.drawRect(0, 0, width, height, dimPaint);
canvas.drawRect(frameRect, framePaint);
drawBorder(canvas);
drawLaser(canvas, timeElapsed);
previousFrameTime = System.currentTimeMillis();
this.invalidate(frameRect);
}
private void drawBorder(Canvas canvas) {
canvas.drawLine(frameRect.left, frameRect.top, frameRect.left, frameRect.top + borderMargin, borderPaint);
canvas.drawLine(frameRect.left, frameRect.top, frameRect.left + borderMargin, frameRect.top, borderPaint);
canvas.drawLine(frameRect.left, frameRect.bottom, frameRect.left, frameRect.bottom - borderMargin, borderPaint);
canvas.drawLine(frameRect.left, frameRect.bottom, frameRect.left + borderMargin, frameRect.bottom, borderPaint);
canvas.drawLine(frameRect.right, frameRect.top, frameRect.right - borderMargin, frameRect.top, borderPaint);
canvas.drawLine(frameRect.right, frameRect.top, frameRect.right, frameRect.top + borderMargin, borderPaint);
canvas.drawLine(frameRect.right, frameRect.bottom, frameRect.right, frameRect.bottom - borderMargin, borderPaint);
canvas.drawLine(frameRect.right, frameRect.bottom, frameRect.right - borderMargin, frameRect.bottom, borderPaint);
}
private void drawLaser(Canvas canvas, long timeElapsed) {
if (laserY > frameRect.bottom || laserY < frameRect.top) laserY = frameRect.top;
canvas.drawLine(frameRect.left + STROKE_WIDTH, laserY, frameRect.right - STROKE_WIDTH, laserY, laserPaint);
laserY += (timeElapsed) / ANIMATION_SPEED;
}
public Rect getFrameRect() {
return frameRect;
}
public void setFrameColor(@ColorInt int borderColor) {
borderPaint.setColor(borderColor);
}
public void setLaserColor(@ColorInt int laserColor) {
laserPaint.setColor(laserColor);
}
}

View File

@ -1,145 +0,0 @@
package com.rncamerakit.camera.barcode;
import android.graphics.Rect;
import android.hardware.Camera;
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.util.Log;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.DecodeHintType;
import com.google.zxing.LuminanceSource;
import com.google.zxing.MultiFormatReader;
import com.google.zxing.ReaderException;
import com.google.zxing.Result;
import com.google.zxing.common.HybridBinarizer;
import com.rncamerakit.camera.CameraViewManager;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
public class BarcodeScanner {
public interface ResultHandler {
void handleResult(Result result);
}
private MultiFormatReader mMultiFormatReader;
private static final List<BarcodeFormat> ALL_FORMATS = new ArrayList<>();
private ResultHandler resultHandler;
private Camera.PreviewCallback previewCallback;
static {
ALL_FORMATS.add(BarcodeFormat.AZTEC);
ALL_FORMATS.add(BarcodeFormat.CODABAR);
ALL_FORMATS.add(BarcodeFormat.CODE_39);
ALL_FORMATS.add(BarcodeFormat.CODE_93);
ALL_FORMATS.add(BarcodeFormat.CODE_128);
ALL_FORMATS.add(BarcodeFormat.DATA_MATRIX);
ALL_FORMATS.add(BarcodeFormat.EAN_8);
ALL_FORMATS.add(BarcodeFormat.EAN_13);
ALL_FORMATS.add(BarcodeFormat.ITF);
ALL_FORMATS.add(BarcodeFormat.MAXICODE);
ALL_FORMATS.add(BarcodeFormat.PDF_417);
ALL_FORMATS.add(BarcodeFormat.QR_CODE);
ALL_FORMATS.add(BarcodeFormat.RSS_14);
ALL_FORMATS.add(BarcodeFormat.RSS_EXPANDED);
ALL_FORMATS.add(BarcodeFormat.UPC_A);
ALL_FORMATS.add(BarcodeFormat.UPC_E);
ALL_FORMATS.add(BarcodeFormat.UPC_EAN_EXTENSION);
}
public BarcodeScanner(@NonNull Camera.PreviewCallback previewCallback, @NonNull ResultHandler resultHandler) {
Map<DecodeHintType, Object> hints = new EnumMap<>(DecodeHintType.class);
hints.put(DecodeHintType.POSSIBLE_FORMATS, ALL_FORMATS);
mMultiFormatReader = new MultiFormatReader();
mMultiFormatReader.setHints(hints);
this.previewCallback = previewCallback;
this.resultHandler = resultHandler;
}
public void onPreviewFrame(byte[] data, final Camera camera) {
try {
Camera.Size size = camera.getParameters().getPreviewSize();
int width = size.width;
int height = size.height;
int tmp = width;
width = height;
height = tmp;
data = getRotatedData(data, camera);
final Result result = decodeResult(getLuminanceSource(data, width, height));
if (result != null) {
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
resultHandler.handleResult(result);
}
});
}
camera.setOneShotPreviewCallback(previewCallback);
} catch (RuntimeException e) {
Log.w("CameraKit", e.toString());
}
}
@Nullable
private Result decodeResult(LuminanceSource source) {
Result rawResult = null;
if (source != null) {
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
try {
rawResult = mMultiFormatReader.decodeWithState(bitmap);
} catch (ReaderException ignored) {
} finally {
mMultiFormatReader.reset();
}
if (rawResult == null && source.isRotateSupported()) {
LuminanceSource rotatedSource = source.rotateCounterClockwise();
bitmap = new BinaryBitmap(new HybridBinarizer(rotatedSource));
try {
rawResult = mMultiFormatReader.decodeWithState(bitmap);
} catch (ReaderException ignored) {
} finally {
mMultiFormatReader.reset();
}
}
}
return rawResult;
}
private LuminanceSource getLuminanceSource(byte[] data, int width, int height) {
Rect rect = CameraViewManager.getFramingRectInPreview(width, height);
try {
return new RotateLuminanceSource(data, width, height, rect.left, rect.top,
rect.width(), rect.height(), false);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private byte[] getRotatedData(byte[] data, Camera camera) {
Camera.Size size = camera.getParameters().getPreviewSize();
int width = size.width;
int height = size.height;
byte[] rotatedData = new byte[data.length];
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++)
rotatedData[x * height + height - y - 1] = data[x + y * width];
}
return rotatedData;
}
}

View File

@ -1,164 +0,0 @@
package com.rncamerakit.camera.barcode;
import com.google.zxing.LuminanceSource;
/**
* This class is mostly copy of {@link com.google.zxing.PlanarYUVLuminanceSource} class.
* The only difference is adding {@code rotateCounterClockwise()} method
* It was copied due to PlanarYUVLuminanceSource being final, so it can not be extended
* Zxing license - {https://github.com/zxing/zxing/blob/master/LICENSE}
*/
public class RotateLuminanceSource extends LuminanceSource {
private static final int THUMBNAIL_SCALE_FACTOR = 2;
private final byte[] yuvData;
private final int dataWidth;
private final int dataHeight;
private final int left;
private final int top;
RotateLuminanceSource(byte[] yuvData,
int dataWidth,
int dataHeight,
int left,
int top,
int width,
int height,
boolean reverseHorizontal) {
super(width, height);
if (left + width > dataWidth || top + height > dataHeight) {
throw new IllegalArgumentException("Crop rectangle does not fit within image data.");
}
this.yuvData = yuvData;
this.dataWidth = dataWidth;
this.dataHeight = dataHeight;
this.left = left;
this.top = top;
if (reverseHorizontal) {
reverseHorizontal(width, height);
}
}
@Override
public byte[] getRow(int y, byte[] row) {
if (y < 0 || y >= getHeight()) {
throw new IllegalArgumentException("Requested row is outside the image: " + y);
}
int width = getWidth();
if (row == null || row.length < width) {
row = new byte[width];
}
int offset = (y + top) * dataWidth + left;
System.arraycopy(yuvData, offset, row, 0, width);
return row;
}
@Override
public byte[] getMatrix() {
int width = getWidth();
int height = getHeight();
// If the caller asks for the entire underlying image, save the copy and give them the
// original data. The docs specifically warn that result.length must be ignored.
if (width == dataWidth && height == dataHeight) {
return yuvData;
}
int area = width * height;
byte[] matrix = new byte[area];
int inputOffset = top * dataWidth + left;
// If the width matches the full width of the underlying data, perform a single copy.
if (width == dataWidth) {
System.arraycopy(yuvData, inputOffset, matrix, 0, area);
return matrix;
}
// Otherwise copy one cropped row at a time.
for (int y = 0; y < height; y++) {
int outputOffset = y * width;
System.arraycopy(yuvData, inputOffset, matrix, outputOffset, width);
inputOffset += dataWidth;
}
return matrix;
}
@Override
public boolean isCropSupported() {
return true;
}
@Override
public LuminanceSource crop(int left, int top, int width, int height) {
return new RotateLuminanceSource(yuvData,
dataWidth,
dataHeight,
this.left + left,
this.top + top,
width,
height,
false);
}
public int[] renderThumbnail() {
int width = getWidth() / THUMBNAIL_SCALE_FACTOR;
int height = getHeight() / THUMBNAIL_SCALE_FACTOR;
int[] pixels = new int[width * height];
byte[] yuv = yuvData;
int inputOffset = top * dataWidth + left;
for (int y = 0; y < height; y++) {
int outputOffset = y * width;
for (int x = 0; x < width; x++) {
int grey = yuv[inputOffset + x * THUMBNAIL_SCALE_FACTOR] & 0xff;
pixels[outputOffset + x] = 0xFF000000 | (grey * 0x00010101);
}
inputOffset += dataWidth * THUMBNAIL_SCALE_FACTOR;
}
return pixels;
}
/**
* @return width of image from {@link #renderThumbnail()}
*/
public int getThumbnailWidth() {
return getWidth() / THUMBNAIL_SCALE_FACTOR;
}
/**
* @return height of image from {@link #renderThumbnail()}
*/
public int getThumbnailHeight() {
return getHeight() / THUMBNAIL_SCALE_FACTOR;
}
private void reverseHorizontal(int width, int height) {
byte[] yuvData = this.yuvData;
for (int y = 0, rowStart = top * dataWidth + left; y < height; y++, rowStart += dataWidth) {
int middle = rowStart + width / 2;
for (int x1 = rowStart, x2 = rowStart + width - 1; x1 < middle; x1++, x2--) {
byte temp = yuvData[x1];
yuvData[x1] = yuvData[x2];
yuvData[x2] = temp;
}
}
}
@Override
public boolean isRotateSupported() {
return true;
}
@Override
public LuminanceSource rotateCounterClockwise() {
byte[] rotatedData = new byte[yuvData.length];
for (int y = 0; y < dataHeight; y++) {
for (int x = 0; x < dataWidth; x++)
rotatedData[x * dataHeight + dataHeight - y - 1] = yuvData[x + y * dataWidth];
}
return new RotateLuminanceSource(rotatedData, dataHeight, dataWidth, top, (dataWidth - left - getWidth()), getHeight(), getWidth(), false);
}
}

View File

@ -1,40 +0,0 @@
package com.rncamerakit.camera.commands;
import android.content.Context;
import android.hardware.Camera;
import com.facebook.react.bridge.Promise;
import com.rncamerakit.camera.CameraViewManager;
import com.rncamerakit.SaveImageTask;
import com.rncamerakit.camera.commands.Command;
public class Capture implements Command {
private final Context context;
private boolean saveToCameraRoll;
public Capture(Context context, boolean saveToCameraRoll) {
this.context = context;
this.saveToCameraRoll = saveToCameraRoll;
}
@Override
public void execute(final Promise promise) {
try {
tryTakePicture(promise);
} catch (Exception e) {
e.printStackTrace();
}
}
private void tryTakePicture(final Promise promise) throws Exception {
CameraViewManager.getCamera().takePicture(null, null, new Camera.PictureCallback() {
@Override
public void onPictureTaken(byte[] data, Camera camera) {
camera.stopPreview();
new SaveImageTask(context, promise, saveToCameraRoll).execute(data);
}
});
}
}

View File

@ -1,7 +0,0 @@
package com.rncamerakit.camera.commands;
import com.facebook.react.bridge.Promise;
public interface Command {
void execute(Promise promise);
}

View File

@ -1,69 +0,0 @@
package com.rncamerakit.camera.permission;
import android.Manifest;
import android.app.Activity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.PermissionChecker;
import com.facebook.react.bridge.Promise;
import com.rncamerakit.SharedPrefs;
public class CameraPermission {
private static final int CAMERA_PERMISSION_REQUEST_CODE = 1002;
private static final int PERMISSION_GRANTED = 1;
private static final int PERMISSION_NOT_DETERMINED = -1;
private static final int PERMISSION_DENIED = 0;
private Promise requestAccessPromise;
public void requestAccess(Activity activity, Promise promise) {
if (isPermissionGranted(activity)) {
promise.resolve(true);
}
requestAccessPromise = promise;
permissionRequested(activity, Manifest.permission.CAMERA);
ActivityCompat.requestPermissions(activity,
new String[]{Manifest.permission.CAMERA},
CAMERA_PERMISSION_REQUEST_CODE);
}
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
if (isCameraPermission(requestCode, permissions)) {
if (requestAccessPromise != null) {
requestAccessPromise.resolve(grantResults[0] == PermissionChecker.PERMISSION_GRANTED);
requestAccessPromise = null;
}
}
}
private boolean isCameraPermission(int requestCode, String[] permissions) {
if (permissions.length > 0) {
return requestCode == CAMERA_PERMISSION_REQUEST_CODE &&
Manifest.permission.CAMERA.equals(permissions[0]);
}
return false;
}
public int checkAuthorizationStatus(Activity activity) {
final int statusCode = PermissionChecker.checkCallingOrSelfPermission(activity, Manifest.permission.CAMERA);
if (statusCode == PermissionChecker.PERMISSION_GRANTED) {
return PERMISSION_GRANTED;
}
if (requestingPermissionForFirstTime(activity, Manifest.permission.CAMERA)) {
return PERMISSION_NOT_DETERMINED;
}
return PERMISSION_DENIED;
}
private boolean requestingPermissionForFirstTime(Activity activity, String permissionName) {
return !SharedPrefs.getBoolean(activity, permissionName);
}
private void permissionRequested(Activity activity, String permissionName) {
SharedPrefs.putBoolean(activity, permissionName, true);
}
private boolean isPermissionGranted(Activity activity) {
return checkAuthorizationStatus(activity) == PermissionChecker.PERMISSION_GRANTED;
}
}

View File

@ -1,16 +0,0 @@
package com.rncamerakit.camera.permission;
import com.rncamerakit.camera.CameraModule;
public class CameraPermissionRequestCallback {
private CameraModule cameraModule;
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
cameraModule.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
public void setCameraModule(CameraModule cameraModule) {
this.cameraModule = cameraModule;
}
}

View File

@ -5,6 +5,7 @@
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-feature android:name="android.hardware.camera.any" />
<application
android:name=".MainApplication"

View File

@ -1,6 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.4.10'
ext {
buildToolsVersion = "28.0.3"
minSdkVersion = 23
@ -13,6 +14,7 @@ buildscript {
}
dependencies {
classpath('com.android.tools.build:gradle:4.1.1')
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files

View File

@ -1,6 +1,6 @@
#Thu Nov 12 13:47:49 PST 2020
#Thu Nov 12 14:10:32 PST 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip

View File

@ -4,20 +4,20 @@ import {
Text,
View,
TouchableOpacity,
Alert
Image,
} from 'react-native';
import { CameraKitCamera } from '../../src';
import CameraScreenExample from './CameraScreenExample';
import BarcodeScreenExample from './BarcodeScreenExample';
import CameraExample from './CameraExample';
import CameraScreen from './CameraScreen';
import BarcodeScreen from './BarcodeScreen';
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
example: undefined
example: undefined,
};
}
@ -34,65 +34,58 @@ export default class App extends Component {
React Native Camera Kit
</Text>
</View>
<View style={styles.container}>
<TouchableOpacity onPress={() => this.setState({ example: BarcodeScreen })}>
<TouchableOpacity style={styles.button} onPress={() => this.setState({ example: CameraExample })}>
<Text style={styles.buttonText}>
Barcode scanner Screen
Camera
</Text>
</TouchableOpacity>
<TouchableOpacity onPress={() => this.setState({ example: CameraScreen })}>
<TouchableOpacity style={styles.button} onPress={() => this.setState({ example: CameraScreenExample })}>
<Text style={styles.buttonText}>
Camera Screen
</Text>
</TouchableOpacity>
<TouchableOpacity onPress={() => this.onCheckCameraAuthoPressed()}>
<TouchableOpacity style={styles.button} onPress={() => this.setState({ example: BarcodeScreenExample })}>
<Text style={styles.buttonText}>
Camera Permission Status
Barcode Scanner
</Text>
</TouchableOpacity>
</View>
</View>
);
}
async onCheckCameraAuthoPressed() {
const success = await CameraKitCamera.checkDeviceCameraAuthorizationStatus();
if (success) {
Alert.alert('You have permission 🤗')
}
else {
Alert.alert('No permission 😳')
}
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
paddingTop: 60,
paddingTop: 30,
alignItems: 'center',
backgroundColor: '#F5FCFF',
marginHorizontal: 24,
},
headerContainer: {
flexDirection: 'column',
backgroundColor: '#F5FCFF',
justifyContent: 'center',
alignItems: 'center',
paddingTop: 100
paddingTop: 100,
},
headerText: {
color: 'black',
fontSize: 24
fontSize: 24,
fontWeight: 'bold',
},
button: {
height: 60,
borderRadius: 30,
marginVertical: 12,
width: '100%',
backgroundColor: '#dddddd',
justifyContent: 'center',
},
buttonText: {
color: 'blue',
marginBottom: 20,
fontSize: 20
}
textAlign: 'center',
fontSize: 20,
},
});

View File

@ -1,9 +1,9 @@
import React, { Component } from 'react';
import { Alert } from 'react-native';
import CameraKitCameraScreen from '../../src/CameraScreen/CameraKitCameraScreen';
import CameraScreen from '../../src/CameraScreen';
import CheckingScreen from './CheckingScreen';
export default class CameraScreen extends Component {
export default class BarcodeScreenExample extends Component {
constructor(props) {
super(props);
this.state = {
@ -27,7 +27,7 @@ export default class CameraScreen extends Component {
return <CameraScreen />;
}
return (
<CameraKitCameraScreen
<CameraScreen
actions={{ rightButtonText: 'Done', leftButtonText: 'Cancel' }}
onBottomButtonPressed={(event) => this.onBottomButtonPressed(event)}
flashImages={{

View File

@ -0,0 +1,44 @@
import React, { Component } from 'react';
import { Alert, View, Text, SafeAreaView, StyleSheet } from 'react-native';
import Camera from '../../src/Camera';
import { CameraType } from '../../src/CameraScreen/CameraScreenBase';
export default class CameraExample extends Component {
render() {
return (
<View style={styles.cameraContainer}>
<Camera
ref={(ref) => (this.camera = ref)}
type={CameraType.Back} // optional
style={{ flex: 1 }}
flashMode="auto"
cameraOptions={{
flashMode: 'auto', // on/off/auto(default)
focusMode: 'on', // off/on(default)
zoomMode: 'on', // off/on(default)
ratioOverlay: '1:1', // optional
ratioOverlayColor: '#00000077', // optional
}}
resetFocusTimeout={0}
resetFocusWhenMotionDetected={false}
saveToCameraRole={false} // iOS only
scanBarcode={false} // optional
showFrame={false} // Barcode only, optional
laserColor="red" // Barcode only, optional
frameColor="yellow" // Barcode only, optional
surfaceColor="blue" // Barcode only, optional
onReadCode={(event) => console.log(event.nativeEvent.codeStringValue)}
/>
</View>
);
}
}
const styles = StyleSheet.create(
{
cameraContainer: {
flex: 1,
backgroundColor: 'black',
},
},
);

View File

@ -1,8 +1,9 @@
import React, { Component } from 'react';
import { Alert } from 'react-native';
import CameraKitCameraScreen from '../../src/CameraScreen/CameraKitCameraScreen';
import CameraScreen from '../../src/CameraScreen';
import { CameraOptions } from '../../src/CameraScreen/CameraScreenBase';
export default class CameraScreen extends Component {
export default class CameraScreenExample extends Component {
onBottomButtonPressed(event) {
const captureImages = JSON.stringify(event.captureImages);
Alert.alert(
@ -14,17 +15,19 @@ export default class CameraScreen extends Component {
}
render() {
const options: CameraOptions = { flashMode: 'auto', focusMode: 'on', zoomMode: 'on' };
return (
<CameraKitCameraScreen
<CameraScreen
actions={{ rightButtonText: 'Done', leftButtonText: 'Cancel' }}
onBottomButtonPressed={(event) => this.onBottomButtonPressed(event)}
cameraOptions={options}
flashImages={{
on: require('../images/flashOn.png'),
off: require('./../images/flashOff.png'),
auto: require('./../images/flashAuto.png'),
off: require('../images/flashOff.png'),
auto: require('../images/flashAuto.png'),
}}
cameraFlipImage={require('./../images/cameraFlipIcon.png')}
captureButtonImage={require('./../images/cameraButton.png')}
cameraFlipImage={require('../images/cameraFlipIcon.png')}
captureButtonImage={require('../images/cameraButton.png')}
/>
);
}

View File

@ -1,53 +0,0 @@
import React, { Component } from 'react';
import {
View,
TouchableOpacity,
Text,
StyleSheet
} from 'react-native';
import BarcodeScreen from './BarcodeScreen';
export default class ExampleScreen extends Component {
constructor(props) {
super(props);
this.state = {
example: undefined
};
}
render() {
if (this.state.example) {
const ExampleScreen = this.state.example;
return <ExampleScreen />;
}
return (
<View style = {styles.container}>
<TouchableOpacity onPress={(() => this.setState({example : BarcodeScreen}))}>
<Text style={styles.buttonText}>
Back button
</Text>
</TouchableOpacity>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
// justifyContent: 'center',
paddingTop: 60,
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
buttonText: {
color: 'blue',
marginBottom: 20,
fontSize: 20
}
});

View File

@ -0,0 +1,40 @@
import React, { Component } from 'react';
import { View, TouchableOpacity, Text, StyleSheet } from 'react-native';
import BarcodeScreen from './BarcodeScreenExample';
export default class CheckingScreen extends Component {
constructor(props) {
super(props);
this.state = {
example: undefined,
};
}
render() {
if (this.state.example) {
const CheckingScreen = this.state.example;
return <CheckingScreen />;
}
return (
<View style={styles.container}>
<TouchableOpacity onPress={() => this.setState({ example: BarcodeScreen })}>
<Text style={styles.buttonText}>Back button</Text>
</TouchableOpacity>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
paddingTop: 60,
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
buttonText: {
color: 'blue',
marginBottom: 20,
fontSize: 20,
},
});

View File

@ -27,6 +27,7 @@
"devDependencies": {
"@react-native-community/eslint-config": "^2.0.0",
"@types/jest": "^26.0.0",
"@types/lodash": "^4.14.165",
"@types/react": "^16.9.19",
"@types/react-native": "0.62.13",
"babel-jest": "22.4.1",
@ -38,7 +39,8 @@
"prettier": "^2.1.2",
"react": "16.11.0",
"react-native": "0.62.2",
"react-test-renderer": "16.6.0"
"react-test-renderer": "16.6.0",
"typescript": "^4.1.2"
},
"peerDependencies": {
"react": "*",

35
src/Camera.android.tsx Normal file
View File

@ -0,0 +1,35 @@
import React from 'react';
import _ from 'lodash';
import { requireNativeComponent, findNodeHandle, NativeModules, processColor } from 'react-native';
const { RNCameraKitModule } = NativeModules;
const NativeCamera = requireNativeComponent('CKCameraManager');
function Camera(props, ref) {
const nativeRef = React.useRef();
React.useImperativeHandle(ref, () => ({
capture: async (options = {}) => {
// Because RN doesn't support return types for ViewManager methods
// we must use the general module and tell it what View it's supposed to be using
return await RNCameraKitModule.capture(options, findNodeHandle(nativeRef.current));
},
requestDeviceCameraAuthorization: async () => {
return await RNCameraKitModule.requestDeviceCameraAuthorization();
},
}));
const transformedProps = _.cloneDeep(props);
_.update(transformedProps, 'cameraOptions.ratioOverlayColor', (c) => processColor(c));
_.update(transformedProps, 'frameColor', (c) => processColor(c));
_.update(transformedProps, 'laserColor', (c) => processColor(c));
_.update(transformedProps, 'surfaceColor', (c) => processColor(c));
return <NativeCamera flashMode={props.flashMode} ref={nativeRef} {...props} />;
}
const { PORTRAIT, PORTRAIT_UPSIDE_DOWN, LANDSCAPE_LEFT, LANDSCAPE_RIGHT } = RNCameraKitModule.getConstants();
export { PORTRAIT, PORTRAIT_UPSIDE_DOWN, LANDSCAPE_LEFT, LANDSCAPE_RIGHT };
export default React.forwardRef(Camera);

52
src/Camera.ios.tsx Normal file
View File

@ -0,0 +1,52 @@
import * as _ from 'lodash';
import React, { useEffect } from 'react';
import { requireNativeComponent, NativeModules, processColor } from 'react-native';
const { CKCameraManager } = NativeModules;
const NativeCamera = requireNativeComponent('CKCamera');
function Camera(props, ref) {
const nativeRef = React.useRef();
useEffect(() => {
CKCameraManager.changeCamera();
}, [props.type]);
useEffect(() => {
CKCameraManager.setFlashMode(props.flashMode);
}, [props.flashMode]);
React.useImperativeHandle(ref, () => ({
capture: async () => {
return await CKCameraManager.capture({});
},
requestDeviceCameraAuthorization: async () => {
return await CKCameraManager.checkDeviceCameraAuthorizationStatus();
},
checkDeviceCameraAuthorizationStatus: async () => {
return await CKCameraManager.checkDeviceCameraAuthorizationStatus();
},
changeCamera: async () => {
return await CKCameraManager.changeCamera();
},
setFlashMode: async (flashMode = 'auto') => {
return await CKCameraManager.setFlashMode(flashMode);
},
setTorchMode: async (torchMode = '') => {
return await CKCameraManager.setTorchMode(torchMode);
},
}));
const transformedProps = _.cloneDeep(props);
_.update(transformedProps, 'cameraOptions.ratioOverlayColor', (c) => processColor(c));
return <NativeCamera ref={nativeRef} {...transformedProps} />;
}
Camera.defaultProps = {
resetFocusTimeout: 0,
resetFocusWhenMotionDetected: true,
saveToCameraRoll: true,
};
export default React.forwardRef(Camera);

View File

@ -1,61 +0,0 @@
import * as _ from 'lodash';
import React, { Component } from 'react';
import {
requireNativeComponent,
NativeModules,
processColor,
} from 'react-native';
const NativeCamera = requireNativeComponent('CameraView', null);
const NativeCameraModule = NativeModules.RNKitCameraModule;
const TORCH_MODE_ON = 'on';
const TORCH_MODE_CALL_ARG = 'torch';
export default class CameraKitCamera extends React.Component {
async logData() {
console.log('front Camera?', await NativeCameraModule.hasFrontCamera());
console.log('hasFlash?', await NativeCameraModule.hasFlashForCurrentCamera());
console.log('flashMode?', await NativeCameraModule.getFlashMode());
}
static async requestDeviceCameraAuthorization() {
return await NativeCameraModule.requestDeviceCameraAuthorization();
}
async capture(saveToCameraRoll = true) {
return await NativeCameraModule.capture(saveToCameraRoll);
}
async changeCamera() {
return await NativeCameraModule.changeCamera();
}
async setTorchMode(torchMode) {
if (torchMode == TORCH_MODE_ON){
return await NativeCameraModule.setFlashMode(TORCH_MODE_CALL_ARG);
}
return await NativeCameraModule.setFlashMode(torchMode);
}
async setFlashMode(flashMode = 'auto') {
return await NativeCameraModule.setFlashMode(flashMode);
}
static async checkDeviceCameraAuthorizationStatus() {
return await NativeCameraModule.checkDeviceCameraAuthorizationStatus();
}
static async hasCameraPermission() {
return await NativeCameraModule.hasCameraPermission();
}
render() {
const transformedProps = _.cloneDeep(this.props);
_.update(transformedProps, 'cameraOptions.ratioOverlayColor', (c) => processColor(c));
_.update(transformedProps, 'frameColor', (c) => processColor(c));
_.update(transformedProps, 'laserColor', (c) => processColor(c));
_.update(transformedProps, 'surfaceColor', (c) => processColor(c));
return <NativeCamera {...transformedProps}/>;
}
}

View File

@ -1,57 +0,0 @@
import * as _ from 'lodash';
import React from 'react';
import {
requireNativeComponent,
NativeModules,
processColor,
Platform,
} from 'react-native';
const NativeCamera = requireNativeComponent('CKCamera', null);
const NativeCameraAction = NativeModules.CKCameraManager;
export default class CameraKitCamera extends React.Component {
static async checkDeviceCameraAuthorizationStatus() {
return await NativeCameraAction.checkDeviceCameraAuthorizationStatus();
}
static async requestDeviceCameraAuthorization() {
return await NativeCameraAction.requestDeviceCameraAuthorization();
}
capture(options) {
if (Platform.OS === 'ios') {
return NativeCameraAction.capture(options);
}
if (Platform.OS === 'android') {
// Android has not been updated to use props yet
return NativeCameraAction.capture(!!this.props.saveToCameraRoll);
}
}
async changeCamera() {
return await NativeCameraAction.changeCamera();
}
async setFlashMode(flashMode = 'auto') {
return await NativeCameraAction.setFlashMode(flashMode);
}
async setTorchMode(torchMode = '') {
return await NativeCameraAction.setTorchMode(torchMode);
}
render() {
const transformedProps = _.cloneDeep(this.props);
_.update(transformedProps, 'cameraOptions.ratioOverlayColor', (c) => processColor(c));
return (
<NativeCamera {...transformedProps} />
);
}
}
CameraKitCamera.defaultProps = {
resetFocusTimeout: 0,
resetFocusWhenMotionDetected: true,
saveToCameraRoll: true,
};

View File

@ -1,63 +0,0 @@
import {NativeModules} from 'react-native';
const NativeGalleryModule = NativeModules.NativeGalleryModule;
async function getAlbumsWithThumbnails() {
return await NativeGalleryModule.getAlbumsWithThumbnails();
}
async function getImageUriForId(imageId) {
// Return what getImagesForIds() typically returns in the 'uri' field.
return `file://${imageId}`;
}
async function getImagesForIds(imagesUris = []) {
return await NativeGalleryModule.getImagesForUris(imagesUris);
}
async function getImageForTapEvent(nativeEvent) {
const selectedImageId = nativeEvent.selected;
const imageUri = selectedImageId && await getImageUriForId(selectedImageId);
return {selectedImageId, imageUri, width: nativeEvent.width, height: nativeEvent.height};
}
async function getImagesForCameraEvent(event) {
if (!event.captureImages) {
return [];
}
const images = [];
event.captureImages.forEach(async (image) => {
images.push({
...image,
uri: await getImageUriForId(image.uri)
});
});
return images;
}
async function checkDevicePhotosAuthorizationStatus() {
return await NativeGalleryModule.checkDeviceStorageAuthorizationStatus();
}
async function requestDevicePhotosAuthorization() {
return await NativeGalleryModule.requestDeviceStorageAuthorization();
}
async function resizeImage(image = {}, quality = 'original') {
if (quality === 'original') {
return images;
}
return await NativeGalleryModule.resizeImage(image, quality);
}
export default {
checkDevicePhotosAuthorizationStatus,
requestDevicePhotosAuthorization,
getAlbumsWithThumbnails,
getImageUriForId,
getImagesForIds,
getImageForTapEvent,
getImagesForCameraEvent,
resizeImage
}

View File

@ -1,69 +0,0 @@
import {
NativeModules,
} from 'react-native';
var CKGallery = NativeModules.CKGalleryManager;
async function getAlbumsWithThumbnails() {
return await CKGallery.getAlbumsWithThumbnails();
}
async function getImageUriForId(imageId, imageQuality) {
const {images} = await CKGallery.getImagesForIds([imageId], imageQuality);
if (!images) {
return;
}
if (images.length === 1) {
return images[0].uri;
}
return;
}
async function getImagesForIds(imagesId = [], imageQuality) {
return await CKGallery.getImagesForIds(imagesId, imageQuality);
}
async function getImageForTapEvent(nativeEvent) {
let selectedImageId;
let imageUri;
if (nativeEvent.selectedId) {
selectedImageId = nativeEvent.selectedId;
imageUri = nativeEvent.selected;
} else {
selectedImageId = nativeEvent.selected;
imageUri = await getImageUriForId(selectedImageId);
}
return {selectedImageId, imageUri, width: nativeEvent.width, height: nativeEvent.height};
}
async function getImagesForCameraEvent(event) {
return event.captureImages || [];
}
async function resizeImage(image = {}, quality = 'original') {
if (quality === 'original') {
return images;
}
return await CKGallery.resizeImage(image, quality);
}
async function checkDevicePhotosAuthorizationStatus() {
return await CKGallery.checkDevicePhotosAuthorizationStatus();
}
async function requestDevicePhotosAuthorization() {
return await CKGallery.requestDevicePhotosAuthorization();
}
export default {
getAlbumsWithThumbnails,
getImageUriForId,
getImagesForIds,
getImageForTapEvent,
getImagesForCameraEvent,
checkDevicePhotosAuthorizationStatus,
requestDevicePhotosAuthorization,
resizeImage
}

View File

@ -1,100 +0,0 @@
import _ from 'lodash';
import React, {Component} from 'react';
import ReactNative, {
requireNativeComponent,
UIManager,
processColor
} from 'react-native';
const resolveAssetSource = require('react-native/Libraries/Image/resolveAssetSource');
const GalleryView = requireNativeComponent('GalleryView', null);
const ALL_PHOTOS = 'All Photos';
const COMMAND_REFRESH_GALLERY = 1;
export default class CameraKitGalleryView extends Component {
constructor(props) {
super(props);
this.onTapImage = this.onTapImage.bind(this);
}
async refreshGalleryView(lastEditedImage = '') {
UIManager.dispatchViewManagerCommand(
ReactNative.findNodeHandle(this),
COMMAND_REFRESH_GALLERY,
[lastEditedImage]
);
return true;
}
modifyGalleryViewContentOffset (offset) {
//do nothing. compatability with ios
}
render() {
const transformedProps = _.cloneDeep(this.props);
transformedProps.albumName = this.props.albumName ? this.props.albumName : ALL_PHOTOS;
if (transformedProps.fileTypeSupport && transformedProps.fileTypeSupport.unsupportedImage) {
_.update(transformedProps, 'fileTypeSupport.unsupportedImage', (image) => resolveAssetSource(image).uri);
}
if (_.get(transformedProps, 'customButtonStyle.image')) {
_.update(transformedProps, 'customButtonStyle.image', (image) => resolveAssetSource(image).uri);
}
const customButtonBkgColor = _.get(transformedProps, 'customButtonStyle.backgroundColor');
if (customButtonBkgColor) {
_.update(transformedProps, 'customButtonStyle.backgroundColor', (color) => processColor(customButtonBkgColor));
}
const selectedImageDeprecated = transformedProps.selectedImageIcon;
if (selectedImageDeprecated && _.isNumber(selectedImageDeprecated)) {
transformedProps.selectedImageIcon = resolveAssetSource(selectedImageDeprecated).uri;
}
const unselectedImageDeprecated = transformedProps.unSelectedImageIcon;
if (unselectedImageDeprecated && _.isNumber(unselectedImageDeprecated)) {
transformedProps.unSelectedImageIcon = resolveAssetSource(unselectedImageDeprecated).uri;
}
const selectedImage = _.get(transformedProps, 'selection.selectedImage');
if (selectedImage && _.isNumber(selectedImage)) {
_.update(transformedProps, 'selection.selectedImage', (image) => resolveAssetSource(image).uri);
}
const unselectedImage = _.get(transformedProps, 'selection.unselectedImage');
if (unselectedImage && _.isNumber(unselectedImage)) {
_.update(transformedProps, 'selection.unselectedImage', (image) => resolveAssetSource(image).uri);
}
const selectionPosition = _.get(transformedProps, 'selection.imagePosition');
if (selectionPosition) {
const positionCode = this.transformSelectedImagePosition(selectionPosition);
_.update(transformedProps, 'selection.imagePosition', (position) => positionCode);
}
const selectionOverlayColor = _.get(transformedProps, 'selection.overlayColor');
if (selectionOverlayColor) {
_.update(transformedProps, 'selection.overlayColor', (color) => processColor(selectionOverlayColor));
}
return <GalleryView {...transformedProps} onTapImage={this.onTapImage}/>
}
onTapImage(event) {
if(this.props.onTapImage) {
this.props.onTapImage(event);
}
}
transformSelectedImagePosition(position) {
switch (position) {
case 'top-right': return 0;
case 'top-left': return 1;
case 'bottom-right': return 2;
case 'bottom-left': return 3;
case 'center': return 4;
default: return null;
}
}
}

View File

@ -1,60 +0,0 @@
import _ from 'lodash';
import React, {Component} from 'react';
import {
requireNativeComponent,
NativeModules,
processColor
} from 'react-native';
const GalleryView = requireNativeComponent('CKGalleryView', null);
const GalleryViewManager = NativeModules.CKGalleryViewManager;
const ALL_PHOTOS = 'All Photos';
const DEFAULT_COLUMN_COUNT = 3;
const resolveAssetSource = require('react-native/Libraries/Image/resolveAssetSource');
export default class CameraKitGalleryView extends Component {
render() {
const transformedProps = _.cloneDeep(this.props);
transformedProps.albumName = this.props.albumName ? this.props.albumName : ALL_PHOTOS;
transformedProps.columnCount = this.props.columnCount && this.props.columnCount > 0 ? this.props.columnCount : DEFAULT_COLUMN_COUNT;
_.update(transformedProps, 'fileTypeSupport.unsupportedOverlayColor', (c) => processColor(c));
_.update(transformedProps, 'fileTypeSupport.unsupportedTextColor', (c) => processColor(c));
if (transformedProps.fileTypeSupport && transformedProps.fileTypeSupport.unsupportedImage) {
_.update(transformedProps, 'fileTypeSupport.unsupportedImage', (image) => resolveAssetSource(image));
}
if (_.get(transformedProps, 'customButtonStyle.image')) {
_.update(transformedProps, 'customButtonStyle.image', (image) => resolveAssetSource(image));
}
if (_.get(transformedProps, 'customButtonStyle.backgroundColor')) {
_.update(transformedProps, 'customButtonStyle.backgroundColor', (c) => processColor(c));
}
if (_.get(transformedProps, 'selection.selectedImage')) {
_.update(transformedProps, 'selection.selectedImage', (image) => resolveAssetSource(image));
}
if (_.get(transformedProps, 'selection.unselectedImage')) {
_.update(transformedProps, 'selection.unselectedImage', (image) => resolveAssetSource(image));
}
if (_.get(transformedProps, 'selection.overlayColor')) {
_.update(transformedProps, 'selection.overlayColor', (c) => processColor(c));
}
return <GalleryView {...transformedProps}/>
}
async getSelectedImages() {
return await GalleryViewManager.getSelectedImages();
}
async refreshGalleryView(selectedImages = []) {
return await GalleryViewManager.refreshGalleryView(selectedImages);
}
modifyGalleryViewContentOffset (offset) {
GalleryViewManager.modifyGalleryViewContentOffset(offset);
}
}

View File

@ -1,12 +1,10 @@
import React from 'react';
import {View} from 'react-native';
import CameraScreenBase from './CameraKitCameraScreenBase';
import { View } from 'react-native';
import CameraScreenBase from './CameraScreenBase';
export default class CameraScreen extends CameraScreenBase {
renderGap() {
return (
<View style={{flex: 10, flexDirection: 'column'}}/>
);
return <View style={{ flex: 10, flexDirection: 'column' }} />;
}
render() {
@ -20,5 +18,3 @@ export default class CameraScreen extends CameraScreenBase {
);
}
}

View File

@ -1,6 +1,6 @@
import React from 'react';
import {View} from 'react-native';
import CameraScreenBase from './CameraKitCameraScreenBase';
import { View } from 'react-native';
import CameraScreenBase from './CameraScreenBase';
export default class CameraScreen extends CameraScreenBase {
render() {

View File

@ -12,10 +12,9 @@ import {
processColor,
} from 'react-native';
import _ from 'lodash';
import CameraKitCamera from './../CameraKitCamera';
import Camera from '../Camera';
const IsIOS = Platform.OS === 'ios';
const GalleryManager = IsIOS ? NativeModules.CKGalleryManager : NativeModules.NativeGalleryModule;
const GalleryManager = Platform.OS === 'ios' ? NativeModules.CKGalleryManager : NativeModules.NativeGalleryModule;
const FLASH_MODE_AUTO = 'auto';
const FLASH_MODE_ON = 'on';
@ -26,8 +25,53 @@ const OVERLAY_DEFAULT_COLOR = '#ffffff77';
const OFFSET_FRAME = 30;
const FRAME_HEIGHT = 200;
export default class CameraScreenBase extends Component {
export enum CameraType {
Front = 'front',
Back = 'back'
}
export type CameraOptions = {
flashMode: 'on' | 'off' | 'auto'
focusMode: 'on' | 'off'
zoomMode: 'on'
ratioOverlay?: string
ratioOverlayColor?: string
}
export type Props = {
allowCaptureRetake: boolean,
cameraRatioOverlay: any,
cameraOptions: CameraOptions
scannerOptions: any,
offsetForScannerFrame: any,
heightForScannerFrame: any,
colorForScannerFrame: any,
cameraFlipImage: any,
hideControls: any,
showFrame: any,
captureButtonImage: any,
scanBarcode: any,
laserColor: any,
frameColor: any,
surfaceColor: any,
onReadCode: () => void;
onBottomButtonPressed: (any) => void;
}
type State = {
captureImages: any[],
flashData: any,
torchData: boolean,
ratios: any[],
cameraOptions: any,
ratioArrayPosition: number,
imageCaptured: any,
captured: boolean,
scannerOptions: any,
type: CameraType,
}
export default class CameraScreenBase extends Component<Props, State> {
static propTypes = {
allowCaptureRetake: PropTypes.bool,
};
@ -36,33 +80,41 @@ export default class CameraScreenBase extends Component {
allowCaptureRetake: false,
};
currentFlashArrayPosition: number;
flashArray: any[];
camera: any;
constructor(props) {
super(props);
this.currentFlashArrayPosition = 0;
this.flashArray = [{
mode: FLASH_MODE_AUTO,
image: _.get(this.props, 'flashImages.auto'),
},
{
mode: FLASH_MODE_ON,
image: _.get(this.props, 'flashImages.on'),
},
{
mode: FLASH_MODE_OFF,
image: _.get(this.props, 'flashImages.off'),
},
this.flashArray = [
{
mode: FLASH_MODE_AUTO,
image: _.get(this.props, 'flashImages.auto'),
},
{
mode: FLASH_MODE_ON,
image: _.get(this.props, 'flashImages.on'),
},
{
mode: FLASH_MODE_OFF,
image: _.get(this.props, 'flashImages.off'),
},
];
this.state = {
captureImages: [],
flashData: this.flashArray[this.currentFlashArrayPosition],
torchData: false,
ratios: [],
cameraOptions: {},
cameraOptions: this.props.cameraOptions,
ratioArrayPosition: -1,
imageCaptured: undefined,
imageCaptured: false,
captured: false,
scannerOptions: {},
type: CameraType.Back,
};
this.onSetFlash = this.onSetFlash.bind(this);
this.onSetTorch = this.onSetTorch.bind(this);
this.onSwitchCameraPressed = this.onSwitchCameraPressed.bind(this);
@ -78,8 +130,8 @@ export default class CameraScreenBase extends Component {
this.setState({
cameraOptions,
scannerOptions,
ratios: (ratios || []),
ratioArrayPosition: ((ratios.length > 0) ? 0 : -1),
ratios: ratios || [],
ratioArrayPosition: ratios.length > 0 ? 0 : -1,
});
}
@ -118,48 +170,56 @@ export default class CameraScreenBase extends Component {
}
renderFlashButton() {
return !this.isCaptureRetakeMode() &&
<TouchableOpacity style={{ paddingHorizontal: 15 }} onPress={() => this.onSetFlash(FLASH_MODE_AUTO)}>
<Image
style={{ flex: 1, justifyContent: 'center' }}
source={this.state.flashData.image}
resizeMode="contain"
/>
</TouchableOpacity>;
return (
!this.isCaptureRetakeMode() && (
<TouchableOpacity style={{ paddingHorizontal: 15 }} onPress={() => this.onSetFlash()}>
<Image
style={{ flex: 1, justifyContent: 'center' }}
source={this.state.flashData.image}
resizeMode="contain"
/>
</TouchableOpacity>
)
);
}
renderSwitchCameraButton() {
return (this.props.cameraFlipImage && !this.isCaptureRetakeMode()) &&
<TouchableOpacity style={{ paddingHorizontal: 15 }} onPress={this.onSwitchCameraPressed}>
<Image
style={{ flex: 1, justifyContent: 'center' }}
source={this.props.cameraFlipImage}
resizeMode="contain"
/>
</TouchableOpacity>;
return (
this.props.cameraFlipImage &&
!this.isCaptureRetakeMode() && (
<TouchableOpacity style={{ paddingHorizontal: 15 }} onPress={this.onSwitchCameraPressed}>
<Image
style={{ flex: 1, justifyContent: 'center' }}
source={this.props.cameraFlipImage}
resizeMode="contain"
/>
</TouchableOpacity>
)
);
}
renderTopButtons() {
return !this.props.hideControls && (
<SafeAreaView style={styles.topButtons}>
{this.renderFlashButton()}
{this.renderSwitchCameraButton()}
</SafeAreaView>
return (
!this.props.hideControls && (
<SafeAreaView style={styles.topButtons}>
{this.renderFlashButton()}
{this.renderSwitchCameraButton()}
</SafeAreaView>
)
);
}
renderCamera() {
return (
<View style={styles.cameraContainer}>
{
this.isCaptureRetakeMode() ?
<Image
style={{ flex: 1, justifyContent: 'flex-end' }}
source={{ uri: this.state.imageCaptured.uri }}
/> :
<CameraKitCamera
ref={(cam) => this.camera = cam}
{this.isCaptureRetakeMode() ? (
<Image style={{ flex: 1, justifyContent: 'flex-end' }} source={{ uri: this.state.imageCaptured.uri }} />
) : (
<Camera
ref={(cam) => (this.camera = cam)}
type={this.state.type}
style={{ flex: 1, justifyContent: 'flex-end' }}
flashMode={this.state.flashData.mode}
cameraOptions={this.state.cameraOptions}
saveToCameraRoll={!this.props.allowCaptureRetake}
showFrame={this.props.showFrame}
@ -167,10 +227,10 @@ export default class CameraScreenBase extends Component {
laserColor={this.props.laserColor}
frameColor={this.props.frameColor}
surfaceColor={this.props.surfaceColor}
onReadCode = {this.props.onReadCode}
scannerOptions = {this.state.scannerOptions}
onReadCode={this.props.onReadCode}
scannerOptions={this.state.scannerOptions}
/>
}
)}
</View>
);
}
@ -187,23 +247,19 @@ export default class CameraScreenBase extends Component {
}
renderCaptureButton() {
return (this.props.captureButtonImage && !this.isCaptureRetakeMode()) &&
<View style={styles.captureButtonContainer}>
<TouchableOpacity
onPress={() => this.onCaptureImagePressed()}
>
<Image
source={this.props.captureButtonImage}
resizeMode="contain"
/>
<View style={styles.textNumberContainer}>
<Text>
{this.numberOfImagesTaken()}
</Text>
</View>
</TouchableOpacity>
</View >;
return (
this.props.captureButtonImage &&
!this.isCaptureRetakeMode() && (
<View style={styles.captureButtonContainer}>
<TouchableOpacity onPress={() => this.onCaptureImagePressed()}>
<Image source={this.props.captureButtonImage} resizeMode="contain" />
<View style={styles.textNumberContainer}>
<Text>{this.numberOfImagesTaken()}</Text>
</View>
</TouchableOpacity>
</View>
)
);
}
renderRatioStrip() {
@ -239,7 +295,7 @@ export default class CameraScreenBase extends Component {
this.setState({ imageCaptured: undefined });
}
} else {
this.sendBottomButtonPressedAction(type, captureRetakeMode);
this.sendBottomButtonPressedAction(type, captureRetakeMode, null);
}
}
@ -257,30 +313,31 @@ export default class CameraScreenBase extends Component {
</TouchableOpacity>
);
} else {
return (
<View style={styles.bottomContainerGap} />
);
return <View style={styles.bottomContainerGap} />;
}
}
renderBottomButtons() {
return !this.props.hideControls && (
<SafeAreaView style={[styles.bottomButtons, { backgroundColor: '#ffffff00' }]}>
{this.renderBottomButton('left')}
{this.renderCaptureButton()}
</SafeAreaView>
return (
!this.props.hideControls && (
<SafeAreaView style={[styles.bottomButtons, { backgroundColor: '#ffffff00' }]}>
{/*this.renderBottomButton('left')*/}
{this.renderCaptureButton()}
</SafeAreaView>
)
);
}
onSwitchCameraPressed() {
this.camera.changeCamera();
const direction = this.state.type === CameraType.Back ? CameraType.Front : CameraType.Back;
console.log('SWITCH: ' + direction);
this.setState({ type: direction });
}
async onSetFlash() {
this.currentFlashArrayPosition = (this.currentFlashArrayPosition + 1) % 3;
const newFlashData = this.flashArray[this.currentFlashArrayPosition];
this.setState({ flashData: newFlashData });
this.camera.setFlashMode(newFlashData.mode);
}
onSetTorch() {
@ -296,73 +353,83 @@ export default class CameraScreenBase extends Component {
this.setState({ imageCaptured: image });
} else {
if (image) {
this.setState({ captured: true, imageCaptured: image, captureImages: _.concat(this.state.captureImages, image) });
this.setState({
captured: true,
imageCaptured: image,
captureImages: _.concat(this.state.captureImages, image),
});
}
this.sendBottomButtonPressedAction('capture', false, image);
}
}
onRatioButtonPressed() {
const newRatiosArrayPosition = ((this.state.ratioArrayPosition + 1) % this.state.ratios.length);
const newCameraOptions = _.update(this.state.cameraOptions, 'ratioOverlay', (val) => this.state.ratios[newRatiosArrayPosition]);
const newRatiosArrayPosition = (this.state.ratioArrayPosition + 1) % this.state.ratios.length;
const newCameraOptions = _.update(
this.state.cameraOptions,
'ratioOverlay',
(val) => this.state.ratios[newRatiosArrayPosition],
);
this.setState({ ratioArrayPosition: newRatiosArrayPosition, cameraOptions: newCameraOptions });
}
render() {
throw ('Implemented in CameraKitCameraScreen!');
throw 'Implemented in CameraScreen!';
}
}
import styleObject from './CameraKitCameraScreenStyleObject';
const styles = StyleSheet.create(_.merge(styleObject, {
textStyle: {
color: 'white',
fontSize: 20,
},
ratioBestText: {
color: 'white',
fontSize: 18,
},
ratioText: {
color: '#ffc233',
fontSize: 18,
},
topButtons: {
flex: 1,
flexDirection: 'row',
justifyContent: 'space-between',
paddingTop: 8,
paddingBottom: 0,
},
cameraContainer: {
flex: 10,
flexDirection: 'column',
},
captureButtonContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
textNumberContainer: {
position: 'absolute',
top: 0,
left: 0,
bottom: 0,
right: 0,
justifyContent: 'center',
alignItems: 'center',
},
bottomButton: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
padding: 10,
},
bottomContainerGap: {
flex: 1,
flexDirection: 'row',
justifyContent: 'flex-end',
alignItems: 'center',
padding: 10,
},
}));
import styleObject from './CameraScreenStyleObject';
const styles = StyleSheet.create(
_.merge(styleObject, {
textStyle: {
color: 'white',
fontSize: 20,
},
ratioBestText: {
color: 'white',
fontSize: 18,
},
ratioText: {
color: '#ffc233',
fontSize: 18,
},
topButtons: {
flex: 1,
flexDirection: 'row',
justifyContent: 'space-between',
paddingTop: 8,
paddingBottom: 0,
},
cameraContainer: {
flex: 10,
flexDirection: 'column',
},
captureButtonContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
textNumberContainer: {
position: 'absolute',
top: 0,
left: 0,
bottom: 0,
right: 0,
justifyContent: 'center',
alignItems: 'center',
},
bottomButton: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
padding: 10,
},
bottomContainerGap: {
flex: 1,
flexDirection: 'row',
justifyContent: 'flex-end',
alignItems: 'center',
padding: 10,
},
}),
);

View File

@ -6,7 +6,8 @@ const styleObject = {
position: 'absolute',
top: 0,
left: 0,
width, height,
width,
height,
},
bottomButtons: {
flex: 2,

View File

@ -0,0 +1,6 @@
import CameraScreen from './CameraScreen';
import CameraScreenBase from './CameraScreenBase';
export default CameraScreen;
export { CameraScreenBase };

View File

@ -1,17 +0,0 @@
import { NativeModules } from "react-native";
import CameraKitGallery from "./CameraKitGallery";
import CameraKitCamera from "./CameraKitCamera";
import CameraKitGalleryView from "./CameraKitGalleryView";
import CameraKitCameraScreen from "./CameraScreen/CameraKitCameraScreen";
const { CameraKit } = NativeModules;
export default CameraKit;
export {
CameraKitGallery,
CameraKitCamera,
CameraKitGalleryView,
CameraKitCameraScreen,
};

10
src/index.ts Normal file
View File

@ -0,0 +1,10 @@
import { NativeModules } from 'react-native';
import Camera from './Camera';
import CameraScreen from './CameraScreen/CameraScreen';
const { CameraKit } = NativeModules;
export default CameraKit;
export { Camera, CameraScreen };

23
tsconfig.json Normal file
View File

@ -0,0 +1,23 @@
{
"compilerOptions": {
"allowJs": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"jsx": "react",
"lib": ["es6"],
"moduleResolution": "node",
"noEmit": true,
"strict": true,
"target": "esnext",
"resolveJsonModule": true, // Required for JSON files
"skipLibCheck": true, // Skips *.d.ts files
"strictFunctionTypes": false,
"noImplicitAny": false,
"outDir": "./lib",
"baseUrl": "app",
"paths": {
"*": ["*", "*.ios", "*.android"]
}
},
"exclude": ["node_modules", "babel.config.js", "metro.config.js", "jest.config.js", "**/*.json", "**/*.spec.ts"]
}

View File

@ -1250,6 +1250,11 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
"@types/lodash@^4.14.165":
version "4.14.165"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.165.tgz#74d55d947452e2de0742bad65270433b63a8c30f"
integrity sha512-tjSSOTHhI5mCHTy/OOXYIhi2Wt1qcbHmuXD1Ha7q70CgI/I71afO4XtLb/cVexki1oVYchpul/TOuu3Arcdxrg==
"@types/node@*":
version "14.14.2"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.2.tgz#d25295f9e4ca5989a2c610754dc02a9721235eeb"
@ -7370,6 +7375,11 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
typescript@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.2.tgz#6369ef22516fe5e10304aae5a5c4862db55380e9"
integrity sha512-thGloWsGH3SOxv1SoY7QojKi0tc+8FnOmiarEGMbd/lar7QOEd3hvlx3Fp5y6FlDUGl9L+pd4n2e+oToGMmhRQ==
ua-parser-js@^0.7.18:
version "0.7.22"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.22.tgz#960df60a5f911ea8f1c818f3747b99c6e177eae3"