Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Animation not starting from the new update value #6903

Closed
scrapecoder opened this issue Jan 15, 2025 · 7 comments
Closed

Animation not starting from the new update value #6903

scrapecoder opened this issue Jan 15, 2025 · 7 comments
Labels
Close when stale This issue is going to be closed when there is no activity for a while Missing repro This issue need minimum repro scenario Platform: iOS This issue is specific to iOS

Comments

@scrapecoder
Copy link

scrapecoder commented Jan 15, 2025

Description

There is an issue with a progress animation where, after updating the progress value and toggling the toggleProgress function, the animation does not restart. Although the new animated value is set correctly, the animation itself fails to start.

   const [isRunning, setIsRunning] = useState(false);
    const progress = useSharedValue(0);
    const segments = useSharedValue<number[]>([]);
    const [completed, setProgressCompleted] = useState<boolean>(false);
    const progressRunning = useRef<boolean>(false);
    const paused = useSharedValue(false);
    const prevVideoStackLength = useRef(videoStack.length);
    const segmentStartTime = useRef<number | null>(null); // Track start time for current segment
    const segmentEndTime = useRef<number | null>(null);
    const segmentsTime = useRef<{startTime: number; endTime: number}[]>([]);

useEffect(() => {
  if (videoStack.length < prevVideoStackLength.current) {
    if (segments.value.length > 0 && segmentsTime.current.length > 0) {
      if (segmentsTime.current.length >= 2) {
        const endTime = segmentsTime.current[segmentsTime.current.length - 1].endTime;
        const startTime = segmentsTime.current[segmentsTime.current.length - 1].startTime;
        const segmentProgress = endTime - startTime;
        segmentsTime.current.pop();

        if (completed) setProgressCompleted(false);
        progress.set(progress.value - segmentProgress); // value is updating here
      } else {
        progress.set(0);
      }
    }
  }

  prevVideoStackLength.current = videoStack.length;
}, [videoStack]);

const handleStart = () => {
  setIsRunning(true);
  if (progress.value > 99) return; // Stop if the progress is already complete
  progressRunning.current = true;
  segmentStartTime.current = progress.value;

  progress.value = withPause(
    withTiming(100, { duration: 60000, easing: Easing.linear }, finished => {
      if (finished) {
        runOnJS(setProgressCompleted)(true);
      }
    }),
    paused,
  );
};

const toggleProgress = () => { // after update value this is called
  if (!isRunning) return;
  if (paused.value) {
    segmentStartTime.current = progress.value;
  } else {
    segmentEndTime.current = progress.value;
    if (segmentStartTime.current != null) {
      segmentsTime.current = [
        ...segmentsTime.current,
        { startTime: segmentStartTime.current, endTime: segmentEndTime.current },
      ];
    }
  }
  paused.value = !paused.value;

  if (!progressRunning.current) {
    progressRunning.current = !progressRunning.current;
    return;
  }

  progressRunning.current = !progressRunning.current;
  if (progress.value < 98) {
    segments.set([...segments.value, progress.value]);
  }
};

Steps to reproduce

Start the progress animation using the handleStart function.
Update the progress.value manually or through some logic (e.g., removing a video segment).
Call the toggleProgress function to pause or resume the animation.
Observe that the progress animation does not restart.

Expected Behavior:
The progress animation should restart whenever the toggleProgress function is invoked after a value update.

Snack or a link to a repository

nil

Reanimated version

3.16.3

React Native version

0.75.2

Platforms

iOS

JavaScript runtime

None

Workflow

None

Architecture

None

Build type

None

Device

None

Device model

No response

Acknowledgements

Yes

@github-actions github-actions bot added Platform: iOS This issue is specific to iOS Missing repro This issue need minimum repro scenario labels Jan 15, 2025
Copy link

Hey! 👋

The issue doesn't seem to contain a minimal reproduction.

Could you provide a snack or a link to a GitHub repository under your username that reproduces the problem?

@szydlovsky
Copy link
Contributor

Hey @scrapecoder your example looks pretty complex. I kinda get what the code does but would love some at least minimal usable example to test out. Also, I believe this case could be written into a much smaller and simpler reproduction because the issue you're seeing seems simpler than the code you provided. Thanks in advance 🙏

@szydlovsky
Copy link
Contributor

Also, I don't recognize a withPause hook, I'd also need to see how it works to see whether it makes situation any worse 😄

@scrapecoder
Copy link
Author

Hi @szydlovsky, thanks for your response.
below is short code -

import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withTiming,
} from 'react-native-reanimated';
import { withPause } from 'react-native-redash';

const paused = useSharedValue(false);
const progress = useSharedValue(0);

useEffect(() => {
  handleStart();
}, []);

const setNewValue = () => {
  paused.value = !paused.value;
  progress.set(20); // Update progress value
};

const handleStart = () => {
  progress.value = withPause(
    withTiming(100, { duration: 60000, easing: Easing.linear }, (finished) => {
      if (finished) {
        // Callback when animation finishes
      }
    }),
    paused
  );
};

const toggleProgress = () => {
  paused.value = !paused.value;
};

const progressStyle = useAnimatedStyle(() => ({
  width: `${progress.value}%`,
  backgroundColor: progressColor,
}));

return (
  <View>
    <Animated.View style={[styles.progress, progressStyle]} />
    <TouchableOpacity onPress={toggleProgress}>
      <Text>Toggle</Text>
    </TouchableOpacity>
    <TouchableOpacity onPress={setNewValue}>
      <Text>Set new value</Text>
    </TouchableOpacity>
  </View>
);

Description - Using react-native-reanimated and react-native-redash, the animation starts successfully when the component mounts, and it can be toggled (paused/resumed) without issues. However, after updating the animated value using a setter (setNewValue), the animation does not restart as expected.

Expected Behavior
The animation should start and continue from the updated value when progress is modified via setNewValue, followed by using the toggle functionality.

@szydlovsky
Copy link
Contributor

@scrapecoder alright, so this is not a bug 😄 In general, setting a value to a Shared Value is currently a way to stop an ongoing animation using it. When you just plain set it, the withPause and withTiming gets overrided and well.. value doesn't fluctuate to create an animation. Instead, you can firstly set the progress to, say, 20, which snaps its width to that number and THEN once again use withPause and withTiming to have it animate again.

Here's changed code (I made the durations a bit shorter):

import React from 'react';
import { StyleSheet, View, TouchableOpacity, Text } from 'react-native';
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withTiming,
  Easing,
} from 'react-native-reanimated';
import { withPause } from 'react-native-redash';

export function EmptyExample() {
  const paused = useSharedValue(false);
  const progress = useSharedValue(0);

  React.useEffect(() => {
    handleStart();
  }, []);

  const setNewValue = () => {
    paused.value = false;

    progress.set(20);
    progress.value = withPause(
      withTiming(
        100,
        { duration: 5000, easing: Easing.linear },
        (finished) => {
          if (finished) {
            // Callback when animation finishes
          }
        }
      ),
      paused
    );
  };

  const handleStart = () => {
    progress.value = withPause(
      withTiming(
        100,
        { duration: 5000, easing: Easing.linear },
        (finished) => {
          if (finished) {
            // Callback when animation finishes
          }
        }
      ),
      paused
    );
  };

  

  const toggleProgress = () => {
    paused.value = !paused.value;
  };

  const progressStyle = useAnimatedStyle(() => ({
    width: `${progress.value}%`,
  }));

  return (
    <View style={styles.container}>
      <Animated.View style={[styles.progress, progressStyle]} />
      <TouchableOpacity onPress={toggleProgress}>
        <Text>Toggle</Text>
      </TouchableOpacity>
      <TouchableOpacity onPress={setNewValue}>
        <Text>Set new value</Text>
      </TouchableOpacity>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'flex-start',
    padding: 50,
  },
  progress: {
    backgroundColor: 'green',
    height: 70,
  },
});

export default EmptyExample;

@szydlovsky
Copy link
Contributor

P.S. it's worth noting that the animation WILL feel slower because it again will be with the same duration but smaller range of values to go through. Since the easing you chose is Linear, you can always make some helper function to easily adjust the duration like this:

import React from 'react';
import { StyleSheet, View, TouchableOpacity, Text } from 'react-native';
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withTiming,
  Easing,
} from 'react-native-reanimated';
import { withPause } from 'react-native-redash';

const BASE_ANIMATION_DURATION = 5000;
const MIN_PROGRESS = 0;
const MAX_PROGRESS = 100;

const progressToRemainingDuration = (progress: number) => {
  const remainingPercentage = 1 - progress / (MAX_PROGRESS - MIN_PROGRESS);
  return remainingPercentage * BASE_ANIMATION_DURATION;
};

export function EmptyExample() {
  const paused = useSharedValue(false);
  const progress = useSharedValue(MIN_PROGRESS);

  React.useEffect(() => {
    handleStart();
  }, []);

  const setNewValue = () => {
    paused.value = false;

    const newValue = 75;
    const newDuration = progressToRemainingDuration(newValue);

    progress.set(newValue);
    progress.value = withPause(
      withTiming(
        MAX_PROGRESS,
        { duration: newDuration, easing: Easing.linear },
        (finished) => {
          if (finished) {
            // Callback when animation finishes
          }
        }
      ),
      paused
    );
  };

  const handleStart = () => {
    progress.value = withPause(
      withTiming(
        MAX_PROGRESS,
        { duration: BASE_ANIMATION_DURATION, easing: Easing.linear },
        (finished) => {
          if (finished) {
            // Callback when animation finishes
          }
        }
      ),
      paused
    );
  };

  const toggleProgress = () => {
    paused.value = !paused.value;
  };

  const progressStyle = useAnimatedStyle(() => ({
    width: `${progress.value}%`,
  }));

  return (
    <View style={styles.container}>
      <Animated.View style={[styles.progress, progressStyle]} />
      <TouchableOpacity onPress={toggleProgress}>
        <Text>Toggle</Text>
      </TouchableOpacity>
      <TouchableOpacity onPress={setNewValue}>
        <Text>Set new value</Text>
      </TouchableOpacity>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'flex-start',
    padding: 50,
  },
  progress: {
    backgroundColor: 'green',
    height: 70,
  },
});

export default EmptyExample;

Anything else you would like to know? 😄 Or can I close the issue?

@szydlovsky szydlovsky added the Close when stale This issue is going to be closed when there is no activity for a while label Jan 20, 2025
@scrapecoder
Copy link
Author

Thanks @szydlovsky, I'm closing the issue

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Close when stale This issue is going to be closed when there is no activity for a while Missing repro This issue need minimum repro scenario Platform: iOS This issue is specific to iOS
Projects
None yet
Development

No branches or pull requests

2 participants