React-Native automatically scroll down - react-native

I have an app with one ScrollView and a button and a long text inside inside:
return (
<ScrollView>
<Button onPress={slowlyScrollDown} title="Slowly scroll a bit down..." />
<Text>
[ Very long text here where user has to scroll ]
</Text>
</ScrollView>
)
When the user presses the button, I want to slowly scroll down a bit, like that he can see like
the first 5-10 lines of the Text.
I would accept any answer that provides a piece of code how I can implement that.

I am not sure it will work but that is the direction:
const [offset,setOffset] = useState(0);
const scrollViewRef = useRef();
const slowlyScrollDown = () => {
const y = offset + 80;
scrollViewRef.current.scrollTo({x: 0, y, animated: true});
setOffset(y);
}
return (
<ScrollView ref={scrollViewRef} >
<Button onPress={slowlyScrollDown} title="Slowly scroll a bit down..." />
<Text>
[ Very long text here where user has to scroll ]
</Text>
</ScrollView>
)

Use scrollTo() to scroll to a given x, y offset, either immediately, with a smooth animation.
For that take a reference of the scroll view using useRef. then follow the below autoScroll() method to implement the programmatic scroll in your application.
import React, {useRef} from 'react';
import { ScrollView, Text, Button } from 'react-native';
export default function App() {
const scrollViewRef = useRef();
const autoScroll = () => {
let offset = 0;
setInterval(()=> {
offset += 20
scrollViewRef.current?.scrollTo({x: 0, y: offset, animated: true})
}, 1000)
}
return (
<ScrollView ref={scrollViewRef} >
<Button onPress={autoScroll} title="Start scrolling" />
<Text>
[ long text ]
</Text>
</ScrollView>
)
}

Related

Increase height of a View on swipe up in react native expo

I have a container that contains multiple views like this :
export default function MyComponent() {
<View *** container *** >
<View> // some stuff </View>
<View> // some stuff </View>
<ScrollView> // some stuff </ScrollView>
</View
}
The ScrollView is about 40% of the container's height, in absolute position.
What I need to do is to be able to extend it over the whole screen with a swipe up.
I tried to use some modals npm package but I can't make it work.
A few things:
From my experience, ScrollViews and FlatLists work best when they have a flex of one and are wrapped in a parent container that limits their size.
I couldnt determine if you wanted to wrap the entire screen in a GestureDector and listen to swipes or if you only wanted the ScrollView to listen for scroll events. Because you want the ScrollView to take up the entire screen, I assume you wanted to listen to onScroll events
So here's a demo I put together:
import * as React from 'react';
import {
Text,
View,
Animated,
StyleSheet,
ScrollView,
useWindowDimensions
} from 'react-native';
import Constants from 'expo-constants';
import Box from './components/Box';
import randomColors from './components/colors'
const throttleTime = 200;
// min time between scroll events (in milliseconds)
const scrollEventThrottle = 100;
// min up/down scroll distance to trigger animatino
const scrollYThrottle = 2;
export default function App() {
const scrollViewAnim = React.useRef(new Animated.Value(0)).current;
let lastY = React.useRef(0).current;
// used to throttle scroll events
let lastScrollEvent = React.useRef(Date.now()).current;
const [{ width, height }, setViewDimensions] = React.useState({});
const [isScrollingDown, setIsScrollingDown] = React.useState(false);
const [scrollViewTop, setScrollViewTop] = React.useState(400);
// scroll view is 40% of view height
const defaultHeight = height * .4;
// call onLayout on View before scrollView
const onLastViewLayout = ({nativeEvent})=>{
// combine the y position with the layout height to
// determine where to place scroll view
setScrollViewTop(nativeEvent.layout.y + nativeEvent.layout.height)
}
const onContainerLayout = ({nativeEvent})=>{
// get width and height of parent container
// using this instead of useWindowDimensions allow
// makes the scrollView scale with parentContainer size
setViewDimensions({
width:nativeEvent.layout.width,
height:nativeEvent.layout.height
})
}
//animation style
let animatedStyle = [styles.scrollView,{
height:scrollViewAnim.interpolate({
inputRange:[0,1],
outputRange:[defaultHeight,height]
}),
width:width,
top:scrollViewAnim.interpolate({
inputRange:[0,1],
outputRange:[scrollViewTop,-10]
}),
bottom:60,
left:0,
right:0
}]
const expandScrollView = ()=>{
Animated.timing(scrollViewAnim,{
toValue:1,
duration:200,
useNativeDriver:false
}).start()
}
const shrinkScrollView = ()=>{
Animated.timing(scrollViewAnim,{
toValue:0,
duration:200,
useNativeDriver:false
}).start()
}
const onScroll=(e)=>{
// throttling by time between scroll activations
if(Date.now() - lastScrollEvent <scrollEventThrottle ){
console.log('throttling!')
return
}
lastScrollEvent = Date.now()
// destructure event object
const {nativeEvent:{contentOffset:{x,y}}} = e;
const isAtTop = y <= 0
const isPullingTop = lastY <= 0 && y <= 0
let yDiff = y - lastY
let hasMajorDiff = Math.abs(yDiff) > scrollYThrottle
// throttle if isnt pulling top and scroll dist is small
if(!hasMajorDiff && !isPullingTop ){
return
}
const hasScrolledDown = yDiff > 0
const hasScrolledUp = yDiff < 0
if(hasScrolledDown){
setIsScrollingDown(true);
expandScrollView()
}
if(isAtTop || isPullingTop){
setIsScrollingDown(false)
shrinkScrollView();
}
lastY = y
}
return (
<View style={styles.container} onLayout={onContainerLayout}>
<Box color={randomColors[0]} text="Some text"/>
<Box color={ randomColors[1]} text="Some other text "/>
<View style={styles.lastView}
onLayout={onLastViewLayout}>
<Text>ScrollView Below </Text>
</View>
<Animated.View style={animatedStyle}>
<ScrollView
onScroll={onScroll}
style={{flex:1}}
>
{randomColors.map((color,i)=>
<Box color={color} height={60} text={"Item Number "+(i+1)}/>
)}
</ScrollView>
</Animated.View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
// justifyContent: 'center',
paddingTop: Constants.statusBarHeight,
padding: 8,
},
scrollView:{
// position:'absolute',
position:'absolute',
marginVertical:10,
height:'40%',
backgroundColor:'lightgray'
},
lastView:{
alignItems:'center',
paddingVertical:5,
borderBottomWidth:1,
borderTopWidth:1
}
});
The result is that on downward scrolling, the scrollview expands and takes up the entire screen, and shrinks when the user scrolls to the top.
Edit : I found that simply grabbing the y position and the height of the view directly before the scroll view made it easy to calculate where the position the ScrollView, allowing for the ScrollView to be positioned absolute all the time.
Here is a very basic example of how to use FlatList (similar to ScrollView) and allow for the scrolling behavior you are wanting:
import React from "react";
import {Text,View} from "react-native";
const App = () => {
const myData = {//everything you want rendered in flatlist}
const renderSomeStuff = () => {
return (
<View>
<Text> Some Stuff </Text>
</View>
)
};
const renderOtherStuff = () => {
return (
<View>
<Text> Other Stuff </Text>
</View>
);
};
return (
<View>
<FlatList
data={myData}
keyExtractor={(item) => `${item.id}`}
showsVerticalScrollIndicator
ListHeaderComponent={
<View>
{renderSomeStuff()}
{renderOtherStuff()}
</View>
}
renderItem={({ item }) => (
<View>
<Text>{item}</Text>
</View>
)}
ListFooterComponent={
<View></View>
}
/>
</View>
);
};
export default App;

Should all things on the map for react-native-mapbox-gl be Markers?

I'm using react-native-mapbox-gl for a project. I want to render a few custom buttons/components on the map, but every time I do I think the map covers the component. I can render the button after the mapview, but then I cannot use data from the map. I checked the component is valid by rendering it somewhere else. The component I'm trying to do is a zoom button.
I could render the component outside the and feed the new zoom prop into MapView, but would it be best practice be to have all buttons and icons on the map be markers?
I have set the Z-index of the button to 10, and the Z-index of the map to -1 but that didn't help.
I also want to have animated objects on my map that are moving. I can render the map and then not have to update it while my new markers move right?
import React, { useState } from "react";
import { View, Text } from "react-native";
import styles from "./styles";
import MapboxGL from "#rnmapbox/maps";
import ZoomButton from "../ZoomButton";
MapboxGL.setAccessToken(
"Token"
);
const coordinates = [100, 100];
const HomeMap = () => {
const markers = [
{
key: 1,
title: "first",
coordinates: [110, -50],
},
{
key: 2,
title: "second",
coordinates: [105, -50],
},
];
const zoom = 15;
return (
<View style={styles.page}>
<View position={"relative"} style={styles.container}>
<MapboxGL.MapView zoomEnabled={true} style={styles.map}>
<MapboxGL.Camera zoomLevel={14} centerCoordinate={coordinates} />
{markers.map((marker) => (
<View style={styles.pointAnnotation} key={marker.key}>
<MapboxGL.PointAnnotation
id={marker.title}
coordinate={marker.coordinates}
style={styles.pointAnnotation}
/>
</View>
))}
<View>
<ZoomButton
style={styles.zoomButton}
name={"zoom-in"}
//onPress={() => {
//this._map.getZoom().then((zoom) => {
//this.camera.zoomTo(zoom + 1);
//});
//}}
/>
<ZoomButton style={styles.zoomButton} name={"zoom-out"} />
</View>
</MapboxGL.MapView>
</View>
</View>
);
};
export default HomeMap;
Did you try moving up the <ZoomButton/> to be the first element in the MapView? I just tried it on mine and it seems to be working fine like this. I believe when the pointAnnotations are rendered they are changing what appears after them, but I am not sure without seeing the exact styles going on in this snippet.

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

React Native Scrollview: scroll to top on button click

So I have a component with ScrollView which contains many elements, so you have to scroll a long way down.
Now there should be a button at the bottom of the page that on click will scroll the page back to top.
I already created the button as a FAB (floating action button) in an extra component.
It is integrated in a parent component, where the ScrollView is located.
What I found was that you have to create a ref in the ScrollView component and implement a button right there that uses this ref to make scrolling work. Simplified, here is what I have so far:
imports ...
const ParentComponent: React.FC<Props> = () => {
const scroll = React.createRef();
return (
<View>
<ScrollView ref={scroll}>
<SearchResult></SearchResult> // creates a very long list
<FloatingButton
onPress={() => scroll.current.scrollTo(0)}></FloatingButton>
</ScrollView>
</View>
);
};
export default ParentComponent;
As you can see, there is the component FloatingButton with the onPress() method.
Here is the implementation:
import React, {useState} from 'react';
import {Container, Content, Button, Icon, Fab} from 'native-base';
const FloatingButton: React.FC<Props> = () => {
return (
<Fab
position="bottomRight"
onPress={(???}>
<Icon name="arrow-round-up" />
</Fab>
);
};
export default FloatingButton;
Now the problem: Where should I do the onPress() method? Because if I leave it in the parent component, it won't work because it is not directly located in the Fab (in FloatingButton). I would like to do the onPress() logic in Fab, but if I do so, the ScrollView that it needs is not available, because it's in the parent component. My idea was to maybe passing the ref as prop into FloatingButton, but for some reason this didn't work.
Can someone please help me?
You could either let the parent hook into the FloatingButton's onPress function or pass the ref down to the FloatingButton directly.
export const Parent : FC<ParentProps> = props => {
const scrollRef = useRef<ScrollView>();
const onFabPress = () => {
scrollRef.current?.scrollTo({
y : 0,
animated : true
});
}
return (
<View>
<ScrollView ref={scrollRef}>
{/* Your content here */}
</ScrollView>
<FloatingButton onPress={onFabPress} />
</View>
);
}
export const FloatingButton : FC<FloatingButtonProps> = props => {
const { onPress } = props;
const onFabPress = () => {
// Do whatever logic you need to
// ...
onPress();
}
return (
<Fab position="bottomRight" onPress={onFabPress}>
<Icon name="arrow-round-up" />
</Fab>
);
}
You should determine the horizontal or vertical value you want to scroll to, like this code snippet.
onPress={()=>
this.scroll.current.scrollTo({ x:0, y:0 });
}
Please have a look at my snack code. Hope it might be helpful for you.
https://snack.expo.io/#anurodhs2/restless-edamame

React Native - Keyboard avoiding not working if ScrollView is not at the top of the screen

<View>
<View style = {{height : X}}></View>
<ScrollView>
<KeyboardAvoidingView>
<View style = {{height : 350}}></View>
<TextInput/>
<View style = {{height : 500}}></View>
</KeyboardAvoidingView>
</ScrollView>
</View>
When I tap on the TextInput, it is scrolled upward but stop at the position of below where it is supposed to be as much as X, which means it is still hidden under the keyboard.
Actually the problem is not about KeyboardAvoidingView because it also happens without the use of it
This is what I did it to resolve this issue
<KeyboardAvoiding behavior={'padding'} keyboardVerticalOffset={64} style={styles.container}>
<View style={styles.container}>
<ScrollView keyboardShouldPersistTaps="always">
<View style = {{height : 350}}></View>
<TextInput/>
<View style = {{height : 500}}></View>
</ScrollView>
</View>
</KeyboardAvoiding>
container: {
flex: 1,
alignItems: 'center',
}
and this is the KeyboardAvoiding class
import React from 'react'
import { Platform, KeyboardAvoidingView as ReactNativeKeyboardAvoidingView } from 'react-native'
class KeyboardAvoidingView extends React.Component {
render() {
if (Platform.OS === 'ios') {
return (
<ReactNativeKeyboardAvoidingView behavior={'padding'} {...this.props}>
{this.props.children}
</ReactNativeKeyboardAvoidingView>
)
}
return this.props.children
}
}
KeyboardAvoidingView.propTypes = {
children: React.PropTypes.element,
}
module.exports = KeyboardAvoidingView
Hope this helps.
render() {
var a = [];
for(var i =1; i< 100; i++) a.push(i);
return (
<View>
<Button title = 'Scroll' onPress = {() => this.refs.scroll.scrollTo({x: 0, y: 0, animated: true})}/>
<ScrollView ref = 'scroll'>{a.map((item, index) => (<TextInput style = {{height : 10 + (Math.floor(Math.random() * 10) * 5)}}
placeholder = {item + ''}
onFocus = {() => {
this.refs.scroll.scrollTo({x : 0, y : this.y[index], animated : true});
}}
onLayout = {event => {
this.y[index] = event.nativeEvent.layout.y;
}}
/>))}</ScrollView>
</View>
);
}
Using this solution, there are 2 drawbacks exist which raise other problems require solutions
Not applicable in case the TextInput is inside a FlatList or perhaps certain components which contain TextInput indirectly. In this case, event.nativeEvent.layout.y will always return 0. No problem for multilevel ScrollView. Temporarily the solution is to avoid placing the TextInput inside a prop component
The TextInput's offset is relative to wrapper component if it's not the ScrollView. Scroll distance must be the summation of TextInput's offset and wrapper's offset if the wrapper is not at the top of ScrollView. Since a parent's post-layout data cannot be passed to a child directly via prop before it is rendered, the TextInput must get the wrapper's offset in it's onFocus handler
Default buggy keyboard avoiding sometimes take action after onFocus, cancelling onFocus's effect. The temporary solution is to use setTimeout to delay onFocus action for at least 200 milliseconds