How do I build a multi-card carousel? - react-native

I'm trying to build something like this in React Native. It will stretch across the whole page and will loop infinitely, there will be a 'next' and 'previous' button.
I'm new to React Native (coming from React), so am a little unsure about how to implement it.
I found this guide on YouTube helpful to get something very basic up and running.
Here is the code I have so far:
import React, {useCallback, useEffect, useRef, useState} from 'react';
import {withTheme} from 'react-native-paper';
import {
View,
StyleSheet,
Text,
Dimensions,
Image,
FlatList,
Pressable,
} from 'react-native';
import PrismicText from '../prismicText';
const {width: windowWidth, height: windowHeight} = Dimensions.get('window');
const Slide = ({data}) => {
return (
<View
style={{
height: 400,
width: 300,
justifyContent: 'center',
alignItems: 'center',
marginRight: 15,
}}>
<Image
source={{uri: data.image}}
style={{width: '100%', height: '100%', borderRadius: 16}}></Image>
</View>
);
};
const Carousel = ({slice, theme}) => {
const slideList = slice.items.map((item, index) => {
return {
id: index,
image: item.image.url,
};
});
const {colors, isTabletOrMobileDevice} = theme;
const styles = isTabletOrMobileDevice ? mobileStyles : desktopStyles;
const flatListRef = useRef(null);
const viewConfig = {viewAreaCoveragePercentThreshold: 50};
const [activeIndex, setActiveIndex] = useState(4);
const onViewRef = useRef(({changed}) => {
if (changed[0].isViewable) {
setActiveIndex(changed[0].index);
}
});
const handlePressLeft = () => {
if (activeIndex === 0)
return flatListRef.current?.scrollToIndex({
animated: true,
index: slideList.length - 1,
});
flatListRef.current?.scrollToIndex({
index: activeIndex - 1,
});
};
const handlePressRight = () => {
if (activeIndex === slideList.length - 1)
return flatListRef.current?.scrollToIndex({
animated: true,
index: 0,
});
flatListRef.current?.scrollToIndex({
index: activeIndex + 1,
});
};
return (
<>
<View
style={{
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingVertical: 8,
}}>
<Pressable style={[styles.chevron]} onPress={handlePressLeft}>
Left
</Pressable>
<Pressable style={[styles.chevron]} onPress={handlePressRight}>
Right
</Pressable>
</View>
<FlatList
ref={ref => (flatListRef.current = ref)}
data={slideList}
horizontal
showsHorizontalScrollIndicator={false}
snapToAlignment="center"
pagingEnabled
viewabilityConfig={viewConfig}
onViewableItemsChanged={onViewRef.current}
renderItem={({item}, i) => <Slide data={item} />}
keyExtractor={item => item}
/>
<View style={styles.index}>
<Text category={'c2'} style={styles.indexText}>
{activeIndex + 1} of {slideList.length} photos
</Text>
</View>
</>
);
};
const mobileStyles = StyleSheet.create({});
const desktopStyles = StyleSheet.create({});
export default withTheme(Carousel);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
The problems I'm experiencing with this code:
I am setting the initial state of the active index to 4, but active index always starts at 0
Clicking the 'right' button doesn't seem to change the active index
Clicking the 'right' button will move the carousel along by 1 increment, but it won't go any further (even on smaller viewports like mobile where you can only see 1.5 cards, so it should move along many times to be able to see all of the cards)
Clicking the 'left' button seems to have the same issues as above
There is no infinite loop of the slides
My feeling is that there are two issues to be addressed:
Active index is broken and needs to be fixed
Modifications required to make the number of cards on the viewport responsive
I've spent a lot of time looking at this and can't seem to figure it out. Any help would be much appreciated.

The first issue is easy to fix. You are expecting that the FlatList scrolls initially to the initial activeIndex, but you are not telling the FlatList to do so. There is a prop called initialScrollIndex that is designed for this purpose.
<FlatList
initialScrollIndex={4}
...
The second issue is caused by a faulty implementation of the functions handlePressLeft and handlePressRight as well as providing
const onViewRef = useRef(({changed}) => {
if (changed[0].isViewable) {
setActiveIndex(changed[0].index);
}
});
I have removed the above completely.
I have changed the activeIndex state to the following.
const [activeIndex, setActiveIndex] = useState({index: 4, direction: 'right'});
I have changed the handlePressLeft and handlePressRight functions to the following.
const handlePressLeft = () => {
setActiveIndex((prev) => ({index: prev.index - 1, direction: 'left'}));
};
const handlePressRight = () => {
setActiveIndex((prev) => ({index: prev.index + 1, direction: 'right'}));
};
I have created an effect as follows.
React.useEffect(() => {
if (activeIndex.index === slideList.length - 1 && activeIndex.direction === 'right') {
setActiveIndex({index: 0, direction: 'right'});
} else if (activeIndex.index < 0 && activeIndex.direction === 'left') {
setActiveIndex({index: slideList.length - 2, direction: 'left'})
} else {
flatListRef.current?.scrollToIndex({
animated: true,
index: activeIndex.index,
});
}
}, [activeIndex, slideList.length]);
I have implemented an adapted snack without images and using a dummy array.

You can use the below third-party library to achieve the above one quickly.
react-native-snap-carousel
You can check all the examples and use them according to your requirement.
Hope it will help you!

You should try react-native-reanimated-carousel.
Why?
highly customizable + easy and fast to implement any carousel
It's new and it uses react-native-reanimated for better performance (by running animations on the UI thread, rather than on JS thread)
solves all the issues that react-native-snap-carousel has (which is deprecated and has lots of bugs)
solves all the issues that you have and handles many edge cases that you may have forgotten about (in case you want to implement it by yourself)

Related

mobile app want graphical text to change font size per line automatically, like on tiktok/instagram alternating lines different sizes

On TikTok and Instagram, they can generate automatically text font size like this image, where alternating lines have different font sizes automatically. I'm trying to figure out how to code that in React Native for mobile IOS and Android: [[enter image description here](https://i.stack.imgur.com/vkhIo.jpg)](https://i.stack.imgur.com/XcjLq.jpg)
I couldn't figure it out. I made something that I'm not crazy about, which is just having a larger font on the first three lines and then a smaller font. See image: But I don't like it. enter image description here
I totally misunderstood what PixelRatio.getFontScale did. I thought it would provide the average width a single character takes up on screen. If you can find a way to get a rough estimate of the width of a single character, then this method will work link:
import { useEffect, useState } from 'react';
import { View, StyleSheet, Text, PixelRatio } from 'react-native';
import rnTextSize from 'react-native-text-size';
import reduce from 'awaity/reduce';
import useViewLayout from './useViewLayout';
const fontScale = PixelRatio.getFontScale();
const showLayoutValues = true
export default function MultiLineText({
width,
containerStyle,
textStyle1 = { fontSize: 16 },
textStyle2 = { fontSize: 22 },
str,
...textProps
}) {
// containerLayout will provide the max width each line can have
const [containerLayout, onContainerLayout] = useViewLayout();
// lines was created in a useMemo hook but I wasnt sure if
// useMemo could handle async
const [lines, setLines] = useState([]);
useEffect(() => {
const calcLines = async () => {
let words = str.split(' ').filter((s) => s.trim().length);
let newLines = await words.reduce(
async (prevPromise, curr, index) => {
const prev = await prevPromise;
let lineIndex = prev.length - 1;
let style = index % 2 == 0 ? textStyle1 : textStyle2;
const fontSize = style.fontSize;
// I wanted to use this https://github.com/aMarCruz/react-native-text-size/
// to measure text width but expo doesnt support it
const config = {
// if you exported from expo and link rnTextSize set this to true
useMeasureModule:false,
fontProps:style
}
const useMeasureModule = false;
let lineWidth = await getTextWidth(
prev[lineIndex],
fontSize,
config
);
let wordWidth = await getTextWidth(curr, fontSize, config);
// if currentLine can fit the next word add it
if (lineWidth + wordWidth < (width || containerLayout.width))
prev[lineIndex] += curr + ' ';
// or put it on the next line
else {
prev[lineIndex + 1] = curr + ' ';
}
return prev;
},
['']
);
setLines(newLines);
};
calcLines();
}, [str, containerLayout, width, textStyle1, textStyle2]);
return (
<>
{showLayoutValues && <Text>Container Layout: {JSON.stringify(containerLayout,null,4)}</Text>}
<View
style={[styles.container, containerStyle]}
onLayout={onContainerLayout}>
{lines.map((line, i) => (
<Text
{...textProps}
// to ensure that lines dont wrap
numberOfLines={1}
style={[textProps.style, i % 2 == 0 ? textStyle1 : textStyle2]}>
{line}
</Text>
))}
</View>
</>
);
}
const getTextWidth = async (str, fontSize, config={}) => {
const {fontProps, useMeasureModule} = config;
if (!useMeasureModule) {
// random mathing
return str.length * fontScale * fontSize ** 0.8;
}
let measure = await rnTextSize.measure({
...fontProps,
text: str,
});
return measure.width;
};
const styles = StyleSheet.create({
container: {
width: '100%',
},
});
And then in use:
export default function App() {
return (
<View style={styles.container}>
<MultiLineText
containerStyle={{
width: '100%',
backgroundColor: '#eef',
alignSelf: 'center',
alignItems: 'center',
}}
textStyle1={styles.text}
textStyle2={styles.text2}
str="I am really not sure as of how long this text needs to be to exceed at least 3 lines. I could copy and paste some stuff here but I think if I just type and type it would be quicker than googling, copying, and pasting"
/>
</View>
);
}
I just found out that onTextLayout is a thing. It gives about each line in the Text component, including info about the characters present on each line. This could be used to figure out where to break lines of text (no planned web support):
After tying the str prop to a text input it became very clear that it is ideal to prevent this component from re-rendering as much as possible so I made additional changes (demo)
import { useState, useCallback, useEffect, memo } from 'react';
import { View, StyleSheet, Text, ScrollView } from 'react-native';
// text lines will alternate between these styles
const defaultLineStyles = [
{ color: 'red', fontSize: 16 },
{ color: 'blue', fontSize: 22 },
{ color: 'green', fontSize: 28 },
];
function MultiLineText({
containerStyle,
lineStyles = defaultLineStyles,
str,
...textProps
}) {
const [lines, setLines] = useState([]);
// each time a substring is added to line,
// remove the substring from remainingStr
const [remainingStr, setRemainingStr] = useState('');
const onTextLayout = useCallback((e) => {
// the first line of text will have the proper styling
let newLine = e.nativeEvent.lines[0].text;
setLines((prev) => {
return [...prev, newLine];
});
// remove newLine from remainingStr
setRemainingStr((prev) => prev.replace(newLine, ''));
}, []);
// when str changes reset lines, and set remainingStr to str
useEffect(() => {
setLines([]);
setRemainingStr(str);
}, [str]);
return (
<>
<View style={[styles.container, containerStyle]}>
<ScrollView style={{ flex: 1 }}>
{lines.map((line, i) => (
<Text
{...textProps}
style={[textProps.style, lineStyles[i % lineStyles.length]]}>
{line}
</Text>
))}
</ScrollView>
{/* this view will be invisible*/}
{remainingStr.length > 0 && (
<View style={{ opacity: 0 }}>
<Text
{...textProps}
onTextLayout={onTextLayout}
style={[
textProps.style,
// use lines.length to get proper style
lineStyles[lines.length % lineStyles.length],
]}>
{remainingStr}
</Text>
</View>
)}
</View>
</>
);
}
const styles = StyleSheet.create({
container: {
width: '100%',
height:'50%'
},
});
// be careful when passing non memoized array/objects
export default memo(MultiLineText)
Its important to note that objects/arrays that arent memoized/state/refs will cause the memoized component to re-render, even if the values are static e.g
<MultiLineText
containerStyle={{
width: '100%',
height: 200,
backgroundColor: '#eef',
alignSelf: 'center',
alignItems: 'center',
}}
style={styles.defaultTextStyle}
str={text}
lineStyles={[styles.text,styles.text2]}
/>
containerStyle and lineStyles are getting new objects and arrays every time its parent component re-render, which will make MultiLineText re-render (even though its memoized). After moving the containerStyle to the stylesheet and memoizing lineStyles re-rendering becomes better:
const lineStyles = React.useMemo(()=>{
return [styles.text,styles.text2]
},[])
return (
<View style={styles.container}>
<TextInput onChangeText={setText} label="Enter some text" value={text} />
<MultiLineText
containerStyle={styles.textContainer}
style={styles.defaultTextStyle}
str={text}
lineStyles={lineStyles}
/>
</View>

React Native TextInput onPressOut fires instantly

I am making a class for a custom TextInput, where the style will change when the field is selected, and will change back as soon as it is pressed out of. It looks as follows...
export function SoftSearchBar({
height=40,
width='100%',
fontSize=20,
fireOnChange={function(){console.log("No Change Function in place")}},
value=false,
placeholder="Placeholder",
type=null
}){
const [isActive, setActive] = useState(false)
const [style, setStyle] = useState({})
useEffect(() => {
console.log(isActive)
if (isActive){
setStyle(style => ({style: styles.softSearchActive, width: width}))
}
else{
setStyle(style => ({style: styles.softSearchInactive, width: width}))
}
}, [isActive])
return(
<View style={{height: height, flexDirection: 'row'}}>
<TextInput
value={value}
onPressIn={() => setActive(true)}
onPressOut={() => setActive(false)}
style={{...style.style, width: width, zIndex: 0, fontSize: fontSize}}
textContentType={type}
text
placeholder={placeholder}
placeholderTextColor={'black'}
autoCorrect={false}
onChangeText={text => {
fireOnChange(text)
}}
/>
</View>
)
}
Almost all of this works as expected, when the field is pressed, an outline appears indicating its selection, and the text changes color. However, onPressOut fires immediately after onPressIn, as the log will look like this as soon as I press the field
true
false
indicating that onPressOut fired, since it is the only way to setIsActive(false)
I saw some solutions recommending using onResponderRelease as opposed to onPressOut but then it just never unselects. Is there some syntax Im missing with onPressOut? This seems like a pretty simple and straightforward syntax so I am unsure
Main Issue with your code is onPressIn and onPressOut you need to change them to onFocus and onBlur
Here is a working example you can paste into this website
https://reactnative.dev/docs/textinput
You can set your default Input style and then when active you can enable the style you want.
outlineStyle: none to get rid of the default blue outline of the textinput when focused
Can also just remove handleFocus & handleBlur and move the function into the actual function calls to reduce the code further
import React from "react";
import { SafeAreaView, StyleSheet, TextInput } from "react-native";
const UselessTextInput = () => {
const [style, setStyle] = React.useState({borderWidth:2 , borderColor: 'red', outlineStyle: 'none'});
const [active, setActive] = React.useState(false)
const handleFocus = () => setActive(true)
const handleBlur = () => setActive(false)
return (
<SafeAreaView>
<TextInput
style={[styles.input, active && style]}
onFocus={handleFocus}
onBlur={handleBlur}
onChangeText={() => {}}
value={null}
placeholder="useless placeholder"
keyboardType="numeric"
/>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
input: {
height: 40,
margin: 12,
borderWidth: 1,
padding: 10,
},
});
export default UselessTextInput;

Prevent inverted Flatlist from scrolling to bottom when new items are added

I am building a chat app, using an inverted Flatlist. I add new items to the top of the list when onEndReached is called and everything works fine.
The problem is that if add items to the bottom, it instantly scrolls to the bottom of the list. That means that the user has to scroll back up to read the messages that were just added (which is terrible).
I tried to call scrollToOffset in onContentSizeChange, but this has a one-second delay where the scroll jumps back and forth.
How can I have the list behave the same way when I add items to the top AND to the bottom, by keeping the same messages on screen instead of showing the new ones?
here is demo: https://snack.expo.io/#nomi9995/flatlisttest
Solution 1:
use maintainVisibleContentPosition props for preventing auto scroll in IOS but unfortunately, it's not working on android. but here is PR for android Pull Request. before merge this PR you can patch by own from this PR
<FlatList
ref={(ref) => { this.chatFlatList = ref; }}
style={styles.flatList}
data={this.state.items}
renderItem={this._renderItem}
maintainVisibleContentPosition={{
minIndexForVisible: 0,
}}
/>
Solution 2:
I found another workaround by keep latest y offset with onScroll and also save content height before and after adding new items with onContentSizeChange and calculate the difference of content height, and set new y offset to the latest y offset + content height difference!
Here I am adding a new item on top and bottom in an inverted Flatlist.
I hope you can compare your requirements with the provided sample code :)
Full Code:
import React, {Component} from 'react';
import {
SafeAreaView,
View,
FlatList,
StyleSheet,
Text,
Button,
Platform,
UIManager,
LayoutAnimation,
} from 'react-native';
if (Platform.OS === 'android') {
if (UIManager.setLayoutAnimationEnabledExperimental) {
UIManager.setLayoutAnimationEnabledExperimental(true);
}
}
const getRandomColor = () => {
var letters = '0123456789ABCDEF';
var color = '#';
for (var i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
};
const DATA = [
getRandomColor(),
getRandomColor(),
getRandomColor(),
getRandomColor(),
getRandomColor(),
];
export default class App extends Component {
scrollValue = 0;
append = true;
state = {
data: DATA,
};
addItem = (top) => {
const {data} = this.state;
let newData;
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
if (top) {
newData = [...data, getRandomColor()];
this.setState({data: newData});
} else {
newData = [getRandomColor(), ...data];
this.setState({data: newData});
}
};
shouldComponentUpdate() {
return this.scrollValue === 0 || this.append;
}
onScrollBeginDrag = () => {
this.append = true;
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
this.setState({});
};
render() {
const {data} = this.state;
return (
<SafeAreaView style={styles.container}>
<Button title="ADD ON TOP" onPress={() => this.addItem(true)} />
<FlatList
data={data}
onScrollBeginDrag={this.onScrollBeginDrag}
renderItem={({item}) => <Item item={item} />}
keyExtractor={(item) => item}
inverted
onScroll={(e) => {
this.append = false;
this.scrollValue = e.nativeEvent.contentOffset.y;
}}
/>
<Button title="ADD ON BOTTOM" onPress={() => this.addItem(false)} />
</SafeAreaView>
);
}
}
function Item({item}) {
return (
<View style={[styles.item, {backgroundColor: item}]}>
<Text style={styles.title}>{item}</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
item: {
backgroundColor: '#f9c2ff',
padding: 20,
height: 100,
},
title: {
fontSize: 32,
},
});
This is one year late, but this works fine:
<FlatList
inverted
initialScrollIndex={1}
{...}
/>
Since inverted renders flatlist but with inverted: 1, thus you need to pass 1 to initialScrollIndex so that it scrolls to bottom in normal list and to top in the inverted one
Have you tried using keyExtractor?
It may help react avoid re-render, so try use unique keys for each item.
you can read more about it here: https://reactnative.dev/docs/flatlist#keyextractor

FlatList ref scrollToIndex is not a function

I am facing what seems to be a long-lasting issue in react native.
I am using Expo SDK35 with RN version 0.59. I have not updated to Expo SDK36 / RN 0.60 yet, due to large code base, but I could update if that makes up for a solution to my issue.
I have an Animated.View component that has a FlatList child, and I am unable to use the static methods (scrollToIndex() in particular) that should be available on the FlatList reference. See the next example code:
class Example extends React.Component{
constructor(props){
super(props);
this.myRef = null;
}
componentDidUpdate = () => {
/*
somewhere in code outside this class, a re-render triggers
and passes new props to this class.
I do have props change detection, and some more other code,
but I have removed it in order to minimize the code example here
*/
// This call throws:
// TypeError: undefined is not a function (near '...this._scrollRef.scrollTo...')
this.myRef.scrollToIndex({
animated: true,
index: 1,
viewOffset: 0,
viewPosition: 0.5
});
// Other suggested solution from SO
// This also throws:
// TypeError: _this.myRef.getNode is not a function. (In '_this.myRef.getNode()', '_this.myRef.getNode' is undefined)
this.myRef.getNode().scrollToIndex({
animated: true,
index: 1,
viewOffset: 0,
viewPosition: 0.5
});
}
render = () => <Animated.View style={{ /* ... some animated props */ }}>
<FlatList ref={(flatListRef) => { this.myRef = flatListRef; }}
// more FlatList related props
/>
</Animated.View>
}
I have tried to use Animated.FlatList instead, still throws the same errors as in the code example above.
I have also tried to use react native's findNodeHandle() utility function on the received flatListRef parameter, but it returns null.
I have found the same issue posted multiple times in the past here on Stack Overflow, most with no answer, or which do not work for me. These posts are also a bit old (a year or so), which is why I am posting again for the same issue.
Did anyone manage to find a solution/workaround for this issue?
EDIT: Possible workaround
As I was playing with code, I tried to use a ScrollView component instead of FlatList - and the scrollTo method works!
The changes were only on the FlatList - ScrollView specific props (so, for a ScrolLView it would be childs instead of data={[...]} and renderItem={()=>{ ... }}, ect.), and the scrollToIndex method in componentDidMount which was replaced by scrollTo.
The render method of the class, with a ScrollView, now looks like this:
render = () => <Animated.View style={{ /* ... some animated props */ }}>
<ScrollView ref={(flatListRef) => { this.myRef = flatListRef; }}>
{/*
this.renderItem is almost the same as the
renderItem method used on the FlatList
*/}
{ this.state.dataArray.map(this.renderItem) }
</ScrollView>
</Animated.View>
Please note that ScrollView does not have a scrollToIndex() method, so you'll have to cope with manually keeping track of child positions, and maybe, implement a scrollToIndex method of your own.
I am not making this the answer to my question, because the underlying issue remains. But as a workaround, maybe you can go with it and call it a day...
TL;DR;
this.myRef = React.createRef();
this.myRef.current.doSomething(); // note the use of 'current'
Long version:
While the idea behind what I was trying was correct, the error in my original post seems to be quite stupid. In my defense, the docs were not clear (probably...). Anyway...
React.createRef returns an object with a few fields on it, all of them useless for the developer (used by React in the back) - except one: current.
This prop holds the current reference to the underlying component that the ref is attached to. The main ref object is not usable for the purpose I meant to in my original question above.
Instead, this is how I should've used the ref correctly:
this.myRef.current.scrollToIndex(...)
Hold up, don't crash
Both the main myRef object, and the current field will be null if the component has not yet mounted, has unmounted at any point later, or if the ref cannot be attached to it for some reason. As you may know (or found out later), null.something will throw an error. So, to avoid it:
if ((this.myRef !== null) && (this.myRef.current !== null)){
this.myRef.current.scrollToIndex(...);
}
Extra insurance
If you try to call an undefined value as a function on a field on the ref, your code will crash. This can happend if you mistakenly reuse the same ref on multiple components, or if the component you attached it to does not have that method (i.e. View does not have a scrollTo method). To fix this you have two solutions:
// I find this to be the most elegant solution
if ((this.myRef !== null) && (this.myRef.current !== null)) {
if (typeof this.myRef.current.scrollToIndex === "function") {
this.myRef.current.scrollToIndex(...);
}
}
or
if ((this.myRef !== null) && (this.myRef.current !== null)) {
if (typeof this.myRef.current.scrollToIndex === "function") {
try {
this.myRef.current.scrollToIndex(...);
} catch (error) {
console.warn("Something went wrong", error);
}
}
}
I hope this to be useful for anyone else learning to use refs in React. Cheers :)
With Animated.ScrollView:
Create a ref to your FlatList (the old way only works):
<ScrollView ref={ (ref) => (this.MyRef=ref) } />
Access scrollToIndex using this.myRef.getNode().scrollToIndex
Animated.FlatList is currently not working unfortunately...
With FlatList:
Create a ref to your FlatList by:
<FlatList ref={ this.flatListRef } />
constructor(props) {
super(props);
this.flatListRef = React.createRef();
}
Access scrollToIndex using this.flatListRef.current.scrollToIndex
Also make sure to wrap your code inside an if statement like:
if (this.myRef.getNode()) { this.flatListRef.getNode().scrollToIndex(); }
o do not know if this will help you... it scroll to a especific item in the list:
/*Example to Scroll to a specific position in scrollview*/
import React, { Component } from 'react';
//import react in our project
import {
View,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
Image,
TextInput,
} from 'react-native';
//import all the components we needed
export default class App extends Component {
constructor() {
super();
//Array of Item to add in Scrollview
this.items = [
'zero',
'one',
'two',
'three',
'four',
'five',
'six',
'seven',
'eight',
'nine',
'ten ',
'eleven',
'twelve',
'thirteen',
'fourteen',
'fifteen',
'sixteen',
'seventeen',
'eighteen',
'nineteen',
'twenty ',
'twenty-one',
'twenty-two',
'twenty-three',
'twenty-four',
'twenty-five',
'twenty-six',
'twenty-seven',
'twenty-eight',
'twenty-nine',
'thirty',
'thirty-one',
'thirty-two',
'thirty-three',
'thirty-four',
'thirty-five',
'thirty-six',
'thirty-seven',
'thirty-eight',
'thirty-nine',
'forty',
];
//Blank array to store the location of each item
this.arr = [];
this.state = { dynamicIndex: 0 };
}
downButtonHandler = () => {
if (this.arr.length >= this.state.dynamicIndex) {
// To Scroll to the index 5 element
this.scrollview_ref.scrollTo({
x: 0,
y: this.arr[this.state.dynamicIndex],
animated: true,
});
} else {
alert('Out of Max Index');
}
};
render() {
return (
<View style={styles.container}>
<View
style={{
flexDirection: 'row',
backgroundColor: '#1e73be',
padding: 5,
}}>
<TextInput
value={String(this.state.dynamicIndex)}
numericvalue
keyboardType={'numeric'}
onChangeText={dynamicIndex => this.setState({ dynamicIndex })}
placeholder={'Enter the index to scroll'}
style={{ flex: 1, backgroundColor: 'white', padding: 10 }}
/>
<TouchableOpacity
activeOpacity={0.5}
onPress={this.downButtonHandler}
style={{ padding: 15, backgroundColor: '#f4801e' }}>
<Text style={{ color: '#fff' }}>Go to Index</Text>
</TouchableOpacity>
</View>
<ScrollView
ref={ref => {
this.scrollview_ref = ref;
}}>
{/*Loop of JS which is like foreach loop*/}
{this.items.map((item, key) => (
//key is the index of the array
//item is the single item of the array
<View
key={key}
style={styles.item}
onLayout={event => {
const layout = event.nativeEvent.layout;
this.arr[key] = layout.y;
console.log('height:', layout.height);
console.log('width:', layout.width);
console.log('x:', layout.x);
console.log('y:', layout.y);
}}>
<Text style={styles.text}>
{key}. {item}
</Text>
<View style={styles.separator} />
</View>
))}
</ScrollView>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
paddingTop: 30,
},
separator: {
height: 1,
backgroundColor: '#707080',
width: '100%',
},
text: {
fontSize: 16,
color: '#606070',
padding: 10,
},
});
if i completly wrong, tell me...
Because ScrollView has no scrollToOffset function and It has only scrollTo function.
So let use function scrollTo with ScrollView or scrollToOffset with FlatList and it works normal.
If you are working with 'KeyboardAwareFlatList' this worked nicely:
https://github.com/APSL/react-native-keyboard-aware-scroll-view/issues/372
In short, use useRef and use the innerRef property of the KeyboardAwareFlatList rather than the ref property.

How can I display 30 pages of text in a (scrolling) screen

I want to display 30 pages of text on a screen. I've tried ScrollView and FlatList but I get a white screen. Only when I try with ScrollView to display only 2 pages, works fine.
I do not want to use a WebView, because I would like to have all data in the app (no internet connection needed).
Here is what I've already tried:
With FlatList:
I have a text.js as a model, which I use to create a Text Object in an array, which I then use as data for the FlatList. For the renderItem function (of FlatList) I use a TextItem to display the text.
text.js
function Text(info) {
this.id = info.id;
this.text = info.text;
}
export default Text;
LongTextModule.js
import Text from '../../models/text';
export const LONGTEXT = [
new Text({
id:'text_1',
text:`.....longtext....`
})
]
TextItem.js
const TextItem = (props) => {
return (
<View style={styles.screen} >
<Text style={styles.textStyle}>{props.longText}</Text>
</View >
);
};
const styles = StyleSheet.create({
screen: {
flex: 1,
},
textStyle: {
justifyContent: 'flex-start',
alignItems: 'flex-start',
fontFamily: 'GFSNeohellenic-Regular',
fontSize: 20,
padding: 10,
}
});
TextDetailScreen.js
const TextDetailScreen = (props) => {
const renderText = data => {
return <TextItem longText={data.item.text} />
}
return <FlatList
data={LONGTEXT}
keyExtractor={(item, index) => item.id}
renderItem={renderText}
/>
};
I think it's needless to show the code with ScrollView, since ScrollView is only for a small list.
I even tried to render the longText like this in the screen.
Without the ScrollView I get the first portion, but with ScrollView a white screen.
const TextDetailScreen = (props) => {
return (
<ScrollView>
<Text> ...longText...</Text>
</ScrollView>
);
};
I'm sure there is a way to display a lot of pages of text on a screen?
But how?
Thank you :)
It seems not to be an unknown Issue, I've also read from time to time about this issue.
But not to use Webview, because you wan't to have all Data in your app - don't have to be an Argument against Webview. With WebView, you also can display Data from your App-Storage.
Example:
<WebView style={styles.myStyle} source={{html: `<p style="font-size:48px">${longtext}</p>`}} />