I am developing an app That contain an image uploader. I found some code on internet that can upload image. And it worked partially. Which means When i press the choose image button, it opens gallery and i am able to select and crop image. And after cropping when i select ok nothing happen. It just show the image and its not uploading to the firebase cloud. Here is my code.
import React, { Component } from 'react';
import {
AppRegistry,
StyleSheet,
Text,
View,
Button,
Image,
ActivityIndicator,
TouchableOpacity
} from 'react-native';
import * as firebase from 'firebase';
import RNFetchBlob from 'react-native-fetch-blob';
import ImagePicker from 'react-native-image-crop-picker';
export default class RNF extends Component {
constructor (props) {
super(props);
this.state = {
loading: false,
dp: null
};
}
openPicker () {
this.setState({ loading: true });
const Blob = RNFetchBlob.polyfill.Blob;
const fs = RNFetchBlob.fs;
window.XMLHttpRequest = RNFetchBlob.polyfill.XMLHttpRequest;
window.Blob = Blob;
// const { uid } = this.state.user
const uid = '12345';
ImagePicker.openPicker({
width: 300,
height: 300,
cropping: true,
mediaType: 'photo'
}).then(image => {
const imagePath = image.path;
let uploadBlob = null;
const imageRef = firebase.storage().ref(uid).child('dp.jpg');
let mime = 'image/jpg';
fs.readFile(imagePath, 'base64')
.then((data) => {
// console.log(data);
return Blob.build(data, { type: `${mime};BASE64` });
})
.then((blob) => {
uploadBlob = blob;
return imageRef.put(blob, { contentType: mime });
})
.then(() => {
uploadBlob.close();
return imageRef.getDownloadURL();
})
.then((url) => {
let userData = {};
// userData[dpNo] = url
// firebase.database().ref('users').child(uid).update({ ...userData})
let obj = {};
obj['loading'] = false;
obj['dp'] = url;
this.setState(obj);
})
.catch((error) => {
console.log(error);
});
})
.catch((error) => {
console.log(error);
});
}
render () {
const dpr = this.state.dp ? (<TouchableOpacity onPress={ () => this.openPicker() }><Image
style={{ width: 100, height: 100, margin: 5 }}
source={{ uri: this.state.dp }}
/></TouchableOpacity>) : (<Button
onPress={ () => this.openPicker() }
title={ 'Change Picture' }
/>);
const dps = this.state.loading ? <ActivityIndicator animating={this.state.loading} /> : (<View style={styles.container}>
<View style={{ flexDirection: 'row' }}>
{ dpr }
</View>
</View>);
return (
<View style={styles.container}>
{ dps }
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF'
},
welcome: {
fontSize: 20,
textAlign: 'center',
margin: 10
},
instructions: {
textAlign: 'center',
color: '#333333',
marginBottom: 5
}
});
AppRegistry.registerComponent('RNF', () => RNF);
Related
I am using react-native-call-detection package to save incoming call number (and send it on server later), I also want to reject/answer the incoming call based on the pressed button (based on server response later).
what package should I use to do it? I just found react-native-call-keep but all examples gave fake phone number to the functons and I don't know how to use its functions or how to get my call uuid.I just know there is reject/answer call function and I should call addEventListener functions before calling functions.
here is my current code:
import React, {useEffect, useState} from 'react';
import RNCallKeep from 'react-native-callkeep';
import {
View,
Text,
StyleSheet,
TouchableOpacity,
TouchableHighlight,
PermissionsAndroid,
} from 'react-native';
import CallDetectorManager from 'react-native-call-detection';
import RNCallKeep from 'react-native-callkeep';
export default class MasterScreen extends React.Component {
constructor(props) {
super(props);
this.state = {
featureOn: false,
incoming: false,
number: null,
};
}
componentDidMount() {
this.askPermission();
this.startListenerTapped();
this.setupCallKeep();
}
setupCallKeep() {
const options = {
android: {
alertTitle: 'Permissions Required',
alertDescription:
'This application needs to access your phone calling accounts to make calls',
cancelButton: 'Cancel',
okButton: 'ok',
imageName: 'ic_launcher',
additionalPermissions: [PermissionsAndroid.PERMISSIONS.READ_CONTACTS],
},
};
try {
RNCallKeep.setup(options);
RNCallKeep.setAvailable(true); // Only used for Android, see doc above.
} catch (err) {
console.error('initializeCallKeep error:', err.message);
}
}
askPermission = async () => {
try {
const permissions = await PermissionsAndroid.requestMultiple([
PermissionsAndroid.PERMISSIONS.READ_CALL_LOG,
PermissionsAndroid.PERMISSIONS.READ_PHONE_STATE,
]);
console.log('Permissions are:', permissions);
} catch (err) {
console.warn(err);
}
};
startListenerTapped = () => {
this.setState({featureOn: true});
this.callDetector = new CallDetectorManager(
(event, phoneNumber) => {
console.log(event);
if (event === 'Disconnected') {
this.setState({incoming: false, number: null});
} else if (event === 'Incoming') {
this.setState({incoming: true, number: phoneNumber});
} else if (event === 'Offhook') {
this.setState({incoming: true, number: phoneNumber});
} else if (event === 'Missed') {
this.setState({incoming: false, number: null});
}
},
true,
() => {},
{
title: 'Phone State Permission',
message:
'This app needs access to your phone state in order to react and/or to adapt to incoming calls.',
},
);
};
stopListenerTapped = () => {
this.setState({featureOn: false});
this.callDetector && this.callDetector.dispose();
};
render() {
return (
<View style={styles.body}>
<Text>incoming call number: {this.state.number}</Text>
<TouchableOpacity
onPress={/*what to do */} style={{
width: 200,
height: 200,
justifyContent: 'center',
}}><Text>answer</Text></TouchableOpacity>
<TouchableOpacity
onPress={/*what to do */} style={{
width: 200,
height: 200,
justifyContent: 'center',
}}>
<Text>reject</Text>
</TouchableOpacity>
</View>
);
}
}
const styles = StyleSheet.create({
body: {
backgroundColor: 'honeydew',
justifyContent: 'center',
alignItems: 'center',
flex: 1,
},
text: {
padding: 20,
fontSize: 20,
},
button: {},
});`
Use RNCallKeep.rejectCall. Like so
<TouchableOpacity
onPr ess={() => RNCallKeep.rejectCall(uuid)}
st yle={{
width: 200,
height: 200,
justifyContent: 'center',
}}
>
<Text>reject</Text>
</TouchableOpacity>
I have a problem with expo-google-app-auth in Android. It's work perfectly fine with IOS.
After successful sign in in Android instead of redirect me back to my app LoggedInPage component, I'm again in LoginPage component. I think it's because Android opens application again and I'm losing the state, so I'm also losing the states from login and I have to sign in again...
In IOS it just sign me in an redirecting to LoggedInPage component perfectly..
import React, { useState } from "react";
import * as Google from "expo-google-app-auth";
import { StyleSheet, Text, View, Image, Button } from "react-native";
export default function App() {
const [signedIn, setSignIn] = useState(false);
const [name, setName] = useState("");
const [photoUrl, setPhotoUrl] = useState("");
signIn = async () => {
try {
console.log("Sign in 1");
const result = await Google.logInAsync({
androidClientId:
“<< android client id >>apps.googleusercontent.com",
iosClientId:
“<< iOS client id >>.apps.googleusercontent.com",
scopes: ["profile", "email"]
});
console.log("Sign in 2");
console.log("Result: ", result);
if (result.type === "success") {
setSignIn(true);
setName(result.user.name);
setPhotoUrl(result.user.photoUrl);
return result.accessToken;
} else {
return { cancelled: true };
}
} catch (e) {
return { error: true };
}
};
const LoginPage = () => {
console.log("Inside LoginPage");
return (
<View>
<Text style={styles.header}>Sign In With Google</Text>
<Button title="Sign in with Google" onPress={() => signIn()} />
</View>
);
};
const LoggedInPage = () => {
return (
<View style={styles.container}>
<Text style={styles.header}>Welcome:{name}</Text>
<Image style={styles.image} source={{ uri: photoUrl }} />
</View>
);
};
console.log("SignedIn: ", signedIn);
return (
<View style={styles.container}>
{signedIn ? <LoggedInPage /> : <LoginPage />}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center"
},
header: {
fontSize: 25
},
image: {
marginTop: 15,
width: 150,
height: 150,
borderColor: "rgba(0,0,0,0.2)",
borderWidth: 3,
borderRadius: 150
}
});
Any ideas/guidance how should I fix my app in Android ?
You need to add a redirectUrl:
const result = await Google.logInAsync({
androidClientId:
“<< android client id >>apps.googleusercontent.com",
iosClientId:
“<< iOS client id >>.apps.googleusercontent.com",
scopes: ["profile", "email"],
redirectUrl: "{Your Bundle ID (com.example.app)}:/oauth2redirect/google",
});
I’m trying to make a prototype application that over and over
1- record a video with the camera for x seconds
2- displays this video
For this I use the components Camera from expo-camera and Video from expo-av
For this I have two views :
I use in my code the stateSequence property and the sequencer() function which displays alternately the view with the Camera component which films for x seconds , and the video view which allows me to display the video.
Sequencer() is triggered with setInterval( this.sequencer , 10000) found in the componentWillMount()
I can switch alternately from a View with the Camera component to a View with the Video component.
To record a video with the Camera component I use recordAsync(), but I get the following error:
Unhandled promise rejection: Error: Camera is not running
I’m using an android phone for my tests.
Can’t you help me
this is my code
import { StyleSheet, Text, View ,TouchableOpacity} from 'react-native';
import * as Permissions from 'expo-permissions';
import { Camera } from 'expo-camera';
import { Video } from 'expo-av';
import { Logs } from 'expo';
export default class SequenceViewer extends Component {
constructor(props) {
super(props);
this.state = {
stateSequence: "SHOOT ",
hasCameraPermission: null,
type: Camera.Constants.Type.front,
}
this.recordVideo = this.recordVideo.bind(this)
}
sequencer = () => {
if(this.state.stateSequence==="WATCH"){
this.setState({ stateSequence: "SHOOT",})
this.recordVideo(); // Error message Camera is not running
} else {
this.setState({stateSequence: "WATCH"})
}
}
async componentWillMount() {
let rollStatus = await Permissions.askAsync(Permissions.CAMERA_ROLL);
let cameraResponse = await Permissions.askAsync(Permissions.CAMERA)
if (rollStatus.status=='granted'){
if (cameraResponse.status == 'granted' ){
let audioResponse = await Permissions.askAsync(Permissions.AUDIO_RECORDING);
if (audioResponse.status == 'granted'){
this.setState({ permissionsGranted: true });
setInterval( this.sequencer , 10000);
}
}
}
}
recordVideo = async () => {
if(this.state.cameraIsRecording){
this.setState({cameraIsRecording:false})
this.camera.stopRecording();
}
else {
this.setState({cameraIsRecording:true})
if (this.camera) {
let record = await this.camera.recordAsync(quality='480p',maxDuration=5,mute=true).then( data =>{
this.setState( {recVideoUri :data.uri})
})
}
}
};
render() {
const { hasCameraPermission } = this.state
if(this.state.stateSequence=="WATCH")
{
return(
<View style={styles.container}>
<Video
source={{ uri:this.state.recVideoUri }}
rate={1.0}
volume={1.0}
isMuted={false}
resizeMode="cover"
shouldPlay
isLooping
style={{ width: 300, height: 300 }}
ref={(ref) => { this.player = ref }}
/>
</View>
)
} else
{
return(
<View style={{ flex: 1 }}>
<Camera style={{ flex: 1 }} type={this.state.type} ref={ref => {this.camera = ref; }}></Camera>
</View>
)
}
}
}
const styles = StyleSheet.create({
viewerText: {
fontSize: 20,
fontWeight: 'bold',
},
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});
Thank you
I had the same problem, my solution was by default camera type have to be "back" and you could change to "front" by:
componentDidMount = () => {
this.props.navigation.addListener('didFocus', async () => {
await setTimeout(() => {
this.setState({ cameraType: Camera.Constants.Type.front })
}, 100)
});
}
I was getting the "Camera is not running" error when i changed screens. So for functional components instead of withNavigationFocus(Camera) use this method:
import { useIsFocused } from '#react-navigation/native';
function MyComponent() {
const isFocused = useIsFocused()
return (
<View>
{ isFocused && <RNCamera /> }
</View>
)
}
I have requirement where i need to get a random image every time when i click an button. I don't want picker to come up for the camera-roll with images, instead random image should selected from the camera folder and display in the image view.
I have followed the official FB tutorial of camera roll. Please find the code as below
_handleButtonPress = () => {
CameraRoll.getPhotos({
first: 20,
assetType: 'Photos',
})
.then(r => {
this.setState({ photos: r.edges });
})
.catch((err) => {
});
};
But this code will select the recently clicked images and display in the picker. Instead of to randomly select the uri of the image and display in the imageview. Any help is appreciated.
Regards,
Sharath
You essentially have the photos and all the necessary metadata once you set the state: this.setState({ photos: r.edges })
All you have to do is pick a random image from there. Here's how I did it:
import React, { Component } from 'react';
import {
StyleSheet,
View,
Image,
CameraRoll,
Button
} from 'react-native';
export default class App extends Component {
constructor(props) {
super(props)
this.state = {
img: null
}
}
getRandomImage = () => {
const fetchParams = {
first: 25,
}
CameraRoll.getPhotos(fetchParams)
.then(data => {
const assets = data.edges
const images = assets.map((asset) => asset.node.image)
const random = Math.floor(Math.random() * images.length)
this.setState({
img: images[random]
})
})
.catch(err => console.log)
}
render() {
return (
<View style={styles.container}>
{ this.state.img ?
<Image
style={styles.image}
source={{ uri: this.state.img.uri }}
/>
: null
}
<Button title="Get Random Image from CameraRoll" onPress={this.getRandomImage}/>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
image: {
width: '100%',
height: '75%',
margin: 10,
}
});
Greeting everyone,
I am working on react-native FlatList and my problem is that I am unable to scroll to bottom. Here is my FlatList code:
<FlatList style = { styles.list }
ref="flatList1"
data = {
this.state.conversation_arr
}
onContentSizeChange={(contentWidth, contentHeight) => {
this.flat_list_height = contentHeight;
// this.scrollNow();
}}
onLayout={this.scrollNow()}
renderItem = { this._renderItem }
keyExtractor = { item => item.message_id }
/>
scrollNow() {
console.log("******",this.refs);
if(this.refs.flatList1){
this.refs.flatList1.scrollToEnd();
}
}
When the code reaches to
this.refs.flatList1.scrollToEnd();
Error appears on the screen saying
Type Error, cannot read property -1 of undefined.
here is the screenshot.
cannot read property -1 of undefined
I am basically trying to implement chat feature for my application, and therefor i need to scroll to bottom of the list once chat is loaded.
Any kind of help is appreciated.
Thanks
COMPLETE CODE
import React, {
Component
} from 'react';
import {
StyleSheet,
View,
Text,
TextInput,
ActivityIndicator,
TouchableOpacity,
FlatList,
} from 'react-native';
export default class Conversation extends Component {
state = {
conversation_arr: undefined,
last_message_loaded: false,
show_activty_indicator: true,
message_text : undefined,
message_text_length : 0
};
params = this.props.navigation.state;
last_evaluated_key = {
message_id: undefined,
timestamp: undefined
}
conversation = undefined;
thread_info = undefined;
flat_list_height = 0;
static navigationOptions = ({ navigation }) => ({
title: navigation.state.params.header_title,
});
constructor(props) {
super(props);
this._renderItem = this._renderItem.bind(this);
this._fetchConversation();
this.scrollNow = this.scrollNow.bind(this);
this.handleInputTextContent = this.handleInputTextContent.bind(this);
this.sendMessage = this.sendMessage.bind(this);
console.disableYellowBox = true;
this.thread_info = this.params.params.thread_info;
}
// scrollNow() {
// console.log("******",this.refs);
// if(this.refs.flatList1){
// // debugger;
// this.refs.flatList1.scrollToEnd();
// }
// console.log("******", this.flatlist1);
// if(this.flatlist1){
// this.flatlist1.scrollToEnd();
// }
// console.log("###", this.flat_list_height);
// }
scrollNow = () => {
console.log("******", this.flatList1);
if(this.state.conversation_arr && this.state.conversation_arr.length > 0 && this.flatList1){
// debugger;
// setTimeout(() => {
// console.log("timer over now");
this.flatList1.scrollToEnd();
// }, 1000)
// this.flatList1.scrollToEnd();
console.log("Scrolling now at length", this.state.conversation_arr.length);
}
// !!this.refs["myFlatList"] && this.refs["myFlatList"].scrollToEnd()
}
componentDidUpdate(){
console.log("componentDidUpdatecomponentDidUpdate")
}
componentWillUnmount(){
console.log("componentWillUnmountv")
}
render() {
return (
<View style={ styles.container }>
{
this.state.show_activty_indicator &&
<View style={[styles.activity_indicator_container]}>
<ActivityIndicator
animating = { this.state.show_activty_indicator }
size="large"
color="#444444"
/>
</View>
}
<FlatList style = { styles.list }
// inverted
// ref="myFlatList"
// ref={(ref) => this.flatList1 = ref}
ref={(ref) => { this.flatList1 = ref; }}
data = {
this.state.conversation_arr
}
onContentSizeChange={(contentWidth, contentHeight) => {
this.flat_list_height = contentHeight;
// this.scrollNow();
}}
onLayout={this.scrollNow}
renderItem = { this._renderItem }
keyExtractor = { item => item.message_id }
/>
<View style={ styles.bottom_box }>
<Text style={ styles.message_length }> {this.state.message_text_length}/160 </Text>
<View style={ styles.message_box }>
<View style={{flex: 3}} >
<TextInput
style={styles.input_text}
value={this.state.message_text}
onChangeText={this.handleInputTextContent}
placeholder='Type a message here...'
underlineColorAndroid='transparent'
/>
</View>
<View style={{flex: 1}} >
<TouchableOpacity
style={styles.send_button}
onPress={this.sendMessage} >
<Text style={[styles.label_send]}>SEND</Text>
</TouchableOpacity>
</View>
</View>
</View>
</View>
);
}
handleInputTextContent(text){
if(text.length > 160){
text = text.substring(0,159)
this.setState({ message_text : text });
} else {
this.setState({ message_text : text });
this.setState({ message_text_length : text.length });
}
}
sendMessage(){
console.log(this.state.message_text);
if(this.state.message_text && this.state.message_text.length > 0) {
console.log("Send message now");
}
}
_renderItem(item, index){
// console.log(item.item);
item = item.item;
let view = (
<Text style = { [styles.msg_common] }> { item.message_content } </Text>
)
return view ;
}
processPaymentMessages(dataArr) {
return dataArr;
}
_fetchConversation() {
new WebApi().fetchThreadConversation().then(result => {
console.log("resutlresult", result);
if (result && result.data && result.data.LastEvaluatedKey) {
this.last_evaluated_key.message_id = result.data.LastEvaluatedKey.message_id;
this.last_evaluated_key.timestamp = result.data.LastEvaluatedKey.timestamp;
} else {
this.setState({
last_message_loaded: true
});
this.last_evaluated_key = null;
}
if (!this.conversation) {
this.conversation = [];
}
for (let i = 0; i < result.data.Items.length; i++) {
item = result.data.Items[i];
this.conversation.push(item);
}
// console.log(this.conversation);
this.setState({ conversation_arr : this.conversation });
this.setState({ show_activty_indicator : false });
console.log(this.state.conversation_arr.length);
}, error => {
console.log("web api fetch data error", error);
})
}
}
const styles = StyleSheet.create({
activity_indicator_container: {
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#fff'
},
container: {
flex: 1,
paddingTop: 8,
backgroundColor:"#fff"
},
list : {
paddingLeft: 8,
paddingRight:8,
marginBottom : 85,
},
gray_item: {
padding: 10,
height: 44,
backgroundColor:"#fff"
},
white_item : {
padding: 10,
height: 44,
backgroundColor:"#f5f5f5"
},
msg_common : {
flex: 1,
justifyContent: 'flex-end',
alignItems: 'flex-end',
},
colored_item_common : {
padding : 12,
flex: 1,
justifyContent: 'center',
alignItems: 'center'
},
item_insider_common : {
backgroundColor:"#fff",
fontSize : 16,
padding : 12,
borderWidth : 1,
borderRadius : 8,
width : "90%",
fontWeight : "bold",
textAlign: 'center',
},
purple_item_insider : {
color : "#7986cb",
borderColor: "#7986cb",
},
green_item_insider : {
color : "#66bb6a",
borderColor: "#66bb6a",
},
bottom_box:{
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
flex: 1,
},
message_length: {
height : 20,
textAlign: 'right', alignSelf: 'stretch'
},
message_box: {
// position: 'absolute',
// left: 0,
// right: 0,
// bottom: 0,
flex: 1,
flexDirection: "row"
},
input_text: {
width: '100%',
height: 60,
color: '#000',
backgroundColor:"#eee",
padding: 16,
},
send_button: {
alignItems: 'center',
flex: 1,
height: 60,
padding: 16,
backgroundColor : '#f1592b'
},
label_send: {
color: '#fff'
}
})
You need to give a callback to onLayout like this:
onLayout={this.scrollNow}
The updated code is: I tried it. It works.
import React, { Component } from 'react';
import { FlatList, Text } from 'react-native';
export default class App extends Component {
render() {
return (
<FlatList
ref={(ref) => { this.flatList1 = ref; }}
data={[0, 0, 0, 0, 0, 0, 0, 0]}
onContentSizeChange={(contentWidth, contentHeight) => { this.flat_list_height = contentHeight; }}
onLayout={this.scrollNow}
renderItem={this.renderItem}
keyExtractor={item => item.message_id}
/>
);
}
renderItem =() => <Text style={{marginTop: 100}}>Just Some Text</Text>
scrollNow =() => {
if (this.flatList1) {
this.flatList1.scrollToEnd();
}
}
}
Have you tried passing an empty array when "this.state.conversation_arr" is undefined or null. Checking this._renderItem can be helpful also, weather it is trying to access -1 of any argument.
There are 2 parts wrong in your code.
the ref in FlatList is a function, not a string.
If you don't use arrow function, the FlatList cannot access the "this" in scrollNow() function.
I tested with similar code. The code below should work.
import * as React from 'react';
import { StyleSheet, View, Text, FlatList } from 'react-native';
export default class Conversation extends React.Component {
flatList1;
_interval;
state = { conversation_arr: [] };
componentDidMount() {
let buffer = [];
for (let i = 1; i < 40; i++) {
buffer.push({ message_id: i, message_content: `I'm the message ${i}` });
}
this._interval = setInterval(() => {
this.setState(() => {
return { conversation_arr: buffer };
});
this.flatList1.scrollToIndex({
index: buffer.length - 1,
viewPosition: 1,
viewOffset: 0,
});
}, 1000);
}
componentWillUnmount() {
clearInterval(this._interval);
}
render() {
return (
<View style={styles.container}>
<FlatList
ref={ref => {
this.flatList1 = ref;
}}
data={this.state.conversation_arr}
renderItem={this._renderItem}
keyExtractor={item => item.message_id}
getItemLayout={(data, index) => ({
length: 40,
offset: 40 * index,
index,
})}
/>
</View>
);
}
_renderItem = ({ item }) => {
return (
<View>
<Text style={styles.msg_common}>{item.message_content}</Text>
</View>
);
};
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
msg_common: {
height: 40,
fontSize: 18,
paddingHorizontal: 10,
},
});
PS: If you have a long list, let's say more than 100. Using onLayout to scroll down might take some time for initial rendering, which provides a really bad user experience. The much better approach may be
to reverse the list before rendering
provide a sort button and set onRefresh to true during sorting
provide a scroll to bottom button list's footer
Update:
I noticed the onContentSizeChange property is NOT in the documentation.
https://facebook.github.io/react-native/docs/flatlist.html#scrolltoindex
Which version of React-Native are you using?
Update2:
The complete code in TypeScript based on yours. I use setInterval to simulate the delay from API call.
Any async function should be in componentDidMount, because constructor doesn't wait.
getItemLayout is used to help FlatList to figure out height. Notice that this is set before the data arrived.