I haven't been able to find an answer to this question but I have seen this exact behaviour in many apps (calendars, agendas etc.). As you can see in the snippet below my container expands with scrolling to both sides - new divs are being inserted inside. When you scroll to the right it feels okay and natural, however, when you scroll to the left, it always adds the element and you stay at 0px needing to scroll a bit back and then to the left again to expand some more. Best if you try below:
import React, { useEffect, useState } from 'react';
import ReactDOM from 'react-dom/client';
function Test() {
const [span, setSpan] = useState<Array<number>>([-1, 0, 1]);
// Append item to the array - scrolling right
const append = () => {
setSpan([
...span,
span[span.length - 1] + 1,
]);
};
// Prepend item to the array - scrolling left
const prepend = () => {
setSpan([
span[0] - 1,
...span,
]);
};
// Center view on load - to the middle of element '0' - e.i. the center
useEffect(() => {
const element = document.getElementById('element-0');
if (element) {
element.scrollIntoView({ behavior: 'auto', inline: 'center' });
}
}, []);
// Register 'scroll' listener
useEffect(() => {
const element = document.getElementById('container');
const scrolling = () => {
if (element) {
if (element.scrollLeft === 0) {
prepend();
}
if (element.offsetWidth + element.scrollLeft >= (element.scrollWidth - 100)) {
append();
}
}
};
element.addEventListener('scroll', scrolling);
return () => {
element.removeEventListener('scroll', scrolling);
};
}, [span.length]);
return (
<div style={{
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}
>
<div
id="container"
style={{
maxWidth: '50vw', maxHeight: '50vh', overflowX: 'auto', whiteSpace: 'nowrap', backgroundColor: 'red',
}}
>
<div style={{ width: 'fit-content' }}>
<div style={{ width: 'fit-content' }}>
<div style={{ display: 'flex' }}>
{span.map((element) => (
<div key={`element-${element}`} id={`element-${element}`} style={{ minWidth: '40vw', minHeight: '100vh', border: '1px solid black' }}>
{ element }
</div>
))}
</div>
</div>
</div>
</div>
</div>
);
}
const root = ReactDOM.createRoot(
document.getElementById('root')
);
root.render(
<React.StrictMode>
<Test />
</React.StrictMode>
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
I tried programatically scrolling a bit to the right before prepending new item, but it only created more issues. Is there an easy way to solve it?
Prepending an element doesn't make its container's scrollLeft increase by as much as the element's width.
Instead, the scrollLeft's value remains the same, and so the new box effectively "pops" into the view:
Since, as you mentioned, the scrollLeft remains at zero after insertion, further mouse wheel movement doesn't result in the container's scroll, so the scroll event is not fired, and hence the box insertion logic is not evaluated.
That can be solved, for example, by listening for the wheel event rather than the scroll. The problem is, that the scrollLeft would still stay at zero, so the boxes would just appear in the view one by one rather than letting the user scroll onto them. Demo. Plus, the mouse wheel is not the only way to scroll.
As such, by the very definition of the problem, we need to manually adjust the scroll position so that the view remains at the same element as before the insertion. In this case, this amount is simply the offsetWidth of the box, so the solution could be as follows:
Demo
const boxWidth = document.getElementById("element-0").offsetWidth;
if (element.scrollLeft < 100) {
element.scrollBy(boxWidth, 0);
prepend();
}
else if (/*...*/) {
I hope this answers your question. :)
Related
I need to add an offset value in y to my mouse position, but when I add the offset value, the mouse position is sometime corrupted and the witness <div></div> disappear, plus the refresh is stopped until I move again my mouse.
When there is no value added, that's work fine.
I try to find a solution with useRef, useEffect, the value prevof useState but my knowledge in React is very limited, and I don't find any solution.
Here a snipped code to resume my problem.
and a link to see the problem in live :)
https://www.knupel.art/
and the code is here :
https://github.com/knupel/knupel_site/blob/master/src/components/image/image_zoom.js#L34
function Test() {
// other code
// here the code probleme
const [mouse, set_mouse] = useState({x:0,y:0});
const mouse_move = (event) => {
// let buf = {x:event.pageX, y: event.pageY} // work fine
let buf = {x:event.pageX, y:event.pageY + 42} // work, but very bad
set_mouse(buf);
}
const info_style = {
zIndex: "999px",
position: 'absolute',
height: '20px',
width: '20px',
background: 'yellow',
opacity: is_over ? 1 : 0,
left: mouse.x,
top: mouse.y,
}
// other code
return(
<div style={container_style}>
<div style={image_style}
onMouseEnter={mouse_enter}
onMouseLeave={mouse_leave}
onMouseMove={mouse_move}>
<GatsbyImage image={getImage(img)} alt={img.base}/>
</div>
<div style={info_style} ></div>
</div>
)
}
here is my sample code
in the browser i want to scroll down the page the the navbar will say what color i am showing.
<div style={{height: "800px"}}>
<h2 style={{backgroundColor: `${nav}`,
position: "fixed",
width: "100%"
}}
>
NaveBar {nav ? "red" : "blue"}!
</h2>
</div>
it's somewhat not changing the name of the title and color also.i just dun know where is the problem.
can somebody help me on this please?
You initialized the state with a string useState("red");
and then you update the state to an object with setNav({ back });
To solve this just change it to setNav(back)
By the way - listening to scroll-events can be laggy, so you might want to "throttle" the event.
import throttle from lodash or just copy paste this function:
https://gist.github.com/abhinavnigam2207/a147abe0213d60467abacd33db7c6d2e
Then you use it by wrapping your function into it, like this:
useEffect(() => {
window.addEventListener(
"scroll",
throttle(() => {
const back = window.scrollY < 70 ? "red" : "blue";
setNav(back);
}, 100)
);
});
I am trying to create an animation for moving a child element from one parent element to another using React.
A user should be able to click on an element and see it move into another div.
I made a simple demo component (without the animation) to show what I mean. When an element is clicked, the state updates and the elements are re-rendered in the correct place.
class App extends React.Component {
state = {
list: ['Alice', 'Bob', 'Charlie', 'David', 'Emily', 'Frank'],
top: [0, 1, 2],
bottom: [3, 4, 5]
}
moveDown = (item) => {
let { top, bottom } = this.state
this.setState({
top: top.filter(x => x !== item),
bottom: [...bottom, item]
})
}
moveUp = (item) => {
let { top, bottom } = this.state
this.setState({
top: [...top, item],
bottom: bottom.filter(x => x !== item)
})
}
render() {
let { top, bottom, list } = this.state
return (
<div style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
alignItems: 'center',
height: '90vh',
width: '100%'
}}>
<div>
{top.map((item) =>
<div
onClick={() => this.moveDown(item)}
style={{color:'red'}}>{list[item]}</div>
)}
</div>
<div>
{bottom.map((item) =>
<div
onClick={() => this.moveUp(item)}
style={{color:'green'}}>{list[item]}</div>
)}
</div>
</div>
)
}
}
Codepen demo: https://codepen.io/ee92/pen/LqrBjL?editors=0010
Big appreciation to and thanks in advance for any help or advice on how to achieve this div-to-div animation.
No it's not possible
It's not possible to animate in that way because the DOM thinks you're removing a div and then adding a new div. Even though it's the same div to you, the DOM doesn't have that context. Animations are controlled by changes to CSS, not HTML.
...but here's how to do it
If you actually need both lists to stay in different divs the best you can do is either:
Animate the old item to the new item position, then delete the old item and show the new item.
Remove the old item and create a new item where the old item was and move it to the new item position.
Same concept, two ways of doing it.
I modified your existing sample to show a simplified version of option 2. Note that there are a number of animation decisions to make like what happens when the list gets smaller, how should the items change from red to green, etc., and I didn't try and objectively solve them. Also, this would be much easier if you could have all the items for both lists in one div, and control their positions absolutely. But if they need to end up in separate divs...
https://codepen.io/sallf/pen/VgBwQr?editors=0010
What's going on
Adding a transition to .item we can make the animation happen when we make adjustments to the transform property.
On item click we update our lists in state and add...
transition.item to know which item is animating...
transition.startTop to know the offset y position the item should start at relative to the bottom of the list it's moving to, and...
transition.startAnim as a flag to control the animation.
Since transitions need something to change before they'll animate, we use setTimeout to delay the change of transition.startAnim which basically causes the animation from the computed position, back to 0.
I want to put content (multiple images vertically arranged) in a React Native ScrollView (iOS only for now, Android will come later) that is bigger than the phone's screen, and start zoomed out so that it is all visible at the same time.
Are there any good examples of using ScrollView.scrollResponderZoomTo in a componentDidMount call that zooms out to fit content in the screen, something like
<ScrollView
style={{width: 500, height: 1000}}
// + whatever other properties make this work required way
>
<View style={{width: 2000, height: 5000}}>
<Image style={{width: 2000, height: 2000}} source={.....}/>
<Image style={{width: 2000, height: 3000}} source={.....}/>
</View>
</ScrollView>
I tried setting the 'zoomScale' property, but that seems to be ignored and always uses the value 1.
According to this issue (https://github.com/facebook/react-native/issues/2176) there is a scrollResponderZoomTo function that can be used, but when I try to use it, it seems that no matter what values I give it it zooms out much too far and off center.
The F8 sample app has a ZoomableImage module (https://github.com/fbsamples/f8app/blob/b5df451259897d1838933f01ad4596784325c2ad/js/tabs/maps/ZoomableImage.js) which uses the Image.resizeMode.contain style to make an image fit the screen, but that loses the quality of image, so when you zoom in it gets blurry.
This may not be the way you intended to do this, but a possible solution:
You may get the devices height and width (var {height, width} = Dimensions.get('window')) and you know your image sizes,so you may easily calculate the needed width and height, let's call them var neededWidth, neededHeight;. You may then calculate the zoom to which you would like to zoom out: var zoom = Math.min(height / neededHeight, width / neededWidth);.
With these values in place, you may set an Animated value for the zoom, starting at 1 ending at zoom like this in your componentWillMount:
Animated.timing(
this.state.animatedZoom,
{toValue: zoom}
).start();
The constructor would look like this:
constructor(props) {
super(props);
this.state = {
animatedZoom: new Animated.Value(1),
};
}
The render function would look like this (reference for transform can be found here):
<ScrollView
style={{width: 500, height: 1000, transform: [{ scale: this.state.animatedZoom }]}}
>
<View style={{width: 2000, height: 5000}}>
<Image style={{width: 2000, height: 2000}} source={.....}/>
<Image style={{width: 2000, height: 3000}} source={.....}/>
</View>
</ScrollView>
I found a way to control the zoom programatically. Let's say you want to set the default zoom level when the scrollview mounts. You can use the method scrollResponderZoomTo of the ScrollView's scroll responder with a timeout.
setScrollViewRef = (ref) => {
this.mapScrollView = ref;
setTimeout(() => {
this.mapScrollView._scrollResponder.scrollResponderZoomTo({
height: this.mapSize, width: this.mapSize, animated: false,
});
}, 1);
};
And render your scrollview with a contentContainerStyle constraining the size you want.
renderMapView = () => {
this.mapSize = Dimensions.get('window').width * 2;
return (
<ScrollView
style={{ flex: 1 }}
ref={this.setScrollViewRef}
maximumZoomScale={4}
minimumZoomScale={0.5}
contentContainerStyle={{ width: this.mapSize, height: this.mapSize }}
centerContent
>
<Map width={this.mapSize} height={this.mapSize} />
</ScrollView>
);
};
This will set the default zoom level to 50% and let the whole <Map /> be visible. If you want another zoom level, you can provide a different height/width to scrollResponderZoomTo. Only works on iOS.
#Levalis's answer save my day.
scrollResponderZoomTo is currently the best shot in React Native according to this issue #2176
If anyone encounter the "zooms out much too far and off center" problem, try to call scrollResponderZoomTo with the same rect as you set in your style:
// declare image size in styles.js...
image: {
width: <image width>,
height: <image height>
}
// ...and inside your component
this.scrollView._scrollResponder.scrollResponderZoomTo({
height: <image height>, width: <image width>, animated: false,
})
I'm trying to use a ScrollView (with paging enabled) in React Native to page through a series of images. Anyone know how to make the image views fill each page of the scroll view? So far I've only had luck hard coding width and height values for the image style.
Here's roughly what I'm doing:
render: function() {
return (
var images = [{ url: 'http://url/to/image.jpg' }, { url: 'http://url/to/another-image.jpg'}];
<ScrollView horizontal={true} pagingEnabled={true} style={styles.myScrollViewStyle}>
{images.map(image => {
return (
<Image source={{uri: image.url}} style={styles.myImageStyle} />
);
})}
</ScrollView>
);
}
The only way images show up is if I hardcode a width/height number in the style. I've been unable to get the Image to just flex to fill 1 whole page.
ScrollView style:
scrollView: {
flex: 1,
backgroundColor: '#000000',
}
Image style:
image: {
width:375,
height:667,
flex: 1,
backgroundColor: 'rgba(0,0,0,0)',
}
To set image dimensions use next code:
var Dimensions = require('Dimensions');
var windowSize = Dimensions.get('window');
...
image: {
width: windowSize.width,
height: windowSize.height,
flex: 1,
backgroundColor: 'rgba(0,0,0,0)',
}
I do not believe it is possible to achieve this using only Flex right now, as it is going to try to contain all the images within your container.
ScrollView does have a contentContainerStyle={} property however, so I could envision a solution being something like setting the width of the container to be (window.width * number of items) which would then allow flex:1 on each image child do what you expect.
Unfortunately there is currently no way to fetch the window width (yet) https://github.com/facebook/react-native/pull/418
At the moment it seems like hard coding dimensions is the only option.