How to render absoulute element inside ListView or FlatList? - react-native

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.

Related

React-native - How to create a scrollable flatList in which the chosen item is the one at the middle?

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.

How can I Animate instantly in React native

I am starting animated using 'onPanResponderMove' to move the animated view whenever the user is holding down the button
onPanResponderMove: (evt, gestureState) => {
if (withinbounds(gestureState.moveX, xOffset + buttonSize, xOffset + buttonSize * 2)) {
this.setState({y: this.state.y + 1});
Animated.timing(
this.state.layerOffset,
{toValue: {x: 0, y: (this.state.y * gridSize}, easing: Easing.in(Easing.ease)},
).start();
}
}
Which is being used to set the offset of a view
<Animated.View style={{position: 'absolute', left: this.state.layerOffset.x, top: this.state.layerOffset.y}}>
<Grid />
</Animated.View>
However, the way this is currently working, the animated view will not move until The button is stopped being press, how can I make the animation start immediately, instead of waiting for the button to no-longer be pressed although this.state.y is continually updated.
When playing around with PanResponder the way to go is usually using Animated.event instead of Animated.timing. Here's some doc that might help you out.
When using Animated.timing you should provide a duration, which you haven't done in this case. The difference between the two is that Animated.timing will change a value over a certain period of time whereas Animated.event instantly sets the value which is what you want.

Flatlist visible position

I'm trying to render an curved vertical list like this iOS component: https://github.com/makotokw/CocoaWZYCircularTableView
That component (written in Obj-c) iterates the visible cells when laying them out, and sets the frame (i.e. indent) using asin.
I know in React Native I can set the leftMargin style in the renderItem callback, but I can't figure out how to get the on-screen index of the item - all I have is the index into the source data. And also, at that point, I don't think I have access to the absolute position.
Any ideas?
The function you are looking for is
onViewableItemsChanged.
You can use it with viewabilityConfig which provides us with
minimumViewTime,viewAreaCoveragePercentThreshold,waitForInteraction
which can be set accordingly
const VIEWABILITY_CONFIG = {
minimumViewTime: 3000,
viewAreaCoveragePercentThreshold: 100,
waitForInteraction: true,
};
_onViewableItemsChanged = (info: {
changed: Array<{
key: string,
isViewable: boolean,
item: any,
index: ?number,
section?: any,
}>
}
){
//here you can have the index which is visible to you
}
<FlatList
renderItem={this.renderItem}
data={this.state.data}
onViewableItemsChanged={this._onViewableItemsChanged}
viewabilityConfig={VIEWABILITY_CONFIG}
/>
Thanks for both answers.
What I have ended up doing is deriving the visible items using the scroll offset of the list. This is simple because the list items all have the same height.
I do this in the onScroll handler, and at that point I calculate the horizontal offset for each item (and I use leftMargin / rightMargin to render this). It's not perfect, but it does give me an elliptical list.
_handleScroll = (event) => {
const topItemIndex = Math.floor(event.nativeEvent.contentOffset.y / LIST_ITEM_HEIGHT);
const topItemSpare = LIST_ITEM_HEIGHT-(event.nativeEvent.contentOffset.y % LIST_ITEM_HEIGHT);
const positionFromEllipseTop = (forIndex-topItemIndex)*LIST_ITEM_HEIGHT+topItemSpare;
const positionFromOrigin = Math.floor(Math.abs(yRadius - positionFromEllipseTop));
const angle = Math.asin(positionFromOrigin / yRadius);
if (orientation === 'Left') {
marginLeft = 0;
marginRight = ((xRadius * Math.cos(angle)))-LIST_ITEM_HEIGHT;
alignSelf = 'flex-end';
}
else if (orientation === 'Right') {
marginLeft = (xRadius * Math.cos(angle))-LIST_ITEM_HEIGHT;
marginRight = 0;
alignSelf = 'flex-start';
}
}
React-native's FlatList component has a prop called onLayout. You can get the position of the component on screen with this prop.
onLayout
Invoked on mount and layout changes with:
{nativeEvent: { layout: {x, y, width, height}}}
This event is fired immediately once the layout has been calculated,
but the new layout may not yet be reflected on the screen at the time
the event is received, especially if a layout animation is in
progress.

React-Native Horizontal Scroll View Pagination: Preview Next Page/Card

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 .

ReactNative: How to measure height of Text Input after programatic change

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)}
/>
)
}