react native circle drag transform translate animation - react-native

Hi how can I drag an image arc(circle) line. I have follow an topic they use PanResponder but only drag free. I just when drag just accept the image follow arc(circle) line like this:
this is the code can make drag free direction. I know we can calculate the translateX: this.state.pan.x and translateY: this.state.pan.y but don't know how to do it:
import React, {
Component
} from 'react';
import {
StyleSheet,
View,
Text,
PanResponder,
Animated,
Easing,
Dimensions
} from 'react-native';
export default class Viewport extends Component {
constructor(props) {
super(props);
this.state = {
showDraggable: true,
dropZoneValues: null,
pan: new Animated.ValueXY(0)
};
this.panResponder = PanResponder.create({
onStartShouldSetPanResponder: () => true,
onPanResponderMove: Animated.event([
null, {
dx: this.state.pan.x,
dy: this.state.pan.y
},
], {
listener: (event, gestureState) => console.log(event.nativeEvent)
}),
onPanResponderRelease: (e, gesture) => {
Animated.spring(this.state.pan, {
toValue: {
x: 0,
y: 0
}
}).start();
}
});
}
getStyle() {
return [
styles.circle,
{
transform: [{
translateX: this.state.pan.x
},
{
translateY: this.state.pan.y
}
]
}
];
}
render() {
return ( <
View style = {
styles.mainContainer
} > {
this.renderDraggable()
} <
/View>
);
}
renderDraggable() {
if (this.state.showDraggable) {
return ( <
View style = {
styles.draggableContainer
} >
<
Animated.View { ...this.panResponder.panHandlers
}
style = {
this.getStyle()
} >
<
/Animated.View> <
/View>
);
}
}
}
let CIRCLE_RADIUS = 36;
let Window = Dimensions.get('window');
let styles = StyleSheet.create({
mainContainer: {
flex: 1
},
text: {
marginTop: 25,
marginLeft: 5,
marginRight: 5,
textAlign: 'center',
color: '#fff'
},
draggableContainer: {
position: 'absolute',
top: Window.height / 2 - CIRCLE_RADIUS,
left: Window.width / 2 - CIRCLE_RADIUS
},
circle: {
backgroundColor: '#1abc9c',
width: CIRCLE_RADIUS * 2,
height: CIRCLE_RADIUS * 2,
borderRadius: CIRCLE_RADIUS
}
});

Related

Animation not showing in React Native mobile app

I have created a React Native app to test animations. I have a background and when the user presses this background I want an animation to appear over the background and animate. The code:
import * as React from 'react';
import { StyleSheet, Text, View, TouchableOpacity, Image, ImageBackground, Animated, Easing, Platform
} from 'react-native';
import { frame1 } from '../app-master-test/assets/index';
import { frame2 } from '../app-master-test/assets/index';
import { frame3 } from '../app-master-test/assets/index';
import { frame4 } from '../app-master-test/assets/index';
import { frame5 } from '../app-master-test/assets/index';
import { frame6 } from '../app-master-test/assets/index';
import { frame7 } from '../app-master-test/assets/index';
import { frame8 } from '../app-master-test/assets/index';
import { background } from '../app-master-test/assets/index';
const Images= [
{ id: 1, source: frame1},
{ id: 2, source: frame2 },
{ id: 3, source: frame3 },
{ id: 4, source: frame4 },
{ id: 5, source: frame5 },
{ id: 6, source: frame6 },
{ id: 7, source: frame7 },
{ id: 8, source: frame8 },
]
const length = Images.length;
export default class Timer extends React.Component {
constructor(props){
super(props);
this.state = {
isOn: false,
}
this.animations = new Animated.Value(0);
this.opacity = [];
Images.map((item, index) => {
this.opacity.push(
this.animations.interpolate({
inputRange: [index - 1, index, index + 1],
outputRange: [0, 1, 0],
}),
);
});
}
onItemMouseDown = () => {
Animated.loop(
Animated.timing(this.animations, {
toValue: length - 1,
duration: 2000 * length,
easing: Easing.linear,
useNativeDriver: true,
}),
).start();
console.log(this.animations)
this.setState({
isOn:true,
}, () => {
console.log(this.state.isOn)
})
}
onItemMouseUp = () => {
this.setState({
isOn:false
}, () => {
console.log(this.state.isOn)
})
}
render() {
return(
<ImageBackground source={background} style={styles.background}>
<TouchableOpacity
onPressIn={this.onItemMouseDown}
onPressOut={this.onItemMouseUp}
>
<Text style={styles.touchbutton}>Touch this</Text>
</TouchableOpacity>
{this.state.isOn === true ?
<View style={styles.container}>
{/* <Image source={frame1}></Image> */} //I can render this image if this.state.isOn is true
{Images.map((item, index) => { //but this does not show ANYTHING!
const opacity = this.opacity[index];
{/* console.log(item)
console.log(index) */}
return(
<Animated.View
key={item.id}
style={[styles.anim, {animations: item, opacity}]}
/>
)
})}
</View>
: null }
</ImageBackground>
)
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: 'center',
},
background: {
flex: 1,
resizeMode: "cover",
justifyContent: "center",
alignItems: 'center',
},
anim: {
width: 100,
height: 100,
},
touchbutton: {
flex: 1,
position: 'relative',
marginTop: 300,
},
touchbuttontest: {
flex:1,
position: 'relative',
marginTop: 200,
}
});
As mentioned, nothing appears. I'm not sure whether it's this bit:
<Animated.View
key={item.id}
style={[styles.anim, {animations: item, opacity}]}
/>
particularly the style line. I'm not sure that is correct. I don't fully understand what is going on there. I don't know what to put instead of animations: item. I think it should be a property of something but what, I don't know.
I've watched this and none of what is suggests works:
https://egghead.io/lessons/react-animate-styles-of-a-react-native-view-with-animated-timing
there is no problem in your code you can give backgroundColor:"red" in anim to check if it is working or not
you have to make position:"absolute" to container for start animation from top and you have to wrap image inside Animated.View
you can check demo here :https://snack.expo.io/#nomi9995/animation
import * as React from "react";
import {
StyleSheet,
Text,
View,
TouchableOpacity,
Image,
ImageBackground,
Animated,
Easing,
Platform,
} from "react-native";
import { frame1 } from "../app-master-test/assets/index";
import { frame2 } from "../app-master-test/assets/index";
import { frame3 } from "../app-master-test/assets/index";
import { frame4 } from "../app-master-test/assets/index";
import { frame5 } from "../app-master-test/assets/index";
import { frame6 } from "../app-master-test/assets/index";
import { frame7 } from "../app-master-test/assets/index";
import { frame8 } from "../app-master-test/assets/index";
import { background } from "../app-master-test/assets/index";
const Images = [
{ id: 1, source: frame1 },
{ id: 2, source: frame2 },
{ id: 3, source: frame3 },
{ id: 4, source: frame4 },
{ id: 5, source: frame5 },
{ id: 6, source: frame6 },
{ id: 7, source: frame7 },
{ id: 8, source: frame8 },
];
const length = Images.length;
export default class Timer extends React.Component {
constructor(props) {
super(props);
this.state = {
isOn: false,
};
this.animations = new Animated.Value(0);
this.opacity = [];
Images.map((item, index) => {
this.opacity.push(
this.animations.interpolate({
inputRange: [index - 1, index, index + 1],
outputRange: [0, 1, 0],
})
);
});
}
onItemMouseDown = () => {
Animated.loop(
Animated.timing(this.animations, {
toValue: length - 1,
duration: 2000 * length,
easing: Easing.linear,
useNativeDriver: true,
})
).start();
console.log(this.animations);
this.setState(
{
isOn: true,
},
() => {
console.log(this.state.isOn, "nominominomi");
}
);
};
onItemMouseUp = () => {
this.setState(
{
isOn: false,
},
() => {
console.log(this.state.isOn);
}
);
};
render() {
return (
<ImageBackground source={background} style={styles.background}>
<TouchableOpacity
onPressIn={this.onItemMouseDown}
onPressOut={this.onItemMouseUp}
>
<Text style={styles.touchbutton}>Touch this</Text>
</TouchableOpacity>
{this.state.isOn === true ? (
<View style={styles.container}>
{Images.map((item, index) => {
const opacity = this.opacity[index];
return (
<Animated.View
key={item.id}
style={[styles.anim, { animations: item, opacity }]}
>
<Image
source={item.source}
style={{ height: 100, width: 100, zIndex: 100 }}
/>
</Animated.View>
);
})}
</View>
) : null}
</ImageBackground>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: 'center',
position:"absolute"
},
background: {
flex: 1,
resizeMode: "cover",
justifyContent: "center",
alignItems: 'center',
},
anim: {
position:"absolute",
width: 100,
height: 100,
},
touchbutton: {
flex: 1,
position: 'relative',
marginTop: 300,
},
touchbuttontest: {
flex:1,
position: 'relative',
marginTop: 200,
}
});

Glitches with draggable components using react native - implemented using Animated and PanResponder

Drawing inspiration from this question, I have implemented two draggable components as children in a view. The parent view is as follows:
import React, { Component } from "react";
import { Text, View, StyleSheet, Dimensions } from "react-native";
import Draggable from "./Draggable";
export default class FloorPlan extends Component {
constructor() {
super();
const { width, height } = Dimensions.get("window");
this.separatorPosition = (height * 2) / 3;
}
render() {
return (
<View style={styles.mainContainer}>
<View style={[...styles.dropZone, { height: this.separatorPosition }]}>
<Text style={styles.text}>Floor plan</Text>
</View>
<View style={styles.drawerSeparator} />
<View style={styles.row}>
<Draggable />
<Draggable />
</View>
</View>
);
}
}
const styles = StyleSheet.create({
mainContainer: {
flex: 1
},
drawerSeparator: {
backgroundColor: "grey",
height: 20
},
row: {
flexDirection: "row",
marginTop: 25
},
dropZone: {
height: 700,
backgroundColor: "#f4fffe"
},
text: {
marginTop: 25,
marginLeft: 5,
marginRight: 5,
textAlign: "center",
color: "grey",
fontSize: 20
}
});
And the draggable component is implemented as follows:
import React, { Component } from "react";
import {
StyleSheet,
View,
PanResponder,
Animated,
Text,
Dimensions
} from "react-native";
export default class Draggable extends Component {
constructor() {
super();
const { width, height } = Dimensions.get("window");
this.separatorPosition = (height * 2) / 3;
this.state = {
pan: new Animated.ValueXY(),
circleColor: "skyblue"
};
this.currentPanValue = { x: 0, y: 0 };
this.panListener = this.state.pan.addListener(
value => (this.currentPanValue = value)
);
}
componentWillMount() {
this.state.pan.removeListener(this.panListener);
}
componentWillMount() {
this.panResponder = PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => false,
onMoveShouldSetPanResponder: (evt, gestureState) => false,
onMoveShouldSetPanResponderCapture: (evt, gesture) => {
return true;
},
onPanResponderGrant: (e, gestureState) => {
this.setState({ circleColor: "red" });
},
onPanResponderMove: (event, gesture) => {
Animated.event([
null,
{
dx: this.state.pan.x,
dy: this.state.pan.y
}
])(event, gesture);
},
onPanResponderRelease: (event, gesture) => {
this.setState({ circleColor: "skyblue" });
if (gesture.moveY < this.separatorPosition) {
this.state.pan.setOffset({
x: this.currentPanValue.x,
y: this.currentPanValue.y
});
this.state.pan.setValue({ x: 0, y: 0 });
// this.state.pan.flattenOffset();
} else {
//Return icon to start position
this.state.pan.flattenOffset();
Animated.timing(this.state.pan, {
toValue: {
x: 0,
y: 0
},
useNativeDriver: true,
duration: 200
}).start();
}
}
});
}
render() {
const panStyle = {
transform: this.state.pan.getTranslateTransform()
};
return (
<Animated.View
{...this.panResponder.panHandlers}
style={[
panStyle,
styles.circle,
{ backgroundColor: this.state.circleColor }
]}
/>
);
}
}
let CIRCLE_RADIUS = 30;
let styles = StyleSheet.create({
circle: {
backgroundColor: "skyblue",
width: CIRCLE_RADIUS * 2,
height: CIRCLE_RADIUS * 2,
borderRadius: CIRCLE_RADIUS,
marginLeft: 25
}
});
A draggable component can be dragged onto the FloorPlan and it's location will be remembered for the next pan action. However, sometimes during dragging, a glitch occurs and the icon jumps at the beginning of the pan or completetely disappears.
What could be the problem? I am developing using React Native 0.55.2 and testing using a device running Android 7.

Using a ScrollView to scroll when ScrollView contains draggable cards - REACT NATIVE

Hopefully you can help me with a bug I'm having a bit of bother sorting out. I'm working on a bug in an app built using React Native. It is building to IOS and Android. I have a ScrollView in a component that contains cards that are draggable objects.
These cards are dragged from the ScrollView they are in, up to buckets at the top of the screen. They disappear from the ScrollView and the remaining ones get reorganised so they stay ordered and neat. That works fine, you press on a box in the list and drag it to the buckets.
There is a bit of whitespace above the list of cards in the ScrollView. The ScrollView functionality works when swiping within this whitespace above the boxes, but I can't swipe on the boxes themselves without it beginning to drag the card.
Here is the component itself:
import React, { Component } from 'react';
import { StyleSheet, Text, View, ScrollView, Dimensions, Alert } from 'react-native';
import { connect } from 'react-redux';
import * as ConstStyles from '../../Consts/styleConsts';
import Bucket from '../Partials/bucketContainers';
import BusyIndicator from 'react-native-busy-indicator';
import loaderHandler from 'react-native-busy-indicator/LoaderHandler';
import CatCard from '../Partials/categoryCard';
import * as FeedActions from '../../../Redux/Feeds/actions';
import * as AuthFunctions from '../../Auth/functions';
export class SetupLikes extends Component {
static navigatorStyle = ConstStyles.standardNav;
constructor(props) {
super(props);
this.props.navigator.setOnNavigatorEvent(this.onNavigatorEvent.bind(this));
this.card = [];
let button = {
leftButtons: [
{
title: 'Reset',
id: 'reset'
}
],
rightButtons: [
{
title: this.props.newAccount ? 'Go' : 'Done',
id: this.props.newAccount ? '' : 'skip',
disabled: this.props.newAccount
}
]
};
this.props.navigator.setButtons(button);
}
state = {
xOffset: 0,
positions: [],
placedCards: [],
loves: [],
okays: [],
hates: []
};
onNavigatorEvent(event) {
setTimeout(async () => {
if (event.type === 'NavBarButtonPress') {
if (event.id === 'skip') {
this.props.navigator.dismissModal({
animationType: 'slide-down'
});
} else if (event.id === 'reset') {
await this.imgTap();
} else if (event.id === 'go') {
await this.setInterests();
}
}
}, 0);
}
async setInterests() {
loaderHandler.showLoader('Setting your interests...');
let newInterests = [];
this.state.loves.forEach(function(element) {
let cat = this.props.Feeds.categories[element];
let newItem = {
categoryid: cat.id,
sentimentid: 1
};
newInterests.push(newItem);
}, this);
this.state.okays.forEach(function(element) {
let cat = this.props.Feeds.categories[element];
let newItem = {
categoryid: cat.id,
sentimentid: 0
};
newInterests.push(newItem);
}, this);
this.state.hates.forEach(function(element) {
let cat = this.props.Feeds.categories[element];
let newItem = {
categoryid: cat.id,
sentimentid: -1
};
newInterests.push(newItem);
}, this);
let sesId = this.props.User.sessionId;
try {
await this.props.dispatch(FeedActions.setMyInterests(sesId, newInterests));
loaderHandler.hideLoader();
} catch (err) {
loaderHandler.hideLoader();
Alert.alert('Uh oh', 'Something went wrong. Please try again later');
return;
}
await AuthFunctions.setupAppLogin(this.props.dispatch, sesId);
}
async imgTap() {
await this.setState({ placedCards: [], loves: [], okays: [], hates: [], positions: [] });
setTimeout(() => {
let cntr = 0;
this.card.forEach(function(element) {
cntr++;
if (this.state.placedCards.includes(cntr - 1)) return;
if (element) element.snapTo({ index: 0 });
}, this);
}, 5);
this.props.navigator.setButtons({
rightButtons: [
{
title: 'Go',
id: '',
disabled: true
}
],
animated: true
});
}
cardPlaced(id, droppedIndex) {
let newList = this.state.placedCards;
newList.push(id);
let cntr = 0;
let offset = 0;
let newPosIndex = [];
this.props.Feeds.categories.forEach(cats => {
let posY = (offset % 2) * -120 - 20;
let xOffset = Math.floor(offset / 2);
let posX = xOffset * 105 + 10;
newPosIndex[cntr] = {
x: posX,
y: posY,
offset: offset % 2
};
if (!newList.includes(cntr)) offset++;
cntr++;
});
if (droppedIndex === 1) {
let newLoves = this.state.loves;
newLoves.push(id);
this.setState({
loves: newLoves,
placedCards: newList,
positions: newPosIndex
});
} else if (droppedIndex === 2) {
let newOkays = this.state.okays;
newOkays.push(id);
this.setState({
okays: newOkays,
placedCards: newList,
positions: newPosIndex
});
} else if (droppedIndex === 3) {
let newHates = this.state.hates;
newHates.push(id);
this.setState({
hates: newHates,
placedCards: newList,
positions: newPosIndex
});
}
}
reShuffle() {
let cntr = 0;
this.card.forEach(function(element) {
cntr++;
if (this.state.placedCards.includes(cntr - 1)) return;
if (element) element.snapTo({ index: 0 });
}, this);
}
setButton() {
this.props.navigator.setButtons({
rightButtons: [
{
title: this.props.newAccount ? 'Go' : 'Done',
id: 'go'
}
],
animated: true
});
}
onChangeSize(scrollWidth, scrollHeight) {
let { height, width } = Dimensions.get('window');
let farRight = this.state.xOffset + width;
if (farRight > scrollWidth && farRight > 0) {
let xOffset = scrollWidth - width;
this.setState({ xOffset });
}
}
onSnap(index, id) {
this.cardPlaced(id, index);
this.reShuffle();
this.setButton();
if (this.props.Feeds.categories.length === this.state.placedCards.length)
setTimeout(async () => {
await this.setInterests();
}, 1);
}
renderCats() {
let cntr = 0;
var { height, width } = Dimensions.get('window');
let res = this.props.Feeds.categories.map(item => {
let ptr = cntr;
let posY = (cntr % 2) * -120 - 20;
let xOffset = Math.floor(cntr / 2);
let posX = xOffset * 105 + 10;
let vertPos = posY - 200 + ((cntr + 1) % 2) * -120;
posX = this.state.positions[ptr] ? this.state.positions[ptr].x : posX;
posY = this.state.positions[ptr] ? this.state.positions[ptr].y : posY;
let off = this.state.positions[ptr] ? this.state.positions[ptr].offset : ptr % 2;
cntr++;
if (this.state.placedCards.includes(cntr - 1)) return null;
item.key = cntr;
return (
<CatCard
key={ptr}
item={item}
ptr={ptr}
cntr={cntr}
xOffset={this.state.xOffset}
odd={off}
posX={posX}
posY={posY}
yDrop={vertPos}
screenWidth={width}
onSnap={(res, id) => this.onSnap(res, id)}
gotRef={ref => (this.card[ptr] = ref)}
/>
);
});
cntr = 0;
res.forEach(ele => {
if (ele !== null) ele.key = cntr++;
});
let test = this.props.Feeds.categories[0];
return res;
}
onScroll(res) {
this.setState({ xOffset: res.nativeEvent.contentOffset.x });
}
render() {
let colWidth = Math.ceil((this.props.Feeds.categories.length - this.state.placedCards.length) / 2) * 106;
return (
<View style={styles.container}>
<View style={styles.bucketContainer1}>
<Bucket
type={'Love'}
imageToUse={require('../../../img/waveLove.png')}
height={this.state.loveHeight}
count={this.state.loves.length}
backgroundColor={'rgb(238, 136, 205)'}
/>
</View>
<View style={styles.bucketContainer2}>
<Bucket
type={'OK'}
imageToUse={require('../../../img/waveOkay.png')}
height={this.state.okayHeight}
count={this.state.okays.length}
backgroundColor={'rgb(250, 179, 39)'}
/>
</View>
<View style={styles.bucketContainer3}>
<Bucket
type={'Dislike'}
imageToUse={require('../../../img/waveHate.png')}
height={this.state.hateHeight}
count={this.state.hates.length}
backgroundColor={'rgb(112, 127, 208)'}
/>
</View>
<View style={styles.descriptionContainer}>
<Text style={styles.dragLikesTitle}>Drag Likes</Text>
<View style={styles.dividingLine} />
<View>
<Text style={styles.descriptionText}>Drag your likes and dislikes into the bucket above,</Text>
<Text style={styles.descriptionText}>so we can generate your profile!</Text>
</View>
</View>
<ScrollView
ref={ref => (this.scroller = ref)}
onMomentumScrollEnd={res => this.onScroll(res)}
style={styles.scroller}
horizontal={true}
onContentSizeChange={(width, height) => this.onChangeSize(width, height)}
>
<View style={[styles.insideView, { width: colWidth }]}>{this.renderCats()}</View>
</ScrollView>
<BusyIndicator size={'large'} overlayHeight={120} />
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignSelf: 'stretch',
alignItems: 'center',
backgroundColor: 'white'
},
bucketContainer1: {
position: 'absolute',
height: 130,
width: 95,
left: 10,
top: 5
},
bucketContainer2: {
position: 'absolute',
height: 130,
width: 95,
top: 5
},
bucketContainer3: {
position: 'absolute',
height: 130,
width: 95,
right: 10,
top: 5
},
insideView: {
width: 2500,
justifyContent: 'flex-end',
overflow: 'visible'
},
cardContainer: {
borderWidth: 1,
borderColor: 'rgb(200,200,200)',
borderRadius: 4,
alignItems: 'center',
width: 100,
backgroundColor: 'white'
},
catImage: {
height: 100,
width: 100,
borderTopRightRadius: 4,
borderTopLeftRadius: 4
},
button: {
backgroundColor: 'rgba(255,0,0,0.2)',
width: 90,
height: 50
},
scroller: {
height: '100%',
width: '100%',
overflow: 'visible'
},
card: {
position: 'absolute',
overflow: 'visible'
},
descriptionContainer: {
top: 140,
width: '100%',
alignItems: 'center',
position: 'absolute'
},
dividingLine: {
height: 1,
width: '100%',
borderWidth: 0.5,
borderColor: 'rgb(150,150,150)',
marginBottom: 5
},
dragLikesTitle: {
fontFamily: 'Slackey',
fontSize: 20,
color: 'rgb(100,100,100)'
},
descriptionText: {
fontSize: 12,
textAlign: 'center',
marginTop: 5
}
});
function mapStateToProps(state) {
return {
User: state.User,
Feeds: state.Feeds
};
}
export default connect(mapStateToProps)(SetupLikes);
Down at the bottom of the render function is where you'll see the ScrollView. It's rendering the categories via a function called renderCats.
It may be that because the cards I am rendering are draggable, that fixing this is an impossibility but I thought I would see if anyone has a better idea of how this may be fixed!
EDIT TO INCLUDE CatCard component...
import React, { Component } from 'react';
import {
AppRegistry,
StyleSheet,
Text,
View,
ScrollView,
TouchableOpacity,
FlatList,
Image,
Platform,
Animated,
Easing,
Dimensions
} from 'react-native';
import * as Consts from '../../Consts/colourConsts';
import * as ConstStyles from '../../Consts/styleConsts';
import PropTypes from 'prop-types';
import Interactable from 'react-native-interactable';
import { CachedImage } from 'react-native-img-cache';
class CatCard extends Component {
state = {
initX: this.props.posX,
initY: this.props.posY,
zIndex: 1
};
onSnap(res, point) {
if (res.nativeEvent.index === 0) {
return;
}
let index = res.nativeEvent.index;
setTimeout(() => {
this.props.onSnap(index, point);
let end = new Date();
}, 100);
Animated.timing(this.opacity, {
toValue: 0,
duration: 100,
useNativeDriver: true
}).start();
}
constructor(props) {
super(props);
this.opacity = new Animated.Value(1);
this.height = Dimensions.get('window').height;
}
gotRef(ref) {
this.props.gotRef(ref);
}
render() {
let upY = this.props.posY + this.height + (1 - this.props.odd) * -120;
upY = upY * -1;
upY += 50;
return (
<Interactable.View
ref={ref => {
this.gotRef(ref);
}}
onSnap={res => this.onSnap(res, this.props.ptr)}
style={[styles.card, { zIndex: this.state.zIndex }]}
animatedNativeDriver={true}
dragToss={0.01}
snapPoints={[
{
x: this.props.posX,
y: this.props.posY,
damping: 0.7,
tension: 300,
id: '0'
},
{
x: this.props.xOffset + 10,
y: upY,
tension: 30000,
damping: 0.1
},
{
x: this.props.xOffset + 10 + this.props.screenWidth * 0.33,
y: upY,
tension: 30000,
damping: 0.1
},
{
x: this.props.xOffset + 10 + this.props.screenWidth * 0.66,
y: upY,
tension: 30000,
damping: 0.1
}
]}
initialPosition={{ x: this.state.initX, y: this.state.initY }}
>
<Animated.View
style={[styles.cardContainer, { opacity: this.opacity }]}
>
<CachedImage
source={{ uri: this.props.item.imageUrl }}
style={styles.catImage}
/>
<Text style={styles.cardText}>{this.props.item.name}</Text>
</Animated.View>
</Interactable.View>
);
}
}
CatCard.PropTypes = {
count: PropTypes.any.isRequired,
type: PropTypes.string.isRequired,
imageToUse: PropTypes.any.isRequired,
height: PropTypes.object.isRequired,
backgroundColor: PropTypes.string.isRequired
};
const styles = StyleSheet.create({
card: {
position: 'absolute',
overflow: 'visible'
},
cardContainer: {
borderWidth: 1,
borderColor: 'rgb(200,200,200)',
borderRadius: 4,
alignItems: 'center',
width: 100,
backgroundColor: 'white'
},
cardText: {
fontFamily: 'Slackey',
fontSize: 10
},
catImage: {
height: 100,
width: 98,
borderTopRightRadius: 4,
borderTopLeftRadius: 4
}
});
export default CatCard;
Without seeing CatCard it is hard to know how the dragging is implemented. If you are doing raw PanResponder then you'll need to keep a something in state that keeps track of whether the ScrollView is scrolling and pass that down as a prop to CatCard which would then disallow the drag if the prop were true.
Alternatively, I'd suggest using react-native-interactable. It's a little to wrap your head around but it's a great abstraction from PanResponder. They have loads of examples and I have used it to make a swipeable list item that worked great, even with touchables inside the item.

React Native: Animate scale, but make it shrink to the corner and not the center

I have a View that I want to shrink to the bottom right with some margin / padding on the bottom and right sides.
I was able to make it shrink, but it shrinks to the center. The video element is the one shrinking:
import React, { Component } from 'react';
import {
AppRegistry,
StyleSheet,
View,
PanResponder,
Animated,
Dimensions,
} from 'react-native';
const styles = StyleSheet.create({
container: {
flex: 1,
alignSelf: 'stretch',
backgroundColor: '#000000',
},
overlay: {
flex: 1,
alignSelf: 'stretch',
backgroundColor: '#0000ff',
opacity: 0.5,
},
video: {
position: 'absolute',
backgroundColor: '#00ff00',
bottom: 0,
right: 0,
width: Dimensions.get("window").width,
height: Dimensions.get("window").height,
padding: 10,
}
});
function clamp(value, min, max) {
return min < max
? (value < min ? min : value > max ? max : value)
: (value < max ? max : value > min ? min : value)
}
export default class EdmundMobile extends Component {
constructor(props) {
super(props);
this.state = {
pan: new Animated.ValueXY(),
scale: new Animated.Value(1),
};
}
componentWillMount() {
this._panResponder = PanResponder.create({
onMoveShouldSetResponderCapture: () => true,
onMoveShouldSetPanResponderCapture: () => true,
onPanResponderGrant: (e, gestureState) => {
this.state.pan.setOffset({x: this.state.pan.x._value, y: 0});
this.state.pan.setValue({x: 0, y: 0});
},
onPanResponderMove: (e, gestureState) => {
let width = Dimensions.get("window").width;
let difference = Math.abs((this.state.pan.x._value + width) / width);
if (gestureState.dx < 0) {
this.setState({ scale: new Animated.Value(difference) });
return Animated.event([
null, {dx: this.state.pan.x, dy: 0}
])(e, gestureState);
}
},
onPanResponderRelease: (e, {vx, vy}) => {
this.state.pan.flattenOffset();
if (vx >= 0) {
velocity = clamp(vx, 3, 5);
} else if (vx < 0) {
velocity = clamp(vx * -1, 3, 5) * -1;
}
if (Math.abs(this.state.pan.x._value) > 200) {
Animated.spring(this.state.pan, {
toValue: {x: -Dimensions.get("window").width, y: 0},
friction: 4
}).start()
Animated.spring(this.state.scale, {
toValue: 0.2,
friction: 4
}).start()
} else {
Animated.timing(this.state.pan, {
toValue: {x: 0, y: 0},
friction: 4
}).start()
Animated.spring(this.state.scale, {
toValue: 1,
friction: 10
}).start()
}
}
});
}
render() {
let { pan, scale } = this.state;
let translateX = pan.x;
let swipeStyles = {transform: [{translateX}]};
let videoScale = scale
let localVideoStyles = {transform: [{scale: videoScale}]};
return (
<View style={styles.container}>
<Animated.View style={[styles.video, localVideoStyles]}></Animated.View>
<Animated.View style={[styles.overlay, swipeStyles]} {...this._panResponder.panHandlers}>
</Animated.View>
</View>
);
}
}
AppRegistry.registerComponent('EdmundMobile', () => EdmundMobile);
Ok I figured out one solution. Kinda hands on, but I think it provides all the customizations I need.
So instead of transforming scale, I animate the width, height, bottom, top styles by attaching them to the state and changing them in response to the pan responder stuff.
import React, { Component } from 'react';
import {
AppRegistry,
StyleSheet,
View,
PanResponder,
Animated,
Dimensions,
} from 'react-native';
const styles = StyleSheet.create({
container: {
flex: 1,
alignSelf: 'stretch',
backgroundColor: '#000000',
},
overlay: {
flex: 1,
alignSelf: 'stretch',
backgroundColor: '#0000ff',
opacity: 0.5,
},
video: {
position: 'absolute',
backgroundColor: '#00ff00',
}
});
function clamp(value, min, max) {
return min < max
? (value < min ? min : value > max ? max : value)
: (value < max ? max : value > min ? min : value)
}
const MIN_VIDEO_WIDTH = 120;
const MIN_VIDEO_HEIGHT = 180;
export default class EdmundMobile extends Component {
constructor(props) {
super(props);
this.state = {
pan: new Animated.ValueXY(),
width: new Animated.Value(Dimensions.get("window").width),
height: new Animated.Value(Dimensions.get("window").height),
bottom: new Animated.Value(0),
right: new Animated.Value(0),
};
}
componentWillMount() {
this._panResponder = PanResponder.create({
onMoveShouldSetResponderCapture: () => true,
onMoveShouldSetPanResponderCapture: () => true,
onPanResponderGrant: (e, gestureState) => {
this.state.pan.setOffset({x: this.state.pan.x._value, y: 0});
this.state.pan.setValue({x: 0, y: 0});
},
onPanResponderMove: (e, gestureState) => {
let width = Dimensions.get("window").width;
let difference = Math.abs((this.state.pan.x._value + width) / width);
console.log(difference);
if (gestureState.dx < 0) {
const newVideoHeight = difference * Dimensions.get("window").height;
const newVideoWidth = difference * Dimensions.get("window").width;
if (newVideoWidth > MIN_VIDEO_WIDTH) {
this.setState({
width: new Animated.Value(newVideoWidth),
});
}
if (newVideoHeight > MIN_VIDEO_HEIGHT) {
this.setState({
height: new Animated.Value(newVideoHeight),
});
}
this.setState({
bottom: new Animated.Value((1- difference) * 20),
right: new Animated.Value((1 - difference) * 20),
});
return Animated.event([
null, {dx: this.state.pan.x, dy: 0}
])(e, gestureState);
}
},
onPanResponderRelease: (e, {vx, vy}) => {
this.state.pan.flattenOffset();
if (vx >= 0) {
velocity = clamp(vx, 3, 5);
} else if (vx < 0) {
velocity = clamp(vx * -1, 3, 5) * -1;
}
if (Math.abs(this.state.pan.x._value) > 200) {
Animated.spring(this.state.pan, {
toValue: {x: -Dimensions.get("window").width, y: 0},
friction: 4
}).start()
Animated.spring(this.state.width, {
toValue: MIN_VIDEO_WIDTH,
friction: 4
}).start()
Animated.spring(this.state.height, {
toValue: MIN_VIDEO_HEIGHT,
friction: 4
}).start()
Animated.timing(this.state.bottom, {
toValue: 20,
}).start()
Animated.timing(this.state.right, {
toValue: 20,
}).start()
} else {
Animated.timing(this.state.pan, {
toValue: {x: 0, y: 0},
}).start()
Animated.timing(this.state.width, {
toValue: Dimensions.get("window").width,
friction: 4
}).start()
Animated.timing(this.state.height, {
toValue: Dimensions.get("window").height,
friction: 4
}).start()
Animated.timing(this.state.bottom, {
toValue: 0,
}).start()
Animated.timing(this.state.right, {
toValue: 0,
}).start()
}
}
});
}
render() {
let { pan, width, height, bottom, right } = this.state;
let translateX = pan.x;
let swipeStyles = {transform: [{translateX}]};
let videoStyles = {
width: width,
height: height,
bottom: bottom,
right: right,
};
return (
<View style={styles.container}>
<Animated.View style={[styles.video, videoStyles]}></Animated.View>
<Animated.View style={[styles.overlay, swipeStyles]} {...this._panResponder.panHandlers}>
</Animated.View>
</View>
);
}
}
AppRegistry.registerComponent('EdmundMobile', () => EdmundMobile);

Animated.Image with PanResponder 'react-native

I am using Animated.Image inside a scrollView. I apply a pan responder to the Animated.Image.
The problem: when I move the image for big distance, it disappears.
The question: how can I adjust the Animated.Image to stay within specific boundaries when i move it?
My code:
import React, { Component } from 'react';
import {
AppRegistry,
StyleSheet,
Text,
View,
Image,
Animated,
PanResponder,
ScrollView
} from 'react-native';
var xPosition, yPosition;
var position;
export default class AvatarEditor extends Component {
constructor(props) {
super(props);
this.state = {
pan: new Animated.ValueXY(), // inits to zero
image: this.props.image,
border: this.props.border,
width: this.props.width,
height: this.props.height,
viewWidth: this.props.width + 2 * this.props.border,
viewHeight: this.props.height + 2 * this.props.border,
first: true,
left: 0,
top: 0,
xPosition: 0,
yPosition: 0,
translateX: 0,
translateY: 0
};
}
componentWillMount() {
this._animatedValueX = 0;
this._animatedValueY = 0;
this.state.pan.x.addListener((value) => this._animatedValueX = value.value);
this.state.pan.y.addListener((value) => this._animatedValueY = value.value);
this._panResponder = PanResponder.create({
onMoveShouldSetResponderCapture: () => true, //Tell iOS that we are allowing the movement
onMoveShouldSetPanResponderCapture: () => true, // Same here, tell iOS that we allow dragging
onPanResponderGrant: (e, gestureState) => {
this.state.pan.setOffset({ x: this._animatedValueX, y: this._animatedValueY });
this.state.pan.setValue({ x: 0, y: 0 }); //Initial value
},
onPanResponderMove: (evt, gestureState) => {
// Animated.event([
// null, { dx: this.state.pan.x, dy: this.state.pan.y }
// ]) // Creates a function to handle the movement and set offsets
newdx = gestureState.dx;
newdy = gestureState.dy;
Animated.event([
null, { dx: this.state.pan.x, dy: this.state.pan.y },
])(evt, { dx: newdx, dy: newdy });
},
onPanResponderRelease: () => {
this.state.pan.flattenOffset(); // Flatten the offset so it resets the default positioning
}
});
}
componentDidMount() {
}
componentWillUnmount() {
this.state.pan.x.removeAllListeners();
this.state.pan.y.removeAllListeners();
}
render() {
var imageStyle = {
width: this.state.width,
height: this.state.height,
resizeMode: 'stretch',
top: this.state.top,
left: this.state.left,
transform: [
{ translateX: this.state.pan.x },//this.state.pan.x
{ translateY: this.state.pan.y },
{ scale: this.props.scale }
]
};
return (
<View
style={[this.props.style, { backgroundColor: 'gray' }]}
>
<ScrollView
style={{
width: this.state.viewWidth,
height: this.state.viewHeight,
borderWidth: this.state.border,
borderColor: 'rgba(100, 100, 100, 0.5)',
overflow: 'hidden',
}}
scrollEnabled={false}
>
<Animated.Image
style={imageStyle}
source={{ uri: this.state.image }}
{...this._panResponder.panHandlers}
>
</Animated.Image>
</ScrollView>
</View>
);
}
}
AvatarEditor.propTypes = {
scale: React.PropTypes.number,
image: React.PropTypes.string,
border: React.PropTypes.number,
width: React.PropTypes.number,
height: React.PropTypes.number,
style: React.PropTypes.object
};
AvatarEditor.defaultProps = {
scale: 1,
border: 25,
width: 200,
height: 200,
style: {
top: 50,
left: 25,
position: 'absolute',
},
image: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQesv5ucRQ1KUNtDipnrhS6Gn9yMn7GOqFdQGTeLMG1fCKGvudEUji_Aw',
};