Animate listview items when they are added/removed from datasource - react-native

Can someone give me an idea of how this can be done, e.g. animate the height from 0 when added and back to 0 when removed?

Animation when added is easy, just use Animated in componentDidMount with your listRow , for example:
componentDidMount = ()=> {
Animated.timing(this.state._rowOpacity, {
toValue: 1,
duration: 250,
}).start()
}
Animate a component before unmount is much harder in react-native. You should set a handler for ListView. When dataSource changed, diff the data, start Animated to hide removed row, and set new dataSource for ListView.

Here you can get full working example for opacity animation:
import React from 'react-native';
export default class Cell extends React.Component {
constructor(props) {
super(props);
this.state = {
opacity: new React.Animated.Value(0)
};
}
componentDidMount() {
React.Animated.timing(this.state.opacity, {
toValue: 1,
duration: 250,
}).start();
}
render() {
return (
<React.Animated.View style={[styles.wrapper, {opacity: this.state.opacity}]}>
<React.Image source={{uri: 'http://placehold.it/150x150'}} style={styles.image}/>
<React.Text style={styles.text}>
Text
</React.Text>
</React.Animated.View>
);
}
}
const styles = React.StyleSheet.create({
wrapper: {
flex: 1,
flexDirection: 'row',
justifyContent: 'flex-start',
alignItems: 'center',
},
image: {
height: 40,
width: 40,
marginRight: 16,
backgroundColor: '#C9D5E6'
},
text: {
fontSize: 20
}
});

In case you need for removing an item from the list, here's how to do the ListRow component:
class DynamicListRow extends Component {
// these values will need to be fixed either within the component or sent through props
_defaultHeightValue = 60;
_defaultTransition = 500;
state = {
_rowHeight : new Animated.Value(this._defaultHeightValue),
_rowOpacity : new Animated.Value(0)
};
componentDidMount() {
Animated.timing(this.state._rowOpacity, {
toValue : 1,
duration : this._defaultTransition
}).start()
}
componentWillReceiveProps(nextProps) {
if (nextProps.remove) {
this.onRemoving(nextProps.onRemoving);
} else {
// we need this for iOS because iOS does not reset list row style properties
this.resetHeight()
}
}
onRemoving(callback) {
Animated.timing(this.state._rowHeight, {
toValue : 0,
duration : this._defaultTransition
}).start(callback);
}
resetHeight() {
Animated.timing(this.state._rowHeight, {
toValue : this._defaultHeightValue,
duration : 0
}).start();
}
render() {
return (
<Animated.View
style={{height: this.state._rowHeight, opacity: this.state._rowOpacity}}>
{this.props.children}
</Animated.View>
);
}
}
i've posted a complete tutorial to this question in this blog post. And it's explaining step by step what you need to do to accomplish both adding and removing an item and animate this process.
For adding is pretty straight forward, but for removing looks like it's a little bit more complex.
http://moduscreate.com/react-native-dynamic-animated-lists/

Here's a full example for height and opacity animation. It supports both adding and removing an element. The key point is that you need to reset the height and opacity after the disappearing animation completes. Then you immediately delete the item from the source.
export const ListItem = (props: ListItemProps) => {
// Start the opacity at 0
const [fadeAnim] = useState(new Animated.Value(0));
// Start the height at 0
const [heightAnim] = useState(new Animated.Value(0));
/**
* Helper function for animating the item
* #param appear - whether the animation should cause the item to appear or disappear
* #param delay - how long the animation should last (ms)
* #param callback - callback to be called when the animation finishes
*/
const _animateItem = (appear: boolean = true, delay: number = 300, callback: () => void = () => null) => {
Animated.parallel([
Animated.timing(
fadeAnim,
{
toValue: appear ? 1 : 0,
duration: delay,
}
),
Animated.timing(
heightAnim,
{
toValue: appear ? 100 : 0,
duration: delay,
}
),
]).start(callback);
};
// Animate the appearance of the item appearing the first time it loads
// Empty array in useEffect results in this only occuring on the first render
React.useEffect(() => {
_animateItem();
}, []);
// Reset an item to its original height and opacity
// Takes a callback to be called once the reset finishes
// The reset will take 0 seconds and then immediately call the callback.
const _reset = (callback: () => void) => {
_animateItem(true,0, callback);
}
// Deletes an item from the list. Follows the following order:
// 1) Animate the item disappearing. On completion:
// 2) Reset the item to its original display height (in 0 seconds). On completion:
// 3) Call the parent to let it know to remove the item from the list
const _delete = () => {
_animateItem(false, 200, () => _reset(props.delete));
};
return (
<Animated.View
style={{height: heightAnim, opacity: fadeAnim, flexDirection: 'row'}}>
<Text>{props.text}</Text>
<Button onPress={() => _delete()}><Text>Delete</Text></Button>
</Animated.View>
);
}

Related

setTimeout in Hooks is not changing state

I am trying to upload image and after 20 seconds, if image is not uplaoded I want to close UploadingIndicator component (which appeared with the start of uploading) and display custom WaitAlert component, where user can choose to wait or try to send image again.
But I have problem with displaying custom WaitAlert component. It is not displayed.
Here is my code:
import WaitAlert from '../components/alert/waitAlert'
import UploadingIndicator from '../components/uploadingIndicator/uploadingInddicator'
let sendingTimeout
const App = (props) => {
const [showUploadingIndicator, setShowUploadingIndicator] = React.useState(false)
const [showWaitAlert, setShowWaitAlert] = React.useState(false);
saveAsset = () => {
setShowUploadingIndicator(true)
sendingTimeout = setTimeout(() => {
setShowUploadingIndicator(false)
setShowWaitAlert(true) //THIS IS NOT DISPLAYING WaitAlert
}, 20000)
updateMutation({
variables: {
assets: [{
files
}]
}
}).then(result => {
setShowUploadingIndicator(false)
clearTimeout(sendingTimeout)
setShowWaitAlert(false)
})
.catch((error) => {
console.log("error.toString()", JSON.stringify(error))
})
}
return (
<View>
<TouchableOpacity onPress={saveAsset} />
{showUploadingIndicator &&
<UploadingIndicator />
}
{showWaitAlert &&
<WaitAlert />
}
</View>
);
}
How can I make WaitAlert appeared?
The sendingTimeout is been cleared in clearTimeout(sendingTimeout), then the setTimeout is not triggered after 20 seconds. Try to remove the line clearTimeout(sendingTimeout) to see if it works.
I found the solution.
The problem was in WaitAlert component.
I added style of main View, where modal was placed to:
mainView: {
top: 0,
left: 0,
flex: 1,
position: 'absolute',
zIndex: 10,
},

Detox: detect that element was displayed

We have a toast component in our app that is adding considerable flakiness to our tests. The toast component displays an animated View for 4s and then disappears. In a lot of tests I need to check what the message content is in order to continue with the test.
The toast component is implemented with the following code:
// #flow
import * as React from "react"
import { StyleSheet, View, Animated, Dimensions, Text } from "react-native"
import type {
TextStyle,
ViewStyle,
} from "react-native/Libraries/StyleSheet/StyleSheet"
import type AnimatedValue from "react-native/Libraries/Animated/src/nodes/AnimatedValue"
import type { CompositeAnimation } from "react-native/Libraries/Animated/src/AnimatedImplementation"
import { AnimationConstants } from "constants/animations"
const styles = StyleSheet.create({
container: {
position: "absolute",
left: 0,
right: 0,
elevation: 999,
alignItems: "center",
zIndex: 10000,
},
content: {
backgroundColor: "black",
borderRadius: 5,
padding: 10,
},
text: {
color: "white",
},
})
type Props = {
style: ViewStyle,
position: "top" | "center" | "bottom",
textStyle: TextStyle,
positionValue: number,
fadeInDuration: number,
fadeOutDuration: number,
opacity: number,
}
type State = {
isShown: boolean,
text: string | React.Node,
opacityValue: AnimatedValue,
}
export const DURATION = AnimationConstants.durationShort
const { height } = Dimensions.get("window")
export default class Toast extends React.PureComponent<Props, State> {
static defaultProps = {
position: "bottom",
textStyle: styles.text,
positionValue: 120,
fadeInDuration: AnimationConstants.fadeInDuration,
fadeOutDuration: AnimationConstants.fadeOutDuration,
opacity: 1,
}
isShown: boolean
duration: number
callback: Function
animation: CompositeAnimation
timer: TimeoutID
constructor(props: Props) {
super(props)
this.state = {
isShown: false,
text: "",
opacityValue: new Animated.Value(this.props.opacity),
}
}
show(text: string | React.Node, duration: number, callback: Function) {
this.duration = typeof duration === "number" ? duration : DURATION
this.callback = callback
this.setState({
isShown: true,
text: text,
})
this.animation = Animated.timing(this.state.opacityValue, {
toValue: this.props.opacity,
duration: this.props.fadeInDuration,
useNativeDriver: true,
})
this.animation.start(() => {
this.isShown = true
this.close()
})
}
close(duration?: number) {
const delay = typeof duration === "undefined" ? this.duration : duration
if (!this.isShown && !this.state.isShown) return
this.timer && clearTimeout(this.timer)
this.timer = setTimeout(() => {
this.animation = Animated.timing(this.state.opacityValue, {
toValue: 0.0,
duration: this.props.fadeOutDuration,
useNativeDriver: true,
})
this.animation.start(() => {
this.setState({
isShown: false,
})
this.isShown = false
if (typeof this.callback === "function") {
this.callback()
}
})
}, delay)
}
componentWillUnmount() {
this.animation && this.animation.stop()
this.timer && clearTimeout(this.timer)
}
render() {
const { isShown, text, opacityValue } = this.state
const { position, positionValue } = this.props
const pos = {
top: positionValue,
center: height / 2,
bottom: height - positionValue,
}[position]
if (isShown) {
return (
<View style={[styles.container, { top: pos }]}>
<Animated.View
style={[
styles.content,
{ opacity: opacityValue },
this.props.style,
]}
>
{React.isValidElement(text) ? (
text
) : (
<Text style={this.props.textStyle}>{text}</Text>
)}
</Animated.View>
</View>
)
}
return null
}
}
Normally we display the toast message for 4s, but I decided to display it in e2e tests for 1.5s in order to make some what faster.
I'm testing for the presence of the toast like this:
await expect(element(by.text(text))).toBeVisible()
await waitFor(element(by.text(text))).toBeNotVisible().withTimeout(2000)
However it happens often that detox fails at "toBeVisible". I can see the message on the screen, but for some reason detox is missing it.
What is the minimum time I should keep the message on the screen for detox to detect it?
On .circleCI we record videos of failing tests. When a test fails with "cannot find element" and I watch the video I clearly see the toast appearing on the screen, but detox fails to find it.
I'm still not sure if there is a better way, but I found a way that currently works for us.
Instead of automatically hiding the toast in e2e test, we mock the modal component to display and stay visible until tapped on.
Once detox sees the element we tap on it, close it and continue with our test.
I also had exactly the same problem in my project and the the solution that we found was to disable detox synchronization around the toast.
As an example, this is how the code would look like:
await device.disableSynchronization();
await element(by.id(showToastButtonId)).tap();
await waitFor(element(by.text('Toast Message')))
.toExist()
.withTimeout(TIMEOUT_MS);
await device.enableSynchronization();
Reference: https://github.com/wix/Detox/blob/master/docs/Troubleshooting.Synchronization.md#switching-to-manual-synchronization-as-a-workaround

Toggling the animated value in order to fade in/out the view

I'm trying to toggle view's opacity with animated value, by handling the button click, but I'm not getting the desired result, except the first time button is clicked, it fades out (opacity = 0) but when I press the button again nothing happens and I can't see my view. Here's the code:
export default class App extends React.Component {
state = {
animation: new Animated.Value(1)
}
startAnimation = () => {
const { animation } = this.state
Animated.timing(animation, {
toValue: animation === 0 ? 1 : 0,
duration: 1000
}).start()
}
render() {
const animatedStyle = {
opacity: this.state.animation
}
return (
<View style={styles.container}>
<Animated.View style={[styles.box, animatedStyle]} />
<Button title="Toggle fade" onPress={this.startAnimation} />
</View>
);
}
} .
Does anybody know what am I doing (understanding) wrong?
Thanks!
I think it is because you don't change the state for your animated values, and this const { animation } = this.state will have always the same value, and toValue: animation === 0 ? 1 : 0, will have the same value too. I try to show you how I did this in my projects, but you have to update it for your needs.
export default class App extends React.Component {
state = {
animation: new Animated.Value(1),
isVisible: false //add a new value to check your state
}
startAnimation = () => {
const { isVisible } = this.state
Animated.timing(animation, {
toValue: isVisible === 0 ? 1 : 0,
duration: 1000
}).start(() => {
this.setState({ isVisible: !this.state.isVisible });//set the new state, so the next click will have different value
})
}
render() {
const animatedStyle = {
opacity: this.state.animation
}
return (
<View style={styles.container}>
<Animated.View style={[styles.box, animatedStyle]} />
<Button title="Toggle fade" onPress={this.startAnimation} />
</View>
);
}
} .
I am using this:
let val = this.state.sliderOpen ? 0.8 : 0;
Animated.timing( // Animate over time
this.state.sliderAnimation, // The animated value to drive
{
toValue: val, // Animate to opacity: 1 (opaque)
duration: 5, // Make it take a while
}
).start();
this.setState({
sliderOpen : !this.state.sliderOpen
})
Maybe try to extract the value to be changed.
Thanks to #oma I was able to get it work, here's the snack:
Toggle opacity in React Native
Besides that, I've found a nice article on this where this feature can be reused:
Animating appearance and disappearance in React Native
And here's the snack of the working example, with slight modification.
Animate opacity
This solution looks pretty well, hope you can benefit from it.
I made a node package react-native-fade-in-out that toggles a view's opacity with an animated value. You can look at the source code to see how it is accomplished, but here's a simplified version:
import React, {PureComponent} from 'react';
import {Animated} from 'react-native';
export default class FadeInOut extends PureComponent {
state = {
fadeAnim: new Animated.Value(this.props.visible ? 1 : 0),
};
componentDidUpdate(prevProps) {
if (prevProps.visible !== this.props.visible) {
Animated.timing(this.state.fadeAnim, {
toValue: prevProps.visible ? 0 : 1,
duration: 300,
}).start();
}
}
render() {
return (
<Animated.View style={{...this.props.style, opacity: this.state.fadeAnim}}>
{this.props.children}
</Animated.View>
);
}
}

Looping a react native animated animation

I am attempting to put the following animation in an infinite loop until a particular state occurs:
class MyModal extends Component {
constructor() {
super()
this.springValue = new Animated.Value(0.3)
}
spring = () => {
this.springValue.setValue(0.3)
Animated.spring(
this.springValue,
{
toValue: 1,
friction: 1,
tension: 1,
duration:5000
}
).start()
}
componentDidMount() {
this.spring()
}
render() {
return (
<View>
<Modal
animationType="none"
transparent={false}
visible={this.state.modalVisible}
onRequestClose={() => null}
>
<View style={styles.backgroundStyle}>
<Animated.Image
source={require("./my-awesome-image.png")}
style={{ width: 143, height: 125, top: 100, transform: [{scale: this.springValue}]}}
/>
</View>
</Modal>
</View>
);
}
}
Everything here works great, the animation completes once (as I'm not looping anywhere).
How do I keep my Animated.Image looping until I reach a particular state? I just want it infinite looping and the ability to either stop the animation or start another animation when I'm ready to.
Store your animation in a variable you can access and just wrap your animation with Animated.loop(). Then you can freely use .start() and .stop() on that variable holding the animation as you please.
So something like this should do:
this.springAnimation = Animated.loop(
Animated.spring(
this.springValue,
{
toValue: 1,
friction: 1,
tension: 1,
duration:5000
}
).start()
)
You can find more information about that here:
https://facebook.github.io/react-native/docs/animated.html#loop
Pass a callback to the start function to check whether to restart the animation. You could break it down to something like this:
onSpringCompletion = () => {
if (arbitraryCondition) {
this.spring();
}
}
spring = () => {
this.springValue.setValue(0.3)
Animated.spring(
this.springValue,
{
toValue: 1,
friction: 1,
tension: 1,
duration:5000
}
).start(this.onSpringCompletion);
}
You can use setInterval to keep the animation playing and remove the interval whenever you want. I would do this:
componentDidMount() {
this.interval = setInterval(() => this.spring(), 5000) // Set this time higher than your animation duration
}
...
// Some where in your code that changes the state
clearInterval(this.interval)
...

Locking scroll position in FlatList (and ScrollView)

I'm trying to create a FlatList that keeps the current scroll position locked and does not change by new items that are inserted at the top of the list.
I've created an expo snack to demonstrate my intention.
The snack presents a ScrollView with green items, and a black item at the end.
When the app launch it scrolls to the bottom of the list. After five seconds 10 items are inserted at the top, and the scroll position is changing according to the total size of these items.
This is the code of the expo snack:
import React, { Component } from 'react';
import { View, FlatList } from 'react-native';
const renderItem = ({ item }) => {
let backgroundColor;
if (item == 10) {
backgroundColor = "black"
}
else {
backgroundColor = item % 2 == 0 ? 'green' : 'blue'
}
return (
<View
style={{
width: 200,
height: 50,
backgroundColor,
margin: 10,
}}
/>
);
};
const MyList = class extends Component {
componentDidMount() {
setTimeout(() => this.ref.scrollToEnd({ animated: false }), 500);
}
render() {
return (
<FlatList
ref={r => this.ref = r}
data={this.props.data}
renderItem={this.props.renderItem}
/>
);
}
};
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
items: [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 10],
};
}
componentDidMount() {
const items = [...this.state.items];
items.unshift(1, 1, 1, 1, 1, 1, 1, 1, 1, 1);
setTimeout(() => this.setState({ items }), 5000);
}
render() {
return <MyList renderItem={renderItem} data={this.state.items} />;
}
}
I want to keep the scroll position locked, meaning - when items are inserted the scroll position will not change (or at least in a way the user doesn't know anything happened)
Is there a way to do it with the current API of FlatList and ScrollView? What's needed to be implemented to achieve this feature?
you should use componentDidUpdate() to achieve this.
componentDidUpdate(prevProps, prevState) {
if(prevProps.data != this.props.data) {
this.ref.scrollToEnd({ animated: false });
}
}
add this in your MyList component. when component gets receive new props.data and that doesn't to your current props.data then it it will call scrollToEnd.
this might be helpfull!
Have you tried use keyExtractor ? ( https://facebook.github.io/react-native/releases/next/docs/flatlist.html#keyextractor ).
It may help react avoid re-render, so try use unique keys for each item.