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:
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,
},
})