Animated.Image with PanResponder 'react-native - 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',
};

Related

Slide to record animations in react-native like whatsapp / viber

I want to replicate the long press to record and slide left to cancel of whatsapp/viber messengers.
import React, {useRef, useState} from 'react';
import {
Dimensions,
TextInput,
TouchableWithoutFeedback,
View,
PanResponder,
Animated as NativeAnimated,
} from 'react-native';
import Animated, {Easing} from 'react-native-reanimated';
import styled from 'styled-components';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
const {Value, timing} = Animated;
let isMoving = false;
const width = Dimensions.get('window').width;
const height = Dimensions.get('window').height;
const RecordButton = ({onPress, onPressIn, onPressOut}) => (
<RecordButton.Container
accessibilityLabel="send message"
accessibilityRole="button"
accessibilityHint="tap to send message">
<TouchableWithoutFeedback
delayPressOut={900}
pressRetentionOffset={300}
onPress={onPress}
onPressIn={onPressIn}
onPressOut={onPressOut}>
<RecordButton.Icon />
</TouchableWithoutFeedback>
</RecordButton.Container>
);
RecordButton.Container = styled(View)`
height: 46px;
justify-content: center;
`;
RecordButton.Icon = styled(MaterialCommunityIcons).attrs({
size: 26,
name: 'microphone',
color: 'red',
})``;
const Input = styled(TextInput).attrs((props) => ({}))`
background-color: grey;
border-radius: 10px;
color: black;
flex: 1;
font-size: 17px;
max-height: 180px;
padding: 12px 18px;
text-align-vertical: top;
`;
const App = () => {
const [isFocused, setIsFocused] = useState(false);
const inputBoxTranslateX = useRef(new Value(0)).current;
const contentTranslateY = useRef(new Value(0)).current;
const contentOpacity = useRef(new Value(0)).current;
const textTranslateX = useRef(new Value(-10)).current;
const position = useRef(new NativeAnimated.ValueXY()).current;
const handlePressIn = () => {
setIsFocused(true);
const input_box_translate_x_config = {
duration: 200,
toValue: -width,
easing: Easing.inOut(Easing.ease),
};
const text_translate_x_config = {
duration: 200,
toValue: -50,
easing: Easing.inOut(Easing.ease),
};
const content_translate_y_config = {
duration: 200,
toValue: 0,
easing: Easing.inOut(Easing.ease),
};
const content_opacity_config = {
duration: 200,
toValue: 1,
easing: Easing.inOut(Easing.ease),
};
timing(inputBoxTranslateX, input_box_translate_x_config).start();
timing(contentTranslateY, content_translate_y_config).start();
timing(contentOpacity, content_opacity_config).start();
timing(textTranslateX, text_translate_x_config).start();
};
const handlePressOut = ({isFromPan, pos}) => {
// console.log(position._value);
if (!isFromPan) {
return;
}
if (isMoving && !isFromPan) {
return;
}
console.log(isMoving);
setIsFocused(false);
const input_box_translate_x_config = {
duration: 200,
toValue: 0,
easing: Easing.inOut(Easing.ease),
};
const text_translate_x_config = {
duration: 200,
toValue: -10,
easing: Easing.inOut(Easing.ease),
};
const content_translate_y_config = {
duration: 0,
toValue: height,
easing: Easing.inOut(Easing.ease),
};
const content_opacity_config = {
duration: 200,
toValue: 0,
easing: Easing.inOut(Easing.ease),
};
timing(inputBoxTranslateX, input_box_translate_x_config).start();
timing(contentTranslateY, content_translate_y_config).start();
timing(contentOpacity, content_opacity_config).start();
timing(textTranslateX, text_translate_x_config).start();
};
const panResponder = React.useRef(
PanResponder.create({
// Ask to be the responder:
onStartShouldSetPanResponder: (evt, gestureState) => true,
onStartShouldSetPanResponderCapture: (evt, gestureState) => {
const {dx, dy} = gestureState;
const shouldCap = dx > 2 || dx < -2;
if (shouldCap) {
isMoving = true;
}
return shouldCap;
},
onMoveShouldSetPanResponder: (evt, gestureState) => true,
onMoveShouldSetPanResponderCapture: (evt, gestureState) => {
const {dx, dy} = gestureState;
const shouldCap = dx > 2 || dx < -2;
if (shouldCap) {
isMoving = true;
}
return shouldCap;
},
onPanResponderMove: NativeAnimated.event(
[null, {dx: position.x, dy: position.y}],
{
useNativeDriver: false,
listener: (event, gestureState) => {
let {pageX, pageY} = event.nativeEvent;
isMoving = true;
console.log({pageX});
if (pageX < width / 2) {
console.log('Message cancelled');
}
},
},
),
onPanResponderTerminationRequest: (evt, gestureState) => true,
onPanResponderRelease: (evt, gestureState) => {
let {pageX, pageY} = evt.nativeEvent;
isMoving = false;
// if (pageX > 300) {
handlePressOut({isFromPan: true});
// }
NativeAnimated.spring(position, {
toValue: {x: 0, y: 0},
friction: 10,
useNativeDriver: true,
}).start();
},
onPanResponderTerminate: (evt, gestureState) => {},
onShouldBlockNativeResponder: (evt, gestureState) => {
return true;
},
}),
).current;
return (
<View
style={{
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
flex: 1,
marginHorizontal: 12,
}}>
<Animated.View
style={{
height: 50,
transform: [{translateX: inputBoxTranslateX}],
flexGrow: 1,
}}>
<Input style={{width: '100%', height: 40}} />
</Animated.View>
<View style={{flexDirection: 'row', alignItems: 'center'}}>
<Animated.View
style={{
opacity: contentOpacity,
transform: [{translateX: textTranslateX}],
}}>
{isFocused ? (
<Animated.Text
style={{
color: 'black',
fontSize: 20,
}}>
Slide Left to cancel
</Animated.Text>
) : null}
</Animated.View>
<NativeAnimated.View
style={[
{
alignItems: 'center',
justifyContent: 'center',
width: 46,
},
{
transform: [
{
translateX: position.x.interpolate({
inputRange: [-width + 80, 0],
outputRange: [-width + 80, 0],
extrapolate: 'clamp',
}),
},
{
scale: position.x.interpolate({
inputRange: [-width - 60, 0],
outputRange: [1.8, 1],
extrapolate: 'clamp',
}),
},
],
},
isFocused
? {
backgroundColor: 'orange',
borderRadius: 10,
}
: {},
]}
{...panResponder.panHandlers}>
<RecordButton
onPressIn={handlePressIn}
onPressOut={() => handlePressOut({pos: position})}
/>
</NativeAnimated.View>
</View>
</View>
);
};
export default App;
snippet above produces the following:
The problems with this snippet are:
the pan responder of the mic button allows it to move horizontally even if I do not press the button (not happening on video but in real device)
pan gesture allows moving both left/right while it should be moving only to left
when the mic button arrives at the middle of the screen, the button should be "released" and return to the initial position.
when dragging the button, the text "slide to cancel" should move along the button and not stay static.
whatsapp demo:
viber demo:

react native circle drag transform translate animation

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
}
});

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);