Drag an animated component from a ScrollView container to another ScrollView container in React Native

67 views Asked by At

I have two containers which are ScrollView. In one ScrollView, I have a list of texts that are animatable and can be dragged over. The problem is when I dragged them into another ScrollView that wrapps some TextInputs, they went behind it. here I show you what exactly happened:

enter image description here

How can I make texts visible when they are dragged into blue container? here is my code:

import React, { useState } from 'react';
import { StyleSheet, View, SafeAreaView, LayoutRectangle, LayoutChangeEvent, ScrollView } from 'react-native';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
import Animated, {
    useAnimatedStyle,
    useSharedValue,
    withSpring,
    withTiming,
    SharedValue,
    runOnJS,
} from 'react-native-reanimated';
import { Controller, useFieldArray, useForm } from 'react-hook-form';
import MagicText from '../../components/text/MagicText';
import useMagicTheme from '../../theme/useMagicTheme';
import { SvgXml } from 'react-native-svg';
import grid_vertical from '../../assets/icons/grid_vertical';
import MagicTextInput from '../../components/input/MagicTextInput';
import { useTranslation } from 'react-i18next';
import IconButton from '../../components/button/IconButton';
import trash from '../../assets/icons/trash';
import plus from '../../assets/icons/plus';

const scannedCardTexts = [...Array(95)].map((_, index) => `MyTextBB ${index + 1}`);

const BOX_SIZE = 60;

const DragableText = ({
    txtInfo,
    textInputLayoutsInfo,
    textInputWrappersInfo,
    wrapperLayoutsInfo,
    handleAddingtextToInputs,
    scrollViewContentOffset,
    textsScrollViewContentOffset,
}: Props) => {
    const { colors } = useMagicTheme();

    const textLayoutsInfo = useSharedValue<null | LayoutRectangle>(null);
    const textOpacity = useSharedValue<number>(1);
    const offsetX = useSharedValue<number>(0);
    const offsetY = useSharedValue<number>(0);
    const zIndexx = useSharedValue<number>(1);

    const panGesture = Gesture.Pan()
        .onUpdate(e => {
            console.log(`${txtInfo} translate ===> `, {
                translationX: e.translationX,
                absX: e.absoluteX,
                translationY: e.translationY,
                absY: e.absoluteY,
            });

            offsetX.value = e.translationX;
            offsetY.value = e.translationY;

            zIndexx.value = withTiming(9999, { duration: 50 });

            let textWidth = textLayoutsInfo.value?.width;
            let textHeight = textLayoutsInfo.value?.height;
            let textXcoord = textLayoutsInfo.value?.x - textsScrollViewContentOffset.x;
            let textYcoord = textLayoutsInfo.value?.y - textsScrollViewContentOffset.y;

            //Calculate inputs' layout
            textInputLayoutsInfo.forEach(item => {
                // console.log('item =====> ', JSON.stringify(item, null, 2));

                let inputWrapperX = 0;
                let inputWrapperY = 0;

                if (item.title.includes('emails')) {
                    inputWrapperX = textInputWrappersInfo['emails'].x;
                    inputWrapperY = textInputWrappersInfo['emails'].y;
                } else if (item.title.includes('phones')) {
                    inputWrapperX = textInputWrappersInfo['phones'].x;
                    inputWrapperY = textInputWrappersInfo['phones'].y;
                }

                let textInputYcoord = item.layout.y + inputWrapperY + wrapperLayoutsInfo.value?.y;
                let textInputHeight = textInputYcoord + item.layout.height;

                let textInputXcoord = item.layout.x + inputWrapperX + wrapperLayoutsInfo.value?.x;
                let textInputWidth = textInputXcoord + item.layout.width;

                let verticalBoundaryCondition =
                    e.translationY + scrollViewContentOffset.y + textYcoord + textHeight > textInputYcoord &&
                    e.translationY + scrollViewContentOffset.y + textYcoord < textInputHeight;

                let horizontalBoundaryCondition =
                    e.translationX + textXcoord + textWidth > textInputXcoord &&
                    e.translationX + textXcoord < textInputWidth;

                if (verticalBoundaryCondition && horizontalBoundaryCondition) {
                } else {
                }
            });
        })
        .onEnd(e => {
            let textWidth = textLayoutsInfo.value?.width;
            let textHeight = textLayoutsInfo.value?.height;
            let textXcoord = textLayoutsInfo.value?.x - textsScrollViewContentOffset.x;
            let textYcoord = textLayoutsInfo.value?.y - textsScrollViewContentOffset.y;

            //Calculate inputs' layout
            textInputLayoutsInfo.forEach(item => {
                // console.log('item =====> ', JSON.stringify(item, null, 2));

                let inputWrapperX = 0;
                let inputWrapperY = 0;

                if (item.title.includes('emails')) {
                    inputWrapperX = textInputWrappersInfo['emails'].x;
                    inputWrapperY = textInputWrappersInfo['emails'].y;
                } else if (item.title.includes('phones')) {
                    inputWrapperX = textInputWrappersInfo['phones'].x;
                    inputWrapperY = textInputWrappersInfo['phones'].y;
                }

                let textInputYcoord = item.layout.y + inputWrapperY + wrapperLayoutsInfo.value?.y;
                let textInputHeight = textInputYcoord + item.layout.height;

                let textInputXcoord = item.layout.x + inputWrapperX + wrapperLayoutsInfo.value?.x;
                let textInputWidth = textInputXcoord + item.layout.width;

                let verticalBoundaryCondition =
                    e.translationY + scrollViewContentOffset.y + textYcoord + textHeight > textInputYcoord &&
                    e.translationY + scrollViewContentOffset.y + textYcoord < textInputHeight;

                let horizontalBoundaryCondition =
                    e.translationX + textXcoord + textWidth > textInputXcoord &&
                    e.translationX + textXcoord < textInputWidth;

                // console.log({ textWidth, textHeight, textInputYcoord, textInputHeight, textInputXcoord, textInputWidth });

                if (verticalBoundaryCondition && horizontalBoundaryCondition) {
                    textOpacity.value = withTiming(0, { duration: 50 });
                    runOnJS(handleAddingtextToInputs)(item.title, txtInfo);
                } else {
                    offsetX.value = withSpring(0);
                    offsetY.value = withSpring(0);
                }
            });
        });

    const animatedStyle = useAnimatedStyle(() => ({
        transform: [{ translateX: offsetX.value }, { translateY: offsetY.value }],
        opacity: textOpacity.value,
        zIndex: zIndexx.value,
    }));

    return (
        <GestureDetector gesture={panGesture}>
            <Animated.View
                onLayout={e => {
                    // console.log(`${txtInfo} layout ===> `, e.nativeEvent.layout)
                    textLayoutsInfo.value = e.nativeEvent.layout;
                }}
                style={[styles.draggableWrapper, animatedStyle]}>
                <SvgXml xml={grid_vertical} width={16} height={16} color={colors.h3Secondary} />
                <MagicText txtStyle={{ fontSize: 12, fontWeight: '500', color: colors.h3Secondary, marginTop: 2 }}>
                    {txtInfo}
                </MagicText>
            </Animated.View>
        </GestureDetector>
    );
};

const OcrScreen = ({ navigation, route }) => {
    const { t } = useTranslation();

    const { colors } = useMagicTheme();
    const wrapperLayoutsInfo = useSharedValue<null | LayoutRectangle>(null);

    const nameTextInputOpacity = useSharedValue<number>(1);
    const phoneTextInputOpacity = useSharedValue<number>(1);
    const addressTextInputOpacity = useSharedValue<number>(1);

    const [textInputLayoutsInfo, setTextInputLayoutsInfo] = useState<{ title: string; layout: LayoutRectangle }[]>([]);
    const [textInputWrappersInfo, setTextInputWrappersInfo] = useState({});
    const [scrollViewContentOffset, setScrollViewContentOffset] = useState<{ x: number; y: number }>({ x: 0, y: 0 });
    const [textsScrollViewContentOffset, setTextsScrollViewContentOffset] = useState<{ x: number; y: number }>({
        x: 0,
        y: 0,
    });
    const inputsAnimatedOpacity = useSharedValue<{ inputName: string; opacityValue: number }[]>([]);

    // const [infos, setInfos] = useState<string[]>(ocrData.texts);
    const [infos, setInfos] = useState<string[]>(scannedCardTexts);
    const [droppedCount, setDroppedCount] = useState(0);

    const isAllTextsBeenDropped = droppedCount === infos.length;

    const { control, watch, getValues, reset, resetField, setValue, handleSubmit, formState } = useForm({
        defaultValues: {
            name: '',
            company: '',
            headline: '',
            address: '',
            zipcode: '',
            emails: [{ emailValue: '' }],
            phones: [{ phoneValue: '' }],
        },
    });

    const {
        fields: emailFields,
        append: appendEmailField,
        remove: removeEmailField,
    } = useFieldArray({ name: 'emails', control });

    const {
        fields: phonesFields,
        append: appendPhoneField,
        remove: removePhoneField,
    } = useFieldArray({
        name: 'phones',
        control,
    });

    const onConfirm = (data: any) => {
        console.log('OnConfirm form ===> ', JSON.stringify(data, null, 2));
    };

    const removeTextFromInfos = (txt: string) => {
        let temp = [...infos];
        let ioi = temp.findIndex(item => item === txt);
        temp.splice(ioi, 1);

        setInfos(temp);
    };

    const saveInputLayout = (inputName: string, event: LayoutChangeEvent, isParent: boolean = false) => {
        let temp = [...textInputLayoutsInfo];
        let ioi = temp.findIndex(item => item.title === inputName);

        const {
            nativeEvent: { layout },
        } = event;

        if (ioi !== -1) {
            temp.splice(ioi, 1);
        }

        temp.push({ title: inputName, layout });
        setTextInputLayoutsInfo(temp);
    };

    const saveInputWrapperLayout = (title: string, e: LayoutChangeEvent) => {
        let temp = { ...textInputWrappersInfo };

        temp[title] = e.nativeEvent.layout;

        setTextInputWrappersInfo(temp);
    };

    const handleAddingtextToInputs = (type: string, txt: string) => {
        setValue(type, Boolean(watch(type)) ? `${watch(type)} ${txt}` : txt, {
            shouldDirty: true,
            shouldValidate: true,
        });

        setDroppedCount(prev => prev + 1);
    };

    return (
        <SafeAreaView style={{ flex: 1, backgroundColor: colors.mainBackground }}>
            {/* <ScrollView style={{ maxHeight: '45%' }}> */}
            <View style={{ height: '30%', display: isAllTextsBeenDropped ? 'none' : 'flex' }}>
                <ScrollView
                    style={{ flex: 1 }}
                    contentContainerStyle={[styles.draggbleTextsWrapper]}
                    onScroll={({
                        nativeEvent: {
                            contentOffset: { x, y },
                        },
                    }) => {
                        // console.log('Scroll View ====> ', JSON.stringify(e.nativeEvent, null, 2));
                        setTextsScrollViewContentOffset({ x, y });
                    }}>
                    {infos.map((item, index) => {
                        return (
                            <DragableText
                                key={index}
                                txtInfo={item}
                                textInputLayoutsInfo={textInputLayoutsInfo}
                                textInputWrappersInfo={textInputWrappersInfo}
                                wrapperLayoutsInfo={wrapperLayoutsInfo}
                                nameTextInputOpacity={nameTextInputOpacity}
                                phoneTextInputOpacity={phoneTextInputOpacity}
                                addressTextInputOpacity={addressTextInputOpacity}
                                removeTextFromInfos={removeTextFromInfos}
                                handleAddingtextToInputs={handleAddingtextToInputs}
                                scrollViewContentOffset={scrollViewContentOffset}
                                textsScrollViewContentOffset={textsScrollViewContentOffset}
                                // handleUpdateStyle={handleUpdateStyle}
                            />
                        );
                    })}
                </ScrollView>
            </View>
            {/* </ScrollView> */}

            {/**Form */}
            <KeyboardAwareScrollView
                style={{ flex: 1, paddingHorizontal: 24, backgroundColor: 'powderblue' }}
                contentContainerStyle={{ rowGap: 24, paddingBottom: 74 }}
                onScroll={({
                    nativeEvent: {
                        contentOffset: { x, y },
                    },
                }) => {
                    // console.log('Scroll View ====> ', JSON.stringify(e.nativeEvent, null, 2));
                    setScrollViewContentOffset({ x, y });
                }}
                onLayout={e => {
                    // console.log(e.nativeEvent.layout);
                    wrapperLayoutsInfo.value = e.nativeEvent.layout;
                }}>
                {/* {[...Array(10)].map((item, index) => (
                    <View key={index} style={{ height: 65, backgroundColor: 'blue', marginBottom: 20 }} />
                ))} */}

                <Controller
                    name="name"
                    control={control}
                    // rules={{ required: true }}
                    render={({ field: { onChange, value } }) => (
                        <Animated.View
                            // style={[{ backgroundColor: '' }, animatedNameTextInputStyle]}
                            // style={[{ backgroundColor: '' }, handleReturnStyle('name')]}
                            onLayout={e => {
                                // console.log(e.nativeEvent.layout);
                                saveInputLayout('name', e);
                            }}>
                            <MagicText
                                txtStyle={{
                                    marginBottom: 4,
                                    fontSize: 12,
                                    color: colors.h3,
                                }}>
                                {t('Name')}
                            </MagicText>
                            <MagicTextInput
                                placeholder="Enter your name"
                                value={value}
                                onChangeText={onChange}
                                // containerStyle={{
                                //     borderColor: errors.name ? colors.error : colors.borderPrimary,
                                // }}
                            />
                        </Animated.View>
                    )}
                />

                <Controller
                    name="company"
                    control={control}
                    // rules={{}}
                    render={({ field: { onChange, value } }) => (
                        <Animated.View
                            // style={[{ backgroundColor: '' }, animatedNameTextInputStyle]}
                            // style={[{ backgroundColor: '' }, handleReturnStyle('company')]}
                            onLayout={e => {
                                // console.log(e.nativeEvent.layout);
                                saveInputLayout('company', e);
                            }}>
                            <MagicText
                                txtStyle={{
                                    marginBottom: 4,
                                    fontSize: 12,
                                    color: colors.h3,
                                }}>
                                {t('Company')}
                            </MagicText>
                            <MagicTextInput
                                placeholder="Company name"
                                value={value}
                                onChangeText={onChange}
                                // containerStyle={{
                                //     borderColor: errors.company ? colors.error : colors.borderPrimary,
                                // }}
                            />
                        </Animated.View>
                    )}
                />

                <Controller
                    name="headline"
                    control={control}
                    // rules={{ required: true }}
                    render={({ field: { onChange, value } }) => (
                        <Animated.View
                            // style={[{ backgroundColor: '' }, animatedNameTextInputStyle]}
                            // style={[{ backgroundColor: '' }, handleReturnStyle('headline')]}
                            onLayout={e => {
                                // console.log(e.nativeEvent.layout);
                                saveInputLayout('headline', e);
                            }}>
                            <MagicText
                                txtStyle={{
                                    marginBottom: 4,
                                    fontSize: 12,
                                    color: colors.h3,
                                }}>
                                {t('Job')}
                            </MagicText>
                            <MagicTextInput
                                placeholder="Job title"
                                value={value}
                                onChangeText={onChange}
                                // containerStyle={{
                                //     borderColor: errors.name ? colors.error : colors.borderPrimary,
                                // }}
                            />
                        </Animated.View>
                    )}
                />

                <Controller
                    name="address"
                    control={control}
                    // rules={{ required: true }}
                    render={({ field: { onChange, value } }) => (
                        <Animated.View
                            // style={[{ backgroundColor: '' }, animatedNameTextInputStyle]}
                            // style={[{ backgroundColor: '' }, handleReturnStyle('address')]}
                            onLayout={e => {
                                // console.log(e.nativeEvent.layout);
                                saveInputLayout('address', e);
                            }}>
                            <MagicText
                                txtStyle={{
                                    marginBottom: 4,
                                    fontSize: 12,
                                    color: colors.h3,
                                }}>
                                {t('Address')}
                            </MagicText>
                            <MagicTextInput
                                placeholder="Enter address"
                                value={value}
                                onChangeText={onChange}
                                // containerStyle={{
                                //     borderColor: errors.name ? colors.error : colors.borderPrimary,
                                // }}
                            />
                        </Animated.View>
                    )}
                />

                <Controller
                    name="zipcode"
                    control={control}
                    // rules={{ required: true }}
                    render={({ field: { onChange, value } }) => (
                        <Animated.View
                            onLayout={e => {
                                // console.log(e.nativeEvent.layout);
                                saveInputLayout('zipcode', e);
                            }}>
                            <MagicText
                                txtStyle={{
                                    marginBottom: 4,
                                    fontSize: 12,
                                    color: colors.h3,
                                }}>
                                {t('Zipcode')}
                            </MagicText>
                            <MagicTextInput
                                placeholder="Enter zipcode"
                                value={value}
                                onChangeText={onChange}
                            />
                        </Animated.View>
                    )}
                />

                {/**Emails */}
                <View
                    style={{ rowGap: 24 }}
                    onLayout={e => {
                        saveInputWrapperLayout('emails', e);
                    }}>
                    {emailFields.map((emailField, index) => {
                        return (
                            <Controller
                                key={emailField.id}
                                name={`emails.${index}.emailValue`}
                                control={control}
                                render={({ field: { onChange, value } }) => (
                                    <Animated.View
                                        style={{ flexDirection: 'row' }}
                                        onLayout={e => {
                                            saveInputLayout(`emails.${index}.emailValue`, e);
                                        }}>
                                        <View style={{ flex: 1 }}>
                                            <MagicTextInput
                                                placeholder="Enter email"
                                                keyboardType="email-address"
                                                importantForAutofill="no"
                                                value={value}
                                                onChangeText={onChange}
                                                containerStyle={{
                                                    borderColor: colors.borderPrimary,
                                                }}
                                            />
                                        </View>
                                        <View style={{ justifyContent: 'center', marginStart: 10 }}>
                                            <SvgXml
                                                style={{ display: index > 0 ? 'flex' : 'none' }}
                                                xml={trash}
                                                color={colors.error}
                                                width={24}
                                                height={24}
                                                onPress={() => removeEmailField(index)}
                                            />
                                        </View>
                                    </Animated.View>
                                )}
                            />
                        );
                    })}

                    <IconButton
                        icon={plus}
                        size={24}
                        iconColor={colors.iconSecondary3}
                        label="Add another email"
                        labelStyle={{ color: colors.alertConfirmText, paddingTop: 4 }}
                        containerStyle={{ columnGap: 4, padding: 0 }}
                        onPress={() => appendEmailField({ emailValue: '' })}
                    />
                </View>

                {/**Phones */}
                <View
                    style={{ rowGap: 24 }}
                    onLayout={e => {
                        saveInputWrapperLayout('phones', e);
                    }}>
                    {phonesFields.map((phoneField, index) => {
                        return (
                            <Controller
                                key={phoneField.id}
                                name={`phones.${index}.phoneValue`}
                                control={control}
                                render={({ field: { onChange, value } }) => (
                                    <Animated.View
                                        style={{ flexDirection: 'row' }}
                                        onLayout={e => {
                                            saveInputLayout(`phones.${index}.phoneValue`, e);
                                        }}>
                                        <View style={{ flex: 1 }}>
                                            <MagicTextInput
                                                placeholder="Enter phonenumber"
                                                keyboardType="number-pad"
                                                importantForAutofill="no"
                                                value={value}
                                                onChangeText={onChange}
                                                containerStyle={{
                                                    borderColor: colors.borderPrimary,
                                                }}
                                            />
                                        </View>
                                        <View style={{ justifyContent: 'center', marginStart: 10 }}>
                                            <SvgXml
                                                style={{ display: index > 0 ? 'flex' : 'none' }}
                                                xml={trash}
                                                color={colors.error}
                                                width={24}
                                                height={24}
                                                onPress={() => removePhoneField(index)}
                                            />
                                        </View>
                                    </Animated.View>
                                )}
                            />
                        );
                    })}

                    <IconButton
                        icon={plus}
                        size={24}
                        iconColor={colors.iconSecondary3}
                        label="Add another phonenumber"
                        labelStyle={{ color: colors.alertConfirmText, paddingTop: 4 }}
                        containerStyle={{ columnGap: 4, padding: 0 }}
                        onPress={() => appendPhoneField({ phoneValue: '' })}
                    />
                </View>
            </KeyboardAwareScrollView>

            <IconButton
                icon={trash}
                label="Confirm"
                iconColor={'red'}
                size={0}
                onPress={handleSubmit(onConfirm)}
                containerStyle={{ position: 'absolute', backgroundColor: 'pink', bottom: 15, right: 24 }}
            />
        </SafeAreaView>
    );
};

export default OcrScreen;

const styles = StyleSheet.create({
    box: {
        height: BOX_SIZE,
        width: BOX_SIZE,
        backgroundColor: '#b58df1',
        borderRadius: 12,
    },

    draggbleTextsWrapper: {
        backgroundColor: 'pink',
        flexDirection: 'row',
        gap: 14,
        flexWrap: 'wrap',
        paddingHorizontal: 24,
        paddingVertical: 16,
    },

    draggableWrapper: {
        alignSelf: 'flex-start',
        flexDirection: 'row',
        alignItems: 'center',
        paddingHorizontal: 4,
        paddingVertical: 4,
        backgroundColor: '#ffffff',
        borderRadius: 8,
        elevation: 4,
        shadowColor: '#000000',
        shadowRadius: 4,
        shadowOpacity: 0.5,
    },

    circle: {
        alignItems: 'center',
        justifyContent: 'center',
        width: 180,
        height: 180,
        borderRadius: 180,
        borderWidth: 4,
        borderColor: 'blue'
    },

    input: {
        borderWidth: 2,
        borderColor: '#000',
        borderRadius: 8,
        height: 54,
        margin: 12,
        paddingHorizontal: 12,
        backgroundColor: 'powderblue',
        zIndex: -1,
        marginTop: 18,
    },
})
0

There are 0 answers