react-nativeimagedraggablereact-native-camerareact-native-image

"Implementing Resizable and Draggable Inner View with Camera Capture in React Native"


I am working on a React Native project where I need to implement a camera functionality with a specific requirement. Below I had shared a image I Want to implement below shown functionality, The centred box is draggable and resizable

Here is reference image:

Here is a image for the reference


Solution

  • Here is working code and this might help you

    import { StyleSheet, Text, View, Dimensions, PanResponder, Animated } 
    from 'react-native'
    import React, { useEffect, useRef, useState } from 'react'
    import { Svg, Defs, Rect, Mask, Circle } from 'react-native-svg';
    import { AppColor, deviceDisplaySize } from '../Theme/AppTheme';
    import { IcnScaleSize } from '../Assets/Icons/SvgIcon';
    
    let _previousLeft = 0;
    let _previousTop = 0;
    
    let bottomRightW = 0;
    let bottomRightH = 0
    
    let viewMaskingPosition = { x: 0, y: 0 }
    
    let viewParentMaskingSize = { height: 0, width: 0 }
    
    const MaskView = ({ onChangeUpdated, maskStyle = {} }) => {
    
    const maskingViewCornerRadius = 20
    const [boxPosition, setBoxPosition] = useState({ x: 0, y: 0 });
    
    const [maskingScaleSize, setMaskingScaleSize] = useState({ height: 100, width: 100 })
    
    const panResponder = useRef(
        PanResponder.create({
            onStartShouldSetPanResponder: () => true,
            onPanResponderMove: (evt, gestureState) => {
                const { dx, dy } = gestureState;
                const left = _previousLeft + dx;
                const top = _previousTop + dy;
    
                const minX = 10;
                const maxX = viewParentMaskingSize.width - maskingScaleSize.width
                const minY = 0;
                const maxY = viewParentMaskingSize.height - maskingScaleSize.height
                const boundedX = Math.min(Math.max(left, minX), maxX);
                const boundedY = Math.min(Math.max(top, minY), maxY);
    
                viewMaskingPosition = { x: boundedX, y: boundedY }
                setBoxPosition({ x: boundedX, y: boundedY });
            },
            onPanResponderRelease: (evt, gestureState) => {
                _previousLeft += gestureState.dx;
                _previousTop += gestureState.dy;
            },
            onPanResponderEnd: (evt, gestureState) => {
                parseCoordinates(gestureState)
            }
        }),
    ).current;
    
    const scalePanResponder = useRef(
        PanResponder.create({
            onStartShouldSetPanResponder: () => true,
            onPanResponderMove: (event, gestureState) => {
    
                const minimumLimit = 100
                const maximumLimit = deviceDisplaySize().width - 50
    
                const width = bottomRightW + gestureState.dx;
                const height = bottomRightH + gestureState.dy;
    
                const minX = 100;
                const maxX = viewParentMaskingSize.width
                const minY = 100;
                const maxY = viewParentMaskingSize.height
                const boundedX = Math.min(Math.max(width, minX), maxX);
                const boundedY = Math.min(Math.max(height, minY), maxY);
    
                setMaskingScaleSize({
                    width: boundedX,
                    height: boundedY
                })
            },
            onPanResponderRelease: (evt, gestureState) => {
                bottomRightW += gestureState.dx;
                bottomRightH += gestureState.dy;
            },
            onPanResponderEnd: (evt, gestureState) => {
                parseCoordinates(gestureState)
            }
        }),
    ).current;
    
    const onLayout = (event) => {
        const { width, height } = event.nativeEvent.layout;
        viewParentMaskingSize = { width: width, height: height }
    
        bottomRightW = width - 40
        bottomRightH = height / 2.5
    
        const latestSize = { width: bottomRightW, height: bottomRightH }
    
        setMaskingScaleSize(latestSize)
    
        const newY = (height / 2) - (height / 3)
    
        viewMaskingPosition = { x: 20, y: newY }
        setBoxPosition({ x: 20, y: newY })
    
        onChangeUpdated({ ...latestSize, ...viewMaskingPosition })
    };
    
    return (
        <View style={{ flex: 1, }} onLayout={onLayout} >
    
            <Svg height="100%" width="100%" style={[StyleSheet.absoluteFill]}>
                <Defs>
                    <Mask id="mask" x="0" y="0" height="100%" width="100%">
                        <Rect height="100%" width="100%" fill="#fff" />
                        <Rect
                            x={boxPosition.x}
                            y={boxPosition.y}
                            height={maskingScaleSize.height}
                            width={maskingScaleSize.width}
                            rx={maskingViewCornerRadius} ry={maskingViewCornerRadius}
                        >
                        </Rect>
                    </Mask>
                </Defs>
                <Rect
                    height="100%"
                    width="100%"
                    fill={AppColor.cameraMasking}
                    mask="url(#mask)"
                    fill-opacity="0"
                />
            </Svg>
    
            <Animated.View
                style={[
                    { height: maskingScaleSize.height, width: maskingScaleSize.width },
                    styles.viewMasking,
                    { transform: [{ translateX: boxPosition.x }, { translateY: boxPosition.y }] }
                ]}
                {...panResponder.panHandlers}
            >
                <View style={{ height: '100%', width: '100%', }}>
    
                    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }} >
                        <View style={{ height: 25, borderRadius: 2, borderWidth: 1.5, backgroundColor: 'white', borderColor: 'white', opacity: 0.4 }} />
                        <View style={{ width: 25, borderRadius: 2, borderWidth: 1.5, backgroundColor: 'white', borderColor: 'white', opacity: 0.4, position: 'absolute' }} />
                    </View>
    
                    <View style={{ position: 'absolute', height: 45, width: 45, top: -2, left: -2, borderColor: '#FED422', borderTopWidth: 4, borderLeftWidth: 4, borderTopLeftRadius: maskingViewCornerRadius }} />
                    <View style={{ position: 'absolute', height: 45, width: 45, top: -2, right: -2, borderColor: '#FED422', borderTopWidth: 4, borderRightWidth: 4, borderTopRightRadius: maskingViewCornerRadius }} />
                    <View style={{ position: 'absolute', height: 45, width: 45, bottom: -2, left: -2, borderColor: '#FED422', borderBottomWidth: 4, borderLeftWidth: 4, borderBottomLeftRadius: maskingViewCornerRadius }} />
                    <View style={{ position: 'absolute', height: 45, width: 45, bottom: -2, right: -2, borderColor: '#FED422', borderBottomWidth: 4, borderRightWidth: 4, borderBottomRightRadius: maskingViewCornerRadius, backgroundColor: 'red' }}
                        {...scalePanResponder.panHandlers}
    
                    >
                        <IcnScaleSize />
                    </View>
                </View>
            </Animated.View>
        </View>
    )
    
    function parseCoordinates(gestureState) {
        //Position
        const left = _previousLeft + gestureState.dx;
        const top = _previousTop + gestureState.dy;
    
        //Scaling 
        const width = bottomRightW + gestureState.dx;
        const height = bottomRightH + gestureState.dy;
    
        const objSize = { width: width, height: height }
        const objCoordinate = { x: left, y: top }
    
        onChangeUpdated({ ...objSize, ...viewMaskingPosition })
    
    }
    
    }
    
    export default MaskView
    const styles = StyleSheet.create({
    
    viewMasking: {
     borderRadius: 23,
     position: 'relative',
     borderWidth: 1,
     borderColor: '#FED422'
     }
     })