How to stay in position when new messages are loaded in a ScrollView in React-Native? - react-native

I have a ScrollView containing messages (most recent messages are further) in a chat application.
I limit the number of pre-loaded messages, and I dynamically load a new batch when the Scroll View is scrolled to the top. So when new messages are loaded, the current scrollPos is at 0.
The problem is that when the new messages arrive, the scrollPos stays at 0, so the user is teleported to the oldest newly loaded message.
I have tried to deal with it by manually scrolling back down to the position using the size of the content change, but this is not satisfying as the user sees a back and forth scrolling.
Can someone think of a way to do this so that the user does not see any change when the new messages appear an can simply gradually scroll up to see them.

I found a way to do it.
The idea comes from the Invertible Scroll View component: https://github.com/expo/react-native-invertible-scroll-view
I didn't use the component but implemented the idea directly on the Scroll View to have minimal changes in my code.
To explain, we translate vertically the Scroll View using the style of the Scroll View and transform: [{ scaleY: -1 }]. We do the same for the children. Then, we revert the order of the messages.
In that setup, the scrollPos() measures from the visual bottom. To trigger the loading of the new messagges, now I use
const isCloseToBottom = ({layoutMeasurement, contentOffset, contentSize}) => {
const paddingToBottom = 20;
return layoutMeasurement.height + contentOffset.y >=
contentSize.height - paddingToBottom;
};
The trick is that now, when the new messages appear on top, you have nothing to do as the distance from the user's point of view to the bottom does not change.

You can use the onContentSizeChange prop to scroll to the bottom anytime it detects a change in content size.
The scrollToEnd function may differ depending on RN version.
<ScrollView
...
onContentSizeChange={() => this.scrollView.scrollToEnd({animated: true})}>
</ScrollView>
Ref: https://reactnative.dev/docs/scrollview#scrolltoend

Related

Get the position of a fading component in react native

The situation:
I am building a tooltip that has to programmatically position itself based on an avatar component that lives in a different package.
The avatar lives in the header part of the screen and I can't colocate the tooltip to be next to it code-wise, because the tooltip needs to be dismissed once the user interacts/touches the screen.
The Problem:
is that I can't just use onLayout event on the avatar because it fades into the view and the layout event always results in { x: 0, y: 0 }, which is not accurate and doesn't reflect the actual position of the avatar.
Thanks in advance for any help or advice that you can offer!
What I tried:
Passing a callback for onLayout, but as I mentioned above it doesn't return the correct values.
Using the measure function, but it resulted in the same values as onLayout.
Using the PanResponder on that avatar and requested to respond to move events, but because the avatar doesn't move the onPanResponderMove doesn't get triggered.

How can I use react-native-gesture-handler to handle react-native-skia touches inside a ScrollView?

I have a ScrollView containing several graphs made with react-native-skia. The graphs are interactable, i.e. I can touch them and move an indicator on the graph along the x-axis of the graph.
My issue is that the whenever the ScrollView events fire (i.e. we scroll up/down), then the graph touch events are ignored which makes for bad UX.
Demo:
Here's a Snack with a reproducible demo: https://snack.expo.dev/#jorundur/supportive-raisins
FYI: For some reason for me the Canvas disappears after 1 second in the Snack, but if you make any change in the file and save (such add a newline somewhere), then it appears. It's fine if you run this locally. But that's some other problem we can ignore for the purpose of this discussion.
Description:
The demo is a react-native ScrollView large enough to make sure we have to scroll, the top component is a graph using react-native-skia. If we drag the cursor in the graph around, then the UX gets bad pretty quickly as the graph touch events seem to be ignored as soon as any vertical scrolling happens. I've tried playing around with ScrollView from react-native-gesture-handler but without luck.
To me, the expected behaviour would be for the graph to be interactable even while scrolling. I.e. if I'm pressing the graph and move my finger diagonally up/down I would expect the ScrollView to scroll and the graph cursor also to change its position accordingly. (I say diagonally since a straight vertical movement wouldn't change the cursor position in this graph.)
Much appreciated if anyone has any ideas on how to solve this! I couldn't work out how to do it via GestureDetector from react-native-gesture-handler like I've heard suggested.
Possible solution (?):
What I think I need to do is remove the onTouch={touchHandler} which I'm using currently in the react-native-skia Canvas component and instead get those touches via gesture detection from react-native-gesture-handler. I then need to make those gestures work simultaneously with the parent ScrollViews scroll event gestures. I've not had any luck implementing that unfortunately.
The solution was to do the following:
Don't use onTouch on Canvas, instead detect gestures via react-native-gesture-handler
Create a gesture and add a ref to it
Add the simultaneousHandlers prop to the ScrollView and use the ref there
This tells the ScrollView that its events should not be blocked by the touch events from the ref
To reiterate, what I wanted to do was to have the touch events of a ScrollView work simultaneously with touch events from a react-native-skia Canvas child component.
Code (relevant bits):
import * as React from 'react';
import {
GestureHandlerRootView,
ScrollView // Note that this is not imported from react-native
} from 'react-native-gesture-handler';
const App = () => {
// Create ref for the ScrollView to know what other gestures it should work simultaneously with.
// My use case required pan but this can be swapped for other gestures.
const panGestureRef = React.useRef<GestureType>(Gesture.Pan());
// The logic that used to be in `onTouch` on the Canvas is now here
const panGesture = Gesture.Pan()
.onChange((e) => {
// Do stuff, also use the other callbacks available as needed
})
.withRef(panGestureRef);
return (
<GestureHandlerRootView style={{ flex: 1 }}>{/* Don't forget this! It should be at the root of your project. */}
<ScrollView simultaneousHandlers={[panGestureRef]}>
<GestureDetector gesture={panGesture}>
<Canvas style={{ width: 200, height: 200 }}>
{/* Your interactive react-native-skia magic */}
</Canvas>
</GestureDetector>
</ScrollView>
</GestureHandlerRootView>
);
};
export default App;

React Native - Show View while Scrolling in Flatlist

I need to SHOW a view when scroll is started and HIDE a view when scroll is stopped.
To detect the scroll movement, there is two ways:
Called when the user begins to drag the scroll view.
onScrollBeginDrag={this.showView}
onScrollEndDrag={this.hideView}
Called when the momentum scroll starts and Ends
onMomentumScrollBegin={this.showView}
onMomentumScrollEnd={this.hideView}
Exptected Behaviour:
If continues scroll is happening, it should not hide the view even onScrollEndDrag is called and still show the view until onMomentumScrollEnd.
If continues scroll is not active, its should hide when onScrollEndDrag called
Actual Behaviour:
If continues scroll is happening, its hide the view when onScrollEndDrag is called and shows the view again until onMomentumScrollEnd. So in between view is disappeared and then its appears when drag released.
Call a debounced function in onScroll.
Debouncing will mean it is called at the end (or start) of a bunch of
events. More Info
// Debounce
this.ViewVisibility = lodash.debounce(this.ViewVisibility, 100);
onScroll={() => {
this.ViewVisibility();
}}
ViewVisibility = () => {
console.log('Debouncing');
this.hideView();
}

Setting an initial scroll position of a React Native ListView

I'm trying to set the initial scroll position of a ListView in react native.
Right now, I'm doing this:
componentDidMount() {
let i = this.props.images.indexOf(this.props.current);
if(i != -1) {
this.refs.listView.scrollTo({ x:i*this.props.width, y:0, animated:false });
}
}
where the images prop is the datasource for the ListView, the current prop is where I want to be initially scrolled to, and the width prop is the width of the component. (The component scrolls horizontally).
This works, but there's a delay between the initial render and the call to componentDidMount, giving me a flash of the end of end of the list before the list is scrolled.
Is there a way of setting an initial scroll position of the list? Or better way of doing this to get rid of that flash?
On iOS you can use the ScrollView#contentOffset to set the initial scroll position of a ListView, which inherits the properties of ScrollView.
If you are looking for an Android solution, the ListView.scrollTo method you are calling seems like the best bet for the general case.
That said, if I interpret your code sample correctly, you are using the ListView for a paginated, horizontal list, perhaps with the help of the pagingEnabled property? If this is the case, on Android you might look at using ViewPagerAndroid instead, and setting the initialPage property.

Sencha Touch: Removing an item then updating view on orientation change etc

thanks for the reply, looks like we might be getting somewhere. I have created a new view thats plain and in a simple form. See below:
var homePanelTop = new Ext.Panel({ id:'topCont',
cls:'topCont'
});
var homePanelBtm = new Ext.Panel({
id:'btmCont',
cls:'btmCont'
});
App.views.HomeIndex = Ext.extend(Ext.Panel, {
id:'mainlayout',
fullscreen : true,
layout: {
type: 'vbox',
align: 'stretch',
},
defaults:{flex:1},
items: [homePanelTop, homePanelBtm],
suspendLayout: true,
monitorOrientation: true,
orientationchange: this.onOrientationChange,
onOrientationChange: function(orientation, w, h){
this.suspendLayout = false;
if(orientation === 'portrait'){
console.log('P: ' + orientation);
this.add(homePanelTop, true);
} else {
console.log('L: ' + orientation);
this.remove(homePanelTop, false);
}
this.doLayout();
}
});
Ext.reg('HomeIndex', App.views.HomeIndex);
What i expect to see with the above view is on first load and portrait, there will be two panels, the top panel(yellow) and a bottom panel (blue). When I rotate as normal I still get the same effect.
But what I am after is that when I rotate to landscape the top panel (yellow) is removed and the bottom panel (blue) fills the rest of the space.
Then when I rotate back to portrait I get both panels back at their design sizes (flex:1)
Whats happening using the code above (testing in chrome) is that the top panel (yellow) remains at the top but slightly smaller in height and does not disappear like it should
Anyway notice the two console trace commands I have and these are showing the following readings on rotation:
After first load, I then rotate to landscape and the output is:
L: landscape
L: landscape
Attempted to remove a component that does not exist. Ext.Container: remove takes an argument of the component to remove. cmp.remove() is incorrect usage.
L: landscape
Attempted to remove a component that does not exist. Ext.Container: remove takes an argument of the component to remove. cmp.remove() is incorrect usage.
L: landscape
Attempted to remove a component that does not exist. Ext.Container: remove takes an argument of the component to remove. cmp.remove() is incorrect usage.
The when I rotate back to portrait I get the following output:
L: landscape
Attempted to remove a component that does not exist. Ext.Container: remove takes an argument of the component to remove. cmp.remove() is incorrect usage.
L: landscape
Attempted to remove a component that does not exist. Ext.Container: remove takes an argument
of the component to remove. cmp.remove() is incorrect usage.
P: portrait
P: portrait
So looking at this on the lanscape rotation it actually some how fires the onorientationchange function 4 times, the first one is ok the other three with an error as the first one already removed it so thats what the warnings are for I believe.
Then with the portrait ones I get two registering as lanscape calls then two hits registering portrait calls.
All this with one movement, so is this somehow causing the remove and add code's not to work as predicted and how to prevent the orientation being called four times on rotation??
If anyone has an idea on the development of this feel free to join in.
Thanks for the help so far
Si
Many years later... and more for someone like me that stumbles on to this looking for help.
Two things.
1) The message "Attempted to remove a component that does not exist" is found only in the "development" version of the ExtJS library and I don't know that it is a reliable indicator of a problem.
2) If you can upgrade to ExtJS 4.1x. It will give you access to better debugging tools. I am thinking specifically of the 'Ext JS Page Analyzer'. This will help you see what the layout engine is doing as apposed to what you hope it is doing.
See: http://www.sencha.com/blog/optimizing-ext-js-4-1-based-applications