I am trying to achieve selection of multiple elements of an array by PanResponder. It works with horizontal or vertical touching sequence but I can't make it to work with diagonal. When I say diagonal, it selects all elements next to the touched elements of the array but I want to keep only the touched ones. For example I only need 1, 7 and 13. How can I achieve this?
The code is as follows
import React, {useState, useEffect, useRef} from 'react';
import {
View,Dimensions,
Text,
StyleSheet,
TouchableOpacity,
PanResponder,
PanResponderGestureState,
SafeAreaView,
LayoutChangeEvent,
} from 'react-native';
const SQUARE_SIZE = Dimensions.get("window").width/5;
const squareList = [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22,
23, 24, 25, 26, 27,
];
type OffsetType = {
id: number;
x: number;
y: number;
height: number;
width: number;
};
export default function App() {
const [selectedList, setSelectedList] = useState([]);
const [gestureSelectionList, setGestureSelectionList] = useState(
[],
);
const [offset, setOffset] = React.useState([]);
const [translate, setTranslate] = useState(
null,
);
console.log(
'offset',
offset.find(item => item.id === 2),
);
useEffect(() => {
console.log("ASDSA");
if (translate !== null) {
offset.map(offsetItem => {
const {moveX, moveY, x0, y0} = translate;
if (
(offsetItem.x >= x0 - SQUARE_SIZE &&
offsetItem.y >= y0 - SQUARE_SIZE &&
offsetItem.x <= moveX &&
offsetItem.y <= moveY)
) {
const isAlreadySelected = gestureSelectionList.find(
item => item === offsetItem.id,
);
if (!isAlreadySelected) {
setGestureSelectionList(prevState => [...prevState, offsetItem.id]);
}
} else {
const isAlreadySelected = gestureSelectionList.find(
item => item === offsetItem.id,
);
if (isAlreadySelected) {
const filterSelectedItem = gestureSelectionList.filter(
item => item !== offsetItem.id,
);
setGestureSelectionList(filterSelectedItem);
}
}
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [translate]);
const onSelectItem = (pressedItem: number) => {
const isAlreadySelected = selectedList.find(item => item === pressedItem);
if (isAlreadySelected) {
const filterSelectedItem = selectedList.filter(
item => item !== pressedItem,
);
setSelectedList(filterSelectedItem);
} else {
setSelectedList(prevState => [...prevState, pressedItem]);
}
};
function removeDuplicateAndMerge(
selectedArray: number[],
gestureSelectedArray: number[],
): number[] {
const myArray = selectedArray.filter(function (el) {
return gestureSelectedArray.indexOf(el) < 0;
});
const revArray = gestureSelectedArray.filter(function (el) {
return selectedArray.indexOf(el) < 0;
});
return [...myArray, ...revArray];
}
useEffect(() => {
if (!translate) {
setSelectedList(
removeDuplicateAndMerge(selectedList, gestureSelectionList),
);
setGestureSelectionList([]);
}
}, [translate]);
const panResponder = useRef(
PanResponder.create({
onMoveShouldSetPanResponder: () => true,
onMoveShouldSetPanResponderCapture: _evt => true,
onPanResponderMove: (_evt, gesture) => {
setTranslate({...gesture});
},
onPanResponderRelease: () => {
setTranslate(null);
},
onShouldBlockNativeResponder: () => true,
}),
).current;
const itemStyle = (item: number) => {
const gestureBGColor = gestureSelectionList.find(
selectedItem => selectedItem === item,
)
? true
: false;
const selectedBGColor = selectedList.find(
selectedItem => selectedItem === item,
)
? true
: false;
return {
backgroundColor: gestureBGColor
? 'gray'
: selectedBGColor
? 'blue'
: 'orangered',
};
};
return (
<View style={styles.listWrapper} {...panResponder.panHandlers}>
{squareList.map(item => {
return (
<TouchableOpacity
onLayout={(event: LayoutChangeEvent | any) => {
event.target.measure(
(
_x: number,
_y: number,
width: number,
height: number,
pageX: number,
pageY: number,
) => {
setOffset(prevOffset => [
...prevOffset,
{
id: item,
x: pageX,
y: pageY,
width,
height,
},
]);
},
);
}}
onPress={() => onSelectItem(item)}
key={item}
style={[styles.squareStyle, itemStyle(item)]}>
<Text style={{color: '#fff', fontSize: 18}}>{item}</Text>
</TouchableOpacity>
);
})}
</View>
);
}
const styles = StyleSheet.create({
listWrapper: {
flexDirection: 'row',
flexWrap: 'wrap',
},
squareStyle: {
backgroundColor: 'orangered',
height: SQUARE_SIZE,
width: SQUARE_SIZE,
borderWidth:1,
justifyContent: 'center',
alignItems: 'center',
},
});
Your calculation for checking if the touch movement is inside a specific rectangle is not correct. You need to change it to the following.
const {moveX, moveY, x0, y0} = translate;
if (moveX > offsetItem.x && moveX < offsetItem.x + SQUARE_SIZE && moveY > offsetItem.y && moveY < offsetItem.y + SQUARE_SIZE) {
...
}
This also fixes an issue that you haven't stated in your question: your selection did not work if you start from bottom to top. Using the above fixes this issue as well.
You also do not need the else
part, where you update the selection list. Notice, that there is chance (if you do not touch very precisely) that the items right next to your touch are selected as well since the diagonal line is very thin). I have added some tolerance to fix this. You might to tweak this a little bit. The correct useEffect
implementation looks as follows.
useEffect(() => {
if (translate !== null) {
offset.map(offsetItem => {
const {moveX, moveY, x0, y0} = translate;
if (moveX > offsetItem.x + 5 && moveX < offsetItem.x + SQUARE_SIZE - 5 && moveY > offsetItem.y + 5 && moveY < offsetItem.y + SQUARE_SIZE- 5) {
const isAlreadySelected = gestureSelectionList.find(
item => item.id === offsetItem.id,
);
if (!isAlreadySelected) {
setGestureSelectionList(prevState => [...prevState, offsetItem.id]);
}
}
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [translate]);
Here are my test cases and here is a snack of the current implementation.
Diagonal Selection
Horizontal Selection
Vertical Selection
Mixed Selection