What is wrong with my React Native Reanimated colour interpolation animation? - javascript

I'm trying to do something extremely simple - have my button be transparent while not pressed, and change its background color to a different colour gradually when a user presses it. To achieve that, I'm using React Native Reanimated. Unfortunately, there's something wrong with my animation and I'm not sure what. The issue is this:
I change isPressed to true when the button is pressed and to false when the user moves his finger away from the button. Then I use that isPressed boolean to change the progress and finally use that progress to interpolate the colour from transparent to my other colour. Sadly what happens is this:
I press on the button and instead of the button changing its background color almost instantly, it takes like 5 seconds before turning to colors.primary50. Then if I unpress the button, it takes another 5 or so seconds to turn back to transparent. Also I don't really see gradual change in color, it just changes instantly.
const TertiaryButton: FunctionComponent<Props> = ({ title, wide = false, style, ...rest }) => {
const [isPressed, setIsPressed] = useState(false);
const progress = useSharedValue(0);
useEffect(() => {
progress.value = withTiming(isPressed ? 1 : 0, { easing: Easing.out(Easing.cubic), duration: 1000 });
}, [isPressed, progress]);
const rStyle = useAnimatedStyle(() => {
const backgroundColor = interpolateColor(progress.value, [0, 1], ['transparent', colors.primary50]);
return { backgroundColor };
});
return (
<Pressable onPressIn={() => setIsPressed(true)} onPressOut={() => setIsPressed(false)} {...rest}>
<Button
wide={wide}
style={[
style,
{
...rStyle,
},
]}
>
<ButtonText variant="h4">{title}</ButtonText>
</Button>
</Pressable>
);
};

Try to change the progress value instantly inside onPressIn and onPressOut.
onPressOut={() => {
console.log("onPressOut")
isPressed.value = withTiming(0)
}}
onPressIn={() => {
console.log("onPressIn")
isPressed.value = withTiming(1)
}}

The possible reason for this not working is that the Button component may not be an Animated component. You need to make sure that Button is an animated component. If it's a custom/library component, you can wrap it with Animated.createAnimatedComponent(...) to make it an Animated component:
const AnimatedButton = Animated.createAnimatedComponent(Button);

Related

Implementation of switching images with fade to black?

There is following code:
import React, { useEffect, useState } from 'react';
const Background: React.FC = () => {
const images = [ "image1.jpg", "image2.jpg", "image3.jpg", "image4.jpg" ];
const [currentImage, setCurrentImage] = useState("")
const [imageIndex, setImageIndex] = useState(0);
const changeId = () => setImageIndex(id => id + 1);
useEffect(() => {
window.addEventListener("click", changeId);
return () => window.removeEventListener("click", changeId);
}, [])
useEffect(() => {
setCurrentImage(images[imageIndex]);
}, [imageIndex])
return (
<img
src={currentImage}
alt={"background"}
/>
);
};
export default Background;
I need to achieve smooth image switching. However, the image should first smoothly fade into black, and then a new image should appear from black. Therefore, using the transition property, I think this effect cannot be achieved.
p.s. If the click is pressed too often, interrupt the animation and change the picture abruptly.
I tried to use css animation, but for some reason the image first "blinked" and then switched.
I tried using the onTransitionEnd event, but sometimes it didn't work, and as a result, the new picture didn't appear, but only a black screen remained.

How to avoid scrollbar staying on top when loading more data (infinite scroll) in React

I'm making an infinite scroll up of the chat room, judging to see the last message and loading the data, but the scrollbar remains at the top of the page, you know, it will automatically execute the call api infinitely,how to keep the scrollbar at the previous message in the position
If you have a reference of the new element that is added, you can use element.scrollIntoView() to make sure it's visible after rendering.
https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView
But:
In chat-rooms, this may not always be the right thing to do, as the user may have scrolled up to copy/paste something, and losing the position would be annoying. So check if the chat was already on the bottom before you scroll via JS.
You can take a look at how twitch-chat works to get a good example. The user gets an info if there are new chat messages.
This solution is for library https://github.com/ankeetmaini/react-infinite-scroll-component
The structure I used is based on props passed to a child component. Parent component is doing the fetching and controlling the state.
Parent Component:
const fetchScrollData = () => {
// Here we set that we are not able to do scrolling fetching
setIsLoadingScrollData(true);
// Apply here some pagination calculation
searchData()
.then((tempData) => {
// Here i am saving the id of the first message
// I had before scrolling
setShouldScrollToId(data[0].id);
const newData = tempData.concat(data);
setData(newData);
// Apply here some logic, depending if you have finished or not
setHasMoreData(true);
})
.then(() => {
setTimeout(() => {
// Change the timeout accordingly
// hack for setting the isloading to false when everything is done
setIsLoadingScrollData(false);
}, 1500);
});
};
Child Component
type PropsType = {
fetchScrollData: () => void;
hasMoreData: boolean;
data: string[];
isLoadingScrollData: boolean;
shouldScrollToId: string;
};
const ChildComponent: FC<PropsType> = (props: PropsType) => {
useEffect(() => {
if (props.shouldScrollToId !== '') {
const element = document.getElementById(props.shouldScrollToId);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
}
});
....
return (
<div
className="message-list overflow-scroll"
id="scrollableDiv"
style={{
display: 'flex',
flexDirection: 'column-reverse'
}}>
<InfiniteScroll
dataLength={props.data.length}
hasMore={props.hasMoreData && !props.isLoadingScrollData}
inverse={true}
next={props.fetchScrollData}
scrollableTarget="scrollableDiv"
style={{ display: 'flex', flexDirection: 'column-reverse', perspective: '1px' }}
loader={<></>}>
<div>
{props.data.map((d, index) => {
// Here i am setting the id which the useEffect will use to do the scroll
<div key={index} id={d.id} >div - #{message}</div>;
})}
</div>
{props.isLoadingScrollData ? <Spinner></Spinner> : null}
</InfiniteScroll>
</div>
I had some problems with the loader, so I decided not use it. Instead I created my own Spinner Component and make it appear or disappear according to the prop which shows if we are still fetching data from the scroll.

Strange scroll behaviour when using useState in React

I started implementing a carousel in react, furthermore I wanted to give it an infinite scroll effect, that is if you keep clicking right and reaches the end. It starts from the beginning.
The funny thing is, I had to use useState and I am noticing very strange behavior. It never reaches the last div but goes to the first one instantly (No scrolling effect) and scrolls to the second one.
This the main file where the scrolling is happening.
Carousel.tsx
import React, { FC, useEffect, useLayoutEffect, useRef, useState } from "react";
import { useStyles, useStylesClasses } from "./CarouselCSS";
import ArrowRightAltIcon from '#mui/icons-material/ArrowRightAlt';
interface Props {
className?: string;
}
type Scroll = "left" | "right";
type Index = {
sign: "To" | "By";
value: number;
}
export const Carousel: FC<Props> = ({className, children}) => {
const {Root, Carousel} = useStyles();
const classes = useStylesClasses();
const ref = useRef(null);
const firstRun = useRef(true);
const [activeIndex, setActiveIndex] = useState<Index>({
sign: "To",
value: 0
});
// Triggers before rendering the page
// useLayoutEffect(()=>{
// console.log(ref.current.children.length);
// }, [ref])
useEffect(() => {
if(!firstRun.current)
if (activeIndex.sign === "To"){
console.log({"To": activeIndex.value})
ref.current.scrollTo({
left: (activeIndex.value*ref.current.clientWidth),
behavior: "smooth",
})
}
else{
console.log("By")
ref.current.scrollBy({
left: ref.current.clientWidth,
behavior: "smooth",
})
}
else
firstRun.current = false;
}, [activeIndex])
// controlling scrolling and making it infinite
const scroll = (value: Scroll) => {
switch(value){
case "left": {
console.log("Left")
if(activeIndex.value === 0){
setActiveIndex({sign: "By", value: ref.current.children.length - 1});
}
else{
setActiveIndex({sign: "To", value: activeIndex.value - 1});
}
}
break;
case "right": {
console.log("Right")
if(activeIndex.value === ref.current.children.length - 1){
setActiveIndex({sign: "To", value: 0});
}
else{
setActiveIndex({sign: "By", value: activeIndex.value + 1});
}
}
break;
}
}
return (
<Root>
<div onClick={() => {
scroll("left");
}
}
>
<ArrowRightAltIcon style={{transform: 'rotate(180deg)'}} className={classes.arrow}/>
</div>
<Carousel className={className} ref={ref}>
{children}
</Carousel>
<div onClick={() => {
scroll("right");
}}
>
<ArrowRightAltIcon className={classes.arrow}/>
</div>
</Root>
)
}
If this doesn't make proper sense, I am also attaching the code sandbox link to the whole thing.
Update
As suggested, the component re-renders and hence why the instant appearance of the 1st div. Now, what I am trying to understand here is, why it re-renders in certain cases and does scrolling in some? Since, I am using useState, shouldn't it re-render it every time?
https://codesandbox.io/embed/keen-wright-9n94s?fontsize=14&hidenavigation=1&theme=dark
Any help is appreciated!
That's because when you push the divs for moving, click events trigger re-render the component. Which means it doesn't scroll, it just shows you the new component with the updated activeIndex. This is why its behavior is not smooth. So you need to find other right way to scroll. To avoid moving to the first one when you hit the last one is to add last index check function into the scroll function. If it's the last index, just return nothing but it's not good solution. I suggest you to go find and learn how to move with scroll api in div box. After learning it, you will find out the solution.

How to make a button visible after function runs. React Native

I'm working a app that will check and API to see if there are active users, if there are active users I want to show a button a tt he bottom of the page
I'm calling the function 2 seconds after the page load, because if I don't do this I get an error
"Too mnany re-renders. React limist the number of.."
and the only
for me to fix this was to do the timeout.
After two second the function is called and setActive to true, but my button doesn't appears.
Yes I'm new to react native :/
export default function Index({navigation}) {
const [active, setActive] = useState(true);
const checkForActive = () => {
console.log("Actives");
setActive(true);
}
setTimeout(() => {
checkForActive();
}, 2000);
return (
<View style={styles.MainContainer}>
{!active && (
<TouchableOpacity activeOpacity={.45} style={styles.BNutton} >
<Text style={styles.Text}> Active</Text>
</TouchableOpacity>
)
}
</View>
);
}
You should use an effect for this:
As checkForActive calls an API, it's asynchronous, so I'm assuming it will eventually return a Promise
useEffect(() => {
checkForActive.then(data => {
// Do things with data
setActive(true);
});
}, []);
The effect will be triggered once on first rendering of the component, then the empty dependencies array as second parameter means that nothing will trigger the effect again during the component life time.

How to get numeric value from Reanmited Value?

I create a simple animation using react native reanimated but I can't access the numeric value of Reanimated Value
I using victory native pie chart, and I want to make a simple effect that pie angle goes from 0 to 360 but I've tried react-native animated API it works well with add listener but I want use reanimated for performance issue
the animation effect that I'm looking for that the chart starts from 0 to 360
run correctly with react-native Animated API:
const Chart = props => {
const { data, width, height } = props;
const endAngleAnimatedValue = new Value(0);
const [endAngle, setEndAngle] = useState(0);
const runTiming = Animated.timing(endAngleAnimatedValue, {
duration: 1000,
to: 360
});
useEffect(() => {
endAngleAnimatedValue.addListener(_endAngle => setEndAngle(_endAngle));
runTiming.start();
return () => {
endAngleAnimatedValue.removeAllListeners();
};
}, [endAngleAnimatedValue]);
return (
<VictoryPie
data={data}
width={width}
height={height}
padding={0}
startAngle={0}
endAngle={endAngle}
style={{
data: {
fill: ({ datum }) => datum.fill,
fillOpacity: 0.7
}
}}
/>
);
};
How I can achieve the desired output with reanimated?
I totally forgot about this question,
Here we can have two implementations if using react-native-reanimated v1:
1. using react-native-svg and some helpers from react-native-redash
2. using `const AnimatedPie = Animated.createAnimatedComponent(VictoryPie)` then passing `endAngleAnimatedValue` as a prop.
In reanimated v2:
You can use explicitly .value to animated sharedValues, derivedValues, but also you have to createAnimatedComponent

Categories

Resources