verdnatura-chat/app/presentation/ImageViewer/ImageViewer.android.tsx

386 lines
10 KiB
TypeScript

import React from 'react';
import { StyleSheet, View } from 'react-native';
import { PanGestureHandler, PinchGestureHandler, State } from 'react-native-gesture-handler';
import Animated, { EasingNode } from 'react-native-reanimated';
import { ImageComponent } from './ImageComponent';
import { themes } from '../../constants/colors';
const styles = StyleSheet.create({
flex: {
flex: 1
},
image: {
backgroundColor: 'transparent'
}
});
const {
set,
cond,
eq,
or,
add,
sub,
min,
max,
multiply,
divide,
lessThan,
decay,
timing,
diff,
not,
abs,
startClock,
stopClock,
clockRunning,
Value,
Clock,
event
} = Animated;
function scaleDiff(value: any) {
const tmp = new Value(1);
const prev = new Value(1);
return [set(tmp, divide(value, prev)), set(prev, value), tmp];
}
function dragDiff(value: any, updating: any) {
const tmp = new Value(0);
const prev = new Value(0);
return cond(updating, [set(tmp, sub(value, prev)), set(prev, value), tmp], set(prev, 0));
}
// returns linear friction coeff. When `value` is 0 coeff is 1 (no friction), then
// it grows linearly until it reaches `MAX_FRICTION` when `value` is equal
// to `MAX_VALUE`
function friction(value: any) {
const MAX_FRICTION = 5;
const MAX_VALUE = 100;
return max(1, min(MAX_FRICTION, add(1, multiply(value, (MAX_FRICTION - 1) / MAX_VALUE))));
}
function speed(value: any) {
const clock = new Clock();
const dt = diff(clock);
return cond(lessThan(dt, 1), 0, multiply(1000, divide(diff(value), dt)));
}
const MIN_SCALE = 1;
const MAX_SCALE = 2;
function scaleRest(value: any) {
return cond(lessThan(value, MIN_SCALE), MIN_SCALE, cond(lessThan(MAX_SCALE, value), MAX_SCALE, value));
}
function scaleFriction(value: any, rest: any, delta: any) {
const MAX_FRICTION = 20;
const MAX_VALUE = 0.5;
const res = multiply(value, delta);
const howFar = abs(sub(rest, value));
const f = max(1, min(MAX_FRICTION, add(1, multiply(howFar, (MAX_FRICTION - 1) / MAX_VALUE))));
return cond(lessThan(0, howFar), multiply(value, add(1, divide(add(delta, -1), f))), res);
}
function runTiming(clock: any, value: any, dest: any, startStopClock: any = true) {
const state = {
finished: new Value(0),
position: new Value(0),
frameTime: new Value(0),
time: new Value(0)
};
const config = {
toValue: new Value(0),
duration: 300,
easing: EasingNode.inOut(EasingNode.cubic)
};
return [
cond(clockRunning(clock), 0, [
set(state.finished, 0),
set(state.frameTime, 0),
set(state.time, 0),
set(state.position, value),
set(config.toValue, dest),
startStopClock && startClock(clock)
]),
timing(clock, state, config),
cond(state.finished, startStopClock && stopClock(clock)),
state.position
];
}
function runDecay(clock: any, value: any, velocity: any) {
const state = {
finished: new Value(0),
velocity: new Value(0),
position: new Value(0),
time: new Value(0)
};
const config = { deceleration: 0.99 };
return [
cond(clockRunning(clock), 0, [
set(state.finished, 0),
set(state.velocity, velocity),
set(state.position, value),
set(state.time, 0),
startClock(clock)
]),
set(state.position, value),
decay(clock, state, config),
cond(state.finished, stopClock(clock)),
state.position
];
}
function bouncyPinch(
value: any,
gesture: any,
gestureActive: any,
focalX: any,
displacementX: any,
focalY: any,
displacementY: any
) {
const clock = new Clock();
const delta = scaleDiff(gesture);
const rest = scaleRest(value);
const focalXRest = cond(lessThan(value, 1), 0, sub(displacementX, multiply(focalX, add(-1, divide(rest, value)))));
const focalYRest = cond(lessThan(value, 1), 0, sub(displacementY, multiply(focalY, add(-1, divide(rest, value)))));
const nextScale = new Value(1);
return cond(
[delta, gestureActive],
[
stopClock(clock),
set(nextScale, scaleFriction(value, rest, delta)),
set(displacementX, sub(displacementX, multiply(focalX, add(-1, divide(nextScale, value))))),
set(displacementY, sub(displacementY, multiply(focalY, add(-1, divide(nextScale, value))))),
nextScale
],
cond(
or(clockRunning(clock), not(eq(rest, value))),
[
set(displacementX, runTiming(clock, displacementX, focalXRest, false)),
set(displacementY, runTiming(clock, displacementY, focalYRest, false)),
runTiming(clock, value, rest)
],
value
)
);
}
function bouncy(value: any, gestureDiv: any, gestureActive: any, lowerBound: any, upperBound: any, f: any) {
const timingClock = new Clock();
const decayClock = new Clock();
const velocity = speed(value);
// did value go beyond the limits (lower, upper)
const isOutOfBounds = or(lessThan(value, lowerBound), lessThan(upperBound, value));
// position to snap to (upper or lower is beyond or the current value elsewhere)
const rest = cond(lessThan(value, lowerBound), lowerBound, cond(lessThan(upperBound, value), upperBound, value));
// how much the value exceeds the bounds, this is used to calculate friction
const outOfBounds = abs(sub(rest, value));
return cond(
[gestureDiv, velocity, gestureActive],
[stopClock(timingClock), stopClock(decayClock), add(value, divide(gestureDiv, f(outOfBounds)))],
cond(
or(clockRunning(timingClock), isOutOfBounds),
[stopClock(decayClock), runTiming(timingClock, value, rest)],
cond(or(clockRunning(decayClock), lessThan(5, abs(velocity))), runDecay(decayClock, value, velocity), value)
)
);
}
const WIDTH = 300;
const HEIGHT = 300;
interface IImageProps {
imageComponentType: string;
}
class Image extends React.PureComponent<IImageProps, any> {
render() {
const { imageComponentType }: any = this.props;
const Component = ImageComponent(imageComponentType);
return <Component {...this.props} />;
}
}
// https://github.com/software-mansion/react-native-reanimated/issues/1717
const AnimatedImage: any = Animated.createAnimatedComponent(Image);
// it was picked from https://github.com/software-mansion/react-native-reanimated/tree/master/Example/imageViewer
// and changed to use FastImage animated component
interface IImageViewerProps {
uri: string;
width: number;
height: number;
theme: string;
imageComponentType: string;
}
export class ImageViewer extends React.Component<IImageViewerProps, any> {
private _onPinchEvent: any;
private _focalDisplacementX: any;
private _focalDisplacementY: any;
private _scale: any;
private _onPanEvent: any;
private _panTransX: any;
private _panTransY: any;
constructor(props: IImageViewerProps) {
super(props);
// DECLARE TRANSX
const panTransX = new Value(0);
const panTransY = new Value(0);
// PINCH
const pinchScale = new Value(1);
const pinchFocalX = new Value(0);
const pinchFocalY = new Value(0);
const pinchState = new Value(-1);
this._onPinchEvent = event([
{
nativeEvent: {
state: pinchState,
scale: pinchScale,
focalX: pinchFocalX,
focalY: pinchFocalY
}
}
]);
// SCALE
const scale = new Value(1);
const pinchActive = eq(pinchState, State.ACTIVE);
this._focalDisplacementX = new Value(0);
const relativeFocalX = sub(pinchFocalX, add(panTransX, this._focalDisplacementX));
this._focalDisplacementY = new Value(0);
const relativeFocalY = sub(pinchFocalY, add(panTransY, this._focalDisplacementY));
this._scale = set(
scale,
bouncyPinch(
scale,
pinchScale,
pinchActive,
relativeFocalX,
this._focalDisplacementX,
relativeFocalY,
this._focalDisplacementY
)
);
// PAN
const dragX = new Value(0);
const dragY = new Value(0);
const panState = new Value(-1);
this._onPanEvent = event([
{
nativeEvent: {
translationX: dragX,
translationY: dragY,
state: panState
}
}
]);
const panActive = eq(panState, State.ACTIVE);
const panFriction = (value: any) => friction(value);
// X
const panUpX = cond(lessThan(this._scale, 1), 0, multiply(-1, this._focalDisplacementX));
const panLowX = add(panUpX, multiply(-WIDTH, add(max(1, this._scale), -1)));
this._panTransX = set(
panTransX,
bouncy(panTransX, dragDiff(dragX, panActive), or(panActive, pinchActive), panLowX, panUpX, panFriction)
);
// Y
const panUpY = cond(lessThan(this._scale, 1), 0, multiply(-1, this._focalDisplacementY));
const panLowY = add(panUpY, multiply(-HEIGHT, add(max(1, this._scale), -1)));
this._panTransY = set(
panTransY,
bouncy(panTransY, dragDiff(dragY, panActive), or(panActive, pinchActive), panLowY, panUpY, panFriction)
);
}
pinchRef = React.createRef();
panRef = React.createRef();
render() {
const { uri, width, height, imageComponentType, theme, ...props } = this.props;
// The below two animated values makes it so that scale appears to be done
// from the top left corner of the image view instead of its center. This
// is required for the "scale focal point" math to work correctly
const scaleTopLeftFixX = divide(multiply(WIDTH, add(this._scale, -1)), 2);
const scaleTopLeftFixY = divide(multiply(HEIGHT, add(this._scale, -1)), 2);
const backgroundColor = themes[theme].previewBackground;
return (
<View style={[styles.flex, { width, height, backgroundColor }]}>
<PinchGestureHandler
ref={this.pinchRef}
simultaneousHandlers={this.panRef}
onGestureEvent={this._onPinchEvent}
onHandlerStateChange={this._onPinchEvent}>
<Animated.View>
<PanGestureHandler
ref={this.panRef}
minDist={10}
avgTouches
simultaneousHandlers={this.pinchRef}
onGestureEvent={this._onPanEvent}
onHandlerStateChange={this._onPanEvent}>
<AnimatedImage
style={[
styles.image,
{
width,
height: '100%'
},
{
transform: [
{ translateX: this._panTransX },
{ translateY: this._panTransY },
{ translateX: this._focalDisplacementX },
{ translateY: this._focalDisplacementY },
{ translateX: scaleTopLeftFixX },
{ translateY: scaleTopLeftFixY },
{ scale: this._scale }
]
}
]}
imageComponentType={imageComponentType}
resizeMode='contain'
source={{ uri }}
{...props}
/>
</PanGestureHandler>
</Animated.View>
</PinchGestureHandler>
</View>
);
}
}