feat(touch): Feature/add on tap events (#2827)

* add TouchEvent on android

* add GestureHandler to detect touches on android

* add property to enable touchDetector on android

* add onTouch event to ios

* add GestureRecognizer to ios

* add onTouch property and js setup

* add missing semicolons

* fix literal notation

* add missing ":"

* fix copy-paste error (wrong var-name)

* pass the native event to the onTouch callback

* replace : with ;

* add onTouch type defs

* add documentation for onTouch property

* scale postion before emitting since the event coordinates are raw pixels

* migrate advanced example to native pinch zoom and onTouch

* split onTouch property into onTap and onDoubleTap
This commit is contained in:
SimonErm 2020-05-12 22:56:42 +02:00 committed by GitHub
parent b6f9bb3b97
commit dc4f65702f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 229 additions and 36 deletions

View File

@ -27,7 +27,9 @@ public class CameraViewManager extends ViewGroupManager<RNCameraView> {
EVENT_ON_PICTURE_TAKEN("onPictureTaken"),
EVENT_ON_PICTURE_SAVED("onPictureSaved"),
EVENT_ON_RECORDING_START("onRecordingStart"),
EVENT_ON_RECORDING_END("onRecordingEnd");
EVENT_ON_RECORDING_END("onRecordingEnd"),
EVENT_ON_TOUCH("onTouch");
private final String mName;
@ -160,6 +162,11 @@ public class CameraViewManager extends ViewGroupManager<RNCameraView> {
view.setUsingCamera2Api(useCamera2Api);
}
@ReactProp(name = "touchDetectorEnabled")
public void setTouchDetectorEnabled(RNCameraView view, boolean touchDetectorEnabled) {
view.setShouldDetectTouches(touchDetectorEnabled);
}
@ReactProp(name = "faceDetectorEnabled")
public void setFaceDetecting(RNCameraView view, boolean faceDetectorEnabled) {
view.setShouldDetectFaces(faceDetectorEnabled);

View File

@ -3,11 +3,15 @@ package org.reactnative.camera;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Color;
import android.media.CamcorderProfile;
import android.os.Build;
import androidx.core.content.ContextCompat;
import android.util.DisplayMetrics;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
@ -40,6 +44,8 @@ public class RNCameraView extends CameraView implements LifecycleEventListener,
private List<String> mBarCodeTypes = null;
private ScaleGestureDetector mScaleGestureDetector;
private GestureDetector mGestureDetector;
private boolean mIsPaused = false;
private boolean mIsNew = true;
@ -62,6 +68,7 @@ public class RNCameraView extends CameraView implements LifecycleEventListener,
private boolean mShouldGoogleDetectBarcodes = false;
private boolean mShouldScanBarCodes = false;
private boolean mShouldRecognizeText = false;
private boolean mShouldDetectTouches = false;
private int mFaceDetectorMode = RNFaceDetector.FAST_MODE;
private int mFaceDetectionLandmarks = RNFaceDetector.NO_LANDMARKS;
private int mFaceDetectionClassifications = RNFaceDetector.NO_CLASSIFICATIONS;
@ -375,6 +382,16 @@ public class RNCameraView extends CameraView implements LifecycleEventListener,
this.mCameraViewHeight = height;
}
public void setShouldDetectTouches(boolean shouldDetectTouches) {
if(!mShouldDetectTouches && shouldDetectTouches){
mGestureDetector=new GestureDetector(mThemedReactContext,onGestureListener);
}else{
mGestureDetector=null;
}
this.mShouldDetectTouches = shouldDetectTouches;
}
public void setUseNativeZoom(boolean useNativeZoom){
if(!mUseNativeZoom && useNativeZoom){
mScaleGestureDetector = new ScaleGestureDetector(mThemedReactContext,onScaleGestureListener);
@ -389,6 +406,9 @@ public class RNCameraView extends CameraView implements LifecycleEventListener,
if(mUseNativeZoom) {
mScaleGestureDetector.onTouchEvent(event);
}
if(mShouldDetectTouches){
mGestureDetector.onTouchEvent(event);
}
return true;
}
@ -604,7 +624,25 @@ public class RNCameraView extends CameraView implements LifecycleEventListener,
return true;
}
}
private int scalePosition(float raw){
Resources resources = getResources();
Configuration config = resources.getConfiguration();
DisplayMetrics dm = resources.getDisplayMetrics();
return (int)(raw/ dm.density);
}
private GestureDetector.SimpleOnGestureListener onGestureListener = new GestureDetector.SimpleOnGestureListener(){
@Override
public boolean onSingleTapUp(MotionEvent e) {
RNCameraViewHelper.emitTouchEvent(RNCameraView.this,false,scalePosition(e.getX()),scalePosition(e.getY()));
return true;
}
@Override
public boolean onDoubleTap(MotionEvent e) {
RNCameraViewHelper.emitTouchEvent(RNCameraView.this,true,scalePosition(e.getX()),scalePosition(e.getY()));
return true;
}
};
private ScaleGestureDetector.OnScaleGestureListener onScaleGestureListener = new ScaleGestureDetector.OnScaleGestureListener() {
@Override

View File

@ -244,7 +244,19 @@ public class RNCameraViewHelper {
}
});
}
// Touch event
public static void emitTouchEvent(final ViewGroup view, final boolean isDoubleTap, final int x, final int y) {
final ReactContext reactContext = (ReactContext) view.getContext();
reactContext.runOnNativeModulesQueueThread(new Runnable() {
@Override
public void run() {
TouchEvent event = TouchEvent.obtain(view.getId(), isDoubleTap, x, y);
reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher().dispatchEvent(event);
}
});
}
// Face detection events
public static void emitFacesDetectedEvent(final ViewGroup view, final WritableArray data) {

View File

@ -0,0 +1,69 @@
package org.reactnative.camera.events;
import androidx.core.util.Pools;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.events.Event;
import com.facebook.react.uimanager.events.RCTEventEmitter;
import org.reactnative.camera.CameraViewManager;
public class TouchEvent extends Event<TouchEvent> {
private static final Pools.SynchronizedPool<TouchEvent> EVENTS_POOL =
new Pools.SynchronizedPool<>(3);
private int mX;
private int mY;
private boolean mIsDoubleTap;
private TouchEvent() {}
public static TouchEvent obtain(int viewTag, boolean isDoubleTap, int x, int y) {
TouchEvent event = EVENTS_POOL.acquire();
if (event == null) {
event = new TouchEvent();
}
event.init(viewTag, isDoubleTap, x, y);
return event;
}
private void init(int viewTag, boolean isDoubleTap, int x, int y) {
super.init(viewTag);
mX = x;
mY = y;
mIsDoubleTap=isDoubleTap;
}
@Override
public short getCoalescingKey() {
return 0;
}
@Override
public String getEventName() {
return CameraViewManager.Events.EVENT_ON_TOUCH.toString();
}
@Override
public void dispatch(RCTEventEmitter rctEventEmitter) {
rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData());
}
private WritableMap serializeEventData() {
WritableMap event = Arguments.createMap();
event.putInt("target", getViewTag());
WritableMap touchOrigin = Arguments.createMap();
touchOrigin.putInt("x", mX);
touchOrigin.putInt("y",mY);
event.putBoolean("isDoubleTap", mIsDoubleTap);
event.putMap("touchOrigin", touchOrigin);
return event;
}
}

View File

@ -431,6 +431,21 @@ Event will contain the following fields:
Function to be called when native code stops recording video, but before all video processing takes place. This event will only fire after a successful video recording, and it will not fire if video recording fails (use the error returned from `recordAsync` instead).
### `onTap`
Function to be called when a touch within the camera view is recognized.
The function is also called on the first touch of double tap.
Event will contain the following fields:
- `x`
- `y`
### `onDoubleTap`
Function to be called when a double touch within the camera view is recognized.
Event will contain the following fields:
- `x`
- `y`
### Bar Code Related props
### `onBarCodeRead`

View File

@ -397,46 +397,46 @@ class Camera extends Component{
}
}
onTapToFocus = (event) => {
onTapToFocus = (touchOrigin) => {
if(!this.cameraStyle || this.state.takingPic){
return;
}
const {pageX, pageY} = event.nativeEvent;
const {x, y} = touchOrigin;
let {width, height, top, left} = this.cameraStyle;
// compensate for top/left changes
let pageX2 = pageX - left;
let pageY2 = pageY - top;
let pageX2 = x - left;
let pageY2 = y - top;
// normalize coords as described by https://gist.github.com/Craigtut/6632a9ac7cfff55e74fb561862bc4edb
const x0 = pageX2 / width;
const y0 = pageY2 / height;
let x = x0;
let y = y0;
let computedX = x0;
let computedY = y0;
// if portrait, need to apply a transform because RNCamera always measures coords in landscape mode
// with the home button on the right. If the phone is rotated with the home button to the left
// we will have issues here, and we have no way to detect that orientation!
// TODO: Fix this, however, that orientation should never be used due to camera positon
if(this.state.orientation.isPortrait){
x = y0;
y = -x0 + 1;
computedX = y0;
computedY = -x0 + 1;
}
this.setState({
focusCoords: {
x: x,
y: y,
x: computedX,
y: computedY,
autoExposure: true
},
touchCoords: {
x: pageX2 - 50,
y: pageY2 - 50
}
});
},this.onSetFocus);
// remove focus rectangle
if(this.focusTimeout){
@ -446,14 +446,12 @@ class Camera extends Component{
}
onTapToFocusOut = () => {
if(this.state.touchCoords){
this.focusTimeout = setTimeout(()=>{
if(this.mounted){
onSetFocus = () => {
this.focusTimeout = setTimeout(() => {
if (this.mounted) {
this.setState({touchCoords: null});
}
}, 1500);
}
}
onPinchStart = () => {
@ -638,6 +636,8 @@ class Camera extends Component{
flashMode={flashMode}
zoom={zoom}
maxZoom={MAX_ZOOM}
useNativeZoom={true}
onTap={this.onTapToFocus}
whiteBalance={WB_OPTIONS[wb]}
autoFocusPointOfInterest={this.state.focusCoords}
androidCameraPermissionOptions={{
@ -666,21 +666,6 @@ class Camera extends Component{
</View>
}
>
<TouchableOpacity
activeOpacity={0.5}
style={flex1}
onPressIn={this.onTapToFocus}
onPressOut={this.onTapToFocusOut}
onLongPress={this.takePictureLong}
delayLongPress={1500}
>
<ZoomView
onPinchProgress={this.onPinchProgress}
onPinchStart={this.onPinchStart}
onPinchEnd={this.onPinchEnd}
style={flex1}
>
{this.state.touchCoords ?
<View style={{
borderWidth: 2,
@ -693,8 +678,6 @@ class Camera extends Component{
}}>
</View>
: null}
</ZoomView>
</TouchableOpacity>
</RNCamera>
{!takingPic && !recording && !this.state.spinnerVisible && cameraReady ?

View File

@ -106,6 +106,7 @@
- (void)onRecordingStart:(NSDictionary *)event;
- (void)onRecordingEnd:(NSDictionary *)event;
- (void)onText:(NSDictionary *)event;
- (void)onTouch:(NSDictionary *)event;
- (void)onBarcodesDetected:(NSDictionary *)event;
- (bool)isRecording;
- (void)onSubjectAreaChanged:(NSDictionary *)event;

View File

@ -25,6 +25,7 @@
@property (nonatomic, copy) RCTDirectEventBlock onAudioConnected;
@property (nonatomic, copy) RCTDirectEventBlock onMountError;
@property (nonatomic, copy) RCTDirectEventBlock onBarCodeRead;
@property (nonatomic, copy) RCTDirectEventBlock onTouch;
@property (nonatomic, copy) RCTDirectEventBlock onTextRecognized;
@property (nonatomic, copy) RCTDirectEventBlock onFacesDetected;
@property (nonatomic, copy) RCTDirectEventBlock onGoogleVisionBarcodesDetected;
@ -76,6 +77,12 @@ BOOL _sessionInterrupted = NO;
self.previewLayer.needsDisplayOnBoundsChange = YES;
#endif
self.rectOfInterest = CGRectMake(0, 0, 1.0, 1.0);
UITapGestureRecognizer * tapHandler=[self createTapGestureRecognizer];
[self addGestureRecognizer:tapHandler];
UITapGestureRecognizer * doubleTabHandler=[self createDoubleTapGestureRecognizer];
[self addGestureRecognizer:doubleTabHandler];
self.autoFocus = -1;
self.exposure = -1;
self.presetCamera = AVCaptureDevicePositionUnspecified;
@ -95,6 +102,39 @@ BOOL _sessionInterrupted = NO;
}
return self;
}
-(UITapGestureRecognizer*)createDoubleTapGestureRecognizer
{
UITapGestureRecognizer *doubleTapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleDoubleTap:)];
doubleTapGestureRecognizer.numberOfTapsRequired = 2;
return doubleTapGestureRecognizer;
}
-(UITapGestureRecognizer*)createTapGestureRecognizer
{
UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];
tapGestureRecognizer.numberOfTapsRequired = 1;
return tapGestureRecognizer;
}
-(void)handleDoubleTap:(UITapGestureRecognizer*)doubleTapRecognizer {
[self handleTouch:doubleTapRecognizer isDoubleTap:true];
}
-(void)handleTap:(UITapGestureRecognizer*)tapRecognizer {
[self handleTouch:tapRecognizer isDoubleTap:false];
}
-(void)handleTouch:(UITapGestureRecognizer*)tapRecognizer isDoubleTap:(BOOL)isDoubleTap{
if (tapRecognizer.state == UIGestureRecognizerStateRecognized) {
CGPoint location = [tapRecognizer locationInView:self];
NSDictionary *tapEvent = [NSMutableDictionary dictionaryWithDictionary:@{
@"isDoubleTab":@(isDoubleTap),
@"touchOrigin": @{
@"x": @(location.x),
@"y": @(location.y)
}
}];
[self onTouch:tapEvent];
}
}
-(float) getMaxZoomFactor:(AVCaptureDevice*)device {
float maxZoom;
if(self.maxZoom > 1){
@ -174,6 +214,12 @@ BOOL _sessionInterrupted = NO;
_onRecordingEnd(event);
}
}
- (void)onTouch:(NSDictionary *)event
{
if (_onTouch) {
_onTouch(event);
}
}
- (void)onText:(NSDictionary *)event
{

View File

@ -26,6 +26,8 @@ RCT_EXPORT_VIEW_PROPERTY(onRecordingEnd, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onTextRecognized, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onSubjectAreaChanged, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(videoStabilizationMode, NSInteger);
RCT_EXPORT_VIEW_PROPERTY(onTouch, RCTDirectEventBlock);
+ (BOOL)requiresMainQueueSetup
{
@ -85,7 +87,7 @@ RCT_EXPORT_VIEW_PROPERTY(videoStabilizationMode, NSInteger);
- (NSArray<NSString *> *)supportedEvents
{
return @[@"onCameraReady", @"onAudioInterrupted", @"onAudioConnected", @"onMountError", @"onBarCodeRead", @"onFacesDetected", @"onPictureTaken", @"onPictureSaved", @"onRecordingStart", @"onRecordingEnd", @"onTextRecognized", @"onGoogleVisionBarcodesDetected", @"onSubjectAreaChanged"];
return @[@"onCameraReady", @"onAudioInterrupted", @"onAudioConnected", @"onMountError", @"onBarCodeRead", @"onFacesDetected", @"onPictureTaken", @"onPictureSaved", @"onRecordingStart", @"onRecordingEnd", @"onTextRecognized", @"onGoogleVisionBarcodesDetected", @"onSubjectAreaChanged",@"onTouch"];
}
+ (NSDictionary *)validCodecTypes

View File

@ -261,6 +261,8 @@ type PropsType = typeof View.props & {
onPictureSaved?: Function,
onRecordingStart?: Function,
onRecordingEnd?: Function,
onTap?: Function,
onDoubleTap?: Function,
onGoogleVisionBarcodesDetected?: ({ barcodes: Array<TrackedBarcodeFeature> }) => void,
onSubjectAreaChanged?: ({ nativeEvent: { prevPoint: {| x: number, y: number |} } }) => void,
faceDetectionMode?: number,
@ -404,6 +406,8 @@ export default class Camera extends React.Component<PropsType, StateType> {
onPictureSaved: PropTypes.func,
onRecordingStart: PropTypes.func,
onRecordingEnd: PropTypes.func,
onTap: PropTypes.func,
onDoubleTap: PropTypes.func,
onGoogleVisionBarcodesDetected: PropTypes.func,
onFacesDetected: PropTypes.func,
onTextRecognized: PropTypes.func,
@ -635,7 +639,14 @@ export default class Camera extends React.Component<PropsType, StateType> {
this.props.onAudioInterrupted();
}
};
_onTouch = ({ nativeEvent }: EventCallbackArgumentsType) => {
if (this.props.onTap && !nativeEvent.isDoubleTap) {
this.props.onTap(nativeEvent.touchOrigin);
}
if (this.props.onDoubleTap && nativeEvent.isDoubleTap){
this.props.onTap(nativeEvent.touchOrigin);
}
};
_onAudioConnected = () => {
if (this.props.onAudioConnected) {
this.props.onAudioConnected();
@ -810,6 +821,7 @@ export default class Camera extends React.Component<PropsType, StateType> {
this.props.onGoogleVisionBarcodesDetected,
)}
onBarCodeRead={this._onObjectDetected(this.props.onBarCodeRead)}
onTouch={this._onTouch}
onFacesDetected={this._onObjectDetected(this.props.onFacesDetected)}
onTextRecognized={this._onObjectDetected(this.props.onTextRecognized)}
onPictureSaved={this._onPictureSaved}
@ -840,6 +852,10 @@ export default class Camera extends React.Component<PropsType, StateType> {
newProps.faceDetectorEnabled = true;
}
if (props.onTap || props.onDoubleTap) {
newProps.touchDetectorEnabled = true;
}
if (props.onTextRecognized) {
newProps.textRecognizerEnabled = true;
}
@ -869,6 +885,7 @@ const RNCamera = requireNativeComponent('RNCamera', Camera, {
accessibilityLabel: true,
accessibilityLiveRegion: true,
barCodeScannerEnabled: true,
touchDetectorEnabled:true,
googleVisionBarcodeDetectorEnabled: true,
faceDetectorEnabled: true,
textRecognizerEnabled: true,
@ -880,6 +897,7 @@ const RNCamera = requireNativeComponent('RNCamera', Camera, {
onAudioConnected: true,
onPictureSaved: true,
onFaceDetected: true,
onTouch:true,
onLayout: true,
onMountError: true,
onSubjectAreaChanged: true,

2
types/index.d.ts vendored
View File

@ -170,6 +170,8 @@ export interface RNCameraProps {
/** iOS only */
onAudioInterrupted?(): void;
onAudioConnected?(): void;
onTap?(origin:Point):void;
onDoubleTap?(origin:Point):void;
/** Use native pinch to zoom implementation*/
useNativeZoom?:boolean;
/** Value: float from 0 to 1.0 */