FlatList : detect when top reached after scrolling - react-native

Is there an easy way to know when the user scrolls back up to top in a FlatList?
Specifically
FlatList renders normally
User scrolls down
And then user scrolls back to the top
I am looking to get an easy way to detect event #3.
I have looked at using onScroll and using the nativeEvent y offsets but is there an easier or more elegant solution?

The easiest way I found involves the e.nativeEvent.contentOffset.y
I noticed that the e.nativeEvent.contentOffset.y becomes 0 on Android when on top
while can become negative on iOS (over scroll)
To be able to act when user just starts to scroll from the top and when the user scrolls back right to the top, the following works fine for me.
(sample using a component)
constructor(props) {
super(props)
this.yOffset = 0;
}
onScroll = (e) => {
if (this.yOffset <= 0 && e.nativeEvent.contentOffset.y > 0) {
// case when user just started scrolling down from the top
} else if (this.yOffset > 0 && e.nativeEvent.contentOffset.y <= 0){
// case when the user scrolls back to the top
}
this.yOffset = e.nativeEvent.contentOffset.y;
}

Related

React native dynamic flatlist height

I have a FlatList that has 2 potential heights on a screen depending on what is being shown. There are top and bottom bars that show by default and when the FlatList is scrolled down, the bars are hidden and the FlatList expands to take up the full screen.
The problem is when the FlatList shifts between the 2 different heights, the content inside will shift slightly. I have already figured out that I need to apply a scroll offset of the height of the top bar (54), however i'm having issues doing that.
const [scrollOffset, setScrollOffset] = useState(0);
const [scrollDir, setScrollDir] = useState("neutral");
<FlatList ...
onScroll={(event) => {
const screenHeight = Dimensions.get('window').height - SAFE_AREA_INSETS.top - SAFE_AREA_INSETS.bottom;
const currentOffset = event.nativeEvent.contentOffset.y;
const diff = currentOffset - (scrollOffset || 0);
if (Math.abs(diff) < 3 || currentOffset < 0 || currentOffset > (event.nativeEvent.contentSize.height - screenHeight)) {
setScrollDir("neutral");
}
else if (diff < 0 && currentOffset <= (event.nativeEvent.contentSize.height - screenHeight)) {
// Top and bottom bars are shown and flatlist height is adjusted ...
if(scrollDir !== 'up') {
flatlistRef.current.scrollToOffset({ animated:false, offset:currentOffset + 54 }); // Not working as intended
}
setScrollDir('up');
}
else {
// Top and bottom bars are hidden and flatlist height is adjusted ...
if(scrollDir !== 'down') {
flatlistRef.current.scrollToOffset({ animated:false, offset:currentOffset - 54 }); // Not working as intended
}
setScrollDir('down');
}
setScrollOffset(currentOffset);
}}
The scrollToOffset method seems to trigger onScroll to think its going in the opposite direction which causes oscillations, which from the code makes sense. Any ideas to make this work correctly?
Edit:
Here is a link to a video of this happening
When I scroll up/down to unhide/hide the top and bottom bar, you'll see the content in the center jump up/down. It makes sense why this is happening, the height of the center content changes which causes the jump. I want the content to stay scrolled in the same position when changing the height.

React Native Image Slider Animation

There are 4 images. Whenever I swap them, I want to have a smooth animated transition. How may I apply that in my current code?
Click here to view the code
Also please check out the 2nd screenshot. I don't want the left swap option to be active when there is no image. But it swaps 1 extra time and stops. What is wrong with my logic?
Here is the screenshot
Have a try with the below code:
const handleSwipeLeft = () => {
if (activeImg < totalImage - 1) {
setActiveImage(currActive => currActive + 1)
}
}
as the length greater than the index.

How to render absoulute element inside ListView or FlatList?

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 - Click on scrollview items during setInterval()

I have a simple ScrollView (scrolling horizontal) with some items that each have a TouchableOpacity around them.
The onPress-method for them is just a console.log so I can see the output.
So far it works!
But I then have made a setInterval() that on each loop makes a scrollTo({x:xValue, y:0, animated:false}) on the ScrollView that increases the X-value.
This way I get like a Newsticker look and feel and it is scrolling nicely.
But when it runs, the click on each item stop working?!
I guess it has something to do with the time set for setInterval(func, time) because when I increase it to a high value it works (but ofcourse the scrolling is not nice).
So I tried made a loop-method that uses requestAnimationFrame() like below, but still nothing happens when I click on the items:
function loop(func, throttle) {
let running;
let speed = throttle || 0;
function insideLoop() {
if (running !== false) {
requestAnimationFrame(insideLoop);
speed--;
if(speed <= 0){
running = func();
speed = throttle || 0;
}
}
}
insideLoop();
}
Any ideas what I need to do?

Back button on first level of nestedlist

I have a nestedList with a few levels that appears when the user presses a button on the screen. When the nestedList appears, there is no back button (because we're at the top of the tree, so understandably there is nowhere to go back to), whereas tapping on items in the list takes you to screens with a back button.
I'd like to add a back button to the first screen. I have managed to do this, but not without adding the same back button to every sublist in the nestedList - this has the effect of 1 back button at the top level, and 2 back buttons (one to take you out of the nestledList completely, and one to take you up a level) at every subsequent level.
Can anyone help me figure out how to have 1 back button on each screen, including the top level to close the list?
Many thanks
PS a nasty workaround that I'm using at the moment is to have a "close" button in the top right of every screen instead.
I don't know how comfortable you are with the inner workings of Sencha Touch so how you go about doing this is up to you--
The back button is there, hidden, when the nested list is shown (created in the initComponent function with hidden: true), and then onBackTap, onItemTap and setActivePath will all call syncToolbar near the end of their functions which is where the back button is hidden when you are at a depth of 0.
So there are 2 places you need to do something about, first is initComponent which is easy-- just implement initComponent in your nestedList, call the superclass' initComponent and then set the backButton visible
var myNestedList = new Ext.NestedList({
...,
initComponent: function() {
myNestedList.superclass.initComponent.call(this);
this.backButton.setVisible(true);
},
...
});
That takes care of showing it intially.. how you care to deal with fixing syncToolbar is up to you. You can use Ext.override, you can straight up copy and paste the whole syncToolbar function into your nestedList object which would also override it or you could do what you're told never to do and just edit the sencha-touch.js file directly. However you decide to do it, what you're looking to change is
syncToolbar: function(card) {
...
backToggleMth = (depth !== 0) ? 'show' : 'hide';
if (backBtn) {
backBtn[backToggleMth]();
if (parentNode) {
backBtn.setText(backBtnText);
}
}
... };
You can either change backToggleMth to = 'show' or just delete the if (backBtn {...} all together.
In the Sencha Touch 2.2 I had to use a different code:
Ext.create('Ext.NestedList', {
...,
listeners: {
initialize: function(obj) {
obj.getBackButton().show();
},
back: function(obj, node, lastActiveList, detailCardActive) {
switch(node.getDepth()) {
case 1:
obj.getBackButton().show();
break;
case 0:
alert("wohooooooo!");
break;
}
}
}
}