英文:
How to sync rendering with react-native-reanimated variable updates?
问题
如果您需要同时
- 更新一些状态以便 UI 重新渲染
- 更新一些动画变量(如 useSharedValue、useAnimatedStyle 等)
对于这种情况,最佳模式是什么?
我尝试了以下方法:
- 相同的代码同时更新状态并分配新值给 Shared Values。
- 更新状态,然后在渲染方法中使用 useEffect 来改变 Shared Values。
- 更新状态,然后在渲染方法中使用 setTimeout(..., 0) 来改变 Shared Values。
无论我使用哪种方法,我总是会得到一个短暂的时间,其中 UI 处于无效状态 - 例如,新渲染的 UI 已经存在,但 Shared Values 仍然是旧的。或者,Shared Values 在 UI 渲染完成之前就已经更新了。这会导致 UI 中出现难看的闪烁。这些闪烁不是确定性的。它们有时会发生,但有时不会。看起来有一种竞争条件。
在我进一步分析之前,从理论上讲,什么是“正确”的做法?我如何同步这两个更新,以便两个更改可以同时可见?
编辑: 由于我仍然没有得到任何答案,我又花了一天的时间来隔离问题。似乎我现在有了一种复现的方法:
https://github.com/yolpsoftware/rea-3-bugrepro-838
如果在这种情况下无法同步 UI 和 JS 线程,我们也欢迎解决问题的变通方法。请查看存储库的 README,了解解决方案的要求。
英文:
If you need to simultaneously
- update some states such that the UI re-renders
- and update some animation variables (useSharedValue, useAnimatedStyle etc.)
what’s the best pattern for this?
I tried the following:
- The same code updates the state and simultaneously assigns new values to the Shared Values.
- Update the state, then in the render method, use an useEffect to change the Shared Values.
- Update the state, then in the render method, use a setTimeout(..., 0) to change the Shared Values.
Regardless of which one I use, I always get a short time where the UI is rendered in an invalid state - for example, the newly rendered UI is there, but the Shared Values are still old. Or, the Shared Values get updated before the UI render is finished. This results in ugly flickers in the UI. These flickers are not deterministic. They happen sometimes, but sometimes not. It seems there is some kind of race condition.
Before I start to analyze this further, what is the “correct” way to do it, from a theoretical standpoint? How can I sync these two updates such that both changes get visible at the same time?
EDIT: Since I still do not get any answers, I spent one more day isolating the problem. Seems I now have sort of a repro:
https://github.com/yolpsoftware/rea-3-bugrepro-838
If is isn't possible to sync the UI and JS thread in such a situation, we are also welcoming workarounds to solve the problem. Please see the README of the repo for requirements for a solution.
答案1
得分: 1
以下是您的翻译内容:
这是我的方法:
在给定时间内,在屏幕上渲染3个元素,其中2个将在屏幕上可见,另一个将在屏幕外渲染,第三个屏幕将用于在第一个元素被滑动时进行动画处理,以便我们不会看到空屏幕。
整个项目可以在这里找到:GitHub链接
值:
const isPanning = useSharedValue(false);
const activeIndex = useSharedValue(0);
const posY = useSharedValue(0);
const secondPosY = useSharedValue(0);
const thirdPosY = useSharedValue(-windowHeight / 2);
通过使用手势处理程序创建拖动手势处理程序,使用手势处理程序中的新手势检测器:
const handler = Gesture.Pan()
.onStart(() => {
isPanning.value = true;
})
.onChange((event) => {
posY.value = event.translationY;
})
.onEnd(() => {
isPanning.value = false;
if (posY.value > 50) {
posY.value = withTiming(windowHeight);
secondPosY.value = withTiming(windowHeight / 2);
activeIndex.value += 1;
thirdPosY.value = withTiming(0, {}, (finished) => {
if (finished) {
runOnJS(renderNextItems)(activeIndex.value);
}
});
} else {
posY.value = withTiming(0);
}
});
注意:在动画完成后才会更改索引,以便我们可以更改渲染的项目,在更新状态以渲染下一组元素之前,重置所有动画值。
文件:
export const Screen = (props: Props) => {
const [itemsToRender, setItemsToRender] = useState<string[]>(
props.items.slice(0, 3)
);
// 布局信息,从LayoutHelper.getStyles样式对象中复制而来
const size = useWindowDimensions();
const windowHeight = size.height;
const isPanning = useSharedValue(false);
const activeIndex = useSharedValue(0);
const posY = useSharedValue(0);
const secondPosY = useSharedValue(0);
const thirdPosY = useSharedValue(-windowHeight / 2);
const renderNextItems = useCallback((value: number) => {
posY.value = 0;
secondPosY.value = 0;
thirdPosY.value = -windowHeight / 2;
setItemsToRender(props.items.slice(value, value + 3));
}, []);
const handler = Gesture.Pan()
.onStart(() => {
isPanning.value = true;
})
.onChange((event) => {
posY.value = event.translationY;
})
.onEnd(() => {
isPanning.value = false;
if (posY.value > 50) {
posY.value = withTiming(windowHeight);
secondPosY.value = withTiming(windowHeight / 2);
activeIndex.value += 1;
thirdPosY.value = withTiming(0, {}, (finished) => {
if (finished) {
runOnJS(renderNextItems)(activeIndex.value);
}
});
} else {
posY.value = withTiming(0);
}
});
const currentItemTransform = useAnimatedStyle(
() => ({
transform: [
{
translateY: windowHeight / 2 + posY.value,
},
],
}),
[itemsToRender.join(", ")]
);
const nextItemTransform = useAnimatedStyle(
() => ({
transform: [
{
translateY: secondPosY.value,
},
],
}),
[itemsToRender.join(", ")]
);
const thirdTransform = useAnimatedStyle(
() => ({
transform: [
{
translateY: thirdPosY.value,
},
],
}),
[itemsToRender.join(", ")]
);
const itemStyles: any[] = [];
if (itemsToRender[0]) {
itemStyles.push([
styles.item,
{
transform: [
{
translateY: windowHeight / 2,
},
],
},
currentItemTransform,
]);
if (itemsToRender[1]) {
itemStyles.push([styles.item, nextItemTransform]);
}
if (itemsToRender[2]) {
itemStyles.push([styles.item, thirdTransform]);
}
}
return (
<View style={styles.container}>
<GestureDetector gesture={handler}>
<Animated.View style={styles.itemContainer}>
{(itemsToRender.length === 3
? [2, 1, 0]
: itemsToRender.length === 1
? [0]
: []
).map((i) => {
const style = itemStyles[i];
return (
<Animated.View key={itemsToRender[i]} style={style}>
<Text style={styles.text}>{itemsToRender[i]}</Text>
</Animated.View>
);
})}
</Animated.View>
</GestureDetector>
</View>
);
};
这是一个示例GIF,显示了在JavaScript线程中有负载时它将如何工作:
英文:
Here is my approach:
at give time render 3 elements on the screen, 2 of them will be visible on screen and one will be rendered outside the screen, the third screen will be used to animate when the first element is panned, so that we don't see empty screen.
the entire project can be found here: GitHub Link
values:
const isPanning = useSharedValue(false);
const activeIndex = useSharedValue(0);
const posY = useSharedValue(0);
const scondPosY = useSharedValue(0);
const thirdPosY = useSharedValue(-windowHeight / 2);
create pan gesture handler by using the new gesture detector from gesture handler:
const handler = Gesture.Pan()
.onStart(() => {
isPanning.value = true;
})
.onChange((event) => {
posY.value = event.translationY;
})
.onEnd(() => {
isPanning.value = false;
if (posY.value > 50) {
posY.value = withTiming(windowHeight);
scondPosY.value = withTiming(windowHeight / 2);
activeIndex.value += 1;
thirdPosY.value = withTiming(0, {}, (finished) => {
if (finished) {
runOnJS(renderNextItems)(activeIndex.value);
}
});
} else {
posY.value = withTiming(0);
}
});
Note: above the index is changed only when animation is finished so that we can change the rendered items, before updating the state to render next set of elements reset all the animation values
file:
export const Screen = (props: Props) => {
const [itemsToRender, setItemsToRender] = useState<string[]>(
props.items.slice(0, 3)
);
// Layout stuff, gets copied from the LayoutHelper.getStyles style object
const size = useWindowDimensions();
const windowHeight = size.height;
const isPanning = useSharedValue(false);
const activeIndex = useSharedValue(0);
const posY = useSharedValue(0);
const scondPosY = useSharedValue(0);
const thirdPosY = useSharedValue(-windowHeight / 2);
const renderNextItems = useCallback((value: number) => {
posY.value = 0;
scondPosY.value = 0;
thirdPosY.value = -windowHeight / 2;
setItemsToRender(props.items.slice(value, value + 3));
}, []);
const handler = Gesture.Pan()
.onStart(() => {
isPanning.value = true;
})
.onChange((event) => {
posY.value = event.translationY;
})
.onEnd(() => {
isPanning.value = false;
if (posY.value > 50) {
posY.value = withTiming(windowHeight);
scondPosY.value = withTiming(windowHeight / 2);
activeIndex.value += 1;
thirdPosY.value = withTiming(0, {}, (finished) => {
if (finished) {
runOnJS(renderNextItems)(activeIndex.value);
}
});
} else {
posY.value = withTiming(0);
}
});
const currentItemTransform = useAnimatedStyle(
() => ({
transform: [
{
translateY: windowHeight / 2 + posY.value,
},
],
}),
[itemsToRender.join(", ")]
);
const nextItemTransform = useAnimatedStyle(
() => ({
transform: [
{
translateY: scondPosY.value,
},
],
}),
[itemsToRender.join(", ")]
);
const thirdTransform = useAnimatedStyle(
() => ({
transform: [
{
translateY: thirdPosY.value,
},
],
}),
[itemsToRender.join(", ")]
);
const itemStyles: any[] = [];
if (itemsToRender[0]) {
itemStyles.push([
styles.item,
{
transform: [
{
translateY: windowHeight / 2,
},
],
},
currentItemTransform,
]);
if (itemsToRender[1]) {
itemStyles.push([styles.item, nextItemTransform]);
}
if (itemsToRender[2]) {
itemStyles.push([styles.item, thirdTransform]);
}
}
return (
<View style={styles.container}>
<GestureDetector gesture={handler}>
<Animated.View style={styles.itemContainer}>
{(itemsToRender.length === 3
? [2, 1, 0]
: itemsToRender.length === 1
? [0]
: []
).map((i) => {
const style = itemStyles[i];
return (
<Animated.View key={itemsToRender[i]} style={style}>
<Text style={styles.text}>{itemsToRender[i]}</Text>
</Animated.View>
);
})}
</Animated.View>
</GestureDetector>
</View>
);
};
here is the example gif how it will work when there is load in js thread:
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论