I want to to a horizontal ScrollView with pagination enabled with one special requirement: each page (or card) is 90% of the container wide. The remaining 10% should be a preview of the next page.
It is possible to do this with ScrollView? Can I somehow specify the width of the pagination instead of taking the width of the container?
(image taken from this similar question: React Native Card Carousel view?)
I spend a lot of time fighting with this until I figured it out so here is my solution if it helps someone.
https://snack.expo.io/H1CnjIeDb
Problem was all these were required and pagination should be turned off
horizontal={true}
decelerationRate={0}
snapToInterval={width - 60}
snapToAlignment={"center"}
You can absolutely do that with ScrollView or, even better, FlatList. However, the really tricky part is the snapping effect. You can use props snapToInterval and snapToAlignment to achieve it (see Vasil Enchev's answer); unfortunately, these are iOS-only.
A co-worker and I created a plugin that answers this particular need. We ended up open-sourcing it, so it's all yours to try: react-native-snap-carousel.
The plugin is now built on top of FlatList (versions >= 3.0.0), which is great to handle huge numbers of items. It provides previews (the effect you're after), snapping effect for iOS and Android, parallax images, RTL support, and more.
You can take a look at the showcase to get a grasp of what can be achieved with it. Do not hesitate to share your experience with the plugin since we're always trying to improve it.
Edit : two new layouts have been introduced in version 3.6.0 (one with a stack of cards effect and the other with a tinder-like effect). Enjoy!
Use disableIntervalMomentum={ true } in your ScrollView. This will only allow the user to scroll one page at a time horizontally. Check official documents
https://reactnative.dev/docs/scrollview#disableintervalmomentum
<ScrollView
horizontal
disableIntervalMomentum={ true }
snapToInterval={ width }
>
<Child 1 />
<Child 2 />
</ScrollView>
You can pass a horizontal props to your scroll view:
https://facebook.github.io/react-native/docs/scrollview.html#horizontal
And then you can create a view inside to specify your width requirements.
<ScrollView
ref={(snapScroll) => { this.snapScroll = snapScroll; }}
horizontal={true}
decelerationRate={0}
onResponderRelease={()=>{
var interval = 300; // WIDTH OF 1 CHILD COMPONENT
var snapTo = (this.scrollingRight)? Math.ceil(this.lastx / interval) :
Math.floor(this.lastx / interval);
var scrollTo = snapTo * interval;
this.snapScroll.scrollTo(0,scrollTo);
}}
scrollEventThrottle={32}
onScroll={(event)=>{
var nextx = event.nativeEvent.contentOffset.x;
this.scrollingRight = (nextx > this.lastx);
this.lastx = nextx;
}}
showsHorizontalScrollIndicator={false}
style={styles.listViewHorizontal}
>
{/* scroll-children here */}
</ScrollView>
Here is example of simple scrollview pagination for bottom:
<ScrollView
......
onMomentumScrollEnd={event => {
if (isScrollviewCloseToBottom(event.nativeEvent)) {
this.loadMoreData();
}
}}
</ScrollView>
.....
....
function isScrollviewCloseToBottom({
layoutMeasurement,
contentOffset,
contentSize,
}) {
const paddingToBottom = 20;
return (
layoutMeasurement.height + contentOffset.y >=
contentSize.height - paddingToBottom
);
}
......
....
same as we can use this for right pagination:
function isScrollviewCloseToRight({
layoutMeasurement,
contentOffset,
contentSize,
}) {
const paddingToRight = 10;
return (
layoutMeasurement.width + contentOffset.x >=
contentSize.width - paddingToRight
);
}
Hope it will helpful..!!
You can look at contentOffset and scrollTo property of ScrollView . Logically what you can do is whenever the page changes(mostly when moved to next page) you can provide a extra offset of 10% or so as per your need so that the next item in the scrollview becomes visible .
Hope this helps, let me know if you need any extra details .
Related
I would like to create a scrollable FlatList to select only one item among a list. After the user scroll the list, the selected item will be the one in the colored rectangle (which have a fixed position) as you can see here :
Actually I'm only able to render a basic FlatList even after some researches.
Do you know how I should do that ?
I found the solution (but it's not a FlatList) !
To do that I use :
https://github.com/veizz/react-native-picker-scrollview.
To define the background of the current selected items I added a new props highLightBackgroundColor in the ScrollPicker Class in the index file of react-native-picker-scrollview :
render(){
...
let highLightBackgroundColor = this.props.highLightBackgroundColor || '#FFFFFF';
...
let highlightStyle = {
...
backgroundColor: highLightBackgroundColor,
};
...
How to use it :
<ScrollPicker
ref={sp => {
this.sp = sp;
}}
dataSource={['a', 'b', 'c', 'd', 'e']}
selectedIndex={0}
itemHeight={50}
wrapperHeight={250}
highLightBackgroundColor={'lightgreen'}
renderItem={(data, index, isSelected) => {
return (
<View>
<Text>{data}</Text>
</View>
);
}}
onValueChange={(data, selectedIndex) => {
//
}}
/>
How it looks without others customizations:
You can implement the same setup with the very popular react-native-snap-carousel package, using the vertical prop. No need to use a smaller, poorly documented/unmaintained package for this.
this is our FlatList, say hello:
<FlatList
data={this.state.dates}
...
/>
we feed it with the following dates:
this.state = {
dates: [
'21/06/2019',
'22/06/2019',
'23/06/2019',
]
};
then when the visible date changes (onViewableItemsChanged), if we end up to the first item (21/06/2019), we prepend data, so that the new state becomes:
dates: [
'18/06/2019',
'19/06/2019',
'20/06/2019',
'21/06/2019',
'22/06/2019',
'23/06/2019',
]
The Problem:
right after we prepend the data, instead of STILL seeing 21/06/2019 (which was the date when the prepend took place) we now see 19/06/2019.
That's because below the hood, 21/06/2019 used to be index 0, but after the prepend, index 0 corresponds to 19/06/2019.
What we want:
I'm trying to make it so that the day remains the same after prepending data.
Please don't tell me to use scrollToPosition because that's a hack really, not the solution.
Is there any good solution to that problem?
Thank you
There is an undocumented prop maintainVisibleContentPosition on ScrollView that do what you want, but unfortunately it's not working on android
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!
I've opened an issue on github also, but there is not any complete solution yet
Thanks to sgtpepper43 for undocumented iOS solution
that maintainVisibleContentPosition does not work if you prepend data while you are at a scroll position of 0 (all the way to the top). It does work if you are prepending while not a the top. Even scroll Y of 1 would be enough.
check this
Unfortunately this still an issue even in React Native 0.63.3 and has not been solved.
Leaving this here, since it took me a while to get a working solution/ workaround to this problem that doesn't leave a bad taste in my mouth: prepopulate the array with empty data, and then use the scrollToIndex method.
this.state = {
dates: [
'',
'',
'',
'21/06/2019',
'22/06/2019',
'23/06/2019',
]
};
And then:
<FlatList
data={this.state.dates}
ref={flatListRef}
getItemLayout={(data, index) => (
{length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index}
)}
...
/>
inside your componentDidMount:
const startingIndex = 4
flatListRef.current.scrollToIndex({index: startingIndex, animated: false, viewPosition: 0})
There's a library: https://github.com/GetStream/react-native-bidirectional-infinite-scroll that allows you to both preprend or append data and preserve scroll position
It's basically an extension over the FlatList and supports all the props available for a plain FlatList
From their example usage tutorial: https://dev.to/vishalnarkhede/react-native-how-to-build-bidirectional-infinite-scroll-32ph#%F0%9F%96%A5-tutorial-chat-ui-with-bidirectional-infinite-scroll
It looks like it would be enough to just prepend that data to the top exactly like you're trying to do
I have a FlatList as shown below:
<FlatList
inverted
data={messages}
keyExtractor={this._keyExtractor}
renderItem={({ item }) => (
<Text style={styles.item}>{item}</Text>
)}
onEndReached={this.handleLoadMore}
onEndReachedThreshold={30}
/>
But here the OnEndReached method does not gets called when I reach the top of the flatlist.
Please help
OnEndReachedThreshold must be a number between 0 and 1. Since you are inverting your flatlist, onEndReachedThreshold would be the distance the user is from the top of the list [in percents]. Therefore a value of 0.5 would trigger the OnEndReached function when the user has scrolled through 50% of the viewable list.
To trigger the function at 50% your code should read something like this:
<FlatList
inverted
data={messages}
keyExtractor={this._keyExtractor}
renderItem={({ item }) => (
<Text style={styles.item}>{item}</Text>
)}
onEndReached={this.handleLoadMore}
onEndReachedThreshold={0.5}
/>
All I could figure out was using of onScroll (performance beware) in here: https://snack.expo.io/#zvona/inverted-list-onbeginreached
The actual function looks like this:
checkIfBeginningReached = ({ nativeEvent }) => {
const { layoutMeasurement, contentOffset } = nativeEvent;
const currentPos = layoutMeasurement.height + contentOffset.y;
const listLength = ITEM_HEIGHT * this.state.items.length;
const reactThreshold = listLength - (ITEM_HEIGHT * THRESHOLD);
if (currentPos >= reactThreshold) {
this.fetchMoreItems(this.state.items.length);
}
}
On that, we pick up necessary info from nativeEvent (which kind of holds everything relevant). Then we just calculate the current position in pixels, length of whole list content in pixels and then threshold point.
In all, this particular solution requires two things:
1) list has fixed and same size of elements
2) list is not multi-column.
All the other functionality in the demo is just faking / mimicking one use case (of fetching 50 more items from server with 500ms delay). But I'll improve my answer if possible. But this should get you started.
My solution is here:
isCloseToBottom = ({layoutMeasurement, contentOffset, contentSize}) => {
const paddingToBottom = 1
return layoutMeasurement.height + contentOffset.y >=
contentSize.height - paddingToBottom}
just set onEndReachedThreshold={0.1} or onEndReachedThreshold={0.2}
Here's a fun one i've been poking at for while:
I have a FlatList (same issue with ListView) and I want to render an element INSIDE the internal scrolling container with the following characteristics:
Absolutely Positioned (thus having no effect on position of list elements)
Position XX distance from top (translateY or top)
zIndex (above list elements)
The use case is i'm rendering a day view calendar grid with a horizontal bar at the current time position fixed at X distance from the beginning of the internal scrollview so it appears as the user scrolls pass that position.
So far i've tried wrapping wrapping FlatList/ListView with another ScrollView... also tried rendering this element as the header element which only works while the header/footer are visible (trashed when out of view).
Any and all ideas welcomed. :)
Thanks
Screenshot Below (red bar is what i'm trying to render):
Here's a working demo of what it sounds like you're trying to achieve: https://sketch.expo.io/BkreW1che. You can click "preview" to see it in your browser.
And here's the main code you need to measure the height of the ListView and place the indicator on top of it (visit the link above to see the full source):
handleLayout(event) {
const { y, height } = event.nativeEvent.layout;
// Now we know how tall the ListView is; let's put the indicator in the middle.
this.setState({ indicatorOffset: y + (height / 2) });
}
renderIndicator() {
const { indicatorOffset } = this.state;
// Once we know how tall the ListView is, put the indicator on top.
return indicatorOffset ? (
<View style={[{ position: 'absolute', left: 0, right: 0, top: indicatorOffset }]} />
) : null;
}
render() {
return (
<View style={styles.container}>
<ListView
onLayout={(event) => this.handleLayout(event)}
dataSource={this.state.dataSource}
renderRow={this.renderRow}
/>
{this.renderIndicator()}
</View>
);
}
Edit: I now understand that you want the indicator to scroll along with the list. That's a simple change from above, just add an onScroll listener to the ListView: https://sketch.expo.io/HkEjDy92e
handleScroll(event) {
const { y } = event.nativeEvent.contentOffset;
// Keep the indicator at the same position in the list using this offset.
this.setState({ scrollOffset: y });
},
With this change, the indicator actually seems to lag behind a bit because of the delay in the onScroll callback.
If you want better performance, you might consider rendering the indicator as part of your renderRow method instead. For example, if you know the indicator should appear at 10:30 am, then you would render it right in the middle of your 10am row.
React Native has documentation for AutoExpandingTextInput: https://facebook.github.io/react-native/docs/textinput.html
The Problem: When the content of the AutoExpandingTextInput is changed programmatically the height never changes.
For example:
componentWillReceiveProps(props) {
this.setState({
richText: this._addHighlights(props.richText)
});
}
//
<AutoExpandingTextInput ref={component => this._text = component}>
{this.state.richText}
</AutoExpandingTextInput>
Say, for example. the user hits a button that adds a link to the text that wraps to the next line; in this case, the AutoExpandingTextInput never expands, because the height only is measured & changed on the onChange event of the TextInput.
I need some work around to get the content height when no onChange is triggered --- or less ideally, a way to programmatically trigger an onChange to the TextInput.
Are there any solutions????
No need to use the AutoExpandingTextInput plugin any more. The functionality you need is supported (sort of) in react-native now and will resize with a programatic update. Try something like this:
_heightChange(event) {
let height = event.nativeEvent.contentSize.height;
if (height < _minHeight) {
height = _minHeight;
} else if (height > _maxHeight) {
height = _maxHeight;
}
if (height !== this.state.height) {
this.setState({height: height});
}
}
render() {
return (
<TextInput
{...this.props}
multiline={true}
onContentSizeChange={this._heightChange.bind(this)}
/>
)
}