React Native - FlatList unable to reach bottom - javascript

I have a panel in which the keyboard is always up since I do not wish the users to dismiss it. In that panel I have a FlatList which looks like this:
<KeyboardAvoidingView style={{ flex: 1 }}>
<FlatList
// This keeps the keyboard up and disables the user's ability to hide it.
keyboardShouldPersistTaps="handled"
data={this.state.examples}
keyExtractor={(item, index) => index.toString()}
renderItem={this._renderItem}
contentContainerStyle={{ flex: 1}}
/>
</KeyboardAvoidingView>
So far so good, I have achieved what I wanted. However, when the keyboard is up - it hides the bottom part of the items rendered by the FlatList. And users cannot scroll up and view the last items because they stay behind the keyboard.
How can I preserve the Keyboard opened (and disable the ability to be dismissed) whilst being able to view and scroll through the whole content of the FlatList?

You can add a keyboard listener event to get the height of the keyboard.
this.keyboardWillShowListener = Keyboard.addListener('keyboardWillShow', (e) => {
this.setState({ keyboardHeight: e.endCoordinates.height, keyboardShow: true })
Animated.timing(this.visibleHeight, {
duration: e.duration,
toValue: 1,
easing: Easing.inOut(Easing.ease)
}).start()
})
View code like this
<Animated.View style={Platform.OS === 'android' ? { flex: 1 } : {
height: this.visibleHeight.interpolate({
inputRange: [0, 1],
outputRange: [height - this.NavHeaderHeight, height - this.state.keyboardHeight - this.NavHeaderHeight]
})
}
} >
/*Your FlatList*/
</Animated.View>
I hope it works for you

I've been to a similar situation. I had a bottom Floating Action Button at the lower right corner, hiding the last item a bit.
So, I added a fake blank item to the end of the list so that I could scroll it up a bit more.
It's simple and tricky. I hope it works for you as well, if you add a few blank itens or one wide enough blank item.
EDIT 1:
Suppose your data array is something like this: [{title: "Item 1"}, {title: "Item 2"}]
You have to concat the new blank item to the data array while passing it to the <FlatList>, like this:
<FlatList
keyboardShouldPersistTaps="handled"
data={this.state.examples.concat({title:"\n\n\n\n\n"})}
keyExtractor={(item, index) => index.toString()}
renderItem={this._renderItem}
contentContainerStyle={{ flex: 1}}/>
Adjust the amount of "\n" until you can scroll the list to be visible. There must be a minimum amount. And make sure your _renderItem don't set the item hight to a fixed value.

Related

Jumping content/unnatural move when scrolling in an element with increasing width

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. :)

MUI - How to get theme.mixins.toolbar.minHeight value responsively?

I wonder how to use mui's theme.mixins.toolbar to calculate height: calc(100vh - toolbar)?
I am currently trying to do
function SwipeCard() {
return (
<Box
sx={{
height: (theme) => `calc(100vh - ${theme.mixins.toolbar.minHeight}px)`,
paddingTop: (theme) => theme.mixins.toolbar.minHeight + "px",
}}
>
hello world
</Box>
);
}
export default SwipeCard;
But when I change viewport size and the toolbar becomes bigger. Theme.mixins.toolbar.minHeight stays the same at 56 instead of expected 64?
So I found out you can just add a second empty Toolbar to act as the padding for your content if your main Toolbar position is fixed. It comes automatically with the same media queries as the main Toolbar.
If you make this {...theme.mixins.toolbar} you will get the minHeight responsively.
So I used this in Box component that I placed above to simulate paddingTop.

React animation for moving an element from one parent to another

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.

React Native nested ScrollView locking up

I'm trying to nest ScrollViews in React Native; a horizontal scroll with nested vertical scrolls.
Here's an example:
var Test = React.createClass({
render: function() {
return (
<ScrollView
style={{width:320, height:568}}
horizontal={true}
pagingEnabled={true}>
{times(3, (i) => {
return (
<View style={{width:320, height:568}}>
<ScrollView>
{times(20, (j) => {
return (
<View style={{width:320, height:100, backgroundColor:randomColor()}}/>
);
})}
</ScrollView>
</View>
);
})}
</ScrollView>
);
},
});
AppRegistry.registerComponent('MyApp', () => Test);
The outer scroller works flawlessly, but the inner one sticks when you touch it while it's moving. What I mean is: if you scroll, lift your finger and touch it again while it's still moving with momentum, it stops and doesn't react at all to touch moves. To scroll more you have to lift your finger and touch again.
This is so reproducible it feels like something to do with the Gesture Responder.
Has anyone seen this issue?
How would I even begin to debug this? Is there a way to see what's responding to touches, granting and releasing, and when?
Thanks.
Update:
It looks like it is the responder system, by putting onResponderMove listeners on the inner and outer scrollers:
<ScrollView
onResponderMove={()=>{console.log('outer responding');}}
...
<ScrollView
onResponderMove={()=>{console.log('inner responding');}}>
...
It's clear that the outer ScrollView is grabbing control. The question, I guess, is how do I stop the outer scroller from taking control when trying to scroll vertically? And why is this only happening when you try to scroll an already moving inner ScrollView?
If you are working with RN > 56.0, just add this prop to your scroll views:
<ScrollView nestedScrollEnabled = {true}>
......
<ScrollView nestedScrollEnabled = {true}>
.....
</ScrollView>
</ScrollView>
That's the only one worked for me.
In your panresponder for the inner one, try setting this:
onPanResponderTerminationRequest: () => false
#IlyaDoroshin and #David Nathan's answer pointed me in the right direction but I had to wrap each item in the scrollview with a touchable, rather than one touchable for everything.
<ScrollView>
...
<ScrollView horizontal>
{items.map(item => (
<TouchableWithoutFeedback>
{ /* your scrollable content goes here */ }
</TouchableWithoutFeedback>
))}
</ScrollView>
</ScrollView>
Wrapping scrollable content of nested ScrollView with fixed this one for me on android:
<ScrollView>
...
<ScrollView horizontal>
<TouchableWithoutFeedback>
{ /* your scrollable content goes here */ }
</TouchableWithoutFeedback>
</ScrollView>
</ScrollView>
Modify node_modules/react-native/Libraries/Components/ScrollResponder.js: Line 136 (See UPDATE):
scrollResponderHandleScrollShouldSetResponder: function(): boolean {
return this.state.isTouching;
},
UPDATE: I find if the scroll view is currently animating and wants to become the responder, then it will reject. Line 189 in ScrollResponder.js. So I modify Line 340 and it work for me:
scrollResponderIsAnimating: function(): boolean {
// var now = Date.now();
// var timeSinceLastMomentumScrollEnd = now - this.state.lastMomentumScrollEndTime;
// var isAnimating = timeSinceLastMomentumScrollEnd < IS_ANIMATING_TOUCH_START_THRESHOLD_MS ||
// this.state.lastMomentumScrollEndTime < this.state.lastMomentumScrollBeginTime;
// return isAnimating;
return false;
},
You can see here: https://github.com/facebook/react-native/issues/41

React Native flexible view sizing within a paged scrollview

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.

Categories

Resources