Search code examples
arraysreactjsreact-nativeloopspanresponder

I am trying to achieve diagonal selection of elements with PanResponder in React Native


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? enter image description here

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',
  },
});

Solution

  • 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

    enter image description here

    Horizontal Selection

    enter image description here

    Vertical Selection

    enter image description here

    Mixed Selection

    enter image description here