How can I make an animated Linear Gradient button? - javascript

I am using React Native, and one of the components we need is a button with a gradient background. OnPress the colors should smoothly animate from their base value to their active value, and when you finish they should smoothly go back to their base value. I'm using a TouchableHighlight component to get access to the onShowUnderlay and onHideUnderlay functions to trigger the gradient change.
I was successfully able to get it to change abruptly on state change, but I'm having a harder time getting it to smoothly animate. When I used the following code, the emulator gives me this error: JSON value '<null>' of type NSNull cannot be converted to a UI Color. Did you forget to call processColor() on the JS side?, which I think is related to LinearGradient not being able to read the interpolated values as an RGBA value.
Am I not using the Animated API or the LinearGradient correctly? Or is it just not possible to do it this way?
import React, { PureComponent } from 'react';
import { Animated, View, TouchableHighlight, Text } from 'react-native';
import LinearGradient from 'react-native-linear-gradient';
import styles from './styles/FooterButton.styles';
const AnimatedGradient = Animated.createAnimatedComponent(LinearGradient);
export default class FooterGradientButton extends PureComponent {
midColor = new Animated.Value(0);
lastColor = new Animated.Value(0);
showUnderlay = () => {
this.midColor.setValue(0);
this.lastColor.setValue(0);
Animated.parallel([
Animated.timing(this.midColor, {
duration: 500,
toValue: 1,
}),
Animated.timing(this.lastColor, {
duration: 500,
toValue: 1,
}),
]).start();
};
hideUnderlay = () => {
this.midColor.setValue(1);
this.lastColor.setValue(1);
Animated.parallel([
Animated.timing(this.midColor, {
duration: 500,
toValue: 0,
}),
Animated.timing(this.lastColor, {
duration: 500,
toValue: 0,
}),
]).start();
};
render() {
const firstColor = 'rgba(52, 85, 219, 1)';
const midColor = this.midColor.interpolate({
inputRange: [0, 1],
outputRange: ['rgba(19, 144, 255, 1)', 'rgba(52,85,219, 1)'],
});
const lastColor = this.lastColor.interpolate({
inputRange: [0, 1],
outputRange: ['rgba(2,194,211, 1)', 'rgba(30,144,255, 1)'],
});
return (
<View style={[styles.margin, styles.shadow]}>
<AnimatedGradient start={{ x: 0.0, y: 0.5 }} end={{ x: 1, y: 0.5 }} style={{ flex: 1 }} colors={[firstColor, midColor, lastColor]}>
<TouchableHighlight
activeOpacity={1}
underlayColor="#ffffff00"
onShowUnderlay={() => this.showUnderlay()}
onHideUnderlay={() => this.hideUnderlay()}
style={[styles.gradientButton, styles.androidButton]}
onPress={() => (!this.props.inactive ? this.props.onPress() : null)}
>
<Text style={[styles.buttonText, { color: 'white' }]}>NEXT</Text>
</TouchableHighlight>
</AnimatedGradient>
</View>
);
}
}

Take a look at react native placeholder. You should be able to pass a linear gradient and animate it.

Related

React native animated object does not move

I am new to React Native animated, and I am trying to incorporate it into my app. I currently have an object that appears to fall from the middle to the bottom of the screen, using frequent state updates like this:
const [objHeight, setObjHeight] = useState((Dimensions.get("screen").height)/2)
useEffect(()=>{
if(objHeight > 0){
timerId = setInterval(()=>{
setObjHeight(objHeight => objHeight - 3)
}, 30) //30ms
return ()=>{
clearInterval(timerId)
}
},[objHeight])
//then the object looks like:
<View
style = {[{
position: "absolute",
backgroundColor: 'blue',
width: 50,
height: 60,
bottom: objHeight,
}]}>
</View>
I understand that this is an inefficient way to do animations on react, and that by using react animated we can make animate this on the UI thread instead. I have tried to replicate the above using react native animated.
const objHeight = new Animated.Value(screenHeight/2)
Animated.loop(
Animated.timing(objHeight, {
toValue: objHeight>0 ? objHeight - gravity : null,
duration: 3000,
useNativeDriver: true
}),
{iterations: 1000}
).start()
<Animated.View
style = {[{
backgroundColor: 'blue',
width: 50,
height: 60,
bottom: 200,
transform:[{translateY: objHeight}]
}]}>
</Animated.View>
However, the object does not move/animate. It just stays at the same height. What am I doing wrong? I find the documentation on react native animated is not particularly helpful.
Thank you
Snack Link
First create an Animated.Value with some initial value:
const bottomAnim = useRef(new Animated.Value(Dimensions.get('screen').height / 2)).current;
Then on component mount start the animation:
useEffect(() => {
Animated.timing(bottomAnim, {
toValue: 0,
duration: 3000,
useNativeDriver: true,
}).start();
}, []);
Finally, bind the animated value to the component:
return (
<Animated.View
style={[
{
position: 'absolute',
backgroundColor: 'blue',
width: 50,
height: 60,
bottom: bottomAnim,
},
]}/>
);

Problems with React native's tintColor

I am very new to React Native and have come across a problem which occurs when I want to animate an images tint color. I expect the color to change gradually over time like Animated.Timing should achieve, but instead it only does the first frame of the animation then freezes. However, for some reason when I just change 'tintColor' to 'backgroundColor' the animation works fine.
Here is my code:
import React from 'react';
import { useState } from 'react';
import { Text, View, TouchableHighlight, Dimensions, Component, Image, Animated } from 'react-native';
import PropTypes from 'prop-types';
function RGBtoCSS(rgb) {
return "rgb(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + ")";
}
class MyImage extends React.Component {
constructor(props) {
super(props);
this.anim = new Animated.Value(0);
this.animate();
}
animate() {
this.props.onPress();
Animated.timing(
this.anim,
{
toValue: 1,
duration: 500,
useNativeDriver: false
}
).start();
}
render() {
var style = {
width: 500,
height: 500,
resizeMode: 'stretch'
};
var col = this.anim.interpolate(
{
inputRange: [0, 1],
outputRange: [RGBtoCSS([0, 0, 0]), RGBtoCSS([140, 74, 140])]
});
return (
<Animated.Image source={this.props.img} style={{ ...style, tintColor: col }} />
);
}
}
MyImage.propTypes = { img: PropTypes.string.isRequired, onPress: PropTypes.func.isRequired, spacing: PropTypes.number.isRequired };
export default function App() {
return (
<View style={{
flex: 1,
backgroundColor: RGBtoCSS([255, 255, 255])
}}>
<MyImage img={{ uri: 'https://img.icons8.com/color/452/google-logo.png' }} onPress={() => { }} spacing={0} />
</View>
);
}
(demo available at https://snack.expo.dev/v0GeiLbOP)
I found a workaround for this. You can stack two <Animated.Image> on top of each other, with 1st Image being of final color, and 2nd image being of starting color. Then you can use opacity to create a fading effect, between these two Images, which also works well with interpolate.
import React from 'react';
import { useState } from 'react';
import { Text, View, TouchableHighlight, Dimensions, Component, Image, Animated } from 'react-native';
import PropTypes from 'prop-types';
function RGBtoCSS(rgb) {
return "rgb(" + rgb[0] + "," + rgb[1] + "," + rgb[2] +")";
}
class MyImage extends React.Component {
constructor(props) {
super(props);
this.anim = new Animated.Value(0);
this.animate();
}
animate() {
this.props.onPress();
console.log(this.anim)
Animated.timing(
this.anim,
{
toValue: 1,
duration: 500,
useNativeDriver: false
}
).start();
}
render() {
var style = {
width: 500,
height: 500,
resizeMode: 'stretch'
};
var col = this.anim.interpolate(
{
inputRange: [0, 1],
outputRange: [1, 0]
});
return (
<View>
<Animated.Image source={this.props.img} style={{ ...style,tintColor: RGBtoCSS([140, 74, 140]) }} />
<Animated.Image source={this.props.img} style={{ ...style, opacity:col, transform:[{translateY:"-100%"}], tintColor: RGBtoCSS([0, 0, 0]) }} />
</View>
);
}
}
MyImage.propTypes = { img: PropTypes.string.isRequired, onPress: PropTypes.func.isRequired, spacing: PropTypes.number.isRequired };
export default function App() {
return (
<View style={{
flex: 1,
backgroundColor: RGBtoCSS([255, 255, 255])
}}>
<MyImage img={{ uri: 'https://img.icons8.com/color/452/google-logo.png' }} onPress={() => { }} spacing={0} />
</View>
);
}
Snack - https://snack.expo.dev/ZYulbKh3g

How can I get my ScrollView to move synchronously with my Scrollbar Custom Indicator in React Native?

So I am trying to get this to work like a scrollbar browser, and have the ScrollView move in sync with the Custom Indicator. Right now I have the scrollTo() being called from within the onPanResponderRelease like so...
onPanResponderRelease: () => {
pan.flattenOffset();
scrollRef.current.scrollTo({x: 0, y:animatedScrollViewInterpolateY.__getValue(), animated: true,});
However to get the effect I want I believe scrollTo() needs to be called from within onPanResponderMove, which is currently set to this...
onPanResponderMove: Animated.event([null, { dx: pan.x, dy: pan.y }], {useNativeDriver: false})
The Animated.event that you see here is what is driving the indicator to move when touched.
But how can I call the scrollTo() from within the onPanResponderMove also? Or is that even a good idea? If I am able to do this, the ScrollView will move synchronously with the indicator. At least I think so...
At the end of the day all I want is a ScrollView that has a scrollbar that works like a browser's scrollbar. Meaning the scrollbar indicator is touchable and draggable. Ideally I want this on a FlatList however I figured doing it on the ScrollView first would be simpler, and then I could just apply the same principles to a FlatList component afterwards.
This is all the code...
import React, { useRef, useState } from "react";
import {
Animated,
View,
StyleSheet,
PanResponder,
Dimensions,
Text,
ScrollView,
TouchableWithoutFeedback,
} from "react-native";
const boxSize = { width: 50, height: 50 };
const App = () => {
const AnimatedTouchable = Animated.createAnimatedComponent(
TouchableWithoutFeedback
);
const { width, height } = Dimensions.get("window");
const pan = useRef(new Animated.ValueXY({ x: 0, y: 50 })).current;
const scrollRef = useRef();
const [scrollHeight, setScrollHeight] = useState(0);
const panResponder = useRef(
PanResponder.create({
onMoveShouldSetPanResponder: () => true,
onPanResponderGrant: () => {
pan.setOffset({
x: pan.x._value,
y: pan.y._value,
});
},
onPanResponderMove: Animated.event([null, { dx: pan.x, dy: pan.y }], {
useNativeDriver: false,
}),
onPanResponderRelease: () => {
pan.flattenOffset();
scrollRef.current.scrollTo({
x: 0,
y: animatedScrollViewInterpolateY.__getValue(),
animated: true,
});
},
})
).current;
function scrollLayout(width, height) {
setScrollHeight(height);
}
const animatedInterpolateX = pan.x.interpolate({
inputRange: [0, width - boxSize.width],
outputRange: [0, width - boxSize.width],
extrapolate: "clamp",
});
const animatedInterpolateY = pan.y.interpolate({
inputRange: [0, height - boxSize.height],
outputRange: [height * 0.2, height * 0.8],
extrapolate: "clamp",
});
const animatedScrollViewInterpolateY = animatedInterpolateY.interpolate({
inputRange: [height * 0.2, height * 0.8],
outputRange: [0, 411],
// extrapolate: "clamp",
});
return (
<View style={styles.container}>
<Animated.View style={{ backgroundColor: "red" }}>
<ScrollView
ref={scrollRef}
onContentSizeChange={scrollLayout}
scrollEnabled={false}
>
<Text style={styles.titleText}>
<Text>Twenty to Last!</Text>
<Text>Ninete to Last!</Text>
<Text>Eight to Last!</Text>
<Text>Sevent to Last!</Text>
<Text>Sixtee to Last!</Text>
<Text>Fiftee to Last!</Text>
<Text>Fourte to Last!</Text>
<Text>Thirte to Last!</Text>
<Text>Twelet to Last!</Text>
<Text>Eleveh to Last!</Text>
<Text>Tenth to Last!</Text>
<Text>Nineth to Last!</Text>
<Text>Eighth to Last!</Text>
<Text>Seventh to Last!</Text>
<Text>Sixth to Last!</Text>
<Text>Fifth to Last!</Text>
<Text>Fourth to Last!</Text>
<Text>Third to Last!</Text>
<Text>Second to Last!</Text>
<Text>The End!</Text>
</Text>
</ScrollView>
</Animated.View>
<Animated.View
style={{position: "absolute",
transform: [
{ translateX: width - boxSize.width },
{ translateY: animatedInterpolateY },
],}}
{...panResponder.panHandlers}>
<View style={styles.box} />
</Animated.View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
titleText: {
fontSize: 54,
fontWeight: "bold",
},
box: {
height: boxSize.height,
width: boxSize.width,
backgroundColor: "blue",
borderRadius: 5,
},
});
export default App;
I'll start by saying that regardless of your use case, it might be a bad user experience to bring web-style scrolling to mobile. The whole view is usually scrollable so that you can scroll from anywhere without holding a scrollbar (also think of left-handed people).
With that said, you can change onPanResponderMove to a regular callback so that you get more control over what you need:
// 1) Transform Animated.event(...) to a simple callback
onPanResponderMove: (event, gestureState) => {
pan.x.setValue(gestureState.dx);
pan.y.setValue(gestureState.dy);
// 2) Scroll synchronously, without animation (..because it's synchronous!)
scrollRef.current.scrollTo({
x: 0,
y: animatedScrollViewInterpolateY.__getValue(),
animated: false,
});
},
onPanResponderRelease: () => {
pan.flattenOffset();
// 3) Remove scrollTo from the release event
},

React native - "Rendered more hooks than during the previous render?"

I only encountered this issue once I incorporated the useEffect() hook as suggested by React native - "this.setState is not a function" trying to animate background color?
With the following, I get
Rendered more hooks than during the previous render
export default props => {
let [fontsLoaded] = useFonts({
'Inter-SemiBoldItalic': 'https://rsms.me/inter/font-files/Inter-SemiBoldItalic.otf?v=3.12',
'SequelSans-RomanDisp' : require('./assets/fonts/SequelSans-RomanDisp.ttf'),
'SequelSans-BoldDisp' : require('./assets/fonts/SequelSans-BoldDisp.ttf'),
'SequelSans-BlackDisp' : require('./assets/fonts/SequelSans-BlackDisp.ttf'),
});
if (!fontsLoaded) {
return <AppLoading />;
} else {
//Set states
const [backgroundColor, setBackgroundColor] = useState(new Animated.Value(0));
useEffect(() => {
setBackgroundColor(new Animated.Value(0));
}, []); // this will be only called on initial mounting of component,
// so you can change this as your requirement maybe move this in a function which will be called,
// you can't directly call setState/useState in render otherwise it will go in a infinite loop.
useEffect(() => {
Animated.timing(this.state.backgroundColor, {
toValue: 100,
duration: 5000
}).start();
}, [backgroundColor]);
var color = this.state.colorValue.interpolate({
inputRange: [0, 300],
outputRange: ['rgba(255, 0, 0, 1)', 'rgba(0, 255, 0, 1)']
});
const styles = StyleSheet.create({
container: { flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: color
},
textWrapper: {
height: hp('70%'), // 70% of height device screen
width: wp('80%'), // 80% of width device screen
backgroundColor: '#fff',
justifyContent: 'center',
alignItems: 'center',
},
myText: {
fontSize: hp('2%'), // End result looks like the provided UI mockup
fontFamily: 'SequelSans-BoldDisp'
}
});
return (
<Animated.View style={styles.container}>
<View style={styles.textWrapper}>
<Text style={styles.myText}>Login</Text>
</View>
</Animated.View>
);
}
};
Im just trying to animate fade the background color of a view. I tried deleting the first useEffect in case it was causing some redundancy, but that did nothing. Im new to ReactNative - what is wrong here?
EDIT:
export default props => {
let [fontsLoaded] = useFonts({
'Inter-SemiBoldItalic': 'https://rsms.me/inter/font-files/Inter-SemiBoldItalic.otf?v=3.12',
'SequelSans-RomanDisp' : require('./assets/fonts/SequelSans-RomanDisp.ttf'),
'SequelSans-BoldDisp' : require('./assets/fonts/SequelSans-BoldDisp.ttf'),
'SequelSans-BlackDisp' : require('./assets/fonts/SequelSans-BlackDisp.ttf'),
});
//Set states
const [backgroundColor, setBackgroundColor] = useState(new Animated.Value(0));
useEffect(() => {
setBackgroundColor(new Animated.Value(0));
}, []); // this will be only called on initial mounting of component,
// so you can change this as your requirement maybe move this in a function which will be called,
// you can't directly call setState/useState in render otherwise it will go in a infinite loop.
useEffect(() => {
Animated.timing(useState(backgroundColor), {
toValue: 100,
duration: 7000
}).start();
}, [backgroundColor]);
// var color = this.state.colorValue.interpolate({
// inputRange: [0, 300],
// outputRange: ['rgba(255, 0, 0, 1)', 'rgba(0, 255, 0, 1)']
// });
//------------------------------------------------------------------->
if (!fontsLoaded) {
return <AppLoading />;
} else {
const styles = StyleSheet.create({
container: { flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: backgroundColor
New errors:
invalid prop 'color' supplied to 'Stylesheet'
Animated useNativeDriver was not specified
On your first render (I'm guessing) only the useFonts hook will be called as you return <AppLoading /> since !fontsLoaded. The rest of your hooks are in the else block, meaning you won't have the same number of hooks on every render.
Check out https://reactjs.org/docs/hooks-rules.html for more explanation, especially https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
The useNativeDriver error exists because you didn't specify it:
Your code:
useEffect(() => {
Animated.timing(useState(backgroundColor), {
toValue: 100,
duration: 7000
}).start();
Fix:
useEffect(() => {
Animated.timing(useState(backgroundColor), {
toValue: 100,
duration: 7000,
useNativeDrive: true
}).start();
Hope this helps!

How does "Animated.createAnimatedComponent" work?

In the Component ProgressBarAndroid, there are props indeterminable={Boolean} which show to a user an animation of what it's going on. I would like to do almost the same on ProgressViewIOS. So I tried to Animate it with Animated...
I saw on docs of Animated method called 'createAnimatedComponent' which they use to create Animated.View
I tried so to create another Animated (Native) Component but it doesn't work at all.
The animation should gradually raise fillValue to 20 % and continue with an original value from the media upload...
This is my Component
// ProgressBar.ios.js
// #flow
import { PropTypes } from 'react';
import Component from 'components/base/Component';
import { ProgressViewIOS, Animated } from 'react-native';
const AnimatedProgressViewIOS = Animated.createAnimatedComponent(ProgressViewIOS);
class ProgressBarIOS extends Component {
static propTypes = {
// Percentage (0 - 100)
fill: PropTypes.number.isRequired,
};
constructor(props, context: any) {
super(props, context);
this.state = {
fillValue: new Animated.Value(props.fill),
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.fill === 0) {
Animated.timing(this.state.fillValue, { toValue: 0.2, duration: 500 }).start();
} else if (nextProps.fill > 19) {
this.state.fillValue.setValue(nextProps.fill / 100);
}
}
shouldComponentUpdate(nextProps) {
return this.props.fill !== nextProps.fill;
}
render() {
return (
<AnimatedProgressViewIOS
style={{ alignSelf: 'stretch' }}
progress={this.state.fillValue} />
);
}
}
export default ProgressBarIOS;
EDIT: AnimatedComponent is used to modify style only. Props could be passed as animated value but remember it is not a number!
Animated.createAnimatedComponent can animate a number of different properties, however only some properties are supported using the native driver, fortunately it appears progress on ProgressViewIOS is one of them.
Here is a working implementation of an animated ProgressViewIOS.
import * as React from 'react';
import { View, SafeAreaView } from 'react-native';
import { ProgressViewIOS, Animated } from 'react-native';
const AnimatedProgressViewIOS = Animated.createAnimatedComponent(
ProgressViewIOS
);
export default function App() {
const value = React.useRef(new Animated.Value(0));
React.useEffect(() => {
Animated.loop(
Animated.timing(value.current, {
duration: 2000,
toValue: 1,
useNativeDriver: true,
})
).start();
}, []);
return (
<SafeAreaView>
<View style={{ padding: 20 }}>
<AnimatedProgressViewIOS
style={{ alignSelf: 'stretch' }}
progress={value.current}
/>
</View>
</SafeAreaView>
);
}
It's worth noting that ProgressViewIOS is now deprecated, but building your own progress view is very straight forward and requires just two Views with simple styling like this (expo snack):
import * as React from 'react';
import { View, SafeAreaView, StyleSheet, Button, Text } from 'react-native';
import { Animated } from 'react-native';
export default function App() {
const [progress, setProgress] = React.useState(() => Math.random());
return (
<SafeAreaView>
<View style={{ padding: 20 }}>
<AnimatedProgressView progress={progress} />
<Text style={{padding: 20, textAlign: 'center'}}>{Math.round(progress * 100)}%</Text>
<Button title="Animate" onPress={() => setProgress(Math.random())} />
</View>
</SafeAreaView>
);
}
function AnimatedProgressView({ progress, style }) {
const value = React.useRef(new Animated.Value(0));
const [width, setWidth] = React.useState(0);
React.useEffect(() => {
Animated.spring(value.current, { toValue: progress }).start();
}, [progress]);
return (
<View
style={[styles.track, style]}
onLayout={(event) => setWidth(event.nativeEvent.layout.width)}>
<Animated.View
style={[
styles.fill,
{
transform: [
{
translateX: value.current.interpolate({
inputRange: [0, 1],
outputRange: [-width, 0],
overflow: 'clamp',
}),
},
],
},
]}
/>
</View>
);
}
const styles = StyleSheet.create({
track: {
minHeight: 4,
borderRadius: 2,
overflow: 'hidden',
backgroundColor: '#ddd',
},
fill: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'blue',
},
});

Categories