React native multi-column FlatList insert banner - react-native

I'm using multi-column FlatList in my React Native application to display items like below (left image). I'm trying to integrate AdMob banner into the application like many other apps did, and insert the ads banner in the middle of the list, like below (right image).
As far as I can tell, FlatList doesn't support this type of layout out-of-the-box. I'm wondering what would be a good practice to implement this feature and doesn't impact app performance.
(Side note, the list supports pull-to-refresh and infinite loading when approaching end of the list.).
Thank you in advance for any suggestions.

In such a case, I always recommend to drop the numColumns property and replace it by a custom render function, which handles the columns by its own.
Let's say we have the following data structure:
const DATA =
[{ id: 1, title: "Item One"}, { id: 2, title: "Item Two"}, { id: 3, title: "Item Three"},
{ id: 4, title: "Item Four"}, { id: 5, title: "Item Five"}, { id: 6, title: "Item Six"},
{ id: 7, title: "Item Seven"}, { id:8, title: "Item Eight"}, { id: 9, title: "Item Nine"},
{ id: 10, title: "Item Ten"}, { id: 11, title: "Item eleven"},
{ id: 12, title: "Item Twelve"}, { id: 13, title: "Item Thirteen"}];
As I said we don't use the numColumns property instead we are restructuring our data so we can render our list how we want. In this case we want to have 3 columns and after six items we want to show an ad banner.
Data Modification:
modifyData(data) {
const numColumns = 3;
const addBannerAfterIndex = 6;
const arr = [];
var tmp = [];
data.forEach((val, index) => {
if (index % numColumns == 0 && index != 0){
arr.push(tmp);
tmp = [];
}
if (index % addBannerAfterIndex == 0 && index != 0){
arr.push([{type: 'banner'}]);
tmp = [];
}
tmp.push(val);
});
arr.push(tmp);
return arr;
}
Now we can render our transformed data:
Main render function:
render() {
const newData = this.modifyData(DATA); // here we can modify the data, this is probably not the spot where you want to trigger the modification
return (
<View style={styles.container}>
<FlatList
data={newData}
renderItem={({item, index})=> this.renderItem(item, index)}
/>
</View>
);
}
RenderItem Function:
I removed some inline styling to make it more clearer.
renderItem(item, index) {
// if we have a banner item we can render it here
if (item[0].type == "banner"){
return (
<View key={index} style={{width: WIDTH-20, flexDirection: 'row'}}>
<Text style={{textAlign: 'center', color: 'white'}}> YOUR AD BANNER COMPONENT CAN BE PLACED HERE HERE </Text>
</View>
)
}
//otherwise we map over our items and render them side by side
const columns = item.map((val, idx) => {
return (
<View style={{width: WIDTH/3-20, justifyContent: 'center', backgroundColor: 'gray', height: 60, marginLeft: 10, marginRight: 10}} key={idx}>
<Text style={{textAlign: 'center'}}> {val.title} </Text>
</View>
)
});
return (
<View key={index} style={{width: WIDTH, flexDirection: 'row', marginBottom: 10}}>
{columns}
</View>
)
}
Output:
Working Example:
https://snack.expo.io/SkmTqWrJS

I'd recommend this beautiful package
https://github.com/Flipkart/recyclerlistview
Actually, we were handling thousands of data list in our app, flatlist was able to handle it quite good, but still we were looking for a high performance listview component to produce a smooth render and memory efficient as well. We stumbled upon this package. Trust me it's great.
Coming to your question, this package has the feature of rendering multiple views out-of-the-box. It has got a good documentation too.
So basically, the package has three important step to setup the listview.
DataProvider - Constructor function the defines the data for each
element
LayoutProvider - Constructor function that defines the layout (height
/ width) of each element
RowRenderer - Just like the renderItem prop in flatlist.
Basic code looks like this:
import React, { Component } from "react";
import { View, Text, Dimensions } from "react-native";
import { RecyclerListView, DataProvider, LayoutProvider } from "recyclerlistview";
const ViewTypes = {
FULL: 0,
HALF_LEFT: 1,
HALF_RIGHT: 2
};
let containerCount = 0;
class CellContainer extends React.Component {
constructor(args) {
super(args);
this._containerId = containerCount++;
}
render() {
return <View {...this.props}>{this.props.children}<Text>Cell Id: {this._containerId}</Text></View>;
}
}
export default class RecycleTestComponent extends React.Component {
constructor(args) {
super(args);
let { width } = Dimensions.get("window");
//Create the data provider and provide method which takes in two rows of data and return if those two are different or not.
let dataProvider = new DataProvider((r1, r2) => {
return r1 !== r2;
});
//Create the layout provider
//First method: Given an index return the type of item e.g ListItemType1, ListItemType2 in case you have variety of items in your list/grid
this._layoutProvider = new LayoutProvider(
index => {
if (index % 3 === 0) {
return ViewTypes.FULL;
} else if (index % 3 === 1) {
return ViewTypes.HALF_LEFT;
} else {
return ViewTypes.HALF_RIGHT;
}
},
(type, dim) => {
switch (type) {
case ViewTypes.HALF_LEFT:
dim.width = width / 2;
dim.height = 160;
break;
case ViewTypes.HALF_RIGHT:
dim.width = width / 2;
dim.height = 160;
break;
case ViewTypes.FULL:
dim.width = width;
dim.height = 140;
break;
default:
dim.width = 0;
dim.height = 0;
}
}
);
this._rowRenderer = this._rowRenderer.bind(this);
//Since component should always render once data has changed, make data provider part of the state
this.state = {
dataProvider: dataProvider.cloneWithRows(this._generateArray(300))
};
}
_generateArray(n) {
let arr = new Array(n);
for (let i = 0; i < n; i++) {
arr[i] = i;
}
return arr;
}
//Given type and data return the view component
_rowRenderer(type, data) {
//You can return any view here, CellContainer has no special significance
switch (type) {
case ViewTypes.HALF_LEFT:
return (
<CellContainer style={styles.containerGridLeft}>
<Text>Data: {data}</Text>
</CellContainer>
);
case ViewTypes.HALF_RIGHT:
return (
<CellContainer style={styles.containerGridRight}>
<Text>Data: {data}</Text>
</CellContainer>
);
case ViewTypes.FULL:
return (
<CellContainer style={styles.container}>
<Text>Data: {data}</Text>
</CellContainer>
);
default:
return null;
}
}
render() {
return <RecyclerListView layoutProvider={this._layoutProvider} dataProvider={this.state.dataProvider} rowRenderer={this._rowRenderer} />;
}
}
const styles = {
container: {
justifyContent: "space-around",
alignItems: "center",
flex: 1,
backgroundColor: "#00a1f1"
},
containerGridLeft: {
justifyContent: "space-around",
alignItems: "center",
flex: 1,
backgroundColor: "#ffbb00"
},
containerGridRight: {
justifyContent: "space-around",
alignItems: "center",
flex: 1,
backgroundColor: "#7cbb00"
}
};
In the LayoutProvider, you can return multiple type of view based on the index or you can add a viewType object in your data array, render views based on that.
this._layoutProvider = new LayoutProvider(
index => {
if (index % 3 === 0) {
return ViewTypes.FULL;
} else if (index % 3 === 1) {
return ViewTypes.HALF_LEFT;
} else {
return ViewTypes.HALF_RIGHT;
}
},
(type, dim) => {
switch (type) {
case ViewTypes.HALF_LEFT:
dim.width = width / 2;
dim.height = 160;
break;
case ViewTypes.HALF_RIGHT:
dim.width = width / 2;
dim.height = 160;
break;
case ViewTypes.FULL:
dim.width = width;
dim.height = 140;
break;
default:
dim.width = 0;
dim.height = 0;
}
}
);
tl;dr: Check the https://github.com/Flipkart/recyclerlistview and use the layoutProvider to render different view.
Run the snack: https://snack.expo.io/B1GYad52b

Related

React Native Table with Row Selection

I am trying to create a React Native screen that allows the user to select which items to send to the server for batch processing.
My thought was to have a table, and allow the user to select the rows they want, then click a button to submit to the server.
I need the state to contain a list of the ids from those rows, so that I can use it to allow the user to send a request with that array of ids.
A mock-up of my code is below, but it doesn't work. When the update of state is in place, I get an error of "selected items is not an object". When it is commented out, of course the state update doesn't work, but it also doesn't set the value of the checkbox from the array if I hard code it in the state initialization (meaning is 70 is in the array, the box is still not checked by default), and it does allow the box to get checked but not unchecked. How do I get it working?
import React, { Component } from 'react';
import { StyleSheet, View } from 'react-native';
import CheckBox from '#react-native-community/checkbox';
import { Table, Row, TableWrapper, Cell } from 'react-native-table-component';
import moment from 'moment';
class FruitGrid extends Component {
constructor(props) {
super(props);
}
state = {
selectedItems : [70],
data: []
};
refresh() {
let rows = [
[69,'David','Apples'],
[70,'Teddy','Oranges'],
[73,'John','Pears']
];
this.setState({data: rows});
}
componentDidMount() {
this.refresh();
}
setSelection(id) {
const { selectedItems } = this.state;
if (id in selectedItems)
{
this.setState({selectedItems: selectedItems.filter(i => i != id)});
}
else
{
this.setState({selectedItems : selectedItems.push(id)});
}
}
render() {
const { selectedItems, data } = this.state;
let columns = ['',
'Person',
'Fruit'];
return (
<View style={{ flex: 1 }}>
<Table borderStyle={{borderWidth: 2, borderColor: '#c8e1ff'}}>
<Row data = {columns} />
{
data.map((rowData, index) =>
(
<TableWrapper key={index} style={styles.row}>
<Cell key={0} data = {<CheckBox value={rowData[0] in selectedItems} onValueChange={this.setSelection(rowData[0])} />} />
<Cell key={1} data = {rowData[1]} textStyle={styles.text}/>
<Cell key={2} data = {rowData[2]} textStyle={styles.text}/>
</TableWrapper>
)
)
}
</Table>
</View>
);
}
}
export default FruitGrid;
const styles = StyleSheet.create({
btn: { width: 58, height: 18, backgroundColor: '#8bbaf2', borderRadius: 2 },
btnText: { textAlign: 'center', color: '#000000' },
text: { margin: 6 },
row: { flexDirection: 'row' },
});
I introduced my own module for this feature from which you will be able to select row easily and much more.
You can use this component like this below
import DataTable, {COL_TYPES} from 'react-native-datatable-component';
const SomeCom = () => {
//You can pass COL_TYPES.CHECK_BOX Column's value in true/false, by default it will be false means checkBox will be uncheck!
const data = [
{ menu: 'Chicken Biryani', select: false }, //If user select this row then this whole object will return to you with select true in this case
{ menu: 'Chiken koofta', select: true },
{ menu: 'Chicken sharwma', select: false }
]
const nameOfCols = ['menu', 'select'];
return(
<DataTable
onRowSelect={(row) => {console.log('ROW => ',row)}}
data={data}
colNames={nameOfCols}
colSettings={[{name: 'select', type: COL_TYPES.CHECK_BOX}]}
/>
)
}
export default SomeCom;
React Native DataTable Component
Thanks to a friend, I found 3 issues in the code that were causing the problem:
When checking to see if the array contains the object, I first need to check that the array is an array and contains items. New check (wrapped in a function for reuse):
checkIfChecked(id, selectedItems)
{
return selectedItems?.length && selectedItems.includes(id);
}
The state update was modifying the state without copying. New state update function:
setSelection(id) {
const { selectedItems } = this.state;
if (this.checkIfChecked(id,selectedItems))
{
this.setState({selectedItems: selectedItems.filter(i => i != id)});
}
else
{
let selectedItemsCopy = [...selectedItems]
selectedItemsCopy.push(id)
this.setState({selectedItems : selectedItemsCopy});
}
}
The onValueChange needed ()=> to prevent immediate triggering, which lead to a "Maximum Depth Reached" error. New version
onValueChange={()=>this.setSelection(rowData[0])} />}
The full working code is here:
import React, { Component } from 'react';
import { StyleSheet, View } from 'react-native';
import CheckBox from '#react-native-community/checkbox';
import { Table, Row, TableWrapper, Cell } from 'react-native-table-component';
import moment from 'moment';
class FruitGrid extends Component {
constructor(props) {
super(props);
}
state = {
selectedItems : [],
data: []
};
refresh() {
let rows = [
[69,'David','Apples'],
[70,'Teddy','Oranges'],
[73,'John','Pears']
];
this.setState({data: rows});
}
componentDidMount() {
this.refresh();
}
setSelection(id) {
const { selectedItems } = this.state;
if (this.checkIfChecked(id,selectedItems))
{
this.setState({selectedItems: selectedItems.filter(i => i != id)});
}
else
{
let selectedItemsCopy = [...selectedItems]
selectedItemsCopy.push(id)
this.setState({selectedItems : selectedItemsCopy});
}
}
checkIfChecked(id, selectedItems)
{
return selectedItems?.length && selectedItems.includes(id);
}
render() {
const { selectedItems, data } = this.state;
let columns = ['',
'Person',
'Fruit'];
return (
<View style={{ flex: 1 }}>
<Table borderStyle={{borderWidth: 2, borderColor: '#c8e1ff'}}>
<Row data = {columns} />
{
data.map((rowData, index) =>
(
<TableWrapper key={index} style={styles.row}>
<Cell key={0} data = {<CheckBox value={this.checkIfChecked(rowData[0],selectedItems)} onValueChange={()=>this.setSelection(rowData[0])} />} />
<Cell key={1} data = {rowData[1]} textStyle={styles.text}/>
<Cell key={2} data = {rowData[2]} textStyle={styles.text}/>
</TableWrapper>
)
)
}
</Table>
</View>
);
}
}
export default FruitGrid;
const styles = StyleSheet.create({
btn: { width: 58, height: 18, backgroundColor: '#8bbaf2', borderRadius: 2 },
btnText: { textAlign: 'center', color: '#000000' },
text: { margin: 6 },
row: { flexDirection: 'row' },
});

React Native Flatlist custom refresh control with Lottie is glitchy

I'm trying to implement a custom refresher animation to my app and im almost there. It just seems like i can't fix this glitch but I know it's possible because the native RefreshControl is so smooth. Here is a video of what I currently have: https://youtu.be/4lMn2sVXBAM
In this video, you can see how it scrolls past where it's supposed to stop once you release the flatlist and then it jumps back to where it's meant to stop. I just want it to be smooth and not go past the necessary stop so it doesn't look glitchy. Here is my code:
#inject('store')
#observer
class VolesFeedScreen extends Component<HomeFeed> {
#observable voleData:any = [];
#observable newVoleData:any = [1]; // needs to have length in the beginning
#observable error:any = null;
#observable lastVoleTimestamp: any = null;
#observable loadingMoreRefreshing: boolean = false;
#observable refreshing:boolean = true;
#observable lottieViewRef: any = React.createRef();
#observable flatListRef: any = React.createRef();
#observable offsetY: number = 0;
#observable animationSpeed: any = 0;
#observable extraPaddingTop: any = 0;
async componentDidMount(){
this.animationSpeed = 1;
this.lottieViewRef.current.play();
this.voleData = await getVoles();
if(this.voleData.length > 0){
this.lastVoleTimestamp = this.voleData[this.voleData.length - 1].createdAt;
}
this.animationSpeed = 0;
this.lottieViewRef.current.reset();
this.refreshing = false;
}
_renderItem = ({item}:any) => (
<VoleCard
voleId={item.voleId}
profileImageURL={item.userImageUrl}
userHandle={item.userHandle}
userId={item.userId}
voteCountDictionary={item.votingOptionsDictionary}
userVoteOption={item.userVoteOption}
shareStat={item.shareCount}
description={item.voleDescription}
imageUrl={item.imageUrl.length > 0 ? item.imageUrl[0] : null} //only one image for now
videoUrl={item.videoUrl.length > 0 ? item.videoUrl[0] : null} //only one video for now
time={convertTime(item.createdAt)}
key={JSON.stringify(item)}
/>
);
onScroll(event:any) {
const { nativeEvent } = event;
const { contentOffset } = nativeEvent;
const { y } = contentOffset;
this.offsetY = y;
if(y < -45){
this.animationSpeed = 1;
this.lottieViewRef.current.play();
}
}
onRelease = async () => {
if (this.offsetY <= -45 && !this.refreshing) {
this.flatListRef.current.scrollToOffset({ animated: false, offset: -40 });
hapticFeedback();
this.extraPaddingTop = 40;
this.newVoleData = [1];
this.refreshing = true;
this.animationSpeed = 1;
this.lottieViewRef.current.play();
this.voleData = await getVoles();
this.extraPaddingTop = 0;
this.animationSpeed = 0;
this.lottieViewRef.current.reset();
if(this.voleData.length > 0){
this.lastVoleTimestamp = this.voleData[this.voleData.length - 1].createdAt;
}
this.refreshing = false;
}
}
render() {
return (
<View>
<LottieView
style={styles.lottieView}
ref={this.lottieViewRef}
source={require('../../../assets/loadingDots.json')}
speed={this.animationSpeed}
/>
<FlatList
contentContainerStyle={styles.mainContainer}
data={this.voleData}
renderItem={this._renderItem}
onScroll={(evt) => this.onScroll(evt)}
scrollEnabled={!this.refreshing}
ref={this.flatListRef}
onResponderRelease={this.onRelease}
ListHeaderComponent={(
<View style={{ paddingTop: this.extraPaddingTop}}/>
)}
ListFooterComponent={this.loadingMoreRefreshing ? <ActivityIndicator style={styles.footer}/> : this.refreshing ? null : <Text style={styles.noMoreVolesText}>No more voles</Text>}
onEndReached={this.newVoleData.length > 0 ? this.loadMoreVoles : null}
onEndReachedThreshold={2}
keyExtractor={item => item.voleId}
/>
<StatusBar barStyle={'dark-content'}/>
</View>
)
}
}
const styles = StyleSheet.create({
mainContainer:{
marginTop: 6,
marginLeft: 6,
marginRight: 6,
},
footer:{
marginBottom: 10,
alignSelf:'center'
},
noMoreVolesText:{
fontSize: 10,
marginBottom: 10,
alignSelf:'center',
color:'#000000',
opacity: .5,
},
lottieView: {
height: 50,
position: 'absolute',
top:0,
left: 0,
right: 0,
},
});
export default VolesFeedScreen;
What i think it's doing is that once on let go of the flatlist, it bounces up past the stop point, which is at an offset of -40 above, before the function onRelease starts. Any help is appreciated!

How to save React Native phone sensor data to an array without using state arrays (getting Maximum update depth exceeded)?

I'm having some trouble figuring out how to handle sensor data in React Native. My goal is to get phone sensor data for a limited time (let's say 10 seconds) and after completion save the list obtained to local database. However pushing new gyroscope value to state array is one option but getting maximum update depth exceeded error in there. The code is below.
import React from 'react';
import {
StyleSheet,
View,
Text,
Dimensions,
} from 'react-native';
import { Gyroscope } from 'expo-sensors';
import { Button } from "native-base";
import CountDown from 'react-native-countdown-component';
import UUID from 'pure-uuid';
import {insertGyroData} from './measureService';
const SAMPLING_RATE = 20; // defaults to 20ms
export class Measure extends React.Component {
state = {
gyroscopeData: {},
measuringStarted: false,
x_gyro: [0],
y_gyro: [0],
z_gyro: [0]
};
componentDidMount() {
this.toggleGyroscope();
}
componentWillUnmount() {
this.unsubscribeAccelerometer();
}
toggleGyroscope = () => {
if (this.gyroscopeSubscription) {
this.unsubscribeAccelerometer();
} else {
this.gyroscopeSubscribe();
}
};
gyroscopeSubscribe = () => {
Gyroscope.setUpdateInterval(20);
this.gyroscopeSubscription = Gyroscope.addListener(gyroscopeData => {
this.setState({ gyroscopeData });
});
};
unsubscribeAccelerometer = () => {
this.gyroscopeSubscription && this.gyroscopeSubscription.remove();
this.gyroscopeSubscription = null;
};
referenceMeasurementCompleted = async (x, y, z) => {
this.setState({ measuringStarted: false });
const uuid = new UUID(4).format();
await insertGyroData([uuid, 'admin', '12345', x.toString(), y.toString(), z.toString()]);
alert('Reference measurements completed');
};
render() {
let { x, y, z } = this.state.gyroscopeData;
const { x_gyro, y_gyro, z_gyro } = this.state;
if (this.state.measuringStarted) {
this.setState(({ x_gyro: [...x_gyro, x], y_gyro: [...y_gyro, y], z_gyro: [...z_gyro, z] }));
return (
<View style={styles.container}>
<Text>Gyroscope:</Text>
<Text>
x: {round(x)} y: {round(y)} z: {round(z)}
</Text>
<Text>Time spent:</Text>
<CountDown
until={5}
size={30}
onFinish={() => this.referenceMeasurementCompleted(x_gyro, y_gyro, z_gyro)}
digitStyle={{ backgroundColor: '#FFF' }}
digitTxtStyle={{ color: '#00c9ff' }}
timeToShow={['M', 'S']}
timeLabels={{ m: 'MM', s: 'SS' }}
/>
</View>
);
} else {
return (
<View style={styles.container}>
<Button
rounded
primary
onPress={() => this.setState({ measuringStarted: true })}
>
<Text style={styles.buttonText}>Start reference measurements</Text>
</Button>
</View>
);
}
}
}
function round(n) {
if (!n) {
return 0;
}
return Math.floor(n * 100) / 100;
}
const styles = StyleSheet.create({
container: {
flex: 1,
height: Dimensions.get('window').height,
backgroundColor: 'white',
alignItems: 'center',
justifyContent: 'center'
},
buttonText: {
color: 'white',
fontSize: 20,
paddingHorizontal: 10,
},
});
With the current solution I'm getting Maximum Depth Update Exceeded error
Please help!
The error is obviously coming from this block since you are changing state inside the render.
if (this.state.measuringStarted) {
this.setState(({ x_gyro: [...x_gyro, x], y_gyro: [...y_gyro, y], z_gyro: [...z_gyro, z] }));
}
One way you can try is updating the state in
componentDidUpdate(prevProps,prevState){}
But take care that it does not lead to Maximum Stack Reached error. With proper conditional statements you can set the state to new data without infinite recursion.

react native todo list with TextInput

Is it possible to build a todo list with react native that can
add new TexInput with the return key
focus the new TextInput when created
remove TextInputs with the delete key if the TextInput is empty and focus another input
I have a basic list that can add items and focus them but not remove items.
https://snack.expo.io/#morenoh149/todo-list-textinput-spike
import * as React from 'react';
import { TextInput, View } from 'react-native';
export default class App extends React.Component {
currentTextInput = null
state = {
focusedItemId: 0,
items: [
{ id: 0, text: 'the first item' },
{ id: 1, text: 'the second item' },
],
};
addListItem = index => {
let { items } = this.state;
const prefix = items.slice(0, index + 1);
const suffix = items.slice(index + 1, items.length);
const newItem = { id: Date.now(), text: '' };
let result = prefix.concat([newItem]);
result = result.concat(suffix);
this.setState({
focusedItemId: newItem.id,
items: result,
});
};
focusTextInput() {
// focus the current input
this.currentTextInput.focus();
}
componentDidUpdate(_, pState) {
// if focused input id changed and the current text input was set
// call the focus function
if (
pState.focusedItemId !== this.state.focusedItemId
&& this.currentTextInput
) {
this.focusTextInput();
}
}
render() {
const { focusedItemId } = this.state;
return (
<View style={{ flex: 1, justifyContent: 'center' }}>
{this.state.items.map((item, idx) => (
<TextInput
style={{ borderWidth: 1, borderColor: 'black' }}
value={item.text}
ref={item.id === focusedItemId
? c => this.currentTextInput = c
: null}
autoFocus={item.id === focusedItemId}
onChangeText={text => {
const newItems = this.state.items;
newItems[idx].text = text;
this.setState({
items: newItems,
});
}}
onSubmitEditing={event => this.addListItem(idx)}
/>
))}
</View>
);
}
}
To remove items you can add a callback to the onKeyPress and check if it was the Backspace (delete) key and if the text field was empty already. If so, you remove the item from the item list.
onKeyPress={({ nativeEvent: { key: keyValue } }) => {
if(keyValue === 'Backspace' && !items[idx].text) {
this.removeListItem(idx)
}
}}
In the removeListItem function you can remove the item at the index and update the selected id to the id previous in the list to focus this one.
removeListItem = index => {
const { items } = this.state;
const newItems = items.filter(item => item.id !== items[index].id)
this.setState({
focusedItemId: items[index - 1] ? items[index - 1].id : -1,
items: newItems.length ? newItems : [this.createNewListItem()],
});
}
Please find the full working demo here: https://snack.expo.io/#xiel/todo-list-textinput-spike

Scrolling issues with FlatList when rows are variable height

I'm using a FlatList where each row can be of different height (and may contain a mix of both text and zero or more images from a remote server).
I cannot use getItemLayout because I don't know the height of each row (nor the previous ones) to be able to calculate.
The problem I'm facing is that I cannot scroll to the end of the list (it jumps back few rows when I try) and I'm having issues when trying to use scrollToIndex (I'm guessing due to the fact I'm missing getItemLayout).
I wrote a sample project to demonstrate the problem:
import React, { Component } from 'react';
import { AppRegistry, StyleSheet, Text, View, Image, FlatList } from 'react-native';
import autobind from 'autobind-decorator';
const items = count => [...Array(count)].map((v, i) => ({
key: i,
index: i,
image: 'https://dummyimage.com/600x' + (((i % 4) + 1) * 50) + '/000/fff',
}));
class RemoteImage extends Component {
constructor(props) {
super(props);
this.state = {
style: { flex: 1, height: 0 },
};
}
componentDidMount() {
Image.getSize(this.props.src, (width, height) => {
this.image = { width, height };
this.onLayout();
});
}
#autobind
onLayout(event) {
if (event) {
this.layout = {
width: event.nativeEvent.layout.width,
height: event.nativeEvent.layout.height,
};
}
if (!this.layout || !this.image || !this.image.width)
return;
this.setState({
style: {
flex: 1,
height: Math.min(this.image.height,
Math.floor(this.layout.width * this.image.height / this.image.width)),
},
});
}
render() {
return (
<Image
onLayout={this.onLayout}
source={{ uri: this.props.src }}
style={this.state.style}
resizeMode='contain'
/>
);
}
}
class Row extends Component {
#autobind
onLayout({ nativeEvent }) {
let { index, item, onItemLayout } = this.props;
let height = Math.max(nativeEvent.layout.height, item.height || 0);
if (height != item.height)
onItemLayout(index, { height });
}
render() {
let { index, image } = this.props.item;
return (
<View style={[styles.row, this.props.style]}>
<Text>Header {index}</Text>
<RemoteImage src = { image } />
<Text>Footer {index}</Text>
</View>
);
}
}
export default class FlatListTest extends Component {
constructor(props) {
super(props);
this.state = { items: items(50) };
}
#autobind
renderItem({ item, index }) {
return <Row
item={item}
style={index&1 && styles.row_alternate || null}
onItemLayout={this.onItemLayout}
/>;
}
#autobind
onItemLayout(index, props) {
let items = [...this.state.items];
let item = { ...items[index], ...props };
items[index] = { ...item, key: [item.height, item.index].join('_') };
this.setState({ items });
}
render() {
return (
<FlatList
ref={ref => this.list = ref}
data={this.state.items}
renderItem={this.renderItem}
/>
);
}
}
const styles = StyleSheet.create({
row: {
padding: 5,
},
row_alternate: {
backgroundColor: '#bbbbbb',
},
});
AppRegistry.registerComponent('FlatListTest', () => FlatListTest);
Use scrollToOffset() instead:
export default class List extends React.PureComponent {
// Gets the total height of the elements that come before
// element with passed index
getOffsetByIndex(index) {
let offset = 0;
for (let i = 0; i < index; i += 1) {
const elementLayout = this._layouts[i];
if (elementLayout && elementLayout.height) {
offset += this._layouts[i].height;
}
}
return offset;
}
// Gets the comment object and if it is a comment
// is in the list, then scrolls to it
scrollToComment(comment) {
const { list } = this.props;
const commentIndex = list.findIndex(({ id }) => id === comment.id);
if (commentIndex !== -1) {
const offset = this.getOffsetByIndex(commentIndex);
this._flatList.current.scrollToOffset({ offset, animated: true });
}
}
// Fill the list of objects with element sizes
addToLayoutsMap(layout, index) {
this._layouts[index] = layout;
}
render() {
const { list } = this.props;
return (
<FlatList
data={list}
keyExtractor={item => item.id}
renderItem={({ item, index }) => {
return (
<View
onLayout={({ nativeEvent: { layout } }) => {
this.addToLayoutsMap(layout, index);
}}
>
<Comment id={item.id} />
</View>
);
}}
ref={this._flatList}
/>
);
}
}
When rendering, I get the size of each element of the list and write it into an array:
onLayout={({ nativeEvent: { layout } }) => this._layouts[index] = layout}
When it is necessary to scroll the screen to the element, I summarize the heights of all the elements in front of it and get the amount to which to scroll the screen (getOffsetByIndex method).
I use the scrollToOffset method:
this._flatList.current.scrollToOffset({ offset, animated: true });
(this._flatList is ref of FlatList)
So what I think you can do and what you already have the outlets for is to store a collection by the index of the rows layouts onLayout. You'll want to store the attributes that's returned by getItemLayout: {length: number, offset: number, index: number}.
Then when you implement getItemLayout which passes an index you can return the layout that you've stored. This should resolve the issues with scrollToIndex. Haven't tested this, but this seems like the right approach.
Have you tried scrollToEnd?
http://facebook.github.io/react-native/docs/flatlist.html#scrolltoend
As the documentation states, it may be janky without getItemLayout but for me it does work without it
I did not find any way to use getItemLayout when the rows have variable heights , So you can not use initialScrollIndex .
But I have a solution that may be a bit slow:
You can use scrollToIndex , but when your item is rendered . So you need initialNumToRender .
You have to wait for the item to be rendered and after use scrollToIndex so you can not use scrollToIndex in componentDidMount .
The only solution that comes to my mind is using scrollToIndex in onViewableItemsChanged . Take note of the example below :
In this example, we want to go to item this.props.index as soon as this component is run
constructor(props){
this.goToIndex = true;
}
render() {
return (
<FlatList
ref={component => {this.myFlatList = component;}}
data={data}
renderItem={({item})=>this._renderItem(item)}
keyExtractor={(item,index)=>index.toString()}
initialNumToRender={this.props.index+1}
onViewableItemsChanged={({ viewableItems }) => {
if (this.goToIndex){
this.goToIndex = false;
setTimeout(() => { this.myFlatList.scrollToIndex({index:this.props.index}); }, 10);
}
}}
/>
);
}
You can use onScrollToIndexFailed to avoid getItemLayout
onScrollToIndexFailed={info => {
const wait = new Promise(resolve => setTimeout(resolve, 100));
wait.then(() => {
refContainer.current?.scrollToIndex({
index: pinPosition || 0,
animated: true
});
});
}}