1

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',
  },
});
lios
  • 218
  • 5
  • 26

1 Answers1

2

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

David Scholz
  • 8,421
  • 12
  • 19
  • 34
  • 1
    Such a well documented and eloquent answer. You were right in your remark about backwards moving, the example in my question didn't apply this but I hoped to solved diagonal first. Your answer solves the diagonal and the backwards issue and it works perfect, given that I can increase the tolerance (tested this and it was great). I accept your answer and I thank you very much for your time. – lios Mar 22 '22 at 19:53
  • You are very welcome! I am glad it helped! – David Scholz Mar 22 '22 at 20:03