React native ScrollView disable one direction on condition - react-native

is it possible to scroll only to one direction?
I have a function that detects the direction the user is a scroll.
But I can't figure out how can I set a flag, that if the user doesn't answer the question it will not allow him to scroll right only left?
thank you

I have created a small example, where only scrolling to the right is allowed. Of course this example can be adapted to allow left scrolling in specific conditions. In the code, I marked the position where to add such a condition.
Demo
Explanation
The example consists of two main parts.
Detecting the scroll direction and disable scroll if necessary
Enabling the scroll again
Detecting scroll direction
See code comments for explanation
handleScroll(event){
// WIDTH originates from Dimensions.get('screen').width
const endOfView = event.nativeEvent.contentSize.width - WIDTH;
const positionX = event.nativeEvent.contentOffset.x;
const positionY = event.nativeEvent.contentOffset.y;
// check if we are scrolling left, also detect if we are at the end of the scrollview
// MARKED: check other conditions here to allow scrolling again
if(this.state.lastPositionX > positionX && endOfView > positionX){
// we are scrolling left, disable scroll, reset the current position
this.setState({ lastPositionX: positionX, lastPositionY: positionY, allowScroll: false });
// scroll back to last valid position. Important! Otherwise users may be able to scroll left
this._scrollview.scrollTo({x: this.state.lastPositionX, y: this.state.lastPositionY});
//call the timer to enable scroll again
this.callTimer();
}else{
// we are scrolling right, everthing is fine
this.setState({ lastPositionX: positionX, lastPositionY: positionY });
}
}
Enabling scroll again:
We are making use of a timer to enable scroll again after a specified amount of time.
timerFn() {
// clear the timer again, otherwise the timer will fire over and over again
clearInterval(this.state.timer);
//enable scroll and reset timer
this.setState({allowScroll: true, timer: null });
}
callTimer() {
if (this.state.timer == null ){
// no timer is available, we create a new one. Maybe you have to fine tune the duration
let timer = setInterval(() => this.timerFn(), 1000);
this.setState({timer});
}
}
Render:
<SafeAreaView style={styles.container}>
<ScrollView
horizontal
scrollEventThrottle={15}
scrollEnabled={this.state.allowScroll}
onScroll={(event) => this.handleScroll(event)}
ref={view => this._scrollview = view}
>
<View style={{width: WIDTH, backgroundColor: 'red'}} />
<View style={{width: WIDTH, backgroundColor: 'green'}} />
<View style={{width: WIDTH, backgroundColor: 'blue'}} />
</ScrollView>
</SafeAreaView>
Working Example
https://snack.expo.io/rJAamRC2E

You can use react-native-directed-scrollview package
Package:
https://www.npmjs.com/package/react-native-directed-scrollview
This package has scrollTo({x: 100, y: 100, animated: true}) method.
if you restrict x-axis coordinate to your conditions, it will be ok.
So try something like this,
if(isAnswered == false){
scrollTo({x: /*x-axis position*/, y: /*y-axis position*/, animated: true})
}
note: you can pass false to animated parameter. It is optional.

For anyone who wants a fixed-height container with hidden content but need the RefreshControl functionality of a ScrollView, what i did was used a ScrollView but fixed its height.
When i want to show other content i animate the height/opacity of each content block to give the effect of scrolling.
<ScrollView
refreshControl={...}
style={{ height: contentBlockHeight, overflow: 'hidden' }}
contentContainerStyle={{ height: contentBlockHeight, overflow: 'hidden' }}
>
...
</ScrollView>

Related

react-native keep scrollbar in the ScrollView After to update a list

I have a list with some data in the view scrollable.
label ----- checkbox
If the user clicks on the checkbox, the button updates my reducer and refreshes the list to display if the checkbox is checked or not.
My problem concern the scrollBar, my scrollbar after every click is initialize on the top of my view.
I find a solution but ... it's not good
let scrollY = 0
function FiltersScreen(props) {
return (
<View style={{height: "90%", padding: 10}}>
<ScrollView style={{flexDirection: 'column', paddingRight: 5}}
ref={ScrollViewRef}
onContentSizeChange={(contentWidth, contentHeight) => {
crollViewRef.current.scrollTo({
y: scrollY,
animated: false,
});
}}
// keep the scrollbar position
onScroll={event => scrollY = event.nativeEvent.contentOffset.y}
onScrollEndDrag={event => scrollY = event.nativeEvent.contentOffset.y}
>
// content
</ScrollView>
</View>
)
}
It's possible to not refresh the scrollbar?
Thanks for your help.
i think that
your FiltersScreen view is a subview in another view(let call MotherView).
when MotherView re-render, the FiltersScreen view is re-render too.
It makes your scrollview re-init.
You should prevent re-render on FiltersScreen view when MotherView re-render by:
split FiltersScreen view to other file and
export React.memo(FiltersScreen);
then import your FiltersScreen View and add to MotherView.
React.memo will prevent re-render in subView if props not use in subView.

React Native: Is there a way to make components scrollable/sticky without using ScrollView?

New to react native. Here is what I am trying to do:
Render a page with three components: top panel, middle row, and content box.
When the user scrolls down, the top panel is scrolled and disappears, middle row is scrolled but is sticky to the top of the screen, and the content box is scrolled all the way down until the end of the content.
Below code serves my intention. However, when I use this code, I get warnings about nesting virtualized views.
return (
<View style={style.profileContainer}>
<ScrollView
stickyHeaderIndices={[1]}
showsVerticalScrollIndicator={false}
>
<TopPanel /> // This is a view component and is scrolled up as the user scrolls down.
<MiddleRow /> // This is a view component that is sticky to the top of the screen
<BottomArray /> //This is a FlatList
</ScrollView>
</View>
)
Below code gets rid of the warning but all the scroll/sticky behaviors of the top/middle components disappear. They just remain fixed as the user scrolls down.
return (
<View style={style.profileContainer}>
<SafeAreaView style={{flex:1}}>
<TopPanel /> // This is a view component and is fixed as the user scrolls down.
<MiddleRow /> // This is a view component and is fixed as the user scrolls down.
<BottomArray /> //This is a FlatList
</SafeAreaView>
</View>
)
Is there a way to make the top panel scrollable and middle row sticky without relying on ScrollView? This is one of the key interfaces of the app and I'd like to keep it alive.
Thanks!
I think if you put the Middle Row inside a Scroll View, that will help. I am new to react-native, so I am pretty sure this won't help you a lot and I think you would have already tried it.
If i understand correctly what you are looking to achieve, you can wrap the 3 boxes inside a view, have the top and middle inside a view and the content inside a scrollview:
<View>
<View><Text>TOP SECTION</Text></View>
<View><Text>Sticky section</Text></View>
<ScrollView><Text>...Content</Text></ScrollView>
</View>
then using animate you can animate the size (for the top section) and the position (of the sticky section) based on the scroll position of the content section.
first you define you scrollY as a ref:
const scrollY= useRef(new Animated.Value(0)).current;
then you convert the views to Animated and "link" the scrollY ref to the scrolling of the scrollview:
<View>
<Animated.View><Text>TOP SECTION</Text></Animated.View>
<Animated.View><Text>Sticky section</Text></Animated.View>
<Animated.ScrollView
scrollEventThrottle={16}
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: scrollY } } }], // attach the scroll to scrollY
{ useNativeDriver: true },
)}>
>
<Text>...Content</Text>
</Animated.ScrollView>
</View>
at this point you can define the function that handle position, size, opacity and so on based on the scrollY position, assuming you know the header height, this function interpolate (map the values) the scroll position of the scrollview from 0 to the height of the header, giving an innverse output range (from 0 to -header heigth):
const headerTranslateY = scrollY.interpolate({
inputRange: [0, HEADER_HEIGHT],
outputRange: [0, -HEADER_HEIGHT],
extrapolate: 'clamp',
});
this function can be attached to the trasform property of the style of the view that you want to animate:
<Animated.View styles={[styles.customstyle, {transform: [{ translateY: headerTranslateY }]}]><Text>TOP SECTION</Text></Animated.View>
basically with interpolate you constatly set values of something, based (in this case) of the scrolling offset of your scrollview (you can read more here on interpolation https://reactnative.dev/docs/animations#interpolation)

How to round button before rendering?

I want to create a button component that will automatically have rounded corners, no matter its dimension.
As you know, to achieve rounded corners, one way to achieve it is to specify the border radius as half of the height of the button.
The way I implemented is that in the custom component I use the onLayout function like this:
onLayout(event: LayoutChangeEvent) {
const { height } = event.nativeEvent.layout;
this.setState({ borderRadius: height / 2 });
}
The problem is that the button will initially appear on screen as a rectangle and only after a millisecond, it will round the corners causing a flicker.
My guess is that onLayout is called after the component renders.
How would one go about implementing this? Thanks!
Before the borderRadius is calculated, you could return transparent button, this would prevent this flickering effect...
// you pass radius, and height from component state
const MyButton = ({ radius, height }) => {
if (radius === null) return <View style={{ backgroundColor: transparent }}>...</View>
else return <View style={{ borderRadius: radius, backgroundColor: 'red' }}>...</View>;
};
To do this precisely, you would need to know the size that the string would take up once rendered. I wasn't able to find a React Native API for this (and I'm assuming you couldn't either), but I know both Android and iOS have such APIs. So therefore the solution would be to create a native module (https://facebook.github.io/react-native/docs/native-modules-android.html) for in iOS and Android which exposes a method called "measureText" or something. Then in each native class you'd use the corresponding API:
Android API mentioned in this answer should work: https://stackoverflow.com/a/7329169/3930970
iOS API: https://developer.apple.com/documentation/foundation/nsstring/1531844-sizewithattributes
I haven't actually tried this so I'm curious if something like this ends up working. Cheers!
You can use the lifecycle method componentWillMount() to calculate the border radius:
componentWillMount() {
const { height } = Dimensions.get('window');
radius = height / 2;
}
This method is only called one time, which is before the initial
render. Since this method is called before render().
And then, you can style your button using the calculated radius:
<TouchableOpacity
style={[styles.button, { borderRadius: radius }]}
onPress={() => alert('Hello World!') }
>
Here is a working demo.

Clickable Background underneath a ScrollView [React Native]

So, I want to create a layout similar to whats below. [Refer the Image]
So the background has a full screen MapView (React Native Maps) with Markers over it, which needs to be clickable.
And there is a Scrollview with full screen height over the MapView which initially has some top-margin associated with its contents.
But the issue if I arrange my Views this way, the Markers on the map are not clickable in the initial state.
<View>
<MapView>
<Marker clickEventHere></Marker>
<Marker clickEventHere></Marker>
</MapView>
<ScrollView fullscreen>
<View marginTop></View>
</ScrollView>
<View>
I am unsure if its really possible to solve this out.
Initial State
After Scrolling
Solution Tried
yScrolled = event.nativeEvent.contentOffset.y;
yValue = this.state.yValue - yScrolled;
upwardScroll = yScrolled > 0;
if(upwardScroll && (yValue > 0)){
this.setState({
yValue: yValue
});
}
if(yScrolled === 0){
yScrolled = -10;
}
if(!upwardScroll && (yValue <= scrollViewMarginTop)){
yValue = this.state.yValue - yScrolled;
console.debug("UPDATE DOWNWARD");
this.setState({
yValue: yValue
});
}
I'd start with adding absolute positioning to your ScrollView, (position: absolute) starting at whatever y coordinate you would like. I would make this y value a state, eg
yValue: new Animated.Value(start_value) or simply yValue: start_value
I would then use ScrollView's prop onScroll event to handle the change in this y coordinate, for example for the first 100 pixels of a scroll it would simply change the yValue instead of scrolling the view.
This should enable you to press the markers you have in your parent view, and use the same component structure that you have provided.
Note: You would need to do this for collapsing the scrollview also.
Note2: if this doesn't give you the result you were looking for, i'd suggest looking into using some sort of collapsable component for this task, eg https://github.com/oblador/react-native-collapsible
Hope this helps
EDIT: To collapse the scrollview you could first identify if the scroll direction is downwards ( which i think youve done via upwardScroll ) and then..
1) Either simply add the content offset to your yValue
2) If you used Animated.Value for yValue, you can have an animate function that animates the scrollview downwards to your desired position. I think this would look nicer as the user would only need a simple downwards flick to collapse the view, which seems like the industry standard.
There is also a library that is called react-native-touch-through-view that can do this for you. This it how it works basically:
<View style={{ flex: 1 }}>
// Your map goes here
</View>
<TouchThroughWrapper style={{
position: 'absolute'',
top: 0,
left: 0,
width: '100%',
height: '100%',
}}>
<ScrollView
style={{
backgroundColor: 'transparent',
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
// Maybe in your case is flex: 1 enough
}}
bounces={false}
showsVerticalScrollIndicator={false}
>
<TouchThroughView
style={{
height: 400, // This is the "initial space" to the top
}}
/>
// The "slide up stuff" goes here
</ScrollView>
</TouchThroughWrapper>
For example, this then can look like this:
So the map is overlayed by the TouchThroughView that passes all the events right to the view behind. In my case this worked better than using pointer-events box-none.

React Native <ScrollView> persistent scrollbar

After perusing the React Native Documentation I couldn't seem to find out how to make a <ScrollView> have a persistent scrollbar that doesn't fade out. How would I achieve that?
iOS
The underlying iOS native component, UIScrollView (technically, RCTEnhancedScrollView), doesn't support keeping the scroll indicators visible. For this reason, the React Native wrapper around it won't either.
There is a hack to get this working with the native component (see this answer for one approach). To accomplish this in React Native, you'd need to implement this hack on the native side, and then either create your own Native Module or fork React Native and modify their ScrollView component.
That said, the iOS Scroll View interface guidelines discourage this, so you may want to leave the indicators' behavior alone.
Android
A few approaches:
set <item name="android:overScrollMode">always</item>,
set android:fadeScrollbars="false" in XML, or
set ScrollView.setScrollbarFadingEnabled(false) in Java (e.g. in your custom native bridge code)
This is similarly discouraged as nonstandard UI unless you have a strong reason for it.
Adding answer since none of the above worked for me.
Android now has the persistentScrollbar props.
iOS does not support this. So I created a JS solution that can be used as follows:
<SBScrollView persistentScrollbar={true}>...</SBScrollView>
Basically, this functional component will use persistentScrollbar when on Android, while add a bar when we are on iOS. It is not smooth for now, but it is functional.
// #flow
import React, {useState} from 'react';
import {Platform, View, ScrollView} from 'react-native';
type Props = {|
persistentScrollbar?: boolean,
children?: React$Node,
|} & View.propTypes;
export default function SBScrollView({
persistentScrollbar = false,
children,
...other
}: Props) {
const [nativeEvent, setNativeEvent] = useState();
if (Platform.OS === 'android' || !persistentScrollbar) {
// Abdroid supports the persistentScrollbar
return (
<ScrollView persistentScrollbar={persistentScrollbar} {...other}>
{children}
</ScrollView>
);
}
const top = nativeEvent
? nativeEvent.contentOffset.y +
(nativeEvent.contentOffset.y / nativeEvent.contentSize.height) *
nativeEvent.layoutMeasurement.height
: 0;
// iOS does not support persistentScrollbar, so
// lets simulate it with a view.
return (
<ScrollView
scrollEventThrottle={5}
showsVerticalScrollIndicator={false}
onScroll={event => setNativeEvent(event.nativeEvent)}
{...other}>
{children}
<View
style={{
position: 'absolute',
top,
right: 4,
height: 200,
width: 4,
borderRadius: 20,
backgroundColor: 'gray',
}}
/>
</ScrollView>
);
}
I hope this can help others.
I was looking for a solution but I didn't find nothing, then I created a solution, I hope can help you with it.
I created a view View with height and width and put it over my scrollview, after that I used the Props of scrollview like onMomentumScrollBegin, onMomentumScrollEnd, onContentSizeChange and onScroll
after that I make a condition with a boolean variable, if this variable is false, the View is visible, if is false the View is hide, How do I active this variable? with the Prop onMomentumScrollBegin that detect when you use the scrollView and the same way to set the variable in false with onMomentumScrollEnd that detects when the scroll ends.
The Prop onContentSizeChange allows me to get the height and width of my scrollview, this values I used to calculate where would be set the scrollbar/scrollIndicator
and finally with the Prop onScroll I get the position.
the example:
<ScrollView
onMomentumScrollBegin={() => {this.setvarScrollState()}}
onMomentumScrollEnd={() => {this.setvarScrollStateRev()}}
scrollEventThrottle={5}
onContentSizeChange={(w, h) => this.state.hScroll = h}
showsVerticalScrollIndicator={false}
onScroll={event => { this.state.wScroll = event.nativeEvent.contentOffset.y }}
style={{ marginVertical: 15, marginHorizontal:15, width: this.state.measurements3.width}}>
{
Mydata.map((value, index) => {
return <TouchableOpacity>
<Text>{ value.MyDataItem }</Text>
</TouchableOpacity>
})
}
the functions:
setvarScrollState() {
this.setState({VarScroll: true});
}
setvarScrollStateRev() {
this.setState({VarScroll: false});
}
and the variable
this.state = {VarScroll: false}
Then my condition is
!this.state.VarScroll ?
<View
style = {{
marginTop: 200*(this.state.wScroll / this.state.hScroll),
marginLeft:338.5,
height: 35,
width: 2,
backgroundColor: 'grey',
position:'absolute'
}}
/>
: null
Why 200? because is the maximum value that my marginTop can set
Check the picture
Final note:
the scrollView have to be inside a View with the another View (scrollbar)
something like this
<View>
{/*---- ScrollBar and conditions----*/}
<View>
<View>
<ScrollView>
</ScrollView>
</View>