我正在使用 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;
这是我的完整示例: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 有关,但我不确定如何修复它。我也不确定为什么更简单的例子没有这个问题。
答案 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;
答案 2 :(得分:1)
问题正在更新父重新渲染子中的状态,这导致 responder
重新创建,因此您可以将 useState
与 useReducer
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