react-native drag and drop multiple items - react-native

Im trying to make two circles that can drag and drop with react-native.
I could have created one circle that can drag and drop, but dont know how with two circles individually.
here is the code for one circle that can drag and drop,
constructor(props){
super(props);
this.state = {
pan : new Animated.ValueXY() //Step 1
};
this.panResponder = PanResponder.create({ //Step 2
onStartShouldSetPanResponder : () => true,
onPanResponderMove : Animated.event([null,{ //Step 3
dx : this.state.pan.x,
dy : this.state.pan.y
}]),
onPanResponderRelease : (e, gesture) => {} //Step 4
});
}
and this is for image
renderDraggable(){
return (
<View style={styles.draggableContainer}>
<Animated.View
{...this.panResponder.panHandlers}
style={[this.state.pan.getLayout(), styles.circle]}>
<Text style={styles.text}>Drag me!</Text>
</Animated.View>
</View>
);
}

import React, { Component } from 'react';
import {
StyleSheet,
Text,
View,
Image, // we want to use an image
PanResponder, // we want to bring in the PanResponder system
Animated // we wil be using animated value
} from 'react-native';
export default class MovingCircle extends React.Component {
constructor(props) {
super(props);
this.state = {
pan: new Animated.ValueXY(),
scale: new Animated.Value(1)
};
}
_handleStartShouldSetPanResponder(e, gestureState) {
return true;
}
_handleMoveShouldSetPanResponder(e, gestureState) {
return true;
}
componentWillMount() {
this._panResponder = PanResponder.create({
onStartShouldSetPanResponder:
this._handleStartShouldSetPanResponder.bind(this),
onMoveShouldSetPanResponder:
this._handleMoveShouldSetPanResponder.bind(this),
onPanResponderGrant: (e, gestureState) => {
// Set the initial value to the current state
this.state.pan.setOffset({x: this.state.pan.x._value, y: this.state.pan.y._value});
this.state.pan.setValue({x: 30*Math.random(), y: 0});
Animated.spring(
this.state.scale,
{ toValue: 1.1, friction: 1 }
).start();
},
// When we drag/pan the object, set the delate to the states pan position
onPanResponderMove: Animated.event([
null, {dx: this.state.pan.x, dy: this.state.pan.y},
]),
onPanResponderRelease: (e, {vx, vy}) => {
// Flatten the offset to avoid erratic behavior
this.state.pan.flattenOffset();
Animated.spring(
this.state.scale,
{ toValue: 1, friction: 1 }
).start();
}
});
}
render() {
// Destructure the value of pan from the state
let { pan, scale } = this.state;
// Calculate the x and y transform from the pan value
let [translateX, translateY] = [pan.x, pan.y];
let rotate = '0deg';
// Calculate the transform property and set it as a value for our style which we add below to the Animated.View component
let imageStyle = {transform: [{translateX}, {translateY}, {rotate}, {scale}]};
return (
<Animated.View style={[imageStyle, styles.container]} {...this._panResponder.panHandlers} >
<View style={styles.rect}>
<Text style={styles.txt} >tgyyHH</Text>
</View>
</Animated.View>
);
}
}
const styles = StyleSheet.create({
container: {
width:50,
height:50,
position: 'absolute'
},
rect: {
borderRadius:4,
borderWidth: 1,
borderColor: '#fff',
width:50,
height:50,
backgroundColor:'#68a0cf',
},
txt: {
color:'#fff',
textAlign:'center'
}
});

Here is how made items independent of each other. This example is in typescript, but should be clear enough to convert to pure javascript. The main idea here is that each animated item needs its own PanResponderInstance and once you update the items, you need to also refresh the PanResponderInstance
interface State {
model: Array<MyAnimatedItem>,
pans: Array<Animated.ValueXY>,
dropZone1: LayoutRectangle,
dropZone2: LayoutRectangle,
}
public render(): JSX.Element {
const myAnimatedItems = new Array<JSX.Element>()
for (let i = 0; i < this.state.model.length; i++) {
const item = this.state.model[i]
const inst = this.createResponder(this.state.pans[i], item)
myAnimatedItems.push(
<Animated.View
key={'item_' + i}
{...inst.panHandlers}
style={this.state.pans[i].getLayout()}>
<Text>{item.description}</Text>
</Animated.View>
)
}
return (
<View>
<View onLayout={this.setDropZone1} style={styles.dropZone}>
<View style={styles.draggableContainer}>
{myAnimatedItems}
</View>
</View>
<View onLayout={this.setDropZone2} style={styles.dropZone}>
<View style={styles.draggableContainer}>
...
</View>
</View>
</View>
)
}
private setDropZone1 = (event: LayoutChangeEvent): void => {
this.setState({
dropZone1: event.nativeEvent.layout
})
}
private setDropZone2 = (event: LayoutChangeEvent): void => {
this.setState({
dropZone2: event.nativeEvent.layout
})
}
private isDropZone(gesture: PanResponderGestureState, dropZone: LayoutRectangle): boolean {
const toolBarHeight = variables.toolbarHeight + 15 // padding
return gesture.moveY > dropZone.y + toolBarHeight
&& gesture.moveY < dropZone.y + dropZone.height + toolBarHeight
&& gesture.moveX > dropZone.x
&& gesture.moveX < dropZone.x + dropZone.width
}
private createResponder(pan: Animated.ValueXY, item: MyAnimatedItem): PanResponderInstance {
return PanResponder.create({
onStartShouldSetPanResponder: () => true,
onPanResponderMove: Animated.event([null, {
dx: pan.x,
dy: pan.y
}]),
onPanResponderRelease: (_e, gesture: PanResponderGestureState) => {
const model = this.state.model
const pans = this.state.pans
const idx = model.findIndex(x => x.id === item.id)
if (this.isDropZone(gesture, this.state.dropZone1)) {
... // do something with the item if needed
// reset each PanResponderInstance
for (let i = 0; i < model.length; i++) {
pans[i] = new Animated.ValueXY()
}
this.setState({ model: model, pans: pans })
return
}
} else if (this.isDropZone(gesture, this.state.dropZone2)) {
... // do something with the item if needed
// reset each PanResponderInstance
for (let i = 0; i < model.length; i++) {
pans[i] = new Animated.ValueXY()
}
this.setState({ model: model, pans: pans })
return
}
Animated.spring(pan, { toValue: { x: 0, y: 0 } }).start()
this.setState({ scrollEnabled: true })
}
})
}

Related

How to scroll FlatList to some index immediately after scrolling?

How to scroll FlatList component to some index/children after dragging the FlatList?
For Example:
As we can see in Youtube/TikTok stories, when we drag the screen the next video appears immidiatly after it. So, I am implementing it with FlatList, if we drag the item below then FlatList should move to the above item/index. So, what I am doing is that storing the currently displayed index and on onScrollEndDrag prop I am checking the position of Y, and accordingly run scrollToIndex function, but it's not working.
Reason
Because while scrolling after drag, FlatList ignores the scrollToIndex function.
Is their anyone to help me out of it???
import React from 'react';
import { View, Text, StyleSheet, AppState, FlatList, Animated, Dimensions } from 'react-native';
import fetchDataFromDirectory from '../data/fetchDataFromWhatsApp';
import PlayerVideo from '../components/VideoPlayer';
import Image from '../components/Image';
const { width, height } = Dimensions.get('window');
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList);
class VideoScreen extends React.Component {
state = {
pdfInfo: [], //[{id, name, path},...]
appState: '',
viewableIndex: 0
}
fetchData = async () => {
const data = await fetchDataFromDirectory('videos');
this.setState({ pdfInfo: data.pdfInfo });
}
componentDidUpdate(prevProps, prevState) {
if (this.state.pdfInfo.length > this.dataLength) { //We are seeing if we need to scroll to top or not
this.dataLength = this.state.pdfInfo.length;
try {
this.list.scrollToIndex({ animated: true, index: 0, viewPosition: 0 })
} catch (err) {
}
}
}
handleAppStateChange = (nextAppState) => {
//the app from background to front
if (this.state.appState.match(/inactive|background/) && nextAppState === 'active') {
this.fetchData();
}
//save the appState
this.setState({ appState: nextAppState });
}
componentDidMount() {
this.videoHeight = height;
this.dataLength = 0;
this.fetchData();
AppState.addEventListener('change', this.handleAppStateChange);
}
onViewableItemsChanged = ({ viewableItems, changed }) => {
// console.log("Visible items are", viewableItems);
// console.log("Changed in this iteration", changed);
try {
this.setState({ viewableIndex: viewableItems[0]['index'] })
} catch (err) {
}
}
componentWillUnmount() {
AppState.removeEventListener('change', this.handleAppStateChange)
}
render() {
return <AnimatedFlatList
onLayout={(e) => {
const { height } = e.nativeEvent.layout;
this.videoHeight = height;
}}
// onResponderRelease={e => {console.log(e.nativeEvent.pageY)}}
// onResponderRelease={(e) => console.log(e.nativeEvent.)}
// onScrollBeginDrag
// snapToAlignment={'top'}
// onMoveShouldSetResponderCapture={(e) => {e.nativeEvent.}}
// decelerationRate={'fast'}
decelerationRate={'fast'}
scrollEventThrottle={16}
// onScroll={(e) => console.log('+++++++++++++++++',Object.keys(e), e.nativeEvent)}
onScrollEndDrag={(e) => {
// this.list.setNativeProps({ scrollEnabled: false })
console.log(e.nativeEvent)
if (e.nativeEvent.velocity.y > 0.1) {
console.log('go to above')
this.list.scrollToIndex({animated: true, index: this.state.viewableIndex - 1, viewPosition: 0})
} else if (e.nativeEvent.velocity.y < -0.9){
console.log('go to below')
this.list.scrollToIndex({animated: true, index: this.state.viewableIndex + 1, viewPosition: 0})
}
// if (e.nativeEvent.velocity.y < 0.1000 && e.nativeEvent.velocity.y >= 0) {
// this.list.scrollToIndex({animated: true, index: this.state.viewableIndex, viewPosition: 0})
// }
// else if (e.nativeEvent.velocity.y < -0.1000 && e.nativeEvent.velocity.y < 0) {
// this.list.scrollToIndex({animated: true, index: this.state.viewableIndex, viewPosition: 0})
// }
// this.list.setNativeProps({ scrollEnabled: true })
console.log('h1')
}}
viewabilityConfig={{
// itemVisiblePercentThreshold: 90,
viewAreaCoveragePercentThreshold: 60
}}
// extraData={this.state.viewableIndex}
onViewableItemsChanged={this.onViewableItemsChanged}
// scr
contentContainerStyle={styles.screen}
data={this.state.pdfInfo}
keyExtractor={item => item.id}
ref={ref => this.list = ref}
renderItem={({ item, index }) => {
// console.log(index)
return <PlayerVideo
source={item.path}
refList={this.list}
height={this.videoHeight}
index={index}
isViewable={this.state.viewableIndex == index ? true : false} />
}}
</View>
}}
/>
}
}
export default VideoScreen;
const styles = StyleSheet.create({
screen: {
backgroundColor: '#111212',
// flex: 1
}
})
First add a ref flatListRef to the flatlist:
<Flatlist
ref={(ref) => this.flatListRef = ref}
data={data}
keyExtractor={keyExtractor}
renderItem={renderItem}
onScrollBeginDrag={onBeginScroll}/>
/>
Then define the onScrollBeginDrag function as below to scroll to a specific index:
onBeginScroll = () => {
this.flatListRef._listRef._scrollRef.scrollToIndex({ animating: true, index: [YOUR_INDEX_HERE] });
}
yes you should access the element _listRef then _scrollRef then call the scrollToIndex
react-native 0.64.1
react 17.0.2

React Native Pan Responder to Animate single dynamic view

I'm creating a series of cubes within a grid and I want to only move the cube that is pressed upon. Currently in my code it is moving all the cubes as one. I tried finding the individual key of the cube to try and target it for animation, but I'm not having any luck so far in figuring this out.
The myPanel component is just to layout the cubes that are defined in the renderCube function. It may be an issue with laying out all of the cubes with Animated.View, even though I added the key, but I can't access or figure out how to specifically animate only the selected cube.
import React, { Component } from 'react';
import { TouchableWithoutFeedback, View, ImageBackground, Dimensions, Animated, PanResponder } from 'react-native';
import myPanel from './myPanel';
const DATA = [
{ id: 1, color: '#ff8080' },
{ id: 2, color: '#80ff80' },
{ id: 3, color: '#ffff80' }
];
const winWidth = Dimensions.get('window').width;
const widthCalc = winWidth/400;
const cubeWidth = widthCalc*74;
const winHeight = Dimensions.get('window').height;
const heightCalc = ((winHeight-winWidth)/2)-5;
const xPush = 5*widthCalc;
const borRadius = 7.4*widthCalc;
let cnt = 0;
let xMove = 0;
let yMove = 0;
let currTarget = '033';
class Board extends Component {
constructor(props) {
super(props);
this.state = {
pan: new Animated.ValueXY()
};
}
componentWillMount() {
this._panResponder = PanResponder.create({
onMoveShouldSetResponderCapture: () => true,
onMoveShouldSetPanResponderCapture: () => true,
onStartShouldSetPanResponder:(e, gestureState) => true,
onPanResponderGrant: (e, gestureState) => {
console.log(this.currTarget);
},
onPanResponderMove: Animated.event([
null,
{
dx: this.state.pan.x,
dy: this.state.pan.y,
},
]),
onPanResponderRelease: () => {
Animated.spring(
this.state.pan,
{toValue: {x: 0, y: 0}},
).start();
}
});
}
renderCube(item) {
return (
<Animated.View {...this._panResponder.panHandlers}
style={this.state.pan.getLayout()} key={item.id}>
<TouchableWithoutFeedback
onPressIn={()=>{this.currTarget = item.id; console.log('Set '+item.id);}}>
<View
style={{
backgroundColor:myColor,
width:cubeWidth,
height:cubeWidth,
position: 'absolute',
top: yMove,
left: xMove,
bottom: 0,
right: 0,
borderRadius:borRadius
}}
></View>
</TouchableWithoutFeedback>
</Animated.View>
);
}
render() {
return (
<myPanel
data={DATA}
renderCube={this.renderCube.bind(this)}
/>
);
}
}
export default Board;

after onPanResponderRelease,auto animation moving does not work correctly

My sample code is to auto move a distance after onPanResponderRelease of PanResponder(looks like a object moving inertia).
When I drag the content and release finger(or release dragging in simulator), the content works correctly(it moved a distance automaticlly).
But when I touch quickly(or click once in simulator),the content moves a distance and back to other position.It has problem because the content should not move with a quick touching.
What's the problem?
Here is my code(it can directly paste and run in app.js).
import React from 'react';
import { View, StyleSheet, PanResponder, Animated, Text, Button, } from 'react-native';
export default class App extends React.Component {
constructor(props) {
super(props);
this._layout = { x: 0, y: 0, width: 0, height: 0, };
this._value = { x: 100, y: 100, };
}
componentWillMount() {
this._animatedValue = new Animated.ValueXY({ x: 100, y: 100 });
this._animatedValue.addListener((value) => {
this._value = value;
});
this._panResponder = PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => true,
onMoveShouldSetPanResponder: (evt, gestureState) => ((gestureState.dx != 0) && (gestureState.dy != 0)),
onPanResponderGrant: (e, gestureState) => {
this._animatedValue.setOffset({ x: this._value.x, y: this._value.y });
},
onPanResponderMove: Animated.event([
null,
{ dx: this._animatedValue.x, dy: this._animatedValue.y },
]),
onPanResponderRelease: () => {
this._animatedValue.flattenOffset();
Animated.timing(
this._animatedValue,
{
toValue: { x: this._value.x, y: this._value.y + 100, },
duration: 600,
}
).start(() => {
// animation finished
});
}
});
}
render() {
return (
<View style={styles.container}>
<Animated.View
style={[
styles.box,
{
left: this._animatedValue.x,
top: this._animatedValue.y,
},
]}
{...this._panResponder.panHandlers}
>
<Text>{'This is a test'}</Text>
<Button
title={'Click Me'}
onPress={() => console.log('Yes, I clicked.')}
/>
</Animated.View>
</View>
);
}
}
var styles = StyleSheet.create({
container: {
flex: 1,
},
box: {
width: 200,
height: 200,
borderWidth: 1,
},
});
Thanks a lot!
I'm not sure how much this matters but you are not adding the event listener in the suggested way: https://facebook.github.io/react-native/docs/animated#event
Try adding it like this:
componentWillMount() {
this._animatedValue = new Animated.ValueXY({ x: 100, y: 100 });
// REMOVE THIS
//this._animatedValue.addListener((value) => {
// this._value = value;
//});
this._panResponder = PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => true,
onMoveShouldSetPanResponder: (evt, gestureState) => ((gestureState.dx != 0) && (gestureState.dy != 0)),
onPanResponderGrant: (e, gestureState) => {
this._animatedValue.setOffset({ x: this._value.x, y: this._value.y });
},
onPanResponderMove: Animated.event([
null,
{ dx: this._animatedValue.x, dy: this._animatedValue.y },
],
{
SEE HERE--->>> listener: this.onMove
}
),
onPanResponderRelease: () => {
this._animatedValue.flattenOffset();
Animated.timing(
this._animatedValue,
{
toValue: { x: this._value.x, y: this._value.y + 100, },
duration: 600,
}
).start(() => {
// animation finished
});
}
});
}
AND SEE HERE --->>>
onMove() {
var { x, y } = this.animatedValue;
this._value.x = x;
this._value.y = y;
}
Also try making your Animated.View position:absolute
Now, it looks like the code is doing this:
Set 'Animated offset' to be equal to 'value offset' (onPanResponderGrant)
Set the 'Animated offset' to be equal to 'dx' 'dy' (onPanResponderMove)
set 'value' to be equal to 'Animated value' (Event Listener)
set the 'animated value' to be equal to 'value' (onPanResponderRelease)
Between steps one and two you are setting the offset twice without flattening (again, not sure how much that matters).
Between steps three and four you are setting 'value offset' equal to 'Animated Value offset' and then 'Animated value offset' to be equal to 'Value offset' again - seems redundant
I wrote a new version and got the result.
import React, { Component } from 'react';
import {
StyleSheet, View, Text, Animated, PanResponder,
} from 'react-native';
export default class App extends Component {
constructor(props) {
super(props);
this._value = { x: 0, y: 0 };
this.pan = new Animated.ValueXY({ x: 0, y: 0 });
}
componentWillMount() {
this.pan.addListener((value) => {
this._value = value;
});
this.panResponder = PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => true,
onMoveShouldSetPanResponder: (evt, gestureState) => ((gestureState.dx != 0) && (gestureState.dy != 0)),
onPanResponderGrant: (e, gestureState) => {
this.pan.setOffset({ x: this._value.x, y: this._value.y });
this.pan.setValue({ x: 0, y: 0 });
},
onPanResponderMove: Animated.event([null, {
dx: this.pan.x,
dy: this.pan.y
}]),
onPanResponderRelease: (e, gesture) => {
this.pan.flattenOffset();
const { x, y } = this._value;
Animated.timing(
this.pan,
{
toValue: { x, y: y + 50 },
duration: 600,
},
).start(() => {
// animation finished
});
}
});
}
componentWillUnmount() {
this.pan.removeAllListener();
}
render() {
return (
<View style={{ flex: 1 }}>
<View style={styles.draggableContainer}>
<Animated.View
{...this.panResponder.panHandlers}
style={[
this.pan.getLayout(),
styles.square,
]}>
<Text>Drag me!</Text>
</Animated.View>
</View>
</View>
);
}
}
let styles = StyleSheet.create({
draggableContainer: {
position: 'absolute',
top: 165,
left: 76.1,
},
square: {
backgroundColor: 'red',
width: 72,
height: 72,
borderWidth: 1,
}
});
the previous version has some problems because I didn't know the react-native animation.
there are several points about the codes:
1)animation view should be wrapped by a position:absolute view.
2)the animation value/valueXY is a relative position.
3)addListener works right.
4)valueXY is a combine object with two value object.
but the addListener param is an object with x/y, like this: {x:0,y:0}
(So, valueXY and listener param cannot assign directly)
5)animation will add the offset of value.
after flattenOffset, the offset will be added to value.
flattenOffset

How to draw over other apps in React-native

I was trying to create facebook messenger like chat heads in my react-native app .I used pan responder to create the dragging effect and collapsing to side effect but I am not able to figure out how to make the chat heads visible over other apps or the home screen of the phone.My app has the SYSTEM_ALERT_WINDOW permission included.Here is my code for the chat heads so far.Any lead would be appreciated.
const width = Dimensions.get('window').width
const widthFactor = width / 375
const heightFactor = (Dimensions.get('window').height - 75) / 667
export default class App extends Component {
constructor(props) {
super(props)
this.state = {
showDraggable: true,
dropZoneValues: null,
pan: new Animated.ValueXY(),
}
this.panResponder = PanResponder.create({
onStartShouldSetPanResponder: () => true,
onPanResponderMove: Animated.event([
null,
{
dx: this.state.pan.x,
dy: this.state.pan.y,
},
]),
onPanResponderRelease: (e, gesture) => {
if (this.isDropZone(gesture)) {
this.setState({
showDraggable: false,
})
} else {
const k =
gesture.dx > 0
? Window.width / 2 - CIRCLE_RADIUS
: 0 - (Window.width / 2 - CIRCLE_RADIUS)
if (gesture.moveY > Window.height - 2 * CIRCLE_RADIUS) {
Animated.spring(this.state.pan, {
toValue: { x: k, y: Window.height - 2 * CIRCLE_RADIUS },
}).start()
} else {
const y = gesture.dy
Animated.spring(this.state.pan, { toValue: { x: k, y } }).start()
}
}
},
})
}
isDropZone(gesture) {
const dz = this.state.dropZoneValues
return gesture.moveY > dz.y && gesture.moveY < dz.y + dz.height
}
renderDraggable() {
if (this.state.showDraggable) {
return (
<View style={styles.draggableContainer}>
<Animated.View
{...this.panResponder.panHandlers}
style={[this.state.pan.getLayout(), styles.circle]}
>
<Text style={styles.text}>Drag me!</Text>
</Animated.View>
</View>
)
}
}
setDropZoneValues(event) {
this.setState({
dropZoneValues: event.nativeEvent.layout,
})
}
render() {
that = this
return (
<View style={styles.mainContainer}>
<View
onLayout={this.setDropZoneValues.bind(this)}
style={styles.dropZone}
>
<Text style={styles.text}>Drop me here!</Text>
</View>
{this.renderDraggable()}
</View>
)
}
}

Scrollable image with pinch-to-zoom in react-native

I'm trying to display an image in my React Native app (Android) and I want to give users an ability to zoom that image in and out.
This also requires the image to be scrollable once zoomed in.
How would I go about it?
I tried to use ScrollView to display a bigger image inside, but on Android it can either scroll vertically or horizontally, not both ways.
Even if that worked there is a problem of making pinch-to-zoom work.
As far as I understand I need to use PanResponder on a custom view to zoom an image and position it accordingly. Is there an easier way?
I ended up rolling my own ZoomableImage component. So far it's been working out pretty well, here is the code:
import React, { Component } from "react";
import { View, PanResponder, Image } from "react-native";
import PropTypes from "prop-types";
function calcDistance(x1, y1, x2, y2) {
const dx = Math.abs(x1 - x2);
const dy = Math.abs(y1 - y2);
return Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2));
}
function calcCenter(x1, y1, x2, y2) {
function middle(p1, p2) {
return p1 > p2 ? p1 - (p1 - p2) / 2 : p2 - (p2 - p1) / 2;
}
return {
x: middle(x1, x2),
y: middle(y1, y2)
};
}
function maxOffset(offset, windowDimension, imageDimension) {
const max = windowDimension - imageDimension;
if (max >= 0) {
return 0;
}
return offset < max ? max : offset;
}
function calcOffsetByZoom(width, height, imageWidth, imageHeight, zoom) {
const xDiff = imageWidth * zoom - width;
const yDiff = imageHeight * zoom - height;
return {
left: -xDiff / 2,
top: -yDiff / 2
};
}
class ZoomableImage extends Component {
constructor(props) {
super(props);
this._onLayout = this._onLayout.bind(this);
this.state = {
zoom: null,
minZoom: null,
layoutKnown: false,
isZooming: false,
isMoving: false,
initialDistance: null,
initialX: null,
initalY: null,
offsetTop: 0,
offsetLeft: 0,
initialTop: 0,
initialLeft: 0,
initialTopWithoutZoom: 0,
initialLeftWithoutZoom: 0,
initialZoom: 1,
top: 0,
left: 0
};
}
processPinch(x1, y1, x2, y2) {
const distance = calcDistance(x1, y1, x2, y2);
const center = calcCenter(x1, y1, x2, y2);
if (!this.state.isZooming) {
const offsetByZoom = calcOffsetByZoom(
this.state.width,
this.state.height,
this.props.imageWidth,
this.props.imageHeight,
this.state.zoom
);
this.setState({
isZooming: true,
initialDistance: distance,
initialX: center.x,
initialY: center.y,
initialTop: this.state.top,
initialLeft: this.state.left,
initialZoom: this.state.zoom,
initialTopWithoutZoom: this.state.top - offsetByZoom.top,
initialLeftWithoutZoom: this.state.left - offsetByZoom.left
});
} else {
const touchZoom = distance / this.state.initialDistance;
const zoom =
touchZoom * this.state.initialZoom > this.state.minZoom
? touchZoom * this.state.initialZoom
: this.state.minZoom;
const offsetByZoom = calcOffsetByZoom(
this.state.width,
this.state.height,
this.props.imageWidth,
this.props.imageHeight,
zoom
);
const left =
this.state.initialLeftWithoutZoom * touchZoom + offsetByZoom.left;
const top =
this.state.initialTopWithoutZoom * touchZoom + offsetByZoom.top;
this.setState({
zoom,
left:
left > 0
? 0
: maxOffset(left, this.state.width, this.props.imageWidth * zoom),
top:
top > 0
? 0
: maxOffset(top, this.state.height, this.props.imageHeight * zoom)
});
}
}
processTouch(x, y) {
if (!this.state.isMoving) {
this.setState({
isMoving: true,
initialX: x,
initialY: y,
initialTop: this.state.top,
initialLeft: this.state.left
});
} else {
const left = this.state.initialLeft + x - this.state.initialX;
const top = this.state.initialTop + y - this.state.initialY;
this.setState({
left:
left > 0
? 0
: maxOffset(
left,
this.state.width,
this.props.imageWidth * this.state.zoom
),
top:
top > 0
? 0
: maxOffset(
top,
this.state.height,
this.props.imageHeight * this.state.zoom
)
});
}
}
_onLayout(event) {
const layout = event.nativeEvent.layout;
if (
layout.width === this.state.width &&
layout.height === this.state.height
) {
return;
}
const zoom = layout.width / this.props.imageWidth;
const offsetTop =
layout.height > this.props.imageHeight * zoom
? (layout.height - this.props.imageHeight * zoom) / 2
: 0;
this.setState({
layoutKnown: true,
width: layout.width,
height: layout.height,
zoom,
offsetTop,
minZoom: zoom
});
}
componentWillMount() {
this._panResponder = PanResponder.create({
onStartShouldSetPanResponder: () => true,
onStartShouldSetPanResponderCapture: () => true,
onMoveShouldSetPanResponder: () => true,
onMoveShouldSetPanResponderCapture: () => true,
onPanResponderGrant: () => {},
onPanResponderMove: evt => {
const touches = evt.nativeEvent.touches;
if (touches.length === 2) {
this.processPinch(
touches[0].pageX,
touches[0].pageY,
touches[1].pageX,
touches[1].pageY
);
} else if (touches.length === 1 && !this.state.isZooming) {
this.processTouch(touches[0].pageX, touches[0].pageY);
}
},
onPanResponderTerminationRequest: () => true,
onPanResponderRelease: () => {
this.setState({
isZooming: false,
isMoving: false
});
},
onPanResponderTerminate: () => {},
onShouldBlockNativeResponder: () => true
});
}
render() {
return (
<View
style={this.props.style}
{...this._panResponder.panHandlers}
onLayout={this._onLayout}
>
<Image
style={{
position: "absolute",
top: this.state.offsetTop + this.state.top,
left: this.state.offsetLeft + this.state.left,
width: this.props.imageWidth * this.state.zoom,
height: this.props.imageHeight * this.state.zoom
}}
source={this.props.source}
/>
</View>
);
}
}
ZoomableImage.propTypes = {
imageWidth: PropTypes.number.isRequired,
imageHeight: PropTypes.number.isRequired,
source: PropTypes.object.isRequired
};
export default ZoomableImage;
There's a much easier way now.
Just make a ScollView with minimumZoomScale and maximumZoomScale:
import React, { Component } from 'react';
import { AppRegistry, ScrollView, Text } from 'react-native';
export default class IScrolledDownAndWhatHappenedNextShockedMe extends Component {
render() {
return (
<ScrollView minimumZoomScale={1} maximumZoomScale={5} >
<Text style={{fontSize:96}}>Scroll me plz</Text>
<Text style={{fontSize:96}}>If you like</Text>
<Text style={{fontSize:96}}>Scrolling down</Text>
<Text style={{fontSize:96}}>What's the best</Text>
<Text style={{fontSize:96}}>Framework around?</Text>
<Text style={{fontSize:80}}>React Native</Text>
</ScrollView>
);
}
}
// skip these lines if using Create React Native App
AppRegistry.registerComponent(
'AwesomeProject',
() => IScrolledDownAndWhatHappenedNextShockedMe);
In my case I have to add images inside Viewpager with Zoom functionality.
So I have used these two library.
import ViewPager from '#react-native-community/viewpager'
import PhotoView from 'react-native-photo-view-ex';
which you can install from.
npm i #react-native-community/viewpager
npm i react-native-photo-view-ex
So I have used this code.
class ResumeView extends React.Component {
render() {
preivewArray = this.props.showPreview.previewArray
var pageViews = [];
for (i = 0; i < preivewArray.length; i++) {
pageViews.push(<View style={style.page}>
<PhotoView
source={{ uri: preivewArray[i].filePath }}
minimumZoomScale={1}
maximumZoomScale={3}
// resizeMode='stretch'
style={{ width: a4_width, height: a4_height, alignSelf: 'center' }} />
</View>);
}
return (
<ViewPager
onPageScroll={this.pageScroll}
style={{ width: '100%', height: a4_height }}>
{pageViews}
</ViewPager>
)
}
pageScroll = (event) => {
console.log("onPageScroll")
}
}
You can simply use the react-native-image-zoom-viewer or react-native-image-pan-zoom library for that. Using this libraries you don't have to code manually.
npm i react-native-photo-view-ex
import PhotoView from 'react-native-photo-view-ex';
<PhotoView
style={{ flex: 1, width: '100%', height: '100%' }}
source={{ uri: this.state.filePath }} // you can supply any URL as well
minimumZoomScale={1} // max value can be 1
maximumZoomScale={2} // max value can be 3
/>
Don't go deep if you are working with react-native because things will go more and more complex as deep you go.
Give it a try...
npm i react-native-image-zoom-viewer --save
or
yarn add react-native-image-zoom-viewer
copy this code and put it in app.js and hit Run button.
import React from 'react';
import {View} from 'react-native';
import ImageViewer from 'react-native-image-zoom-viewer';
const image = [
{
url:
'https://static8.depositphotos.com/1020341/896/i/950/depositphotos_8969502-stock-photo-human-face-with-cracked-texture.jpg',
},
];
const App = () => {
return (
<View style={{flex: 1}}>
<ImageViewer imageUrls={image} />
</View>
);
};
export default App;
Features like zoom, pan, tap/swipe to switch image using react-native-gesture-handler,react-native-reanimated. Perfectly and smoothly running on android/ios.
USAGE
<ImageZoomPan
uri={'https://picsum.photos/200/300'}
activityIndicatorProps={{
color: COLOR_SECONDARY,
}}
onInteractionStart={onInteractionStart}
onInteractionEnd={onInteractionEnd}
onLongPressActiveInteration={onPressIn}
onLongPressEndInteration={onPressOut}
onSwipeTapForNext={onSwipeTapForNext}
onSwipeTapForPrev={onSwipeTapForPrev}
minScale={0.8}
onLoadEnd={start}
resizeMode={isFullScreen ? 'cover' : 'contain'} //'stretch'
/>
Image Zoom component
import React, {useRef, useState} from 'react';
import {ActivityIndicator,Dimensions, Image} from 'react-native';
import {
LongPressGestureHandler,
PanGestureHandler,
PinchGestureHandler,
State,
TapGestureHandler,
} from 'react-native-gesture-handler';
import Animated, {
useAnimatedGestureHandler,
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated';
import styles from './styles';
const clamp = (value, min, max) => {
'worklet';
return Math.min(Math.max(min, value), max);
};
const noop = () => {};
const getDeviceWidth = () => {
return Dimensions.get('window').width;
};
const AnimatedImage = Animated.createAnimatedComponent(Image);
export default function ImageZoom({
uri = '',
minScale = 1,
maxScale = 5,
minPanPointers = 2,
maxPanPointers = 2,
isPanEnabled = true,
isPinchEnabled = true,
onLoadEnd = noop,
onInteractionStart = noop,
onInteractionEnd = noop,
onPinchStart = noop,
onPinchEnd = noop,
onPanStart = noop,
onPanEnd = noop,
onLongPressActiveInteration = noop,
onLongPressEndInteration = noop,
onSwipeTapForNext = noop,
onSwipeTapForPrev = noop,
style = {},
containerStyle = {},
imageContainerStyle = {},
activityIndicatorProps = {},
renderLoader,
resizeMode = 'cover',
...props
}) {
const panRef = useRef();
const pinchRef = useRef();
const isInteracting = useRef(false);
const isPanning = useRef(false);
const isPinching = useRef(false);
const doubleTapRef = useRef(null);
const [isLoading, setIsLoading] = useState(true);
const [state, setState] = useState({
canInteract: false,
centerX: 0,
centerY: 0,
});
const {canInteract, centerX, centerY} = state;
const scale = useSharedValue(1);
const initialFocalX = useSharedValue(0);
const initialFocalY = useSharedValue(0);
const focalX = useSharedValue(0);
const focalY = useSharedValue(0);
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
const onInteractionStarted = () => {
if (!isInteracting.current) {
isInteracting.current = true;
onInteractionStart();
}
};
const onInteractionEnded = () => {
if (isInteracting.current && !isPinching.current && !isPanning.current) {
isInteracting.current = false;
onInteractionEnd();
}
};
const onPinchStarted = () => {
onInteractionStarted();
isPinching.current = true;
onPinchStart();
};
const onPinchEnded = () => {
isPinching.current = false;
onPinchEnd();
onInteractionEnded();
};
const onPanStarted = () => {
onInteractionStarted();
isPanning.current = true;
onPanStart();
};
const onPanEnded = () => {
isPanning.current = false;
onPanEnd();
onInteractionEnded();
};
const panHandler = useAnimatedGestureHandler({
onActive: event => {
translateX.value = event.translationX;
translateY.value = event.translationY;
},
onFinish: () => {
translateX.value = withTiming(0);
translateY.value = withTiming(0);
},
});
const pinchHandler = useAnimatedGestureHandler({
onStart: event => {
initialFocalX.value = event.focalX;
initialFocalY.value = event.focalY;
},
onActive: event => {
// onStart: focalX & focalY result both to 0 on Android
if (initialFocalX.value === 0 && initialFocalY.value === 0) {
initialFocalX.value = event.focalX;
initialFocalY.value = event.focalY;
}
scale.value = clamp(event.scale, minScale, maxScale);
focalX.value = (centerX - initialFocalX.value) * (scale.value - 1);
focalY.value = (centerY - initialFocalY.value) * (scale.value - 1);
},
onFinish: () => {
scale.value = withTiming(1);
focalX.value = withTiming(0);
focalY.value = withTiming(0);
initialFocalX.value = 0;
initialFocalY.value = 0;
},
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{translateX: translateX.value},
{translateY: translateY.value},
{translateX: focalX.value},
{translateY: focalY.value},
{scale: scale.value},
],
}));
const onLayout = ({
nativeEvent: {
layout: {x, y, width, height},
},
}) => {
setState(current => ({
...current,
canInteract: true,
centerX: x + width / 2,
centerY: y + height / 2,
}));
};
const onImageLoadEnd = () => {
onLoadEnd();
setIsLoading(false);
};
const onLongPress = event => {
if (event.nativeEvent.state === State.ACTIVE) {
onLongPressActiveInteration();
}
if (
event.nativeEvent.state === State.END ||
event.nativeEvent.state === State.CANCELLED
) {
onLongPressEndInteration();
}
};
const onSingleTapEvent = event => {
let e = event.nativeEvent;
if (e.state === State.ACTIVE) {
if (e.x < getDeviceWidth() / 2) {
onSwipeTapForPrev();
} else {
onSwipeTapForNext();
}
}
};
return (
<PinchGestureHandler
ref={pinchRef}
simultaneousHandlers={[panRef]}
onGestureEvent={pinchHandler}
onActivated={onPinchStarted}
onCancelled={onPinchEnded}
onEnded={onPinchEnded}
onFailed={onPinchEnded}
enabled={isPinchEnabled && canInteract}>
<Animated.View style={[styles.container, containerStyle]}>
<PanGestureHandler
ref={panRef}
simultaneousHandlers={[pinchRef]}
onGestureEvent={panHandler}
onActivated={onPanStarted}
onCancelled={onPanEnded}
onEnded={onPanEnded}
onFailed={onPanEnded}
minPointers={minPanPointers}
maxPointers={maxPanPointers}
enabled={isPanEnabled && canInteract}>
<Animated.View
onLayout={onLayout}
style={[styles.content, imageContainerStyle]}>
<TapGestureHandler
waitFor={doubleTapRef}
onHandlerStateChange={onSingleTapEvent}>
<TapGestureHandler
ref={doubleTapRef}
onHandlerStateChange={() => null}
numberOfTaps={2}>
<LongPressGestureHandler
onHandlerStateChange={onLongPress}
minDurationMs={800}>
<AnimatedImage
style={[styles.container, style, animatedStyle]}
source={{uri}}
resizeMode={resizeMode}
onLoadEnd={onImageLoadEnd}
{...props}
/>
</LongPressGestureHandler>
</TapGestureHandler>
</TapGestureHandler>
{isLoading &&
(renderLoader ? (
renderLoader()
) : (
<ActivityIndicator
size="large"
style={styles.loader}
color="dimgrey"
{...activityIndicatorProps}
/>
))}
</Animated.View>
</PanGestureHandler>
</Animated.View>
</PinchGestureHandler>
);
}