Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
581 views
in Technique[技术] by (71.8m points)

react native - Pan Responder only fires once when updating parent component's state?

I'm using React Native's Pan Responder. I need to update some state in a parent when a Pan Responder in it's children is dragged.

The complex bit is that I also need these children to insert their own element into the draggable area.

For context, here is simpler example that works fine https://snack.expo.io/@jamesweblondon/drag-items

import * as React from 'react';
import { useRef, useState } from 'react';
import { Text, View, PanResponder } from 'react-native';

const items = ['1', '2', '3'];
const ITEM_HEIGHT = 100;

const Parent = () => {
  const [y, setY] = useState(0);
  const [index, setIndex] = useState(null);
  return (
    <View style={{ marginTop: 50 }}>
      <Text>Index: {index}</Text>
      <Text>Y: {y}</Text>
      <View
        style={{ height: ITEM_HEIGHT * items.length, backgroundColor: 'gold' }}>
        {items.map((item, itemIndex) => {
          const isBeingDragged = itemIndex === index;
          const top =
            isBeingDragged
              ? (ITEM_HEIGHT * itemIndex) + y
              : (ITEM_HEIGHT * itemIndex);
          return (
            <View
              style={{
                top,
                width: '100%',
                position: 'absolute',
                zIndex: isBeingDragged ? 1 : 0
              }}
              key={itemIndex}>
              <Child
                index={itemIndex}
                setIndex={setIndex}
                setY={setY}
                item={item}
              />
            </View>
          );
        })}
      </View>
    </View>
  );
};

const Child = ({ index, setIndex, setY, item }) => {
  const panResponder = useRef(
    PanResponder.create({
      onStartShouldSetPanResponder: (evt, gestureState) => true,
      onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
      onMoveShouldSetPanResponder: (evt, gestureState) => true,
      onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
      onPanResponderGrant: (evt, gestureState) => {
        setIndex(index);
      },
      onPanResponderMove: (evt, gestureState) => {
        setY(gestureState.dy);
      },
      onPanResponderTerminationRequest: (evt, gestureState) => true,
      onPanResponderRelease: (evt, gestureState) => {
        setY(0);
        setIndex(null);
      },
      onPanResponderTerminate: (evt, gestureState) => {},
      onShouldBlockNativeResponder: (evt, gestureState) => true,
    })
  ).current;

  return (
    <View
      style={{
        flexDirection: 'row',
        justifyContent: 'space-between',
        backgroundColor: 'tomato',
        padding: 10,
        borderBottomColor: 'black',
        borderBottomWidth: 1,
        height: ITEM_HEIGHT,
      }}>
      <View
        {...panResponder.panHandlers}
        style={{ background: 'grey', height: '100%', width: 40 }}
      />
      <Text>Child {item}</Text>
    </View>
  );
};

export default Parent;

enter image description here

Here is my full example: https://snack.expo.io/@jamesweblondon/drag2

import * as React from 'react';
import { useRef, useState } from 'react';
import { Text, View, PanResponder } from 'react-native';

const CHILD_A = 'CHILD_A';
const CHILD_B = 'CHILD_B';
const CHILD_A_HEIGHT = 100;
const CHILD_B_HEIGHT = 200;

const items = [
  { type: CHILD_A, text: '1' },
  { type: CHILD_B, text: '2' },
  { type: CHILD_A, text: '3' },
];

const Parent = () => {
  const [y, setY] = useState(0);
  const [index, setIndex] = useState(null);

  const heights = items.map((item) =>
    item.type === CHILD_A ? CHILD_A_HEIGHT : CHILD_B_HEIGHT
  );

  let heightsSum = 0;
  const heightsCumulative = heights.map(
    (elem) => (heightsSum = heightsSum + elem)
  );

  return (
    <View style={{ marginTop: 50 }}>
      <Text>Index: {index}</Text>
      <Text>Y: {y}</Text>
      <View style={{ height: heightsSum, backgroundColor: 'gold' }}>
        {items.map((item, itemIndex) => {
          if (item.type === CHILD_A) {
            return (
              <ChildA
                index={itemIndex}
                setIndex={setIndex}
                setY={setY}
                text={item.text}
                DragHandle={(props) => (
                  <DragHandle
                    {...props}
                    index={itemIndex}
                    setIndex={setIndex}
                    setY={setY}
                  />
                )}
              />
            );
          }
          return (
            <ChildB
              index={itemIndex}
              setIndex={setIndex}
              setY={setY}
              text={item.text}
              DragHandle={(props) => (
                <DragHandle
                  {...props}
                  index={itemIndex}
                  setIndex={setIndex}
                  setY={setY}
                />
              )}
            />
          );
        })}
      </View>
    </View>
  );
};

const DragHandle = ({ index, setIndex, setY, children }) => {
  const panResponder = useRef(
    PanResponder.create({
      onStartShouldSetPanResponder: (evt, gestureState) => true,
      onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
      onMoveShouldSetPanResponder: (evt, gestureState) => true,
      onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
      onPanResponderGrant: (evt, gestureState) => {
        setIndex(index);
      },
      onPanResponderMove: (evt, gestureState) => {
        console.log(gestureState.dy); // This works when the line below is removed :)
        setY(gestureState); // This does not work :(
      },
      onPanResponderTerminationRequest: (evt, gestureState) => true,
      onPanResponderRelease: (evt, gestureState) => {},
      onPanResponderTerminate: (evt, gestureState) => {},
      onShouldBlockNativeResponder: (evt, gestureState) => true,
    })
  ).current;
  return (
    <View
      {...panResponder.panHandlers}
      style={{ background: 'grey', height: '100%', width: 40, padding: 10 }}>
      {children}
    </View>
  );
};

const ChildA = ({ index, setIndex, setY, text, DragHandle }) => {
  return (
    <View
      style={{
        flexDirection: 'row',
        justifyContent: 'space-between',
        backgroundColor: 'gold',
        padding: 10,
        borderBottomColor: 'black',
        borderBottomWidth: 1,
        height: CHILD_A_HEIGHT,
      }}>
      <DragHandle>
        <View
          style={{ backgroundColor: 'goldenrod', width: '100%', height: '100%' }}
        />
      </DragHandle>
      <Text>Child A: {text}</Text>
    </View>
  );
};

const ChildB = ({ index, setIndex, setY, text, DragHandle }) => {
  return (
    <View
      style={{
        flexDirection: 'row',
        justifyContent: 'space-between',
        backgroundColor: 'green',
        padding: 10,
        borderBottomColor: 'black',
        borderBottomWidth: 1,
        height: CHILD_B_HEIGHT,
      }}>
      <DragHandle>
      <View
          style={{ backgroundColor: 'lawngreen', width: '100%', height: '100%' }}
        />
      </DragHandle>
      <Text>Child B: {text}</Text>
    </View>
  );
};

export default Parent;

When you drag on the DragHandle component these 2 functions fire once initially but don't again as you continue to drag:

console.log(gestureState.dy); // This works when the line below is removed :)
setY(gestureState); // This does not work :(

If I comment out this line: setY(gestureState); // This does not work :(

Then the console.log above it does work. It continues to log many times per second as you drag over it: console.log(gestureState.dy); // This works when the line below is removed :)

I therefore think it's to do with the Pan Responder being recreated when the parent's state changes but I'm not sure how to fix it. I'm also not sure why the simpler example doesn't have this issue.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

I fixed your example by rewriting your code. You need to take care to not recreate your pan-responder on every render (which happens on you call setY). which you make your view to switch panhandlers on every render. Actually, setY was rerendering your parent component which causes the render of the child

Demo: https://snack.expo.io/@nomi9995/drag2fix

import * as React from 'react';
import { useRef, useState, useEffect } from 'react';
import { Text, View, PanResponder } from 'react-native';

const CHILD_A = 'CHILD_A';
const CHILD_B = 'CHILD_B';
const CHILD_A_HEIGHT = 100;
const CHILD_B_HEIGHT = 200;

const all_items = [
  { type: CHILD_A, text: 'A', height:CHILD_A_HEIGHT },
  { type: CHILD_B, text: 'B', height:CHILD_B_HEIGHT },
  { type: CHILD_A, text: 'C', height:CHILD_A_HEIGHT },
];

const Parent = () => {
  const [y, setY] = useState(0);
  const [index, setIndex] = useState(null);
    const [items, setItems] = useState(all_items);
    
    const setPosition=(index,y)=>{
        const _items=items;
        _items[index]['position']=y;
        setItems(_items)
    }

  const heights = items.map((item) =>
    item.type === CHILD_A ? CHILD_A_HEIGHT : CHILD_B_HEIGHT
  );

  let heightsSum = 0;
  const heightsCumulative = heights.map(
    (elem) => (heightsSum = heightsSum + elem)
  );

  return (
    <View style={{ marginTop: 50 }}>
      <Text>Index: {index}</Text>
      <Text>Y: {y}</Text>
      <View style={{ height: heightsSum, backgroundColor: 'gold' }}>
        {items.map((item, itemIndex) => {
                    const isBeingDragged = itemIndex === index;
          const top = isBeingDragged ? item.position + y : item.position;
          if (item.type === CHILD_A) {
            return (
                            <Child
                                top={top}
                                isBeingDragged={isBeingDragged}
                                height={item.height}
                                backgroundColor="gold"
                                childBackgroundColor="goldenrod"
                                position={item.position}
                                setPosition={setPosition}
                                index={itemIndex}
                setIndex={setIndex}
                setY={setY}
                                text={item.text}
              />
            );
          }
          return (
                        <Child
                            top={top}
                            isBeingDragged={isBeingDragged}
                            height={item.height}
                            backgroundColor="green"
                            childBackgroundColor="lawngreen"
                            position={item.position}
                            setPosition={setPosition}
              index={itemIndex}
              setIndex={setIndex}
              setY={setY}
              text={item.text}
            />
          );
        })}
      </View>
    </View>
  );
};

const DragHandle = ({ index, setIndex, setY, children }) => {
  const panResponder = useRef(
    PanResponder.create({
      onStartShouldSetPanResponder: (evt, gestureState) => true,
      onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
      onMoveShouldSetPanResponder: (evt, gestureState) => true,
      onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
      onPanResponderGrant: (evt, gestureState) => {
        setIndex(index);
      },
      onPanResponderMove: (evt, gestureState) => {
        setY(gestureState.dy); // This does not work :(
      },
      onPanResponderTerminationRequest: (evt, gestureState) => true,
      onPanResponderRelease: (evt, gestureState) => {
                setY(0);
        setIndex(null);
            },
      onPanResponderTerminate: (evt, gestureState) => {},
      onShouldBlockNativeResponder: (evt, gestureState) => true,
    })
  ).current;
  return (
    <View
      {...panResponder.panHandlers}>
      {children}
    </View>
  );
};
 
const Child = ({ top, isBeingDragged, position, height, backgroundColor, childBackgroundColor, setPosition, text2, index, setIndex, setY, text }) => {
    return (
        <View 
        style={{
            top,
            width: '100%',
            position: position?'absolute':'relative',
            zIndex: isBeingDragged ? 1 : 0,
        }}
        key={index}
        onLayout={(e)=>position!==undefined?null:setPosition(index,e.nativeEvent.layout.y)}
        >
        <View
      style={{
        flexDirection: 'row',
        justifyContent: 'space-between',
        backgroundColor: backgroundColor,
        padding: 10,
        borderBottomColor: 'black',
        borderBottomWidth: 1,
        height: height,
            }}>
            <DragHandle index={index} setIndex={setIndex} setY={setY}>
        <View
          style={{ backgroundColor: childBackgroundColor, width: 20, height: '100%' }}
                />
            </DragHandle>
      <Text>Child {text2}: {index+1}</Text>
        </View>
        </View>
  );
};

export default Parent;

enter image description here


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...