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
Related
I want to get the user's location even when the App is in the background. I am using expo-location and expo-task-manager in the following manner:
import * as React from "react";
import {
StyleSheet,
StatusBar,
Platform,
SafeAreaView,
TouchableOpacity,
Text,
} from "react-native";
import * as TaskManager from "expo-task-manager";
import * as Location from "expo-location";
const STATUSBAR_HEIGHT = Platform.OS === "os" ? 20 : StatusBar.currentHeight;
const LOCATION_TASK_NAME = "background-location-task";
export default function TestingGround({ navigation }) {
const onPress = async () => {
const { status } = await Location.requestPermissionsAsync();
if (status === "granted") {
await Location.startLocationUpdatesAsync(LOCATION_TASK_NAME, {
accuracy: Location.Accuracy.Highest,
});
}
};
return (
<SafeAreaView style={styles.container}>
<TouchableOpacity
style={{
height: 50,
width: 300,
backgroundColor: "red",
justifyContent: "center",
alignItems: "center",
}}
onPress={onPress}
>
<Text>Enable background location</Text>
</TouchableOpacity>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "white",
paddingTop: STATUSBAR_HEIGHT,
justifyContent: "center",
alignItems: "center",
},
});
TaskManager.defineTask(LOCATION_TASK_NAME, ({ data, error }) => {
if (error) {
// Error occurred - check `error.message` for more details.
console.log("error", error);
return;
}
if (data) {
const { locations } = data;
// do something with the locations captured in the background
console.log("locations", locations);
}
});
On press, I get the error: Unhandled promise rejection: Error: Not authorized to use background location services.
Location services are enabled. I don't understand what I need to do.
I also added the following to my App.json but with no success:
"android": {
...
"permissions": [
"CAMERA",
"ACCESS_FINE_LOCATION",
"ACCESS_BACKGROUND_LOCATION"]
},
"ios": {
"supportsTablet": true,
"infoPlist": {
"UIBackgroundModes": [
"location",
"fetch"
]
}
}
Well after a week of battling it I finally found a solution. It turns out that the reason I am getting this error is simply because I am running this code on Expo which does not allow background location fetching. There is nothing wrong with the code, all I had to do was to build a standalone App (expo build:android) and the standalone version of the App worked just fine and could fetch background location 😁
I also passed an extra parameter to my Location.startLocationUpdatesAsync which increased the effectiveness of background-location fetching and actually allowed me to visualize that the App is fetching the background-location via a notification like this:
const onPress = async () => {
const { status } = await Location.requestPermissionsAsync();
if (status === "granted") {
await Location.startLocationUpdatesAsync(LOCATION_TASK_NAME, {
accuracy: Location.Accuracy.BestForNavigation,
timeInterval: 3000,
foregroundService: {
notificationTitle: "BackgroundLocation Is On",
notificationBody: "We are tracking your location",
notificationColor: "#ffce52",
},
});
}
};
I hope this helps someone out there and don't hesitate to contact me for further assistance on this matter.
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,
},
I want to create my own Endomono/Runtastic-like app using RN + expo (This app will be just for me, and I have android phone with pretty decent performance/battery life (Redmi note 7) so I don't worry about performance too much). I wanted to use all-in-one library for that, or just and library that allows me to execute some code each X seconds in background (and getAsyncLocation there). My point is just to send lat/lon data every X seconds to my backend HTTP django-rest-framework powered server.
I just spent whole day trying figure out any way to do that, I tried couple of libraries like this ones: react-native-background-geolocation, react-native-background-timer, react-native-background-job and few more. I followed step by step instalation guide, and I kept getting errors like: null is not an object (evaluating 'RNBackgroundTimer.setTimeout') .
I also tried this: I fixed some errors in this code (imports related), it seemed to work, but when I changed my GPS location using Fake GPS, and only one cast of didFocus functions appears in the console. Here's code:
import React from 'react';
import { EventEmitter } from 'fbemitter';
import { NavigationEvents } from 'react-navigation';
import { AppState, AsyncStorage, Platform, StyleSheet, Text, View, Button } from 'react-native';
import MapView from 'react-native-maps';
import * as Permissions from 'expo-permissions';
import * as Location from 'expo-location';
import * as TaskManager from 'expo-task-manager';
import { FontAwesome, MaterialIcons } from '#expo/vector-icons';
const STORAGE_KEY = 'expo-home-locations';
const LOCATION_UPDATES_TASK = 'location-updates';
const locationEventsEmitter = new EventEmitter();
export default class MapScreen extends React.Component {
static navigationOptions = {
title: 'Background location',
};
mapViewRef = React.createRef();
state = {
accuracy: 4,
isTracking: false,
showsBackgroundLocationIndicator: false,
savedLocations: [],
initialRegion: null,
error: null,
};
didFocus = async () => {
console.log("Hello")
let { status } = await Permissions.askAsync(Permissions.LOCATION);
if (status !== 'granted') {
AppState.addEventListener('change', this.handleAppStateChange);
this.setState({
error:
'Location permissions are required in order to use this feature. You can manually enable them at any time in the "Location Services" section of the Settings app.',
});
return;
} else {
this.setState({ error: null });
}
const { coords } = await Location.getCurrentPositionAsync();
console.log(coords)
const isTracking = await Location.hasStartedLocationUpdatesAsync(LOCATION_UPDATES_TASK);
const task = (await TaskManager.getRegisteredTasksAsync()).find(
({ taskName }) => taskName === LOCATION_UPDATES_TASK
);
const savedLocations = await getSavedLocations();
const accuracy = (task && task.options.accuracy) || this.state.accuracy;
this.eventSubscription = locationEventsEmitter.addListener('update', locations => {
this.setState({ savedLocations: locations });
});
if (!isTracking) {
alert('Click `Start tracking` to start getting location updates.');
}
this.setState({
accuracy,
isTracking,
savedLocations,
initialRegion: {
latitude: coords.latitude,
longitude: coords.longitude,
latitudeDelta: 0.004,
longitudeDelta: 0.002,
},
});
};
handleAppStateChange = nextAppState => {
if (nextAppState !== 'active') {
return;
}
if (this.state.initialRegion) {
AppState.removeEventListener('change', this.handleAppStateChange);
return;
}
this.didFocus();
};
componentWillUnmount() {
if (this.eventSubscription) {
this.eventSubscription.remove();
}
AppState.removeEventListener('change', this.handleAppStateChange);
}
async startLocationUpdates(accuracy = this.state.accuracy) {
await Location.startLocationUpdatesAsync(LOCATION_UPDATES_TASK, {
accuracy,
showsBackgroundLocationIndicator: this.state.showsBackgroundLocationIndicator,
});
if (!this.state.isTracking) {
alert(
'Now you can send app to the background, go somewhere and come back here! You can even terminate the app and it will be woken up when the new significant location change comes out.'
);
}
this.setState({ isTracking: true });
}
async stopLocationUpdates() {
await Location.stopLocationUpdatesAsync(LOCATION_UPDATES_TASK);
this.setState({ isTracking: false });
}
clearLocations = async () => {
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify([]));
this.setState({ savedLocations: [] });
};
toggleTracking = async () => {
await AsyncStorage.removeItem(STORAGE_KEY);
if (this.state.isTracking) {
await this.stopLocationUpdates();
} else {
await this.startLocationUpdates();
}
this.setState({ savedLocations: [] });
};
onAccuracyChange = () => {
const next = Location.Accuracy[this.state.accuracy + 1];
const accuracy = next ? Location.Accuracy[next] : Location.Accuracy.Lowest;
this.setState({ accuracy });
if (this.state.isTracking) {
// Restart background task with the new accuracy.
this.startLocationUpdates(accuracy);
}
};
toggleLocationIndicator = async () => {
const showsBackgroundLocationIndicator = !this.state.showsBackgroundLocationIndicator;
this.setState({ showsBackgroundLocationIndicator }, async () => {
if (this.state.isTracking) {
await this.startLocationUpdates();
}
});
};
onCenterMap = async () => {
const { coords } = await Location.getCurrentPositionAsync();
const mapView = this.mapViewRef.current;
if (mapView) {
mapView.animateToRegion({
latitude: coords.latitude,
longitude: coords.longitude,
latitudeDelta: 0.004,
longitudeDelta: 0.002,
});
}
};
renderPolyline() {
const { savedLocations } = this.state;
if (savedLocations.length === 0) {
return null;
}
return (
<MapView.Polyline
coordinates={savedLocations}
strokeWidth={3}
strokeColor={"black"}
/>
);
}
render() {
if (this.state.error) {
return <Text style={styles.errorText}>{this.state.error}</Text>;
}
if (!this.state.initialRegion) {
return <NavigationEvents onDidFocus={this.didFocus} />;
}
return (
<View style={styles.screen}>
<MapView
ref={this.mapViewRef}
style={styles.mapView}
initialRegion={this.state.initialRegion}
showsUserLocation>
{this.renderPolyline()}
</MapView>
<View style={styles.buttons} pointerEvents="box-none">
<View style={styles.topButtons}>
<View style={styles.buttonsColumn}>
{Platform.OS === 'android' ? null : (
<Button style={styles.button} onPress={this.toggleLocationIndicator} title="background/indicator">
<Text>{this.state.showsBackgroundLocationIndicator ? 'Hide' : 'Show'}</Text>
<Text> background </Text>
<FontAwesome name="location-arrow" size={20} color="white" />
<Text> indicator</Text>
</Button>
)}
</View>
<View style={styles.buttonsColumn}>
<Button style={styles.button} onPress={this.onCenterMap} title="my location">
<MaterialIcons name="my-location" size={20} color="white" />
</Button>
</View>
</View>
<View style={styles.bottomButtons}>
<Button style={styles.button} onPress={this.clearLocations} title="clear locations">
Clear locations
</Button>
<Button style={styles.button} onPress={this.toggleTracking} title="start-stop tracking">
{this.state.isTracking ? 'Stop tracking' : 'Start tracking'}
</Button>
</View>
</View>
</View>
);
}
}
async function getSavedLocations() {
try {
const item = await AsyncStorage.getItem(STORAGE_KEY);
return item ? JSON.parse(item) : [];
} catch (e) {
return [];
}
}
if (Platform.OS !== 'android') {
TaskManager.defineTask(LOCATION_UPDATES_TASK, async ({ data: { locations } }) => {
if (locations && locations.length > 0) {
const savedLocations = await getSavedLocations();
const newLocations = locations.map(({ coords }) => ({
latitude: coords.latitude,
longitude: coords.longitude,
}));
savedLocations.push(...newLocations);
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(savedLocations));
locationEventsEmitter.emit('update', savedLocations);
}
});
}
const styles = StyleSheet.create({
screen: {
flex: 1,
},
mapView: {
flex: 1,
},
buttons: {
flex: 1,
flexDirection: 'column',
justifyContent: 'space-between',
padding: 10,
position: 'absolute',
top: 0,
right: 0,
bottom: 0,
left: 0,
},
topButtons: {
flexDirection: 'row',
justifyContent: 'space-between',
},
bottomButtons: {
flexDirection: 'column',
alignItems: 'flex-end',
},
buttonsColumn: {
flexDirection: 'column',
alignItems: 'flex-start',
},
button: {
paddingVertical: 5,
paddingHorizontal: 10,
marginVertical: 5,
},
errorText: {
fontSize: 15,
color: 'rgba(0,0,0,0.7)',
margin: 20,
},
});
If you know any way to easily complete my target (of sending simple HTTP GET with location from background of Expo + RN app to my DRF backend) please let me know.
If you're using Expo you can simply use expo-task-manager and expo-location to get background location updates.
Here's a simplified version that I'm using (and it's working for sure on Android) on the App I'm currently developing:
import * as Location from 'expo-location';
import * as TaskManager from 'expo-task-manager';
import axios from 'axios';
const TASK_FETCH_LOCATION = 'TASK_FETCH_LOCATION';
// 1 define the task passing its name and a callback that will be called whenever the location changes
TaskManager.defineTask(TASK_FETCH_LOCATION, async ({ data: { locations }, error }) => {
if (error) {
console.error(error);
return;
}
const [location] = locations;
try {
const url = `https://<your-api-endpoint>`;
await axios.post(url, { location }); // you should use post instead of get to persist data on the backend
} catch (err) {
console.error(err);
}
});
// 2 start the task
Location.startLocationUpdatesAsync(TASK_FETCH_LOCATION, {
accuracy: Location.Accuracy.Highest,
distanceInterval: 1, // minimum change (in meters) betweens updates
deferredUpdatesInterval: 1000, // minimum interval (in milliseconds) between updates
// foregroundService is how you get the task to be updated as often as would be if the app was open
foregroundService: {
notificationTitle: 'Using your location',
notificationBody: 'To turn off, go back to the app and switch something off.',
},
});
// 3 when you're done, stop it
Location.hasStartedLocationUpdatesAsync(TASK_FETCH_LOCATION).then((value) => {
if (value) {
Location.stopLocationUpdatesAsync(TASK_FETCH_LOCATION);
}
});
It doesn't necessarily work with Expo, but if "eject" your project or start with the React Native CLI (via react-native init) then you could use an Android specific React Native "NativeModule" to accomplish your goal. I like using the react-native-location package, which has great support on iOS for background location updates, but on Android there is a bug currently. I put together an example project which has the necessary Android specific code inside a NativeModule you could use to start from:
https://github.com/andersryanc/ReactNative-LocationSample
How to remove network request failed error screen and display message "No internet connection" for better user experience in react-native when there is no internet connection.
You can use NetInfo in React-Native to check network state. This is link:
https://facebook.github.io/react-native/docs/netinfo.html
This is example code:
NetInfo.getConnectionInfo().then((connectionInfo) => {
if (connectionInfo.type === 'none') {
alert("No internet connection")
} else {
// online
// do something
}
});
Here i wrote an component for handling internet status issues refer this:
import React, { Component } from "react";
import {
View,
NetInfo,
Animated,
Easing,
Dimensions,
Platform,
AppState
} from "react-native";
// import { colors, Typography, primaryFont } from "../../Config/StylesConfig";
import { connect } from "react-redux";
import { changeConnectionStatus } from "../../actions/authActions";
import CustomText from "./CustomText";
import firebaseHelper from "../../helpers/firebaseHelper";
const { width } = Dimensions.get("window");
class InternetStatusBar extends Component {
constructor(props) {
super(props);
this.state = {
isNetworkConnected: true
};
this._updateConnectionStatus = this._updateConnectionStatus.bind(this);
this.positionValue = new Animated.Value(-26);
this.colorValue = new Animated.Value(0);
this.isMount = true;
this.isOnline = true;
}
_handleAppStateChange = nextAppState => {
if (nextAppState.match(/inactive|background/) && this.isOnline) {
firebaseHelper.goOffline();
// console.log("offline");
this.isOnline = false;
} else if (nextAppState === "active" && this.isOnline === false) {
firebaseHelper.goOnline();
// console.log("online");
this.isOnline = true;
}
};
componentDidMount() {
AppState.addEventListener("change", this._handleAppStateChange);
// NetInfo.isConnected.fetch().done(isNetworkConnected => {
// this._updateConnectionStatus(isNetworkConnected);
// });
NetInfo.isConnected.addEventListener(
"connectionChange",
this._updateConnectionStatus
);
}
componentWillUnmount() {
AppState.removeEventListener("change", this._handleAppStateChange);
NetInfo.isConnected.removeEventListener(
"connectionChange",
this._updateConnectionStatus
);
}
_updateConnectionStatus(isNetworkConnected) {
// this.setState({ isNetworkConnected });
if (this.isMount) {
this.isMount = false;
} else {
if (isNetworkConnected) {
this.animateColorChange(isNetworkConnected);
setTimeout(() => {
this.animateErrorView(isNetworkConnected);
}, 1000);
} else {
this.animateErrorView(isNetworkConnected);
this.colorValue = new Animated.Value(0);
}
}
// this.props.changeConnectionStatus(isNetworkConnected);
}
// componentWillReceiveProps = nextProps => {
// if (
// nextProps.isInternetConnected &&
// nextProps.isInternetConnected != this.state.isInternetConnected
// ) {
// const date = new Date();
// Actions.refresh({ refreshContent: date.getTime() });
// }
// };
animateErrorView(connected) {
Animated.timing(this.positionValue, {
toValue: connected ? -40 : Platform.OS === "ios" ? 20 : 0,
easing: Easing.linear,
duration: 600
}).start();
}
animateColorChange(connected) {
Animated.timing(this.colorValue, {
toValue: connected ? 150 : 0,
duration: 800
}).start();
}
render() {
return (
<Animated.View
style={[
{
position: "absolute",
backgroundColor: this.colorValue.interpolate({
inputRange: [0, 150],
outputRange: ["rgba(0,0,0,0.6)", "rgba(75, 181, 67, 0.8)"]
}),
zIndex: 1,
width: width,
top: 0,
transform: [{ translateY: this.positionValue }]
}
]}
>
<View
style={[
{
padding: 4,
flexDirection: "row",
flex: 1
}
]}
>
<CustomText
style={{
fontSize: 12,
textAlign: "center",
flex: 1
}}
>
{this.state.isInternetConnected ? "Back online" : "No connection"}
</CustomText>
</View>
</Animated.View>
);
}
}
const mapStateToProps = state => {
return {
isInternetConnected: state.user.isInternetConnected
};
};
export default connect(mapStateToProps, { changeConnectionStatus })(
InternetStatusBar
);
A Snackbar is a good way to convey this. Have a look at this library :
https://github.com/9gag-open-source/react-native-snackbar-dialog
Easy and Simple with good user experience
use "react-native-offline-status"
Reference:
https://github.com/rgabs/react-native-offline-status
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>
);
}