如何在React Native中停止循环动画?

12

我在我的组件中有一个简单的循环动画,就像这样:

runAnimation() {
    console.log('run animation');
    this.state.angle.setValue(0);
    Animated.timing(this.state.angle, {
        toValue: 360,
        duration: 8000,
        easing: Easing.linear
    }).start(() => this.runAnimation());
}

...

<Animated.Image
    style={[
        styles.rotate,
        { transform: [
            { rotate: this.state.angle.interpolate({
                inputRange: [0, 360],
                outputRange: ['0deg', '360deg']
            })},
        ]}
    ]}
    source={require('./spinning_ball.png')}
/>

我该如何停止这个动画呢?例如,当导航到另一个屏幕或用户点击按钮后。

我尝试使用this.state.angle.stopAnimation(),但是注意到控制台仍在打印运行动画。我应该调用不同的停止方法来阻止启动回调函数吗?

6个回答

21

基于我的评论,针对Nguyên Hoàng的答案。如果你调用this.state.angle.stopAnimation(),这是另一种停止循环动画的方法:

runAnimation() {
  this.state.angle.setValue(0);
  Animated.timing(this.state.angle, {
    toValue: 360,
    duration: 8000,
    easing: Easing.linear
  }).start((o) => {
    if(o.finished) {
      this.runAnimation();
    }
  });
}

1
两种解决方案都可行,但这个看起来更加简洁。 - Michael Cheng
不错的捕捉 o.finished - Vraj Solanki

11

我喜欢用hooks来封装我的动画,这比类组件更干净、更容易重用。以下是我如何在TypeScript中实现一个带有简单启动/停止控制的循环动画:

export const useBounceRotateAnimation = (running: boolean = true, rate: number = 300) => {
    //Example of making an infinite looping animation and controlling it with a boolean
    //Note that this assumes the "rate" is constant -- if you wanted to change the rate value after creation the implementation would have to change a bit

    //Only create the animated value once
    const val = useRef(new Animated.Value(0))

    //Store a reference to the animation since we're starting-stopping an infinite loop instead of starting new animations and we aren't changing any animation values after creation. We only want to create the animation object once.
    const anim = useRef(
        Animated.loop(
            Animated.timing(val.current, {
                toValue: 1,
                duration: rate,
                easing: Easing.linear,
                useNativeDriver: true,
                isInteraction: false,
            })
        )
    ).current

    //Interpolate the value(s) to whatever is appropriate for your case
    const interpolatedY = val.current.interpolate({
        inputRange: [0, 0.5, 1],
        outputRange: [0, 6, 0],
    })
    const interpolatedRotate = val.current.interpolate({
        inputRange: [0, 0.25, 0.5, 1],
        outputRange: ["0deg", "-3deg", "3deg", "0deg"],
    })

    //Start and stop the animation based on the value of the boolean prop
    useEffect(() => {
        if (running) {
            anim.start()
        } else {
            //When stopping reset the value to 0 so animated item doesn't stop in a random position
            anim.stop()
            val.current.setValue(0)
        }

        //Return a function from useEffect to stop the animation on unmount
        return () => anim.stop()
     //This useEffect should rerun if "running" or "anim" changes (but anim won't change since its a ref we never modify)
    }, [running, anim])

     //Return the animated values. Use "as const" const assertion to narrow the output type to exactly the two values being returned. 
    return [interpolatedY, interpolatedRotate] as const
}

我使用这个实际的钩子实现在我的游戏中让一个角色图像“行走”。现在在我的组件中使用它非常容易:
const MyComponent = () => {
    const [isRunning, setIsRunning] = useState(true)

    const [translation, rotation] = useBounceRotateAnimation(isRunning)

    return (
        <Animated.View
            style={{
                transform: [{ translateY: translation}, { rotate: rotation}],
            }}
        >
             ....rest of your component here, just setIsRunning(false) to stop animation whenever you need
        </Animated.View>
    )
}

正如您所看到的,此模式非常干净且可重复使用。动画组件不需要知道任何关于动画的信息,它只是消费动画的值并告诉动画何时运行。

1
我发现这种方法对我非常有效,谢谢!在我的情况下,我想在离开页面时停止动画,并在重新进入同一页时重新启动整个动画,我还发现我需要在返回的(“卸载”)函数中调用anim.reset() - ScottM

5

您可以创建一个名为stopAnimation的变量,只有在stopAnimation === false时才会回调runAnimation函数,从而在任何时候停止动画。例如:

this.state = { stopAnimation: false }

runAnimation() {
  this.state.spinValue.setValue(0);
  Animated.timing(
    this.state.spinValue,
    {
      toValue: 1,
      duration: 3000,
      easing: Easing.linear
    }
  ).start( () => {
    if(this.state.stopAnimation === false) {
      this.runAnimation();
    }
  });
}

所以,您只需要创建一个按钮,调用函数this.state = { stopAnimation: true }来停止动画。

这里有一个示例:https://rnplay.org/apps/Lpmh8A


这是唯一的方法吗?我已经考虑过这种可能性,但感觉不是最干净的方法。我还想知道为什么stopAnimation()会在动画技术上没有完成时触发回调。或者它确实完成了吗?对我来说似乎没有意义。 - Michael Cheng
你在哪里调用了 this.state.angle.stopAnimation() 方法,MichaelCheng? - Nguyên Hoàng
@MichaelCheng 无论如何,start() 中的回调函数都将被调用。如果它正常完成,您将在回调中获得一个 {finished: true} 对象。但是,如果它被中断(例如通过 stopAnimation()),则在回调中将获得 {finished: false} - max23_
@NguyênHoàng我曾尝试在componentWillUnmount和componentDidUnmount中使用它。 - Michael Cheng

1

通过调用setValue()方法并赋值的方式,停止循环动画是最简单的方法。例如:

const [opacity, setOpacity] = useState(new Animated.value(0));

// declare your looping animated function

// Any time to stop the looping animated function just call setValue() 
// on your stop animation event or function using opcacity reference. like
opacity.setValue(0)

更多细节请参考https://reactnative.dev/docs/animatedvaluexy#setvalue


1
您可以使用以下代码停止循环:

componentWillUnmount() {
   Animated.timing(this.state.angle).stop();
}

0

设置全局变量

let dataloaded = false;

想停止动画时,随时覆盖变量 (dataloaded) 即可。

componentWillUnmount() {
    dataloaded = true;
}

onPress={() => {
    dataloaded = true;
}}

最终代码将会是这样的

runAnimation() {
    console.log('run animation');
    this.state.angle.setValue(0);
    Animated.timing(this.state.angle, {
        toValue: 360,
        duration: 8000,
        easing: Easing.linear
    })
    .start(() => {
        if ( ! dataloaded) {
            this.runAnimation();
        }
    })
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接