software-mansion / react-native-reanimated

React Native's Animated library reimplemented
https://docs.swmansion.com/react-native-reanimated/
MIT License
9.05k stars 1.31k forks source link

Clock passed through props to child component pause/resuming Timing issue #665

Closed ninjz closed 4 years ago

ninjz commented 4 years ago

I'm experiencing this issue where passing an instance of a Clock which is started->stopped->resumed in the parent component, is not being properly resumed in the timing function in the child. Whereas if I was to place the logic of starting and stopping the clock within the child component the Clock is able to pause and resume from the last played position.

To clarify, by pausing and resuming; I mean that once resuming, I expect the position to resume from the last position. In the code below, I see that the position is ahead once I resume as if the clock was not being properly stopped.

The use case I have for this is to create a master timer which is counting down a total duration using a Timing function in the parent on the same clock. And within the child component, I want to be able to run another Timing function with the same clock to keep track of another duration. Is this the correct approach or should I be creating to separate clocks to be managing two timers?

Any help on this would be greatly appreciated.

Thanks.

(Irrelevant parts of the code have been omitted.)

// Parent Component

  const {
    isPaused,
    clock,
  } = useMemoOne(
    () => ({
      isPaused: new Value<0 | 1>(0),
      clock: new Clock(),
    }),
    []
  )

  isPaused.setValue(paused ? 1 : 0)

  useCode(() => block([
    // Start/stop logic to control the clock
    cond(and(not(isPaused), not(clockRunning(clock))), startClock(clock)),
    cond(and(isPaused, clockRunning(clock)), stopClock(clock)),
  ]), [isPaused, clock])

  return (
      <CircularProgress
         {...{
             paused,
             clock,
          }}
      />
  )
// Child component

const runTiming = (clock: Animated.Clock, duration: Animated.Value<number>): Animated.Node<number> => {
  const state: Animated.TimingState = {
    finished: new Value(0),
    position: new Value(0),
    time: new Value(0),
    frameTime: new Value(0)
  };
  const config = {
    toValue: new Value(1),
    duration,
    easing: Easing.linear
  };
  return block([
    cond(
      not(clockRunning(clock)),
      [
        set(state.time, 0),  // Set time to 0, so we can resume from here
      ],
      timing(clock, state, config)
    ),
    state.position
  ]);
};

const CircularProgress = ({ clock, duration }) => {
  const {
    progress,
  } = useMemoOne(
    () => ({
      progress: new Value<0 | 1>(0),
    }),
    []
  )

  useCode(() => block([
    set(progress, runTiming(clock, duration)),
  ]))
}
likern commented 4 years ago

I think I experienced something similar on Android. After stopping clock I see that frameTime progresses. Even though if was reset to 0.

ninjz commented 4 years ago

@likern Did you come into any workaround for this? I've verified just now that starting and stopping the clock in the parent causes no update to the clock state in the child. I placed debug statements within the cond to see if the negated state was being executed when the clock stopped and it isn't.

likern commented 4 years ago

I'm not sure if it's related by that is used child component. I experienced that from my main component - when I stopped clock I expected timing function should not update position and return old one. And when I resume clock it should update from position when it was initially stopped.

My use case - to be able to interrupt animation if uses presses on animated screen. And resume on finger release.

I don't know is it a bug or working as expected. But frameTime is a wall clock - it is updated even on stopped clock. I think it's responsible for (hidden) animation.

My solution was - at animation freeze I save both frameTime and position (in other variables created in useMemoOne) and set them back to state's frameTime and position when I restore animation.

Also I set time to 0 when restore animation. And it's working!

ninjz commented 4 years ago

hmm, I think in your case just setting time to 0 should be enough to handle that case, as looking into the implementation here setting time to zero would preserve the last frameTime. Right now it's looking like:

  return block([
    cond(
      not(clockRunning(clock)),
      [
        set(state.time, 0),  // <---- Not being called when clock is stopped
      ],
      timing(clock, state, config) // <---- Called when clock is running
    ),
    state.position
  ]);

the truthy condition in the above code block is not executed when the clock has stopped running. My hunch is that this is a bug, or I am just going against how the Clock is supposed to be used.

likern commented 4 years ago

I haven't dig in the internal implementation details. But in my opinion there is definitely something broken in implementation. I can use PanGesture or just animation. But as soon as I try to run animation after PanGesture - things become messed up. I see dragging when I use timing even if I don't use it's state at all. I calculate position based on PanGesture and if I add timing (without using it's calculation) position unexpectedly changed so I see draggin and twitching.

It's fifth day I try to make this working.

jakub-gonet commented 4 years ago

This is intended (but unexpected) behavior. Stopped clocks don't keep their value frozen but still update. Stopping a clock means that nodes that depend on that clock won't be updated.

Clocks work internally by synchronizing to singleton main clock. The easiest way to work around that is to keep the clock's value in some other Animated.Value.

scrapecoder commented 4 years ago

@jakub-gonet @likern facing a similar issue I try to pause the clock when the app is going to the background state and resume the clock when the app is coming to the active state.

clock config

    const runTiming = (clock,timing) => {
    const state = {
    finished  : new Value(0),
    position  : new Value(0),
    frameTime : new Value(0),
    time      : new Value(0)
};
const config = {
    toValue  : new Value(1),
    duration :timing ,
    easing   : Easing.in(Easing.ease)
};

return block([
    cond(not(clockRunning(clock)), set(state.time, 0), timing(clock, state, config)),
    cond(eq(state.finished, 1), stopClock(clock)),

    state.position
]);
}

SpecialTestTimer.js

     const SpecialTestTimer = ({ preparationTime, appState,timing }) => {
     const [ isCompleted, setIsCompleted ] = useState(false);
     const clock = useClock();
     const progress = useValue(0);
     const [ play, setPlay ] = useState(true);
     const isPlaying = useValue(0);
     useEffect(
    () => {
        if (appState == 'active') setPlay(true);
        else setPlay(false);
    },
    [ appState ]
      );

      useCode(() => set(isPlaying, play ? 1 : 0), [ play ]);
      useCode(
    () => [
        cond(and(isPlaying, not(clockRunning(clock))), startClock(clock)),
        cond(and(not(isPlaying), clockRunning(clock)), stopClock(clock)),

        set(progress, runTiming(clock,timing))
    ],
    []
   );
     ... 
  }