react-native-ui-lib
Version:
[](https://stand-with-ukraine.pp.ua)
509 lines (503 loc) • 15.6 kB
JavaScript
// @ts-nocheck
// @flow
// Similarly to the DrawerLayout component this deserves to be put in a
// separate repo. Although, keeping it here for the time being will allow us
// to move faster and fix possible issues quicker
// TODO: use Swipeable from react-native-gesture-handler once they support RTL
/* eslint-disable */
import React, { Component } from 'react';
import { Animated, StyleSheet, View, I18nManager } from 'react-native';
import { PanGestureHandler, TapGestureHandler, State } from 'react-native-gesture-handler';
import { Constants } from "../../commons/new";
import { HapticService, HapticType } from "../../services";
const DRAG_TOSS = 0.05;
const LEFT_TOGGLE_THRESHOLD = 0.6;
// Math.sign polyfill for iOS 8.x
if (!Math.sign) {
Math.sign = function (x) {
return Number(x > 0) - Number(x < 0) || +x;
};
}
class Swipeable extends Component {
static displayName = 'IGNORE';
static defaultProps = {
friction: 1,
overshootFriction: 1,
useNativeAnimations: false,
// issue in iPhone5
fullLeftThreshold: 0.45,
fullRightThreshold: 0.45
};
// _onGestureEvent: ?Animated.Event;
// _transX: ?Animated.Interpolation;
// _showLeftAction: ?Animated.Interpolation | ?Animated.Value;
// _leftActionTranslate: ?Animated.Interpolation;
// _showRightAction: ?Animated.Interpolation | ?Animated.Value;
// _rightActionTranslate: ?Animated.Interpolation;
constructor(props) {
super(props);
const dragX = new Animated.Value(0);
// 0 -> open from either left/right,
// 1 -> closing to the left
// -1 -> closing to the right
this.rowState = 0;
this.dragThresholdReached = false;
this.state = {
dragX,
rowTranslation: new Animated.Value(0),
rowWidth: Constants.screenWidth,
leftWidth: undefined,
rightOffset: undefined,
measureCompleted: false
};
this._onGestureEvent = Animated.event([{
nativeEvent: {
translationX: dragX
}
}], {
useNativeDriver: props.useNativeAnimations,
listener: this._handleDrag
});
}
_triggerHaptic = () => {
return !this.props.disableHaptic && HapticService.triggerHaptic(HapticType.impactMedium, 'Drawer');
};
_handleDrag = e => {
const {
onToggleSwipeLeft
} = this.props;
if (onToggleSwipeLeft) {
// Drag left toggle
const {
rowWidth,
leftWidth
} = this.state;
const x = e.nativeEvent.translationX;
const threshold = rowWidth * LEFT_TOGGLE_THRESHOLD;
if (!this.dragThresholdReached && x >= threshold && x < threshold + 10) {
// move item right
this.dragThresholdReached = true;
this._triggerHaptic();
onToggleSwipeLeft({
rowWidth,
leftWidth,
dragX: x
});
}
if (this.dragThresholdReached && x < threshold - 10) {
// move item left
this.dragThresholdReached = false;
onToggleSwipeLeft({
rowWidth,
leftWidth,
dragX: x,
resetItemPosition: true
});
}
}
};
getTransX = () => {
const {
friction,
overshootFriction
} = this.props;
const {
dragX,
rowTranslation,
leftWidth = 0,
rowWidth = 0
} = this.state;
const {
rightOffset = rowWidth
} = this.state;
const rightWidth = Math.max(0, rowWidth - rightOffset);
const {
overshootLeft = leftWidth > 0,
overshootRight = rightWidth > 0
} = this.props;
const transX = Animated.add(rowTranslation, dragX.interpolate({
inputRange: [0, friction],
outputRange: [0, 1]
})).interpolate({
inputRange: [-rightWidth - (overshootRight ? 1 : overshootFriction), -rightWidth, leftWidth, leftWidth + (overshootLeft ? 1 : overshootFriction)],
outputRange: [-rightWidth - (overshootRight || overshootFriction > 1 ? 1 : 0), -rightWidth, leftWidth, leftWidth + (overshootLeft || overshootFriction > 1 ? 1 : 0)]
});
return transX;
};
getShowLeftAction = () => {
const transX = this.getTransX();
const {
leftWidth = 0
} = this.state;
const showLeftAction = leftWidth > 0 ? transX.interpolate({
inputRange: [-1, 0, leftWidth],
outputRange: [0, 0, 1]
}) : new Animated.Value(0);
return showLeftAction;
};
getLeftActionTranslate = () => {
const showLeftAction = this.getShowLeftAction();
const leftActionTranslate = showLeftAction.interpolate({
inputRange: [0, Number.MIN_VALUE],
outputRange: [-10000, 0],
extrapolate: 'clamp'
});
return leftActionTranslate;
};
getShowRightAction = () => {
const transX = this.getTransX();
const {
rowWidth = 0
} = this.state;
const {
rightOffset = rowWidth
} = this.state;
const rightWidth = Math.max(0, rowWidth - rightOffset);
const showRightAction = rightWidth > 0 ? transX.interpolate({
inputRange: [-rightWidth, 0, 1],
outputRange: [1, 0, 0]
}) : new Animated.Value(0);
return showRightAction;
};
getRightActionTranslate = () => {
const showRightAction = this.getShowRightAction();
const rightActionTranslate = showRightAction.interpolate({
inputRange: [0, Number.MIN_VALUE],
outputRange: [-10000, 0],
extrapolate: 'clamp'
});
return rightActionTranslate;
};
_onTapHandlerStateChange = ({
nativeEvent
}) => {
if (this.rowState !== 0) {
if (nativeEvent.oldState === State.ACTIVE) {
this.close();
}
}
};
_onHandlerStateChange = ({
nativeEvent
}) => {
if (nativeEvent.oldState === State.ACTIVE) {
this._handleRelease(nativeEvent);
}
if (nativeEvent.state === State.ACTIVE) {
this.props.onDragStart && this.props.onDragStart(this.props);
}
};
_hasLeftActions = this.props.renderLeftActions !== undefined;
_hasRightActions = this.props.renderRightActions !== undefined;
_handleRelease = nativeEvent => {
const {
velocityX,
translationX: dragX
} = nativeEvent;
const {
leftWidth = 0,
rowWidth = 0
} = this.state;
const {
rightOffset = rowWidth
} = this.state;
const rightWidth = rowWidth - rightOffset;
const {
fullSwipeLeft,
fullSwipeRight,
friction,
leftThreshold = leftWidth / 2,
rightThreshold = rightWidth / 2,
fullLeftThreshold,
fullRightThreshold,
onToggleSwipeLeft
} = this.props;
const startOffsetX = this._currentOffset() + dragX / friction;
const translationX = (dragX + DRAG_TOSS * velocityX) / friction;
let toValue = 0;
if (this.rowState === 0) {
if (Constants.isRTL && this._hasLeftActions && onToggleSwipeLeft && translationX < -(rowWidth * LEFT_TOGGLE_THRESHOLD) && !this.dragThresholdReached) {
// Swipe left toggle RTL
toValue = -(rowWidth * LEFT_TOGGLE_THRESHOLD);
} else if (this._hasLeftActions && onToggleSwipeLeft && translationX > rowWidth * LEFT_TOGGLE_THRESHOLD && !this.dragThresholdReached) {
// Swipe left toggle
toValue = rowWidth * LEFT_TOGGLE_THRESHOLD;
} else if (!onToggleSwipeLeft && fullSwipeLeft && translationX > rowWidth * fullLeftThreshold) {
// Full left swipe
this._triggerHaptic();
toValue = rowWidth;
} else if (this._hasRightActions && fullSwipeRight && translationX < -rowWidth * fullRightThreshold) {
// Full right swipe
this._triggerHaptic();
toValue = -rowWidth;
} else if (this._hasLeftActions && translationX > leftThreshold) {
// left swipe
if (!onToggleSwipeLeft || onToggleSwipeLeft && translationX < rowWidth * LEFT_TOGGLE_THRESHOLD) {
// left swipe with toggle
toValue = leftWidth;
}
} else if (this._hasRightActions && translationX < -rightThreshold) {
// right swipe
toValue = -rightWidth;
}
} else if (this.rowState === 1) {
// swiped to the right (left swipe)
if (translationX > -leftThreshold) {
toValue = leftWidth;
}
} else {
// swiped to the left (right swipe)
if (translationX < rightThreshold) {
toValue = -rightWidth;
}
}
this._animateRow(startOffsetX, toValue, velocityX / friction);
};
_animateRow = (fromValue, toValue, velocityX) => {
const {
dragX,
rowTranslation,
rowWidth,
leftWidth
} = this.state;
const {
useNativeAnimations,
animationOptions,
onSwipeableLeftOpen,
onSwipeableRightOpen,
onSwipeableClose,
onSwipeableOpen,
onSwipeableLeftWillOpen,
onSwipeableRightWillOpen,
onSwipeableWillClose,
onSwipeableWillOpen,
onFullSwipeLeft,
onToggleSwipeLeft,
onWillFullSwipeLeft,
onFullSwipeRight,
onWillFullSwipeRight
} = this.props;
dragX.setValue(0);
rowTranslation.setValue(fromValue);
this.rowState = Math.sign(toValue);
Animated.spring(rowTranslation, {
toValue,
restSpeedThreshold: 1.7,
restDisplacementThreshold: 0.4,
velocity: velocityX,
bounciness: 0,
useNativeDriver: useNativeAnimations,
...animationOptions
}).start(({
finished
}) => {
if (finished) {
// Final Callbacks
if (toValue === rowWidth && onFullSwipeLeft) {
onFullSwipeLeft();
} else if (toValue === -rowWidth && onFullSwipeRight) {
onFullSwipeRight();
} else if (toValue > 0 && onSwipeableLeftOpen) {
onSwipeableLeftOpen();
} else if (toValue < 0 && onSwipeableRightOpen) {
onSwipeableRightOpen();
}
if (toValue === 0) {
onSwipeableClose && onSwipeableClose();
} else {
onSwipeableOpen && onSwipeableOpen();
}
}
});
// Transition Callbacks
if (Constants.isRTL && this._hasLeftActions && onToggleSwipeLeft && (toValue === -(rowWidth * LEFT_TOGGLE_THRESHOLD) || this.dragThresholdReached)) {
// left toggle RTL
onToggleSwipeLeft({
rowWidth,
leftWidth,
released: true,
triggerHaptic: !this.dragThresholdReached
});
} else if (this._hasLeftActions && onToggleSwipeLeft && (toValue === rowWidth * LEFT_TOGGLE_THRESHOLD || this.dragThresholdReached)) {
// left toggle
onToggleSwipeLeft({
rowWidth,
leftWidth,
released: true,
triggerHaptic: !this.dragThresholdReached
});
this.dragThresholdReached = false;
} else if (toValue === rowWidth && onWillFullSwipeLeft) {
onWillFullSwipeLeft();
} else if (toValue === -rowWidth && onWillFullSwipeRight) {
onWillFullSwipeRight();
} else if (toValue > 0 && onSwipeableLeftWillOpen) {
onSwipeableLeftWillOpen();
} else if (toValue < 0 && onSwipeableRightWillOpen) {
onSwipeableRightWillOpen();
}
if (toValue === 0) {
onSwipeableWillClose && onSwipeableWillClose();
} else {
onSwipeableWillOpen && onSwipeableWillOpen();
}
};
_currentOffset = () => {
const {
leftWidth = 0,
rowWidth = 0
} = this.state;
const {
rightOffset = rowWidth
} = this.state;
const rightWidth = rowWidth - rightOffset;
if (this.rowState === 1) {
return leftWidth;
} else if (this.rowState === -1) {
return -rightWidth;
}
return 0;
};
close = () => {
this._animateRow(this._currentOffset(), 0);
};
openLeft = () => {
const {
leftWidth = 0
} = this.state;
this._animateRow(this._currentOffset(), leftWidth);
};
openLeftFull = () => {
if (this._hasLeftActions) {
const {
rowWidth
} = this.state;
this._animateRow(this._currentOffset(), rowWidth);
}
};
toggleLeft = () => {
// Programmatically left toggle
const shouldAnimate = Constants.isRTL ? this._hasRightActions : this._hasLeftActions;
if (shouldAnimate) {
const {
rowWidth
} = this.state;
this._animateRow(this._currentOffset(), rowWidth * LEFT_TOGGLE_THRESHOLD * (Constants.isRTL ? -1 : 1));
}
};
openRight = () => {
const {
rowWidth = 0
} = this.state;
const {
rightOffset = rowWidth
} = this.state;
const rightWidth = rowWidth - rightOffset;
this._animateRow(this._currentOffset(), -rightWidth);
};
openRightFull = () => {
if (this._hasRightActions) {
const {
rowWidth
} = this.state;
this._animateRow(this._currentOffset(), -rowWidth);
}
};
_onRowLayout = ({
nativeEvent
}) => this.handleMeasure('rowWidth', nativeEvent);
_onLeftLayout = ({
nativeEvent
}) => this.handleMeasure('leftWidth', nativeEvent);
_onRightLayout = ({
nativeEvent
}) => this.handleMeasure('rightOffset', nativeEvent);
handleMeasure = (name, nativeEvent) => {
const {
width,
x
} = nativeEvent.layout;
switch (name) {
case 'rowWidth':
this.rowWidth = width;
break;
case 'leftWidth':
this.leftWidth = x;
break;
case 'rightOffset':
this.rightOffset = x;
break;
default:
break;
}
const leftRender = this._hasLeftActions ? this.leftWidth : true;
const rightRender = this._hasRightActions ? this.rightOffset : true;
if (this.rowWidth && leftRender && rightRender) {
this.setState({
rowWidth: this.rowWidth,
leftWidth: this.leftWidth,
rightOffset: this.rightOffset,
measureCompleted: true
});
}
};
render() {
const {
children,
renderLeftActions,
renderRightActions,
leftActionsContainerStyle,
rightActionsContainerStyle,
containerStyle,
childrenContainerStyle,
testID
} = this.props;
const left = this._hasLeftActions && <Animated.View style={[styles.leftActions, leftActionsContainerStyle, {
transform: [{
translateX: this.getLeftActionTranslate()
}]
}]}>
{renderLeftActions(this.getShowLeftAction(), this.getTransX())}
<View onLayout={this._onLeftLayout} />
</Animated.View>;
const right = this._hasRightActions && <Animated.View style={[styles.rightActions, rightActionsContainerStyle, {
transform: [{
translateX: this.getRightActionTranslate()
}]
}]}>
{renderRightActions(this.getShowRightAction(), this.getTransX())}
<View onLayout={this._onRightLayout} />
</Animated.View>;
return <PanGestureHandler {...this.props}
// minDeltaX={10}
activeOffsetX={[-44, 44]} onGestureEvent={this._onGestureEvent} onHandlerStateChange={this._onHandlerStateChange}>
<Animated.View onLayout={this._onRowLayout} style={[styles.container, containerStyle]}>
{left}
{right}
<TapGestureHandler onHandlerStateChange={this._onTapHandlerStateChange}>
<Animated.View style={[{
transform: [{
translateX: this.getTransX()
}]
}, childrenContainerStyle]}>
{children}
</Animated.View>
</TapGestureHandler>
</Animated.View>
</PanGestureHandler>;
}
}
export default Swipeable;
const styles = StyleSheet.create({
container: {
overflow: 'hidden'
},
leftActions: {
...StyleSheet.absoluteFillObject,
flexDirection: I18nManager.isRTL ? 'row-reverse' : 'row'
},
rightActions: {
...StyleSheet.absoluteFillObject,
flexDirection: I18nManager.isRTL ? 'row' : 'row-reverse'
}
});