Pan Responder 在更新父组件的状态时只触发一次?

时间:2021-01-03 15:58:13

标签: react-native

我正在使用 React Native 的 Pan Responder。当拖动其子项中的 Pan Responder 时,我需要更新父项中的某些状态。

复杂的一点是我还需要这些孩子将他们自己的元素插入可拖动区域。

对于上下文,这里有一个更简单的例子,效果很好 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

这是我的完整示例: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;

当您拖动 DragHandle 组件时,这两个函数最初会触发一次,但在您继续拖动时不会再次触发:

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

如果我注释掉这一行:setY(gestureState); // This does not work :(

然后它上面的console.log 就起作用了。当您拖动它时,它会继续每秒记录多次:console.log(gestureState.dy); // This works when the line below is removed :)

因此,我认为这与在父状态更改时重新创建 Pan Responder 有关,但我不确定如何修复它。我也不确定为什么更简单的例子没有这个问题。

3 个答案:

答案 0 :(得分:2)

import 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, height: CHILD_A_HEIGHT, text: 'A' },
  { type: CHILD_B, height: CHILD_B_HEIGHT, text: 'B' },
  { type: CHILD_A, height: CHILD_A_HEIGHT, text: 'C' },
];

const Parent = () => {
  const [y, setY] = useState(0);
  const [index, setIndex] = useState(null);
  const [items, setItems] = useState(_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)
  );

  function setPosition(index, position) {
    setItems(items => {
      if (!items[index].hasOwnProperty('position')) {
        items[index]['position'] = position;
      }
      return items;
    })
  }

  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;

          const childProps = {
              top,
              setPosition,
              index: itemIndex,
              setIndex: setIndex,
              setY: setY,
              text: item.text,
              isBeingDragged: isBeingDragged
          };

          if (item.type === CHILD_A) {
            return (
              <ChildA
                 {...childProps}
              />
            );
          }
          return (
            <ChildB
                {...childProps}
            />
          );
        })}
      </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);
      },
      onPanResponderTerminationRequest: (evt, gestureState) => true,
      onPanResponderRelease: (evt, gestureState) => {
        setY(0);
        setIndex(null);
      },
      onPanResponderTerminate: (evt, gestureState) => {},
      onShouldBlockNativeResponder: (evt, gestureState) => true,
    })
  ).current;
  return (
    <View
      {...panResponder.panHandlers}
      style={{ background: 'grey', width: 40, padding: 10 }}>
      {children}
    </View>
  );
};

const ChildA = ({ index, top, isBeingDragged, setPosition, setIndex, setY, text }) => {
  return (
    <View
      style={{
        top,
        width: '100%',
        position: top == undefined ? 'relative' : 'absolute',
        zIndex: isBeingDragged ? 1 : 0,
      }}
      onLayout={(evt) => setPosition(index, evt.nativeEvent.layout.y)}
      key={index}>
      <View
        style={{
          flexDirection: 'row',
          justifyContent: 'space-between',
          backgroundColor: 'gold',
          padding: 10,
          borderBottomColor: 'black',
          borderBottomWidth: 1,
          height: CHILD_A_HEIGHT,
        }}>
        <DragHandle index={index} setIndex={setIndex} setY={setY} text={text}>
          <View
            style={{
              backgroundColor: 'goldenrod',
              width: '100%',
              height: '100%',
            }}
          />
        </DragHandle>
        <Text>Child A: {text}</Text>
      </View>
    </View>
  );
};

const ChildB = ({ index, top, isBeingDragged, setPosition, setIndex, setY, text }) => {
  return (
    <View
      style={{
        top,
        width: '100%',
        position: top == undefined ? 'relative' : 'absolute',
        zIndex: isBeingDragged ? 1 : 0,
      }}
      onLayout={(evt) => setPosition(index, evt.nativeEvent.layout.y)}
      key={index}>
      <View
        style={{
          flexDirection: 'row',
          justifyContent: 'space-between',
          backgroundColor: 'green',
          padding: 10,
          borderBottomColor: 'black',
          borderBottomWidth: 1,
          height: CHILD_B_HEIGHT,
        }}>
        <DragHandle index={index} setIndex={setIndex} setY={setY} text={text}>
          <View
            style={{
              backgroundColor: 'lawngreen',
              width: '100%',
              height: '100%',
            }}
          />
        </DragHandle>
        <Text>Child B: {text}</Text>
      </View>
    </View>
  );
};

export default Parent;

注意: ChildA 和 ChildB 组件也可以简化为单个组件

在这里工作example

答案 1 :(得分:1)

我通过重写您的代码修复您的示例。您需要注意不要在每次渲染时重新创建泛响应(这发生在您调用 setY 时)。您可以在每次渲染时切换 panhandler。实际上,setY 正在重新渲染导致子组件渲染的父组件

演示: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

答案 2 :(得分:1)

问题正在更新父重新渲染子中的状态,这导致 responder 重新创建,因此您可以将 useStateuseReducer api 一起使用,而不是使用 Context

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

const items = ['A', 'B', 'A'];

const initialState = {
  y: 0,
};
const store = createContext(initialState);
const { Provider, Consumer } = store;

const StateProvider = ({ children }) => {
  const [state, dispatch] = useReducer((state, action) => {
    switch (action.type) {
      case 'UPDATE_Y':
        return { y: action.payload };
      default:
        return state;
    }
  }, initialState);

  return <Provider value={{ state, dispatch }}>{children}</Provider>;
};

const Parent = () => {
  return (
    <StateProvider>
      <View style={{ padding: 30, marginTop: 50 }}>
        <Consumer>{({ state }) => <Text>{state.y}</Text>}</Consumer>
        {items.map((item, index) => {
          if (item === 'A') {
            return (
              <ChildA
                key={index}
                Dragger={(props) => <DragMe {...props} index={index} />}
              />
            );
          }
          return (
            <ChildB
              key={index}
              Dragger={(props) => <DragMe {...props} index={index} />}
            />
          );
        })}
      </View>
    </StateProvider>
  );
};

const DragMe = ({ children }) => {
  const reduxState = useContext(store);
  const panResponder = useRef(
    PanResponder.create({
      onStartShouldSetPanResponder: (evt, gestureState) => true,
      onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
      onMoveShouldSetPanResponder: (evt, gestureState) => true,
      onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
      onPanResponderGrant: (evt, gestureState) => {},
      onPanResponderMove: (evt, gestureState) => {
        reduxState.dispatch({
          type: 'UPDATE_Y',
          payload: gestureState.dy,
        });
      },
      onPanResponderTerminationRequest: (evt, gestureState) => true,
      onPanResponderRelease: (evt, gestureState) => {},
      onPanResponderTerminate: (evt, gestureState) => {},
      onShouldBlockNativeResponder: (evt, gestureState) => true,
    })
  ).current;
  return (
    <View
      style={{ backgroundColor: 'grey', width: 100, height: 100, zIndex: 1 }}
      {...panResponder.panHandlers}>
      {children}
    </View>
  );
};

const ChildA = ({ Dragger }) => {
  return (
    <View
      style={{
        flexDirection: 'row',
        justifyContent: 'space-between',
        backgroundColor: 'gold',
      }}>
      <Dragger>
        <Text>Drag Child A</Text>
      </Dragger>
      <Text>Child A</Text>
    </View>
  );
};

const ChildB = ({ Dragger }) => {
  return (
    <View
      style={{
        flexDirection: 'row',
        justifyContent: 'space-between',
        backgroundColor: 'tomato',
      }}>
      <Dragger>
        <Text>Drag Child B</Text>
      </Dragger>
      <Text>Child B</Text>
    </View>
  );
};

export default Parent;

在这里工作example