Debugging FlashCard flickering in React Native
While developing my K-PopRama Slang app, I added a FlashCard feature to the Study screen. Users can tap a card to flip it over and reveal the meaning of a Korean slang word, along with examples and other details. After reviewing the answer, they can mark it as correct or wrong and move to the next word.
The basic functionality worked, but I encountered two visual bugs while testing the feature:
On Android, the FlashCard started flickering abnormally after the first card transition.
When moving to the next word, the previous word briefly appeared again for approximately 0.1 seconds before the new word was displayed.
Both issues were related to how the card animation, React state updates, and UI rendering interacted with each other. I investigated them separately and adjusted the animation sequence in my StudyFlashCard.tsx component.
My FlashCard animation design
The FlashCard consists of two views: a front and a back. They occupy the same position, with the back initially hidden. When a user taps the front, the card rotates around the Y-axis to reveal the back.
After the user presses the Correct or Wrong button, the card moves sideways and tilts before the next word appears. This creates a card-swiping effect similar to the interaction used in some flashcard and matching apps.
I used React Native's Animated API to control the rotation, horizontal movement, tilt, and opacity.
The main animation values were:
flip: Controls the rotation between the front and back.translateX: Moves the card horizontally off the screen.tilt: Adds a slight rotation as the card moves away.fadeAnim: Controls the card's visibility while the next word is being rendered.isAnimating: Tracks whether an animation and its associated transition are in progress.
I also combined opacity values so that the front and back could appear and disappear at the appropriate points during the rotation. This was important because both sides existed simultaneously in the component tree.
Bug one: The FlashCard flickered on Android
What I observed
On iOS, the card transition appeared normal. On Android, however, the first card initially worked correctly. After I flipped the card, pressed Correct or Wrong, and moved to the next word, the FlashCard began flickering abnormally.
The problem continued during subsequent card transitions.
Because the same feature worked on iOS, I began investigating differences in rendering behavior rather than assuming that the animation logic itself was entirely incorrect.
My first investigation: Animation completion and state updates
One of the most important things I examined was what happened immediately after the horizontal animation finished.
The card transition involved several operations:
The card moved outside the visible screen.
The animation values returned to their initial positions.
The flip state was reset.
The next word was loaded.
The animation state was changed back to inactive.
Initially, these operations happened too close together. Even though the animation's completion callback had fired, resetting the animated values and changing the displayed word could trigger additional rendering work at nearly the same time.
My working theory was that Android was not handling this sequence smoothly. The visible flickering appeared to be associated with the timing of the animation reset and the replacement of the current word.
I therefore changed the sequence so that the card would first move off-screen, then reset its animation values, and only afterward update the displayed word.
Solution A: Separate the animation reset from the word change
Instead of performing every operation immediately inside the animation callback, I used a short scheduled delay.
The essential structure became:
Animated.parallel([ Animated.timing(translateX, { toValue: direction * SCREEN_WIDTH, duration: 300, easing: Easing.out(Easing.ease), useNativeDriver: true, }), Animated.timing(tilt, { toValue: direction, duration: 300, useNativeDriver: true, }), ]).start(() => { fadeAnim.setValue(0); setTimeout(() => { // Reset animation values while the card is hidden. translateX.setValue(0); tilt.setValue(0); flip.setValue(0); setIsFlipped(false); // Update the word only after resetting the card. onNext?.(result); setTimeout(() => { // Reveal the new card after the UI has had time to update. Animated.timing(fadeAnim, { toValue: 1, duration: 10, easing: Easing.ease, useNativeDriver: true, }).start(() => { setIsAnimating(false); }); }, 50); }, 0); });
This is the relevant part of my implementation, not the complete component.
The setTimeout(..., 0) does not mean that the operation runs instantaneously. It schedules the callback to run later, allowing the current JavaScript execution to finish first. In my implementation, this helped separate the animation completion callback from the subsequent reset and word update.
The additional 50 ms delay gave the UI a little more time to render the new word before I marked the animation as finished.
These delays were empirical adjustments for my app, not universal timing requirements for React Native animations. The important idea was to control the order of operations rather than reset every value at once.
Solution B: Use Android hardware texture rendering only during animation
I also investigated renderToHardwareTextureAndroid.
This property can allow an Android view to be rendered into a hardware texture, which may help with certain animations. However, keeping it enabled after the animation had finished appeared to contribute to the flickering I observed when the card content changed.
I changed the component to enable it only while the card was animating:
const [isAnimating, setIsAnimating] = useState(false); <Animated.View renderToHardwareTextureAndroid={isAnimating} style={/* animated card styles */} > {/* Card content */} </Animated.View>
The same approach was applied to both the front and back views.
This meant that the hardware texture was used during the transition and disabled after the transition and content update had completed.
Solution C: Simplify elevation and control front/back visibility manually
Two other changes helped stabilize the animation.
First, I removed the animated elevation interpolation.
I had experimented with changing the card's elevation throughout the rotation to control its shadow. On Android, animating elevation at the same time as rotation and translation added more rendering work than I wanted.
Instead, I used a fixed elevation value outside the animation and removed the elevation while the card was moving:
elevation: isAnimating ? 0 : 4
This was simpler than animating the shadow elevation continuously.
Second, I stopped relying on backfaceVisibility to manage the two sides.
The front and back were overlapping, rotating views. In my Android tests, relying on backfaceVisibility: 'hidden' did not consistently prevent the two sides from appearing incorrectly during rotation.
I removed that setting and controlled their opacity manually.
For example, the front opacity was based on the flip value:
const frontOpacity = flip.interpolate({ inputRange: [0, 0.5, 0.5, 1], outputRange: [1, 1, 0, 0], });
The back used the inverse transition:
const backOpacity = flip.interpolate({ inputRange: [0, 0.5, 0.5, 1], outputRange: [0, 0, 1, 1], });
At the midpoint of the rotation, visibility switches from the front to the back. I then combined these opacity values with the horizontal movement opacity and fadeAnim.
These changes gave me more direct control over which side should be visible at each stage of the animation.
Bug two: The previous word appeared briefly before the next word
Fixing the Android flickering was only part of the problem. I also noticed a separate visual issue on the transition to the next word.
After pressing Correct or Wrong, the card moved off-screen. It then returned to the front-facing position, but the previous word briefly appeared before the new word replaced it.
The delay was approximately 0.1 seconds. It was short, but noticeable enough to make the transition feel unfinished.
Why was the previous word visible?
The card's animation state and its displayed content were updated through different operations.
After the card moved off-screen, I reset the flip value and changed isFlipped to false. The card was now configured to show its front side again.
However, the next word was supplied through the onNext callback. The React state update and subsequent render did not necessarily become visible at exactly the same moment as the animation reset.
As a result, the front side could briefly display the previous word before the new word had been rendered.
The solution was not simply to change the word earlier. I needed to prevent the card from being visible during that short transition.
Solution: Hide the card while the next word is being rendered
I introduced a separate animated value:
const fadeAnim = useRef(new Animated.Value(1)).current;
The card starts fully visible. When the user answers and the card finishes moving off-screen, I set its opacity to zero:
fadeAnim.setValue(0);
I then reset the animation values, change the word, and allow a short interval for the new content to render. After that, I animate the opacity back to one.
The important detail was that fadeAnim was combined with the existing front and back opacity values:
const finalFrontOpacity = Animated.multiply( combinedFrontOpacity, fadeAnim ); const finalBackOpacity = Animated.multiply( combinedBackOpacity, fadeAnim );
This allowed me to hide both sides of the card during the transition.
In my final implementation, I used a 50 ms delay before starting a very short 10 ms fade-in. The short fade-in was deliberate: my original 100 ms fade-in felt too slow, so I reduced it while keeping the brief delay needed for the content update.
The result was a much cleaner transition. The previous word was no longer briefly visible while the next word was being prepared.
Preventing overlapping interactions
I also added checks to prevent users from triggering another card transition while an animation was already in progress.
For example:
const handleAnswer = (result: 'correct' | 'wrong') => { if (!isFlipped || isAnimating) return; setIsAnimating(true); // Start the card transition... };
The first condition prevents an answer from being submitted while the card is still showing its front. The second prevents another answer from starting while the previous transition is running.
Without these checks, rapid repeated taps could initiate overlapping state changes and make the transition sequence harder to control.
This was particularly important because the animation involved several coordinated values and a change to the current study word.
What I learned from debugging this component
The most useful lesson was that a visual bug does not always originate in the animation itself. In this case, I had to consider the interaction between animated values, React state updates, rendering, and Android-specific view behavior.
Here are the main lessons I took away from the process:
Separate animation completion from content updates. Moving a view off-screen does not mean every subsequent UI operation should happen immediately in the same callback.
Hide content while replacing it when necessary. A short opacity transition can prevent stale content from flashing on the screen.
Treat Android rendering properties carefully. Hardware texture rendering and elevation can affect animated views differently across platforms.
Control overlapping views explicitly. When a card has front and back views occupying the same position, manually managing opacity can be more predictable than relying entirely on backface visibility.
Test on both platforms. The initial implementation appeared fine on iOS, but Android revealed a problem that was not obvious during the first tests.
I did not solve these issues by replacing the entire FlashCard implementation. Instead, I kept the basic animation design and refined the order in which its values and content were updated.
That experience reminded me that polishing an interactive component often takes more than getting the animation to run. The transition also needs to remain visually stable while the underlying data changes.
The final result was a more reliable FlashCard interaction for the Study screen in K-PopRama Slang, with smoother Android transitions and no noticeable flash of the previous word when moving to the next card.
Comments
Post a Comment