I'll try making my question as clear as possible.
Generally speaking, how can I get a component location attribute: left, top etc') if it changes constantly?
Specifically:
I've created a component in react-native that is animating left and top properties of his son styles (this is an the code for the component animating the left position of the object):
class MovementAnimation extends Component {
state = {
moveAnim: new Animated.Value(100), // Initial value for left: 100
}
componentDidMount() { //Call the animation
this.runMovementAnimation()
}
runMovementAnimation(){
var firstPosRand = parseInt(randomize('0', 3)) % 300;
var secondPosRand = parseInt(randomize('0', 3)) % 300;
var firstTimeRand = (parseInt(randomize('0',5)) % 3000) + 3000; //Stupid temporary solution to set a number between 500-3500
var secondTimeRand = (parseInt(randomize('0',5))% 3000) + 3000;
Animated.sequence([
Animated.timing(this.state.moveAnim, {
toValue: firstPosRand, // Animation
duration: firstTimeRand
}),
Animated.timing(this.state.moveAnim, {
toValue: secondPosRand,
duration: secondTimeRand
})
]).start(() => this.runMovementAnimation()) // repeat the animation
}
render() {
let { moveAnim } = this.state;
return (
<Animated.View ref='myElement' // Special animatable View
style={{
...this.props.style,
left: moveAnim, // Bind opacity to animated value
}}
>
{this.props.children}
</Animated.View>
);
}
}
Now, in my mainPage I'm controlling a circle component I've made with this animation component, and it works great. BUT, I want to use this specific circle 'left' attribute to control a different one, how can I access it from a different class?
Thanks in advance!
Amit
Related
I am rotating the items in an array of images inside an interval of 5 seconds. Then I have a css animation with styled components that eases in the gallery images with a fade that occurs at the same time interval.
const fadeIn = keyframes`
5%, 95% { opacity: 1 }
100% { opacity: 0 }
`
export const Gallery = styled.div<{ lapse: number }>`
position: relative;
margin: 0 auto;
max-width: 1064px;
opacity: 0;
animation: ${fadeIn} ease-in-out ${({ lapse }) => `${lapse}s`} infinite;
`
The problem is that when I change the state even thou at first it seems in sync, eventually the setState takes a bit longer
import React, { useState, useEffect } from 'react'
const [images, setImages] = useState<string[]>([])
// Time in seconds for each image swap, fading in between
const lapse = 5
...
useEffect(() => {
// Clone the images array
const imgs = [...images]
// Time interval same as css animation to fade in
const interval = setInterval(() => {
// Take the first element and put it at the end
imgs.push(...imgs.splice(0, 1))
// Update the state, this seems to desync as time passes
setImages(imgs)
}, lapse * 1000)
return () => clearInterval(interval)
}, [images])
return (
<Gallery lapse={lapse}>
<Portal>
<img src={imgs/${images[0]}`}
</Portal>
<Thumbnails>
<Thumbwrapper>
<img src={imgs/${images[1]}`}
</Thumbwrapper>
<Thumbwrapper>
<img src={imgs/${images[2]}`}
</Thumbwrapper>
</Thumbnails>
</Gallery>
)
Is there a way I can make sure the swapping happends smoothly?
Make the component class based,
constructor(props) {
super(props);
this.state = {
interval: null,
images: ["1.jpg", "2.jpg", "3.jpg"],
timeChanged: null
};
}
// setInterval when component is mounted
componentDidMount() {
var interval = setInterval(()=> {
// use callback argument for setState
// when new value (images) depends on old value
this.setState((state)=>({
timeChanged: (new Date()).toString(),
images: state.images.slice(1).concat(state.images[0])
// add 1st img at the end
});
}, 3000);
this.setState({interval: interval});
}
// clearInterval before component is unmounted
componentWillUnmount() {
clearInterval(this.state.interval);
}
When the state is updated using its old value the callback argument should be used.
(See https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous)
slice returns a portion of an array.
Also see how to update state arrays in React.
If you still wish to use hooks. Just call setImages with
(images)=>images.slice(1).concat(images[0]).
I'm not sure where the clearTimeout should be, when using hooks.
It would be great if we can have example in codesandbox. Also I would like to ask you, maybe I am missing something, but how do you determine when to stop using effect, since it depends on image, which is, I assume, part of the useSate hook, and in that same effect you are updating that same image, which will lead to another effect usage(leading to infinity loop)?
UPDATED ANSWER
I made similar example on codesandbox to look it on my own. It looks to me that main problem here is sync between applied CSS and actual update in your react component. When using setInterval with 5000ms it does not mean that it will be triggered right after 5000ms, but CSS animation on the other side is always right on time, so it stuck when two of those become unsynced.
It looks like the only way to solve this is to recreate img elements on each reorder, by introducing timestamp state and use it as part of the each img key in order to force img recreation on each update. Also you would need to change animation in simpler way.
CSS would besomething like this:
#keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.image {
position: relative;
margin: 0 auto;
max-width: 1064px;
animation: fadeIn 3s;
opacity: 1;
}
And functional component would be something like this(hook part only):
const [images, setImages] = useState(["1.jpg", "2.jpg", "3.jpg"]);
const [dateTimeWhenChanged, setDatetimeWhenChanged] = useState("");
useEffect(() => {
const imgs = [...images];
// Time interval same as css animation to fade in
const interval = setInterval(() => {
// Take the first element and but it to the end
imgs.push(...imgs.splice(0, 1));
// Update the state, this seems to desync as time passes
setImages(imgs);
setDatetimeWhenChanged(new Date().toString());
}, 5 * 1000);
return (
<div>
{images.map((img, ind) => (
<img
key={`${ind}-${dateTimeWhenChanged}`}
alt={img}
src={`images/${img}`}
className="imgg"
/>
))}
</div>
);
I came to the conclusion that having two 'clocks' to keep the time, one being react setState and the other css animation, was at the core of the problem.
So I am now keeping a single interval with a setTimeout to make sure it goes in order then substract the time taken on the timeouts from the interval and toggle the class for css
export const Gallery = styled.div<{ fadeIn: boolean }>`
position: relative;
margin: 0 auto;
max-width: 1064px;
transition: opacity 0.3s;
opacity: ${({ fadeIn }) => (fadeIn ? 1 : 0)};
`
import React, { useState, useEffect } from 'react'
const [images, setImages] = useState<string[]>([])
const [fadeIn, setFadeIn] = useState<boolean>(true)
useEffect(() => {
let interval: ReturnType<typeof setInterval>
if (images.length) {
interval = setInterval(() => {
setFadeIn(false)
setTimeout(() => {
setImages(images => images.slice(1).concat(images[0]) )
setFadeIn(true)
}, 400)
}, (lapse - 0.4) * 1000)
}
return () => clearInterval(interval)
}, [images])
const getImage = (i: number) => <img src={imgs/${images[i]}`} />
<Gallery fadeIn={fadeIn}>
<Portal>{getImage(0)}</Portal>
<Thumbnails>
<Thumbwrapper>{getImage(1)}</Thumbwrapper>
<Thumbwrapper>{getImage(2)}</Thumbwrapper>
</Thumbnails>
</Gallery>
I did however used #Nice Books better setState aproach
I have this landing page, and there's this part of the page where i want to generate a simple animation with opacity using framer-motion.
The problem, is that those types of animations only happen when you first go to the page, but i want it to happen based on scroll position, how can i make that happen?
This is what i've tried so far
const [scrollY, setScrollY] = useState(0);
const { scrollYProgress } = useViewportScroll();
useEffect(() => {
scrollYProgress.onChange(number => setScrollY(number));
}, [scrollYProgress, scrollY]);
// Framer motion - PopUp
const PopUp = {
hidden: { opacity: 0 },
visible: { opacity: 1, transition: { duration: 0.5 } }
};
return (
<DescriptionHero
variants={Math.round(scrollY * 100) / 100 === 0.52 && PopUp}
initial="hidden"
animate="visible"
>
</DescriptionHero>
)
So basically, I'm trying to see the scrollY progress in scrollY using useState and useViewportScroll from framer motion, and, in the actual component i've made an animation using variants and all that jazz.
The problem, is that, Math.round(scrollY * 100) / 100 , when the value of that expressin is equal to 0.52, i want the component execute the animation, but it just doesn't work, and i'm having a hard time trying to animate based on scroll position, what can i do ?
I think you need to change animate prop, not variants
return (
<DescriptionHero
variants={PopUp}
initial="hidden"
animate={Math.round(scrollY * 100) / 100 === 0.52 ? "visible" : "hidden"}
>
</DescriptionHero>
Although it is still quite bad way to trigger animation, what whould happen when you scroll is not equal 0.52? What happens on 0.53?
I'm trying to just make 20 - 50 stars "twinkle" (opacity change with random interval), but it's slowing the app to a crawl.
I'm using reanimated 2.2.0.
I tried sharing the opacity value but only 1 element had the styles applied, which I think may be because of this: "Animated styles cannot be shared between views."
This is my entire component
function TwinklingStar({ position, size }) {
const starOpacity = useSharedValue(1);
useEffect(() => {
setInterval(() => {
starOpacity.value = withSpring(starOpacity.value ? 0 : 1)
}, getRandomInt(500, 1000))
}, []);
const twinkling = useAnimatedStyle(() => ({
opacity: starOpacity.value
}));
return (
<Animated.Image
source={sparkleImage}
resizeMode="cover"
style={[{
width: size,
height: size,
position: 'absolute',
top: position.y,
left: position.x
}, twinkling]}
/>
)
}
There are perhaps 5 of them per card, and a random amount of cards, 5-20 lets say.
I've read over the docs but don't think I'm understanding them properly cause I cant figure out why app is crawling/freezing/crashing. It's like all the animations are blocking the js thread, but they should be running on ui thread since the opacity changes are "captured" by the useAnimatedStyle hook which is a "worklet"?
Confused...
This is my actual input range:
This is what I want:
And this is my code.
RangeComponent.tsx:
const active = '#64c3ef'
const inactive = '#dbdbdb'
export class RangeComponent extends React.Component<Props> {
inputRef: any = React.createRef()
state = { value: 10 }
handleChange = (min: number, max: number) => (event: any) => {
const value = event.target.value
const progress = (value / max) * 100 + '%'
this.setState({ value: value })
const newBackgroundStyle = `linear-gradient(90deg, ${active} 0% ${progress}%, ${inactive} ${progress}% 100%)`
this.inputRef.current.style.background = newBackgroundStyle
}
render() {
const minValue = 10
const maxValue = 300
const progress = (this.state.value / maxValue) * 100 + '%'
const styleInput = {
background: `linear-gradient(90deg, ${active} 0% ${progress}%, ${inactive} ${progress}% 100%)`,
}
return (
<div>
<input
ref={this.inputRef}
id="sliderId"
className="inputR"
name="sliderName"
type="range"
min={minValue}
max={maxValue}
value={this.state.value}
onChange={this.handleChange(minValue, maxValue)}
style={styleInput}
/>
<div className="label">
{this.state.value}
</div>
</div>
)
}
}
style.css:
.inputR::-webkit-slider-thumb {
background: rgba(255, 208, 255, 0.75);
}
If possible, I would like to control the thumb color not in style.css but in RangeComponent.tsx.
I don't understand why the class inputR has no effects on the input style.
How can I change the style using CSS from javascript object (styleInput)?
So to answer your last question first: "why the class inputR has no effects on the input style."
The first part is that in order to style the slider / thumb you need to set the "appearance: none". Remember to add the -webkit-appearance for cross-browser support. This will effectively turn the slider / thumb invisible for you to style over. Do this for both inputR and inputR::-webkit-slider-thumb. (See codepen solution)
The second part is that while you can do .inputR {-webkit-appearance: none}
in CSS or
const styleInput = {
WebkitAppearance: 'none'
}
in JSX,
you cannot do pseudo-classes in JSX, eg. .inputR::-webkit-slider-thumb {} . Which you need to do in order to style the thumb.
My proposal is that you set up the basic styling for the slider / thumb in CSS and then do the dynamic part via inline styling or as a style object.(See codepen solution)
The next thing you will have to look at is your linear gradient. you are using the variable progress and adding a "%" sign.
const styleInput = {
background:linear-gradient(90deg, ${active} 0% ${progress}%, ${inactive} ${progress}% 100%),
}
But in your code you already add the "%" into progress.
const progress = (value / max) * 100 + '%'
So the string literal resolves to 100%% and your gradient does not render. I removed the "%" from the style object (See codepen solution)
I hope that helps, let me know if anything is unclear.
How to I scale a react component already created into the browser that already has a height and width predefined???
componentDidMount(x,y,z){
this.setState({height:window.innerHeight+'px'});
}
Not sure if this is the right way to go, I am not sure if I have to get th viewport size first and then later try to scale the on the browser later. How to can be accomplished?
You can assign the styles directly to the DOM element. For example:
render() {
return <div style={{ height: window.innerHeight }} />;
}
If you need to calculate the style after initial render, perhaps after some user interaction, you could calculate and store the styles in state:
// ... calculate and set state logic somewhere
// eg: someFunction() { this.setState({ height: window.innerHeight }); }
render() {
let styles = {};
if (this.state.height) {
styles.height = this.state.height;
}
return <div style={styles} />;
}