React Native Lottie Animation Only Plays On First Tap - javascript

So essentially, I want to play the lottie animation everytime it is tapped. Here is my UI code for the lottie animation:
<Pressable onPress={playGame}>
<LottieView
ref={loseAnimationRef}
style={styles.egg}
source={Lost}
autoPlay={false}
loop={false}
onAnimationFinish={() => {
resetAnimation();
}}
/>
</Pressable>
Here is my state code for the lottie animation:
const loseAnimationRef = useRef(null);
const playGame = async () => {
await mainGameLogicUC();
playAnimation()
};
const playAnimation = () => {
loseAnimationRef.current.play()
}
const resetAnimation = () => {
loseAnimationRef.current.reset()
}
On the first tap, the animation play perfefctly fine. But on all other taps, the animation won't play. I tried pausing the animation in the onAnimationFinish and then resuming it, but that also didn't work. Am I missing something?
EDIT
I got rid of the resetAnimation() in the onAnimationFinish and that solved the initial problem. But the thing is, I want the animation to be reset to the beginning every time. Why does it break when I reset the animation?

Have you tried something like this?
const animationRef = useRef<AnimatedLottieView>()
const isAnimating = useRef<boolean>(false)
const onAnimationPress = () => {
if (!isAnimating.current) {
isAnimating.current = true
animationRef.current.play()
}
}
<TouchableWithoutFeedback onPress={onAnimationPress}>
<AnimatedLottieView
source={source}
ref={animationRef}
autoPlay={false}
loop={false}
onAnimationFinish={() => {
isAnimating.current = false
animationRef.current.reset()
}}
/>
</TouchableWithoutFeedback>

After coming back to this problem a few days later, I found the solution
Playing the lottie animation seems to be considered a side effect, therefore, editing the references to the animations should be done in a useEffect hook
The solution that worked for me:
(again, in this code, I want the animation to reset to the beginning before the user taps the screen screen again.
state code
const isMounted = useRef(false);
const [isWonAnimationShowing, setIsWonAnimationShowing] = useState(false);
const [isAnimationPlaying, setIsAnimationPlaying] = useState(false);
const loseAnimationRef = useRef(null);
const winAnimationRef = useRef(null);
useEffect(() => {
if (isMounted.current) {
if (isAnimationPlaying) {
_playAnimation();
} else {
_resetAnimation();
}
} else {
isMounted.current = true;
}
}, [isAnimationPlaying]);
const playAnimation = () => {
setIsAnimationPlaying(true);
};
const _playAnimation = () => {
if (isWonAnimationShowing) {
winAnimationRef.current.play();
} else {
loseAnimationRef.current.play();
}
};
const resetAnimation = () => {
setIsAnimationPlaying(false);
};
const _resetAnimation = () => {
if (isWonAnimationShowing) {
winAnimationRef.current.reset();
} else {
loseAnimationRef.current.reset();
}
};
UI code
<View style={styles.body}>
<Pressable disabled={isAnimationPlaying} onPress={playGame}>
{isWonAnimationShowing ? (
<LottieView
ref={winAnimationRef}
style={styles.egg}
source={Won}
autoPlay={false}
loop={false}
onAnimationFinish={() => {
resetAnimation();
}}
/>
) : (
<LottieView
ref={loseAnimationRef}
style={styles.egg}
source={Lost}
autoPlay={false}
loop={false}
onAnimationFinish={() => {
resetAnimation();
}}
/>
)}
</Pressable>
</View>

Related

React component renders twice despite Strict mode being disabled

I have a specific component that renders twice. Basically, I am generating two random numbers and displaying them on the screen and for a split second, it shows undefined for both then shows the actual numbers. To test it further I did a simple console.log and it indeed logs it twice. So my question splits into two - 1. Why does the typography shows undefined for a split second before rendering? 2 - Why does it render twice?
Here's the related code:
Beginnging.js - this a counter that counts down from 3 and fires an RTK action, setting gameStart true.
function Beginning() {
const [count, setCount] = useState(3);
const [message, setMessage] = useState("");
const dispatch = useDispatch();
const countRef = useRef(count);
useEffect(() => {
countRef.current = count;
}, [count]);
const handleCount = () => {
if (countRef.current === 1) {
return setMessage("GO");
}
return setCount((count) => count - 1);
};
useEffect(() => {
const interval = setInterval(() => {
handleCount();
}, 1000);
if (message==='GO') {
setTimeout(() => {
dispatch(start());
}, 1000);
}
return () => clearInterval(interval);
}, [count, message]);
return (
<>
<Typography variant="h1" fontStyle={'Poppins'} fontSize={36}>GET READY...</Typography>
<Typography fontSize={48} >{count}</Typography>
<Typography fontSize={60} >{message}</Typography>
</>
);
}
export default Beginning;
AdditionMain.js - based on gameStart, this is where I render Beginning.js, once it counts down, MainInput is rendered.
const AdditionMain = () => {
const gameStart = useSelector((state) => state.game.isStarted);
const operation = "+";
const calculation = generateNumbersAndResults().addition;
if (!gameStart){
return <Beginning/>
}
return (
<>
<MainInput operation={operation} calculation={calculation} />
</>
);
};
export default AdditionMain;
MainInput.js - the component in question.
const MainInput = ({ operation, calculation }) => {
const [enteredValue, setEnteredValue] = useState("");
const [correctValue, setCorrectValue] = useState(false);
const [calculatedNums, setCalculatedNums] = useState({});
const [isIncorrect, setIsIncorrect] = useState(false);
const [generateNewNumbers, setGenerateNewNumbers] = useState(false);
const [haveToEnterAnswer, setHaveToEnterAnswer] = useState(false);
const dispatch = useDispatch();
const seconds = useSelector((state) => state.game.seconds);
const points = useSelector((state) => state.game.points);
const lives = useSelector((state) => state.game.lives);
const timerValid = seconds > 0;
const newChallenge = () => {
setIsIncorrect(false);
setHaveToEnterAnswer(false);
dispatch(restart());
};
const handleCount = () => {
dispatch(loseTime());
};
useEffect(() => {
let interval;
if (timerValid) {
interval = setInterval(() => {
handleCount();
}, 1000);
}
return () => {
console.log("first");
clearInterval(interval);
};
}, [timerValid]);
useEffect(() => {
setCalculatedNums(calculation());
setGenerateNewNumbers(false);
setCorrectValue(false);
setEnteredValue("");
}, [generateNewNumbers]);
const submitHandler = () => {
if (correctValue) {
setGenerateNewNumbers(true);
dispatch(gainPoints());
dispatch(gainTime());
}
if (+enteredValue === calculatedNums.result) {
setCorrectValue(true);
} else if (enteredValue.length === 0) {
setHaveToEnterAnswer(true);
} else {
setIsIncorrect(true);
dispatch(loseLife());
}
};
const inputValueHandler = (value) => {
setIsIncorrect(false);
setHaveToEnterAnswer(false);
setEnteredValue(value);
};
const submitOrTryNewOne = () => {
return correctValue ? "Try new one" : "Submit";
};
return (
<>
<Navbar />
{console.log("hello")}
{seconds && lives > 0 ? (
<>
<GameInfo></GameInfo>
<Container component="main" maxWidth="xs">
<Box
sx={{
marginTop: 8,
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<Typography>
Fill in the box to make the equation true.
</Typography>
<Typography fontSize={28}>
{operation !== "/"
? `${calculatedNums.number1} ${operation} ${calculatedNums.number2}`
: `${calculatedNums.number2} ${operation} ${calculatedNums.number1}`}{" "}
=
</Typography>
<TextField
inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }}
type="number"
name="sum"
id="outlined-basic"
label=""
variant="outlined"
onChange={(event) => {
inputValueHandler(event.target.value);
}}
disabled={correctValue}
value={enteredValue}
></TextField>
{haveToEnterAnswer && enterAnswer}
{correctValue && correctAnswer}
{isIncorrect && wrongAnswer}
<Button
type="button"
sx={{ marginTop: 1 }}
onClick={() => submitHandler()}
variant="outlined"
>
{isIncorrect ? "Try again!" : submitOrTryNewOne()}
</Button>
</Box>
</Container>
</>
) : (
<>
<Typography>GAME OVER</Typography>
<Typography>Final Score: {points}</Typography>
<Button onClick={newChallenge}>New Challenge</Button>
</>
)}
</>
);
};
export default MainInput;
PS: I'm trying to figure out how to get this running on Codesandbox
The problem is that you render the page before the calculaed nums are established.
Try this instead. It will prevent the element from displaying until nums are generated.
const [calculatedNums, setCalculatedNums] = useState(null);
// This is down in your render area. Only render once calculatedNums are non-null.
{ calculatedNums &&
<Typography fontSize={28}>
operation !== "/"
? `${calculatedNums.number1} ${operation} ${calculatedNums.number2}`
: `${calculatedNums.number2} ${operation} ${calculatedNums.number1}`}{" "}
=
</Typography>
}
In addition, you are probably generating your numbers twice, because you will generate new numbers on initial load, but then when you hit setGenerateNewNumbers(true); it will trigger a new calculation, which will then set the generatedNewNumbers to false, which will call the calc again, since it triggers whenever that state changes. It stops after that because it tries to set itself to false again and doesn't change.
You are changing a dependency value within the hook itself, causing it to run again. Without looking a lot more into your program flow, a really hacky way of fixing that would just be to wrap your useEffect operation inside an if check:
useEffect(()=>{
if (generateNewNumbers === true) {
//All your stuff here.
}
}, [generateNewNumbers]
That way, it won't run again when you set it to false.

React Native Admob Google Mobile Ads button not working. When I click on interstitial.show

I have added two onPress event so that I can go to the next page and show the ads on click. When I press the button the page navigates to another page but the ads is not showing. I have not get any errors, what is the issue here
import React, { useEffect, useState } from 'react';
import { Button, TouchableOpacity } from 'react-native';
import { InterstitialAd, AdEventType, TestIds } from 'react-native-google-mobile-ads';
const adUnitId = __DEV__ ? TestIds.INTERSTITIAL : 'ca-app-pub-3940256099942544/1033173712';
const interstitial = InterstitialAd.createForAdRequest(adUnitId, {
requestNonPersonalizedAdsOnly: true,
keywords: ['fashion', 'clothing'],
});
const Testing = ({ navigation }) =>{
const [loaded, setLoaded] = useState(false);
useEffect(() => {
const unsubscribe = interstitial.addAdEventListener(AdEventType.LOADED, () => {
setLoaded(true);
});
// Start loading the interstitial straight away
interstitial.load();
// Unsubscribe from events on unmount
return unsubscribe;
}, []);
// No advert ready to show yet
if (!loaded) {
return null;
}
return (
<View>
<TouchableOpacity onPress={() => {interstitial.show();}}>
<Button onPress = {() => navigation.navigate('FirstPage')} title='Next Screen'></Button>
</TouchableOpacity>
<Text>Hello World Testing</Text>
</View>
);}
export default Testing
try with hooks - its working for me
ref - https://docs.page/invertase/react-native-google-mobile-ads/displaying-ads-hook
import { useInterstitialAd, TestIds } from 'react-native-google-mobile-ads';
export default function App({ navigation }) {
const { isLoaded, isClosed, load, show } = useInterstitialAd(TestIds.Interstitial, {
requestNonPersonalizedAdsOnly: true,
});
useEffect(() => {
// Start loading the interstitial straight away
load();
}, [load]);
useEffect(() => {
if (isClosed) {
// Action after the ad is closed
navigation.navigate('NextScreen');
}
}, [isClosed, navigation]);
return (
<View>
<Button
title="Navigate to next screen"
onPress={() => {
if (isLoaded) {
show();
} else {
// No advert ready to show yet
navigation.navigate('NextScreen');
}
}}
/>
</View>
);
}

react-webcam - disable buttons until stream starts

I am using the react-webcam to capture images and videos in my react app. I have implemented the Screenshot (via Ref) example:
const videoConstraints = {
width: 1280,
height: 720,
facingMode: "user"
};
const WebcamCapture = () => {
const webcamRef = React.useRef(null);
const capture = React.useCallback(
() => {
const imageSrc = webcamRef.current.getScreenshot();
},
[webcamRef]
);
return (
<>
<Webcam
audio={false}
height={720}
ref={webcamRef}
screenshotFormat="image/jpeg"
width={1280}
videoConstraints={videoConstraints}
/>
<button onClick={capture}>Capture photo</button>
</>
);
};
Works nicely, however it seems to take a few seconds for the stream to start and video to start showing. I want to be able to disable the button whilst this is happening. I have found there is a flag in the webcamRef that states if it is loading:
useEffect(() => {
if (webcamRef.current) {
const camStarted = webcamRef.current.state.hasUserMedia;
debugger;
}
}, [webcamRef]);
In the above useEffect, whilst the video is initialising, the hasUserMedia is false, once it has loaded it changes to true. This sounds like exactly what I need however as it is in a useRef, it doesn't hit the useEffect when it changes.
Is there any kind of neat trick I can implement to be able to identify when this value in the useRef is changed, or anything else that might help me get the functionality I am after?
If you just want to disable the button until the camera has started streaming, you can use a check that can be triggered using the onUserMedia prop of Webcam.
const videoConstraints = {
width: 1280,
height: 720,
facingMode: "user"
};
const WebcamCapture = () => {
const [loadingCam, setLoadingCam] = useState(true);
const webcamRef = React.useRef(null);
const capture = React.useCallback(
() => {
const imageSrc = webcamRef.current.getScreenshot();
},
[webcamRef]
);
const handleUserMedia = () => {
setTimeout(() => {
// timer is optional if the loading is taking some time.
setLoadingCam(false);
}, 1000);
};
return (
<>
<Webcam
audio={false}
height={720}
ref={webcamRef}
screenshotFormat="image/jpeg"
width={1280}
videoConstraints={videoConstraints}
onUserMedia={handleUserMedia}
/>
<button disable={loadingCam} onClick={capture}>Capture photo</button>
</>
);
};

variable is false in console log, but react rendering as if it were true

I am making a slider that has arrows to scroll through the items only if the items are too wide for the div. It works, except when resizing the window to make it large enough, the arrows don't disappear. The arrows should only appear if scroll arrows is true.
{scrollArrows && (
<div className="arrow arrow-left" onClick={goLeft}>
<
</div>
)}
But when I console.log it, even if the arrows are there, scroll arrows ALWAYS is false. Here is the relevant code:
import "./ProjectRow.css";
import Project from "../Project/Project";
import React, { useEffect, useState } from "react";
const ProjectRow = (props) => {
const rowRef = React.useRef();
const [hasLoaded, setHasLoaded] = useState(false);
const [scrollArrows, setScrollArrows] = useState(false);
const [left, setLeft] = useState(0);
const [rowWidth, setRowWidth] = useState(null);
function debounce(fn, ms) {
let timer;
return () => {
clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
fn.apply(this, arguments);
}, ms);
};
}
useEffect(() => {
const setVariables = () => {
console.log(scrollArrows);
if (rowRef.current?.offsetWidth < rowRef.current?.scrollWidth) {
setRowWidth(rowRef.current.offsetWidth);
if (!scrollArrows) {
console.log("scrollArrows true now");
setScrollArrows(true);
}
} else if (scrollArrows) {
setScrollArrows(false);
}
};
if (!hasLoaded) setVariables();
const debouncedHandleResize = debounce(setVariables, 300);
window.addEventListener("resize", debouncedHandleResize);
setHasLoaded(true);
return () => {
window.removeEventListener("resize", debouncedHandleResize);
};
}, []);
const goLeft = () => {
const projectWidth = rowRef.current.childNodes[2].firstChild.offsetWidth;
if (rowRef.current.childNodes[2].firstChild.getBoundingClientRect().x < 0)
setLeft(left + projectWidth + 20);
};
const goRight = () => {
const projectWidth = rowRef.current.childNodes[2].firstChild.offsetWidth;
if (
rowRef.current.childNodes[2].lastChild.getBoundingClientRect().x +
projectWidth >
rowWidth
)
return setLeft(left - (projectWidth + 20));
};
return (
<div className="project-row" ref={rowRef}>
<h3 className="project-row-title light-gray bg-dark-gray">
{props.data.groupName}
</h3>
<hr className="gray-bar" />
<div className="slider" style={{ left: `${left}px` }}>
{props.data.projects.map((project, i) => (
<Project data={project} key={i} />
))}
</div>
{scrollArrows && (
<div className="arrow arrow-left" onClick={goLeft}>
<
</div>
)}
{scrollArrows && (
<div className="arrow arrow-right" onClick={goRight}>
>
</div>
)}
</div>
);
};
export default ProjectRow;
The "scroll arrows true now" gets logged, but scrollArrows variable stays false, even after waiting to resize or just resizing between two widths that need it.
EDIT: Fixed it by removing the if on the else if. I figured that might be the issue, but I don't know why it was preventing it from functioning properly, so it feels bad.
Your 'useEffect' doesn't look well structured. You have to structure it this way:
React.useEffect(()=>{
// Functions
window.addEventListener('resize', ...
return ()=>{
window.removeEventListener('resize', ...
}
},[])
Please follow this answer and create a Hook: Why useEffect doesn't run on window.location.pathname changes?
Try changing that and it might update your value and work.

How to hide top bar after 3 seconds in React Native

I have been trying to achieve hiding (or slide up) my react native startup app top bar after 3 seconds, then have a button to press to show the top bar, but could not. I've searched online how to do it, but have not seen good solution for it. Please I need help here, the below is snippet code of what I tried doing but it did not work.
<HeaderHomeComponent /> is the top header I created and imported it in my Home screen
const Home = () => {
const camera = useRef();
const [isRecording, setIsRecording] = useState(false);
const [flashMode, setFlashMode] = useState(RNCamera.Constants.FlashMode.off);
const [cameraType, setCameraType] = useState(RNCamera.Constants.Type.front);
const [showHeader, setShowHeader] = useState(true);
const onRecord = async () => {
if (isRecording) {
camera.current.stopRecording();
} else {
setTimeout(() => setIsRecording && camera.current.stopRecording(), 23*1000);
const data = await camera.current.recordAsync();
}
};
setTimeout(()=> setShowHeader(false),3000);
const DisplayHeader = () => {
if(showHeader == true) {
return <HeaderHomeComponent />
} else {
return null;
}
}
// useEffect(() => {
// DisplayHeader();
// }, [])
return (
<View style={styles.container}>
<RNCamera
ref={camera}
type={cameraType}
flashMode={flashMode}
onRecordingStart={() => setIsRecording(true)}
onRecordingEnd={() => setIsRecording(false)}
style={styles.preview}
/>
<TouchableOpacity
style={styles.showHeaderButton}
onPress={() => {
setShowHeader(!showHeader);
}}>
<Button title='Show' />
</TouchableOpacity>
<HeaderHomeComponent />
You were really close.
This is how it should be done:
useEffect(() => {
setTimeout(toggleHeader,3000);
}, []);
const toggleHeader = () => setShowHeader(prev => !prev);
Then inside the "return":
{showHeader && <HeaderHomeComponent />}
As simple as that.
This should help you get started in the right direction, you can use animation based on your preference to this code.
import React, {useState, useEffect} from 'react';
import { Text, View, StyleSheet, Button } from 'react-native';
import Constants from 'expo-constants';
export default function App()
{
const [showStatusbar, setShowStatusbar] = useState(true)
useEffect(() =>
{
setTimeout(() =>
{
setShowStatusbar(false)
}, 3000)
}, [])
return (
<View style={styles.container}>
{
showStatusbar
? <View style = {styles.statusBar}>
<Text>Status Bar</Text>
</View>
: null
}
<Button title = "Show Status bar" onPress = {() => setShowStatusbar(true)}/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#ecf0f1',
},
statusBar:
{
height: 50,
backgroundColor:'lightblue',
justifyContent:'center',
alignItems:'center'
}
});
Instead of setTimeout use Animation.
or
Check this lib : https://www.npmjs.com/package/react-native-animated-hide-view
or
check below answers which might help you
Hide and Show View With Animation in React Native v0.46.

Categories