react-native-camera / react-native-camera

A Camera component for React Native. Also supports barcode scanning!
https://react-native-camera.github.io/react-native-camera/
9.63k stars 3.51k forks source link

Pinch to zoom with RN Camera #2178

Closed rsimbu89 closed 4 years ago

rsimbu89 commented 5 years ago

Question

Hi Team, Is there any way to integrate Pinch to zoom the camera view for both iOS and Android with RNCamera?

sibelius commented 5 years ago

There are some snippet to do this, check issues

rsimbu89 commented 5 years ago

@sibelius   I tried this ZoomView implementation https://github.com/react-native-community/react-native-camera/issues/1282, But not satisfies with the zoom result. Is there any better solution than this?

rsimbu89 commented 5 years ago

Team, Is there any other better solutions?

stale[bot] commented 5 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. You may also mark this issue as a "discussion" and i will leave this open.

FaggioniHQ commented 5 years ago

I implemented this functionality using react-native-gesture-handler module. But is not good enough, the camera does not respond well under continues changes on the zoom prop. Is there any option for this feature? Thanks in advanced

FaggioniHQ commented 5 years ago

Sorry for the late reply,

Here is the code.

import React, {Component} from "react";
import {PinchGestureHandler, State} from "react-native-gesture-handler";

class PinchGesture extends Component{
    constructor(props){
        super(props);
    }
    onPinchGestureEvent({nativeEvent}){
        if (nativeEvent.state === State.ACTIVE) {
            this.props.onPinchScreen(nativeEvent);
        }
    }
    render(){
        return(
            <PinchGestureHandler
                onGestureEvent={this.onPinchGestureEvent.bind(this)}>
                {this.props.children}
            </PinchGestureHandler>
            );
    }
}
export default PinchGesture;
import React, { Component } from "react";
import { View } from "react-native";
import { cameraStyles as styles } from './styles';
import BackButtonCamera from '../buttons/BackButtonCamera';
import { RNCamera } from 'react-native-camera';
/*
 * Buttons
 */
import TakeAPicture from '../../components/buttons/camera/TakeAPicture';
import FlashButton from '../../components/buttons/camera/FlashButton';
import ChangeCamera from'../../components/buttons/camera/ChangeCamera';
import GalleryButton from '../../components/buttons/camera/GalleryButton';
import CameraZoomSlider from '../../components/sliders/CameraZoomSlider';
/**
 * Gestures
 */
import LongTapGesture from '../../components/gestures/LongTapGesture';
import PinchGesture from '../../components/gestures/PinchGesture';

/**
 * Handler
 */
import CameraHandler from '../../modules/helpers/periphericals/CameraHandler';
import RecordBlinkIndicator from "../blinkers/RecordBlinkIndicator";
import VibrationHandler from '../../modules/helpers/periphericals/VibrationHandler';
import SoundHandler from '../../modules/helpers/periphericals/SoundHandler';

class Camera extends Component {
    constructor(props) {
        super(props);
        this.styles = styles;
        this.zoomRate = 0.005;
        this.helper = new CameraHandler(this);
        this.vibrato = new VibrationHandler();
        this.sound = null;
        this.state = {
            // auto - on -off
            flashMode: RNCamera.Constants.FlashMode.auto,
            // front -> true , back -> false
            cameraMode: true,
            //Mode counter
            // mod -> 0 'auto', mod -> 1, 'off', mod -> 2 'on'
            flashModeCounter: 0,
            //iconName off -> flash-off, auto -> flash-auto, on -> flash-on
            flashButtonName: "flash-auto",
            zoom: this.zoomRate,
            recordOn: false,
            soundLoaded: false
        };
    }
    onPinchGestureEvent(nativeEvent){
        this.helper.resolvePinchGesture(nativeEvent);
    }
    onPressFlashButton(){
        this.setState({ flashModeCounter: this.state.flashModeCounter + 1 });
        this.helper.resolveOnPressCameraButton();

    }
    onPressChangeCameraMode(){
        this.setState({ cameraMode: ! this.state.cameraMode });
    }
    takePicture(){
        this.sound = new SoundHandler("camera.mp3");
        this.vibrato.vibrate();
        this.props.onTakePicture(this.camera);
    }
    onPressBackButton(){
        this.props.onPressBackButton();
    }
    startRecording(){
        this.vibrato.vibrate();
        this.setState({ recordOn: true });
        this.props.onStartRecording(this.camera);
    }
    stopRecording(){
        this.vibrato.vibrate();
        this.setState({ recordOn: false });
        this.camera.stopRecording();
    }
    showRecordIndicator(){
        if(this.state.recordOn){
            return (
                <RecordBlinkIndicator />
            );
        }
    }
    onChangeZoomValue(newValue){
        this.setState({ zoom: newValue })
    }
    render() {
        return(
            <View style={ this.styles.container }>
                <PinchGesture
                    onPinchGestureEvent={this.onPinchGestureEvent.bind(this)}
                >
                    <BackButtonCamera
                        onPress={ this.onPressBackButton.bind(this) }
                    />
                    <CameraZoomSlider
                        onChangeZoomValue={this.onChangeZoomValue.bind(this)}
                    />
                    {this.showRecordIndicator()}
                    <View style={{ flex: 1 }}>
                        <RNCamera
                            ref={ref => {
                                this.camera = ref;
                            }}
                            style={styles.preview}
                            type={ (!this.state.cameraMode) ? RNCamera.Constants.Type.front : RNCamera.Constants.Type.back }
                            flashMode={this.state.flashMode}
                            autoFocus={RNCamera.Constants.AutoFocus.on}
                            androidCameraPermissionOptions={ this.helper.permissionsOptions}
                            zoom={this.state.zoom}
                        />
                        <View style={this.styles.buttonBarWrapper}>
                            <ChangeCamera
                                onPress={this.onPressChangeCameraMode.bind(this)}
                            />
                            <LongTapGesture
                                onSingleTap={this.takePicture.bind(this)}
                                onLongTap={this.startRecording.bind(this)}
                                onLongTapEnd={this.stopRecording.bind(this)}
                            >
                                <TakeAPicture />
                            </LongTapGesture>
                            {/*
                        Next release
                        <GalleryButton
                            onPress={this.props.onPressGalleryButton}
                            iconName={this.state.flashButtonName}
                        />
                        */}
                            <FlashButton
                                onPress={this.onPressFlashButton.bind(this)}
                                iconName={this.state.flashButtonName}
                            />
                        </View>
                    </View>
                </PinchGesture>
            </View>
        );
    }
}
export default Camera;
import { RNCamera } from "react-native-camera";
import FileStorage from '../../helpers/storage/FileStorage';

class CameraHandler{
    constructor(cameraComponent){
        this.storage = new FileStorage();
        // This is the camera component
        this.camera = cameraComponent;
        this.permissionsOptions = {
            title: 'Permission to use camera',
            message: 'We need your permission to use your camera',
            buttonPositive: 'Ok',
            buttonNegative: 'Cancel',
        };
        //Camera Options
        this.options = {
            quality: 0.35,
            base64: true,
            width: 1280,
            mirrorImage:false,
            //(can take up to 5 seconds on some devices)
            fixOrientation:false,
            //"portrait", "portraitUpsideDown", "landscapeLeft" or "landscapeRight"
            orientation: "portrait",
            doNotSave: false

        };
        this.optionsRecording = {
            /*
            *quality. This option specifies the quality of the video to be taken. The possible values are:
                    RNCamera.Constants.VideoQuality.2160p.
                    ios Specifies capture settings suitable for 2160p (also called UHD or 4K) quality (3840x2160 pixel) video output.
                    android Quality level corresponding to the 2160p (3840x2160) resolution. (Android Lollipop and above only!).
                    RNCamera.Constants.VideoQuality.1080p.
                    ios Specifies capture settings suitable for 1080p quality (1920x1080 pixel) video output.
                    android Quality level corresponding to the 1080p (1920 x 1080) resolution.
                    RNCamera.Constants.VideoQuality.720p.
                    ios Specifies capture settings suitable for 720p quality (1280x720 pixel) video output.
                    android Quality level corresponding to the 720p (1280 x 720) resolution.
                    RNCamera.Constants.VideoQuality.480p.
                    ios Specifies capture settings suitable for VGA quality (640x480 pixel) video output.
                    android Quality level corresponding to the 480p (720 x 480) resolution.
                    RNCamera.Constants.VideoQuality.4:3.
                    ios Specifies capture settings suitable for VGA quality (640x480 pixel) video output. (Same as RNCamera.Constants.VideoQuality.480p).
                    android Quality level corresponding to the 480p (720 x 480) resolution but with video frame width set to 640.
            * */
            quality: RNCamera.Constants.VideoQuality['1080p'],
            videoBitrate: 25,
            // 100 MB for now
            maxFileSize: 100 * 1024 * 1024,
            //"portrait", "portraitUpsideDown", "landscapeLeft" or "landscapeRight"
            orientation: 'portrait',
            /*
            RNCamera.Constants.VideoCodec['H264']
            RNCamera.Constants.VideoCodec['JPEG']
            RNCamera.Constants.VideoCodec['HVEC'] (iOS >= 11)
            RNCamera.Constants.VideoCodec['AppleProRes422'] (iOS >= 11)
            RNCamera.Constants.VideoCodec['AppleProRes4444'] (iOS >= 11)
            * */
            //codec: RNCamera.Constants.VideoCodec['JPEG'],
            mirrorVideo: false,
            //Max video duration
            maxDuration: 120,
            // x*1024*1024 in bytes
            //maxFileSize: 20,
            //mute: false,
            //path: '',
            doNotSave: false
        }
    }
    takeAPicture = async function(camera){
        try{
            if (camera) {
                return await camera.takePictureAsync(this.options);
            }
             return null;
        }catch(error){
            return error;
        }
    };
    startRecording = async (camera, successCallBack, failedCallback) => {
        try{
            if (camera) {
                camera.recordAsync(this.optionsRecording).then( (video) => {
                    successCallBack(video);
                } ).catch( (error) => {
                    failedCallback(error);
                } );
            }
        }catch(error){
            failedCallback(error);
        }
    };
    resolveOnPressCameraButton(){
        if(this.camera.state.flashModeCounter % 3 === 0){
            this.camera.setState({
                flashButtonName: 'flash-auto',
                flashMode: RNCamera.Constants.FlashMode.auto
            });
        }else if(this.camera.state.flashModeCounter % 3 === 1){
            this.camera.setState({
                flashButtonName: 'flash-off',
                flashMode: RNCamera.Constants.FlashMode.off
            });
        }else if(this.camera.state.flashModeCounter % 3 === 2){
            this.camera.setState({
                flashButtonName: 'flash-on',
                flashMode: RNCamera.Constants.FlashMode.torch
            });
        }
    }
    resolvePinchGesture(nativeEvent){
        if(nativeEvent.scale > 1){
            if(this.camera.state.zoom + this.camera.zoomRate > 1){
                this.camera.setState({ zoom: 1 });
            }else{
                this.camera.setState({ zoom: this.camera.state.zoom + this.camera.zoomRate });
            }
        }else if(nativeEvent.scale < 1){
            if(this.camera.state.zoom - this.camera.zoomRate < 0){
                this.camera.setState({ zoom: 0 });
            }else{
                this.camera.setState({ zoom: this.camera.state.zoom - this.camera.zoomRate })
            }
        }
    }
}
export default CameraHandler;

Not sure if this the best channel for this. But how you guys deal with large files/videos on RN. How do you manage to upload those files in the background?. Thanks in advanced

MateusAndrade commented 5 years ago

@FaggioniHQ you can give a look at react-native-fetch-blob and react-native-fs.

FaggioniHQ commented 5 years ago

Yeah, I'm using react-native-fetch-blob already. But seems like this process its a bit slow... moving the videos/images to the filesystem and uploading the file to our backend. I implemented a file queue but seems to take a bit of time. Let me know if you guys have a better approach to this functionality.

Thanks in advanced

MateusAndrade commented 5 years ago

It can be related to How react-native works. Btw, i recommend reading about that when working with process that demand a higher process. :smile:

ddizh commented 5 years ago

@FaggioniHQ thanks for the code snippet. I managed to make it work better with continuous pitch events by changing if(nativeEvent.scale > 1) to if (nativeEvent.scale > this.prevZoomScale) and setting this.prevZoomScale = nativeEvent.scale at the bottom of the resolvePinchGesture in your case. Also, keep in mind that zoom min/max is different on iOS and Android. See this comment for reference: https://github.com/react-native-community/react-native-camera/issues/1772#issuecomment-419068946

This is how I handle it (only for iOS atm):

    onPitchGestureEventHandler = ({ nativeEvent }) => {
        if (!nativeEvent || nativeEvent.state !== State.ACTIVE) {
            return;
        }

        if (nativeEvent.scale > this.prevZoomScale) {
            this.setState({ zoom: Math.min(0.05, this.state.zoom + this.zoomRate) });
        } else {
            this.setState({ zoom: Math.max(0, this.state.zoom - (this.zoomRate + this.zoomRate / 2)) });
        }

        this.prevZoomScale = nativeEvent.scale;
    }
FaggioniHQ commented 5 years ago

Thanks for the feedback. I will check this later. 👍

fabOnReact commented 4 years ago

@FaggioniHQ @d-dizhevsky

this is my existing code adapted and cleaned from the following example. Can you help me completing this functionality? Thanks a lot !

import React, { Component } from 'react';
import { View } from 'react-native';
import { PinchGestureHandler, State } from 'react-native-gesture-handler';

export default class ZoomView extends Component {
  onGesturePinch = ({ nativeEvent }) => {
    this.props.onPinchProgress(nativeEvent.scale)
  }

  onPinchHandlerStateChange = event => {
    const pinch_start = event.nativeEvent.state === State.END
    const pinch_begin = event.nativeEvent.oldState === State.BEGAN
    const pinch_active = event.nativeEvent.state === State.ACTIVE
    if (pinch_start) {
      this.props.onPinchEnd()
    }
    else if (pinch_begin && pinch_active) {
      this.props.onPinchStart()
    }
  }

  render() {
    const { style } = this.props
    return (
      <PinchGestureHandler
        onGestureEvent={this.onGesturePinch}
        onHandlerStateChange={this.onPinchHandlerStateChange}>
        { this.props.children }
      </PinchGestureHandler>
    )
  }
}

and the Camera Component

import React, { Component } from 'react'
import { RNCamera } from 'react-native-camera';
iimport ZoomView from './ZoomView';

export default class Recorder extends Component {
  _render() {
    return (
      <ZoomView
        onPinchStart={() => {}}
        onPinchEnd={() => {}}
        onPinchProgress={zoom => this.setState({ zoom })}>
        <RNCamera
          ref={ref => {
            this.camera = ref;
          }}
          zoom={this.state.zoom} />
      </ZoomView>
    )
  }
}
fabOnReact commented 4 years ago

the solution was provided by @cristianoccazinsp with the following answer

Initial testing without using this.camera.setNativeProps({zoom: zoom}) have smooth zoom effect on even old android and iphone 5 devices (as displayed in the below gif)

Thanks a lot for the support!

example

sibelius commented 4 years ago

can someone add this to the docs?

fabOnReact commented 4 years ago

credits go to @cristianoccazinsp with the following answer and the ongoing discussion at #1282

I still need to test the functionality with setNativeProps and I am not sure that the solution will work for all devices. I currently can only test it on iPhone 5 and Android Oppo A83.

iPhone 5 has relatively small screen so it not a good subject for testing the funcionality.

Thanks a lot

stale[bot] commented 4 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. You may also mark this issue as a "discussion" and i will leave this open.

stale[bot] commented 4 years ago

Closing this issue after a prolonged period of inactivity. Fell free to reopen this issue, if this still affecting you.

fabOnReact commented 4 years ago

@sibelius I want to contribute writing code. Would you consider having this functionality in this library or as a separate npm package? Thanks a lot

sibelius commented 4 years ago

@fabriziobertoglio1987 I think it is better as a new package, so we can reuse in expo camera as well

let me know if you need any help

cristianoccazinsp commented 4 years ago

Considering the extra dependencies (gesture handler?), definitely looks like something that should be added as another library. Not sure if it would be worth it trying to implement it with react native's pan handlers instead.

sibelius commented 4 years ago

we can link to the docs to make it easier to find it

fabOnReact commented 4 years ago

Thanks @sibelius and @cristianoccazinsp

This is the result from my research. I exclude OPTION 2 as there is no explicit requirement to implement setNativeZoom() in react-native-camera. I would be happy to work on this or other features you approve/open. Thanks a lot.

OPTION 1 - Wrapping CameraViewManager in a ScaleGestureDetector instance

The zoom is controlled with CameraViewManager method setZoom().

public class CameraViewManager extends ViewGroupManager<RNCameraView> {
  @ReactProp(name = "zoom")
  public void setZoom(RNCameraView view, float zoom) {
    view.setZoom(zoom);
  }
}

The zoom functionality could be built by wrapping a ScaleGestureDetector instance around the appropriate view and pass the correct value to the method setZoom()

OPTION 2 - Implementing setNativeProps for CameraViewManager (avoid re-render)

The RNCamera component is the interface layer between Java (CameraViewManager) and Javascript (React Camera component).

React Camera component does not implement setNativeProps. Most of the ReactNative components (for example View) implement the method setNativeProps with the NativeMethodsMixin

NativeMethodsMixin provides methods to access the underlying native component directly.

phyzical commented 4 years ago

@fabriziobertoglio1987 Hey is this contained in a package anywhere yet for testing (even if its rough)? if not any ETA if its sooonish i can hold off on writing my own implementation and just test what you come up with :)

Thanks!

fabOnReact commented 4 years ago

Thanks @sibelius and @phyzical , but after an extensive research, I don't believe that it makes sense creating a separate package for this functionality. The package would receive negative feedback from the community as it does not solve the main problem explained here.

I agree with @sibelius that this functionality should remain in user land, but I would consider implementing setNativeProps as discussed here (I may not be able to do this completely alone, but I will help on my evening time).

The feature request is closed and potential pull request will not be merged.

For testing: fork this project and yarn install the advanced example

cristianoccazinsp commented 4 years ago

One of the demo apps has a pinch to zoom implemented as well. You can always copy paste ;)

El mié., 29 de enero de 2020 06:53, fabriziobertoglio1987 < notifications@github.com> escribió:

Thanks @sibelius https://github.com/sibelius and @phyzical https://github.com/phyzical , but after an extensive research https://github.com/react-native-community/react-native-camera/issues/2178#issuecomment-574228101, I don't believe that it makes sense creating a separate package for this functionality. The package would receive negative feedback from the community as it does not solve the main problem explained here https://github.com/react-native-community/react-native-camera/issues/2178#issuecomment-574228101 .

I agree with @sibelius https://github.com/sibelius that this functionality should remain in user land https://github.com/react-native-community/react-native-camera/issues/1282#issuecomment-392520205, but I would consider implementing setNativeProps as discussed here https://github.com/react-native-community/react-native-camera/issues/2178#issuecomment-574228101 (I may not be able to do this completely alone).

The feature request is closed and potential pull request will not be merged.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/react-native-community/react-native-camera/issues/2178?email_source=notifications&email_token=ALU263G2V2XTUBALMH4W3S3RAFG2NA5CNFSM4HCRBCT2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEKGTDOY#issuecomment-579678651, or unsubscribe https://github.com/notifications/unsubscribe-auth/ALU263FM2WDTR5L3VYBPJQTRAFG2NANCNFSM4HCRBCTQ .

SimonErm commented 4 years ago

2815