RN gesture handler: 1 image is dragged but 2 images are moving together - react-native

In my React Native 0.62.2 app, react-native-gesture-handler 1.6.1 and react-native-animated 10.10.1 are used to make image grid draggable. The problem is that all uploaded images grids are moving together instead of individually draggable. Here is the code for draggable image grid:
import { Col, Row, Grid } from 'react-native-easy-grid';
import { PanGestureHandler } from "react-native-gesture-handler";
import Animated from "react-native-reanimated";
import FastImage from 'react-native-fast-image';
export default DisplayImages = ({pics, deleteImage}) => { //<<==component to display images passed in from 'pics'
const translateX = new Animated.Value(0) //<<==draggable related code
const translateY = new Animated.Value(0)
const handleGesture = Animated.event([{nativeEvent: {translationX: translateX,translationY:translateY}}], { useNativeDriver: true });
//VV== code below displays single image grid
const displayImg = (img_source, width, ht, index, modalWidth, modalHt) => {
let aniStyle = {
transform:[
{ translateY : translateY },
{ translateX : translateX }
]
};
return (
<>
<PanGestureHandler onGestureEvent={handleGesture}>
<Animated.View style={aniStyle}>
<TouchableOpacity onPress={()=>{setModalDialog(index)}} >
<FastImage
source={{uri:img_source}}
resizeMode={FastImage.resizeMode.cover}
key={index}
style={{
width:width,
height:ht,
verticalAlign:0,
paddingTop:0,
}}
/>
</TouchableOpacity>
</Animated.View>
</PanGestureHandler>
)
}
//VV==code blow to display 2 images as an example
return (
<Grid style={{position:"absolute", paddingTop:0,paddingLeft:0}}>
<Row style={styles.row}>
{pics.map((item, index) => {
return (displayImg(item, screen_width*half, screen_width*half, index, screen_width, item.height*(screen_width/item.width)))
})}
</Row>
</Grid>
);
}
Here is the 2 image grids were dragged towards the left together but not only one grid moved
1 image was dragged but 2 images were moving together

The property of the gesture needs to be defined for each of the grid. It can be done by moving the declaration of the property into the definition of method displayImg:
const displayImg = (img_source, width, ht, index, modalWidth, modalHt) => {
const translateX = new Animated.Value(0) //<<==draggable related code
const translateY = new Animated.Value(0)
const handleGesture = Animated.event([{nativeEvent: {translationX: translateX,translationY:translateY}}], { useNativeDriver: true });
let aniStyle = {
transform:[
{ translateY : translateY },
{ translateX : translateX }
]
};
return (
<>
<PanGestureHandler onGestureEvent={handleGesture}>
<Animated.View style={aniStyle}>
...
After that, each grid can be dragged on its own.

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;

React-Native FlatList with 3 cards paging layout

In this snack I am trying to have 3 cards in the center of the screen with a horizontal FlatList and enabled paging to jump to the next 3 cards on scroll.
But the layout starts getting destroyed after scrolling and some pixels of the next/previous card appears in the view.
How should I style this list to always have exactly 3 cards in the center of the screen and scroll will jump to the next page with the next 3 cards ? Or like the GooglePlay store, a fixed pixels of previous/next card be visible to the left and right of the main 3 cards. (Example screenshots below)
<View style={{flex:1,justifyContent: 'center', marginLeft: 5, marginRight: 5,}}>
<FlatList
horizontal
pagingEnabled
data={data}
keyExtractor={(item) => `ìtem-${item}`}
renderItem={({ item }) => (
<Card style={{width:Dimensions.get("window").width/3-5,marginRight:5}}>
{/* some content */}
</Card>
)}
/>
</View>
I do not need a library like snap-carousel or so ...
use Scrollview prop snapToOffsets to achieve that.
like google play example ( one by one ) try snack.
your example ( three by three ) try snack.
how to use snapToOffsets?
const snapToOffsetsLikeGooglePlay = data.map((x, i) => {
return ((i * itemWidth) + startScroll)
})
const snapToOffsetsLikeYourExample = data.map((x, i) => {
return ((i * (itemWidth) * previewCount) + startScroll)
})
//see the example below to know
//what is `startScroll` and `previewCount` mean?
//and how to calculate `itemWidth`?
here the full example
import React from 'react';
import {FlatList, Text} from 'react-native';
import { View, StyleSheet, ScrollView, Dimensions } from 'react-native';
const { width } = Dimensions.get('window');
//you need to preview n items.
const previewCount = 3;
//to center items
//the screen will show `previewCount` + 1/4 firstItemWidth + 1/4 lastItemWidth
//so for example if previewCount = 3
//itemWidth will be =>>> itemWidth = screenWidth / (3 + 1/4 + 1/4)
const itemWidth = width/(previewCount + .5);
//to center items you start from 3/4 firstItemWidth
const startScroll = (itemWidth * 3/4);
const App = () => {
const data = [...Array(24).keys()];
const flatlistRef = React.useRef();
React.useEffect(() => {
if (flatlistRef.current) flatlistRef.current.scrollToOffset({
offset:startScroll, animated: false
});
}, [flatlistRef]);
const snapToOffsetsLikeGooglePlay = data.map((x, i) => {
return ((i * itemWidth) + startScroll)
})
const snapToOffsets = data.map((x, i) => {
return ((i * (itemWidth) * previewCount) + startScroll)
})
return (
<FlatList
ref={flatlistRef}
style={styles.container}
pagingEnabled={true}
horizontal= {true}
decelerationRate={0}
snapToOffsets={snapToOffsets}
snapToAlignment={"center"}
data={data}
renderItem={({item, index}) => (
<View style={styles.view} >
<Text style={styles.text}>{index}</Text>
</View>
)}/>
);
}
export default App;
const styles = StyleSheet.create({
container: {
},
view: {
marginTop: 100,
backgroundColor: '#eee',
width: itemWidth - 20, //20 is margin left and right
margin: 10,
height: 140,
borderRadius: 10,
justifyContent : 'center',
alignItems : 'center',
},
text : {
fontSize : 60,
fontWeight : 'bold',
color : '#aaa',
},
});
update: start from zero as #Amir-Mousavi comment
one by one try snack
1-) comment useEffect.
2-) set const startScroll = (itemWidth * 3/4)
three by three try snack
1-) comment useEffect.
2-) set const startScroll = (itemWidth * 2.75)
Ok after much work and testing I finally was able to fix this.
snapToInterval have to snap to interval a full length of the screen.
if you use pWidth *3 it wont work. Now you may ask why, I really do not understand , it may have something to do with float values.
But if you use snapToInterval={Dimensions.get('window').width} it should work.
Have a look at snack example

In React Native, how can I adjust translation values when scaling an animated view, so that scaling appears to have an origin at an arbitrary point?

I am using PanGestureHandler and PinchGestureHandler to allow a large image to be panned and zoomed in/out on the screen. However, once I introduce the scaling transformation, panning behaves differently.
I have three goals with this implementation:
I am trying to scale down the image to fit a specific height when the view is first loaded. This is so the user can see as much of the image as possible. If you only apply a scale transformation to the image, translation values of 0 will not put the image in the upper left corner (as a result of the centered scale origin).
I am trying to make it so that when someone uses the pinch gesture to zoom, the translation values are adjusted as well to make it seem as if the zoom origin is at the point where the user initiated the gesture (React Native only currently supports a centered origin for scale transformations). To accomplish this, I assume I will need to adjust the translation values when a user zooms (if the scale is anything other than 1).
When panning after a zoom, I want the pan to track the user's finger (rather than moving faster/slower) by adjusting the translation values as they relate to the scale from the zoom.
Here is what I have so far:
import React, { useRef, useCallback } from 'react';
import { StyleSheet, Animated, View, LayoutChangeEvent } from 'react-native';
import {
PanGestureHandler,
PinchGestureHandler,
PinchGestureHandlerStateChangeEvent,
State,
PanGestureHandlerStateChangeEvent,
} from 'react-native-gesture-handler';
const IMAGE_DIMENSIONS = {
width: 2350,
height: 1767,
} as const;
export default function App() {
const scale = useRef(new Animated.Value(1)).current;
const translateX = useRef(new Animated.Value(0)).current;
const translateY = useRef(new Animated.Value(0)).current;
const setInitialPanZoom = useCallback((event: LayoutChangeEvent) => {
const { height: usableHeight } = event.nativeEvent.layout;
const scaleRatio = usableHeight / IMAGE_DIMENSIONS.height;
scale.setValue(scaleRatio);
// TODO: should these translation values be set based on the scale?
translateY.setValue(0);
translateX.setValue(0);
}, []);
// Zoom
const onZoomEvent = Animated.event(
[
{
nativeEvent: { scale },
},
],
{
useNativeDriver: true,
}
);
const onZoomStateChange = (event: PinchGestureHandlerStateChangeEvent) => {
if (event.nativeEvent.oldState === State.ACTIVE) {
// Do something
}
};
// Pan
const handlePanGesture = Animated.event([{ nativeEvent: { translationX: translateX, translationY: translateY } }], {
useNativeDriver: true,
});
const onPanStateChange = (_event: PanGestureHandlerStateChangeEvent) => {
// Extract offset so that panning resumes from previous location, rather than resetting
translateX.extractOffset();
translateY.extractOffset();
};
return (
<View style={styles.container}>
<PanGestureHandler
minPointers={1}
maxPointers={1}
onGestureEvent={handlePanGesture}
onHandlerStateChange={onPanStateChange}
>
<Animated.View style={styles.imageContainer} onLayout={setInitialPanZoom}>
<PinchGestureHandler onGestureEvent={onZoomEvent} onHandlerStateChange={onZoomStateChange}>
<Animated.View style={{ transform: [{ scale }, { translateY }, { translateX }] }}>
<Animated.Image
source={require('./assets/my-image.png')}
style={{
width: IMAGE_DIMENSIONS.width,
height: IMAGE_DIMENSIONS.height,
}}
resizeMode="contain"
/>
</Animated.View>
</PinchGestureHandler>
</Animated.View>
</PanGestureHandler>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
imageContainer: {
flex: 1,
backgroundColor: 'orange',
width: '100%',
},
});
I have tried something along the lines of subtracting the difference in dimensions from the translation values:
translateX.setValue(0 - (IMAGE_DIMENSIONS.width / 2) - (IMAGE_DIMENSIONS.width * scaleRatio / 2))
The numbers don't quite work with this implementation, so I'm probably not doing this right. Also, this would only account for my first goal, and I am guessing that I will need to do something like interpolate the translation values based on the scale value to accomplish the other two.

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

Translate a View inside an Scrollview in React Native

I'm trying to build this sticky header navbar in my RN app. Basically, an horizontal scrollview of categories that highlight the current category based on Y scrolling.
Thanks to the video of great William Candillon (https://www.youtube.com/watch?v=xutPT1oZL2M&t=1369s) I'm pretty close, but I have a main problem.
I'm using interpolation to translate the X position of category View while scrolling. And then I have a Scrollview wrapping this Animated View. The problem is that Scrollview is not functional as is does not have the reference of the position of the Animated View. As you can see in the gif below (blue -> Animated.View / red -> Scrollview)
I like the interpolation approach as it's declarative and runs on native thread, so I tried to avoid as much as possible create listener attached to scrollTo() function.
What approach would you consider?
export default ({ y, scrollView, tabs }) => {
const index = new Value(0);
const [measurements, setMeasurements] = useState(
new Array(tabs.length).fill(0)
);
const indexTransition = withTransition(index);
const width = interpolate(indexTransition, {
inputRange: tabs.map((_, i) => i),
outputRange: measurements
});
const translateX = interpolate(indexTransition, {
inputRange: tabs.map((_tab, i) => i),
outputRange: measurements.map((_, i) => {
return (
-1 *
measurements
.filter((_measurement, j) => j < i)
.reduce((acc, m) => acc + m, 0) -
8 * i
);
})
});
const style = {
borderRadius: 24,
backgroundColor: 'black',
width,
flex: 1
};
const maskElement = <Animated.View {...{ style }} />;
useCode(
() =>
block(
tabs.map((tab, i) =>
cond(
i === tabs.length - 1
? greaterOrEq(y, tab.anchor)
: and(
greaterOrEq(y, tab.anchor),
lessOrEq(y, tabs[i + 1].anchor)
),
set(index, i)
)
)
),
[index, tabs, y]
);
return (
<Animated.View style={[styles.container, {}]}>
<Animated.ScrollView
scrollEventThrottle={16}
horizontal
style={{ backgroundColor: 'red', flex: 1 }}
>
<Animated.View
style={{
transform: [{ translateX }],
backgroundColor: 'blue'
}}
>
<Tabs
onPress={i => {
if (scrollView) {
scrollView.getNode().scrollTo({ y: tabs[i].anchor + 1 });
}
}}
onMeasurement={(i, m) => {
measurements[i] = m;
setMeasurements([...measurements]);
}}
{...{ tabs, translateX }}
/>
</Animated.View>
</Animated.ScrollView>
</Animated.View>
);
};
For anyone facing this issue, I solved it by adding the following on the animated scrollview to auto scroll the to the active tab
// Tabs.tsx
const scrollH = useRef<Animated.ScrollView>(null);
let lastScrollX = new Animated.Value<number>(0);
//Here's the magic code to scroll to active tab
//translateX is the animated node value from the position of the active tab
useCode(
() => block(
[cond(
or(greaterThan(translateX, lastScrollX), lessThan(translateX, lastScrollX)),
call([translateX], (tranX) => {
if (scrollH.current && tranX[0] !== undefined) {
scrollH.current.scrollTo({ x: tranX[0], animated: false });
}
})),
set(lastScrollX, translateX)
])
, [translateX]);
// Render the Animated.ScrollView
return (
<Animated.ScrollView
horizontal
ref={scrollH}
showsHorizontalScrollIndicator={false}
>{tabs.map((tab, index) => (
<Tab ..../> ..... </Animated.ScrollView>