I am trying to set up a slider inside a horizontal ScrollView that would allow me to scroll the page faster. I am able to link position of the page to the value of the slider, so that when I scroll the page, the thumb of the slider moves accordingly.
I am using React Native Slider and a ScrollView.
Here is the result that I am unfortunately having.
I am quite new to RN, so I am probably missing something important here.
class Comp extends Component {
state = {
width : 0,
value : 0,
cursor : 0
}
moveTheCursor = (val) => {
this.scrollView.scrollTo({x: val, y: 0, animated: true})
}
scrollTheView = (event) => {
this.setState({
value : Math.floor(event.nativeEvent.contentOffset.x),
cursor : Math.floor(event.nativeEvent.contentOffset.x)
})
}
checkWidth = ({nativeEvent}) => {
arrayWidth.push(nativeEvent.layout.width)
let ww = (arrayWidth[0] * this.props.data.length) - Math.round(Dimensions.get('window').width);
this.setState({
width : ww,
})
}
render() {
return (
<React.Fragment>
<ScrollView
ref={ref => this.scrollView = ref}
horizontal
style={styles.car}
scrollEventThrottle={1}
onScroll={this.scrollTheView}
showsHorizontalScrollIndicator={false}
decelerationRate={0}
//snapToInterval={200} //your element width
snapToAlignment={"center"}
>
</ScrollView>
<Slider
style={styles.containerSlide}
thumbImage={require("./../../assets/img/buttons/thumb.png")}
trackImage={require("./../../assets/img/buttons/bar.png")}
minimumValue={0}
maximumValue={this.state.width}
onValueChange={this.moveTheCursor}
value={this.state.cursor}
/>
</React.Fragment>
);
}
}
The problem is, when I use the thumb of the slider to scroll the page, it triggers the scroll that inevitably resets the position of the slider thumb, so it is not behaving correctly (flickers but it is mostly inaccurate).
Is there a way to fix this loop?
You can achieve desired behaviour using Slider's onSlidingStart and onSlidingComplete + ScrollView's scrollEnabled
It can look like
....
const [isSliding, setIsSliding] = useState(false);
....
<ScrollView scrollEnabled={!isSliding}>
<Slider
onSlidingStart={() => setIsSliding(true)}
onSlidingComplete={() => setIsSliding(false)}
/>
</ScrollView>
Related
I'm making a calculator App for react-native and I want to render different layout for buttons in landscape and portrait mode, how can I do it in a function component
my return:
return (
<View style={styles.container}>
<View style={styles.resultContainer}>
<Text style={styles.resultText}>
{displayValue}
</Text>
</View>
<View style={styles.inputContainer } >
{renderButtons()}
</View>
</View>
);
and my RenderButtons funtion
renderButtons = () =>{
height = Dimensions.get('window').height
width = Dimensions.get('window').width
if(height > width){
let layouts = buttons.map((buttonRows,index)=>{
let rowItem = buttonRows.map((buttonItems,buttonIndex)=>{
return <InputButton
value={buttonItems}
handleOnPress={handleInput.bind(buttonItems)}
key={'btn-'+ buttonIndex}/>
});
return <View style={styles.inputRow} key={'row-' + index}>{rowItem}</View>
});
return layouts
}
else
{
let layouts = buttons_landscape.map((buttonRows,index)=>{
let rowItem = buttonRows.map((buttonItems,buttonIndex)=>{
return <InputButton
value={buttonItems}
handleOnPress={handleInput.bind(buttonItems)}
key={'btn-'+ buttonIndex}/>
});
return <View style={styles.inputRow} key={'row-' + index}>{rowItem}</View>
});
return layouts
}
}
of course (height > width) condition doesn't work because apparently the values are undefined, I'm kinda new to JS so please don't be to harsh on me for not knowing the obvious
You can use react-native-orientation-locker
import Orientation from 'react-native-orientation-locker';
_onOrientationDidChange = (orientation) => {
if (orientation == 'LANDSCAPE-LEFT') {
//do something with landscape left layout
} else {
//do something with portrait layout
}
};
componentWillMount() {
//The getOrientation method is async. It happens sometimes that
//you need the orientation at the moment the js starts running on device.
//getInitialOrientation returns directly because its a constant set at the
//beginning of the js code.
var initial = Orientation.getInitialOrientation();
if (initial === 'PORTRAIT') {
//do stuff
} else {
//do other stuff
}
},
componentDidMount() {
Orientation.getAutoRotateState((rotationLock) => this.setState({rotationLock}));
//this allows to check if the system autolock is enabled or not.
Orientation.lockToPortrait(); //this will lock the view to Portrait
//Orientation.lockToLandscapeLeft(); //this will lock the view to Landscape
//Orientation.unlockAllOrientations(); //this will unlock the view to all Orientations
//get current UI orientation
/*
Orientation.getOrientation((orientation)=> {
console.log("Current UI Orientation: ", orientation);
});
//get current device orientation
Orientation.getDeviceOrientation((deviceOrientation)=> {
console.log("Current Device Orientation: ", deviceOrientation);
});
*/
Orientation.addOrientationListener(this._onOrientationDidChange);
},
componentWillUnmount: function() {
Orientation.removeOrientationListener(this._onOrientationDidChange);
}
Another approach can be Here
You can use this library react-native-orientation
Example:
import Orientation from 'react-native-orientation';
const AppScreen = () => {
// ...
const [deviceOrientation, setDeviceOrientation] = React.useState('');
React.useEffect(() => {
// The getOrientation method is async. It happens sometimes that
// you need the orientation at the moment the JS runtime starts running on device.
// `getInitialOrientation` returns directly because its a constant set at the
// beginning of the JS runtime.
// this locks the view to Portrait Mode
Orientation.lockToPortrait();
// this locks the view to Landscape Mode
// Orientation.lockToLandscape();
// this unlocks any previous locks to all Orientations
// Orientation.unlockAllOrientations();
Orientation.addDeviceOrientationListener(handleChangeOrientation);
return () => {
Orientation.removeAllListeners(handleChangeOrientation);
console.log('Bye see you soon ! 👋');
};
}, []);
const handleChangeOrientation = orientation => {
console.log(' Device orientation change to:', orientation);
setDeviceOrientation(orientation);
};
return (
<View style={{flex: 1}}>
<Text>{deviceOrientation}</Text>
</View>
);
};
export default AppScreen;
A simple use-case is to allow a user to either click buttons to paginate in a slider, or drag. Both events call the same paginate function with a param to either go forward or back--simple stuff.
However, the trigger from drag seems to cause bizarre behavior where the slider wants to start the animation from several slides back as if it ignores the updated props. This doesn't happen when using the buttons and both use the same simple paginate call.
Any tips appreciated.
Minimal example:
export default function App() {
const [position, setPosition] = useState<number>(0);
const paginate = (direction: Direction) => {
setPosition((prev) => {
return direction === Direction.Forward
? Math.max(-800, prev - 200)
: Math.min(0, prev + 200);
});
};
return (
<div className="App">
<Slider>
<Wrapper
animate={{ x: position }}
transition={{
x: { duration: 1, type: "tween" }
}}
drag="x"
dragConstraints={{
top: 0,
left: 0,
right: 0,
bottom: 0
}}
onDragEnd={(e, { offset, velocity }) => {
const swipe = swipePower(offset.x, velocity.x);
if (swipe < -swipeConfidenceThreshold) {
paginate(Direction.Forward);
} else if (swipe > swipeConfidenceThreshold) {
paginate(Direction.Back);
}
}}
>
<Slide>1</Slide>
<Slide className="alt">2</Slide>
<Slide>3</Slide>
<Slide className="alt">4</Slide>
<Slide>5</Slide>
</Wrapper>
</Slider>
<button onClick={() => paginate(Direction.Back)}>prev</button>
<button onClick={() => paginate(Direction.Forward)}>next</button>
</div>
);
}
Codesandbox Demo
I have to say, this problem is quite interesting. However, I think I figured out a way for you to handle this. One thing I noticed is that if you comment out
onDragEnd={(e, { offset, velocity }) => {
// const swipe = swipePower(offset.x, velocity.x);
// if (swipe < -swipeConfidenceThreshold) {
// paginate(Direction.Forward);
// } else if (swipe > swipeConfidenceThreshold) {
// paginate(Direction.Back);
// }
}}
the entire onDragEnd prop function, this example still doesn't work, since by the looks of things, the draggable component is not respecting your offset.
I realized that at this point, the problem is the internal state of the component is out of sync with your state. And would you look at that, the Framer Motion API actually provides a way to inspect this.
https://www.framer.com/api/motion/motionvalue/#usemotionvalue
It's the hook useMotionValue() which allows us to see what's actually happening. Turns out, our value is being set wrong when the user starts dragging:
useEffect(
() =>
motionX.onChange((latest) => {
console.log("LATEST: ", latest);
}),
[]
);
We can see this, because the state "jumps" to 200 as soon as we start dragging.
So fixing in theory is easy, we just need to make sure to let that value "know" about our offset, and that way it's gonna start with the proper offset in mind!
Anyway, that was my thought process, here's the solution, all you need to do is set the left constraint to make it work:
dragConstraints={{
top: 0,
left: position,
right: 0,
bottom: 0
}}
And tada! This makes it work. Here's my working solution: https://codesandbox.io/s/lingering-waterfall-2tsfi?file=/src/App.tsx
The error I receive is:
[Invariant Violation: [369,"RCTImageView",31,{"source":[{"__packager_asset":true,"width":52,"height":80,"uri":"...assets/assets/glass.png?platform=ios&hash=b11854c2a0e54026f26338d7c7cbf7fc?platform=ios&dev=true&hot=false&minify=false","scale":1}],"width":"<>","height":"<>","overflow":"hidden","marginTop":4,"marginHorizontal":"<>","resizeMode":"contain"}] is not usable as a native method argument. It appears that the width, height and marginHorizontal are not available on render - I calculate these values based on screen size and the error disappears when I remove the dynamic styling. This error only happens when I use useEffect. I make the dynamic styling based on the width of a container:
<View
style={styles.container}
onLayout={(event) => {
setContainerWidth(event.nativeEvent.layout.width);
}}
>
{glasses.map(() => glassContainer())}
</View>
This is the component which uses the dynamic styling.
const ratio = 78 / 50;
const width = containerWidth / 12;
const glassContainer = () => (
<View style={styles.singleGlassContainer}>
<Image
source={require("../../assets/glass.png")}
style={[
styles.glass,
{
width: width,
height: width * ratio,
marginHorizontal: containerWidth / 72,
},
]}
resizeMode="contain"
/>
</View>
);
Then finally I use useEffect like below
useEffect(() => {
renderGlasses();
}, [hydrationTarget]);
The renderGlasses function:
const renderGlasses = () => {
//Calculates amounth of glasses to reach target
const targetGlasses = Math.ceil(hydrationTarget / glassVolume);
for (let i = 0; i < targetGlasses; i++) {
setGlasses((prevArray) => [...prevArray, i]);
}
};
I think what goes wrong is useEffect fires before the dynamic styling is calculated, but I might be wrong here. Anyone knows what is causing the error, and how to fix it? If any additional code is needed please let me know.
I want to use the 'contain' feature when the vertical image comes and the 'stretch' feature when the horizontal image comes in the react-native. How do I check the horizontal or vertical status?
For example:
<Image resizeMode={status==horizantal?'contain':'stretch'} />
I would handle this in a conditional rendering function and would figure out the size using the static getSize method from the Image class like this :
buildImage = (imageUri) => {
Image.getSize(imageUri, (width, height) => {
if (width > height) {
//image is horizontal
return [<Image resizeMode={'contain'}/>];
} else {
//image is vertical
return [<Image resizeMode={'stretch'}/>];
}
});
return [];
}
render() {
return(
{this.buildImage(this.state.image)}
)
}
getSize Docs for reference.
I want to get the current y-offset of my scrollView by using a PanResponder. this is what i did so far, but i couldn't find a way to get the current y-offset
componentWillMount() {
this._panResponder = PanResponder.create({
onMoveShouldSetPanResponder: (evt, gestureState) => true,
onPanResponderRelease: (evt, gestureState) => {
console.log(this.scrollView) //this logs my scrollView
}
});
}
<ScrollView ref={(view) => this._scrollView = view} {...this._panResponder.panHandlers}>
Assuming that by touch release you mean the moment when one touch stops existing, you can do that with ScrollVIew's onTouchEnd prop.
I would do that by using onScroll to save the offset somewhere and onTouchEnd to do something with that offset.
<ScrollView
onScroll={(event) => {
this.offsetY = event.nativeEvent.contentOffset.y;
}}
onTouchEnd={(event) => {
console.log('offsetY:', this.offsetY);
console.log('touch info:', event.nativeEvent);
}}
>
{content of the scroll view}
</ScrollView>
If by touch release you meant that there should be zero touches, you can check that with the event that is passed to onTouchEnd.
Not sure why you wanna access y offset in panResponder response. But there is a way to do it. First you need to define a local variable in your component like:
this.currentScrollViewX=0;
this.currentScrollViewY=0;
Normally, you can trace the scrollView offset when scroll event happening. You can add props in ScrollView like
<ScrollView onScroll = {e => this._onScroll(e)} ...>
...
</ScrollView>
then define _onScroll method in your component, update the currentScrollViewX and currentScrollViewY when scrolling happens
_onScroll(e){
this.currentScrollViewX = e.nativeEvent.contentOffset.x;
this.currentScrollViewY = e.nativeEvent.contentOffset.x;
}
Then in your onPanResponderRelease handler, you can always get the latest y offset by referring this.currentScrollViewY
If you wanna do drag and drop animation in ScrollView using PanResponder, it will have some issues when PanResponder drag and scrollView scroll.
More detailed is here https://github.com/facebook/react-native/issues/1046