@react-slip-and-slide/native
Version:
react-slip-and-slide native
668 lines (660 loc) • 17.9 kB
JavaScript
import { typedMemo, useScreenDimensions, useDynamicDimension, useItemsRange, getCurrentDynamicIndex, rubberband, getNextDynamicOffset, isInRange, Styled, LazyLoad, displacement, AnimatedBox } from '@react-slip-and-slide/utils/dist/index.native';
import { SpringValue, to } from '@react-spring/native';
import { clamp } from 'lodash';
import React from 'react';
import { TouchableWithoutFeedback } from 'react-native';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
function _extends() {
_extends = Object.assign ? Object.assign.bind() : function (target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
}
return target;
};
return _extends.apply(this, arguments);
}
function ReactSlipAndSlideComponent({
data,
snap,
centered,
infinite: _infinite,
containerWidth,
overflowHidden = true,
itemHeight,
itemWidth = 0,
pressToSlide,
interpolators,
animateStartup = true,
rubberbandElasticity = 4,
visibleItems = 0,
renderItem,
onChange,
onEdges,
onReady
}, ref) {
const mode = itemWidth && itemHeight ? "fixed" : "dynamic";
const infinite = mode === "fixed" && !!_infinite;
const eagerLoading = mode === "dynamic" || visibleItems === 0;
const shouldAnimatedStartup = animateStartup && eagerLoading;
const index = React.useRef(0);
const [_, reRender] = React.useState(0);
const {
width: screenWidth
} = useScreenDimensions();
const lastOffset = React.useRef(0);
const [container, setContainerDimensions] = React.useState({
width: containerWidth || screenWidth || 0,
height: itemHeight || 0
});
const [_wrapperWidth, _setWrapperWidth] = React.useState(0);
const containerRef = React.useRef(null);
const isIntentionalDrag = React.useRef(false);
const direction = React.useRef("center");
const lastValidDirection = React.useRef(null);
const isDragging = React.useRef(false);
const OffsetX = React.useMemo(() => {
return new SpringValue(0, {
config: {
tension: 260,
friction: 32,
mass: 1
}
});
}, []);
const Opacity = React.useMemo(() => {
const initialOpacity = shouldAnimatedStartup ? 0 : 1;
return new SpringValue(initialOpacity, {
config: {
tension: 260,
friction: 32,
mass: 1
}
});
}, [shouldAnimatedStartup]);
const {
itemRefs,
itemDimensionMap
} = useDynamicDimension({
mode,
dataLength: data.length,
onMeasure: ({
itemWidthSum
}) => {
if (itemWidthSum) {
_setWrapperWidth(itemWidthSum);
}
}
});
const {
ranges
} = useItemsRange({
mode,
itemDimensionMap,
offsetX: OffsetX.get()
});
React.useEffect(() => {
if (containerRef.current && (!containerWidth || !itemHeight)) {
setTimeout(() => {
var _containerRef$current;
(_containerRef$current = containerRef.current) == null ? void 0 : _containerRef$current.measure((_, __, width, height) => {
setContainerDimensions({
width,
height
});
});
}, 200);
} else {
setContainerDimensions(prev => _extends({}, prev, {
width: screenWidth
}));
}
}, [containerWidth, containerRef, itemHeight, screenWidth]);
const processClampOffsets = React.useCallback(({
wrapperWidth,
sideMargins
}) => {
const MIN = 0;
let MAX = -wrapperWidth + container.width;
if (centered) {
const _MAX_CENTERED = MAX - sideMargins * 2;
MAX = _MAX_CENTERED;
} else {
if (wrapperWidth < container.width) {
MAX = MIN;
}
}
return {
MIN,
MAX
};
}, [centered, container.width]);
const {
dataLength,
wrapperWidth,
clampOffset
} = React.useMemo(() => {
const wrapperWidth = mode === "fixed" ? data.length * itemWidth : _wrapperWidth;
const sideMargins = (container.width - itemWidth) / 2;
const {
MIN,
MAX
} = processClampOffsets({
wrapperWidth,
sideMargins
});
return {
dataLength: data.length,
wrapperWidth,
sideMargins,
halfItem: itemWidth / 2,
clampOffset: {
MIN,
MAX
}
};
}, [_wrapperWidth, container.width, data.length, itemWidth, mode, processClampOffsets]);
const clampReleaseOffset = React.useCallback(offset => {
if (infinite && mode === "fixed") {
return offset;
}
if (offset > clampOffset.MIN) {
return clampOffset.MIN;
} else if (offset < clampOffset.MAX) {
return clampOffset.MAX;
}
return offset;
}, [clampOffset.MAX, clampOffset.MIN, infinite, mode]);
const clampIndex = React.useCallback(index => clamp(index, 0, dataLength - 1), [dataLength]);
const processIndex = React.useCallback(({
offset
}) => {
const modIndex = offset / itemWidth % dataLength;
return offset <= 0 ? Math.abs(modIndex) : Math.abs(modIndex > 0 ? dataLength - modIndex : 0);
}, [dataLength, itemWidth]);
const getCurrentIndex = React.useCallback(({
offset
}) => {
if (infinite) {
return -Math.round(offset / itemWidth);
}
return Math.round(processIndex({
offset
}));
}, [infinite, itemWidth, processIndex]);
const getRelativeIndex = React.useCallback(({
offset
}) => {
return Math.floor(processIndex({
offset
}));
}, [processIndex]);
const getCurrentOffset = React.useCallback(({
index
}) => {
const finalOffset = -index * itemWidth;
return finalOffset;
}, [itemWidth]);
const checkEdges = React.useCallback(({
offset
}) => {
if (offset >= clampOffset.MIN) {
return {
start: true,
end: false
};
} else if (offset <= clampOffset.MAX) {
return {
start: false,
end: true
};
} else {
return {
start: false,
end: false
};
}
}, [clampOffset.MAX, clampOffset.MIN]);
const springIt = React.useCallback(({
offset,
immediate,
actionType,
onRest
}) => {
const clampedReleaseOffset = clampReleaseOffset(offset);
OffsetX.start({
to: actionType === "drag" || actionType === "correction" ? offset : clampedReleaseOffset,
immediate: immediate || actionType === "drag",
onRest: x => {
onRest == null ? void 0 : onRest(x);
if (actionType === "release") {
if (mode === "fixed") {
index.current = clampIndex(getRelativeIndex({
offset: clampedReleaseOffset
}));
} else {
index.current = getCurrentDynamicIndex(offset, ranges);
}
if (!eagerLoading) {
reRender(index.current);
}
onChange == null ? void 0 : onChange(index.current);
}
}
});
if (actionType === "release") {
lastOffset.current = clampedReleaseOffset;
if (!infinite) {
onEdges == null ? void 0 : onEdges(checkEdges({
offset
}));
}
}
}, [OffsetX, checkEdges, clampIndex, clampReleaseOffset, eagerLoading, getRelativeIndex, infinite, mode, onChange, onEdges, ranges]);
const getCurrentIndexByOffset = React.useCallback(offset => {
let finalIndex = 0;
const neutralIndex = offset / wrapperWidth * dataLength;
const left = Math.ceil(neutralIndex);
const right = Math.floor(neutralIndex);
if (!snap) {
return right;
}
switch (direction.current) {
case "left":
finalIndex = left;
break;
case "right":
finalIndex = right;
break;
default:
if (lastValidDirection.current === "left") {
finalIndex = left;
} else if (lastValidDirection.current === "right") {
finalIndex = right;
}
break;
}
return finalIndex;
}, [dataLength, snap, wrapperWidth]);
const drag = React.useCallback(x => {
const offset = infinite ? x : rubberband(x, rubberbandElasticity, [clampOffset.MIN, clampOffset.MAX]);
springIt({
offset,
actionType: "drag"
});
}, [clampOffset, infinite, rubberbandElasticity, springIt]);
const withSnap = React.useCallback(({
offset
}) => {
if (mode === "fixed") {
const page = getCurrentIndexByOffset(-offset);
const finalOffset = -page * itemWidth;
return finalOffset;
} else {
const edges = checkEdges({
offset
});
return getNextDynamicOffset({
offsetX: edges.start ? clampOffset.MIN : offset,
ranges,
dir: lastValidDirection.current,
centered: !!centered
});
}
}, [centered, checkEdges, clampOffset.MIN, getCurrentIndexByOffset, itemWidth, mode, ranges]);
const withMomentum = React.useCallback(({
offset,
v
}) => {
const momentumOffset = offset + v;
return momentumOffset;
}, []);
const release = React.useCallback(({
offset,
v
}) => {
let offsetX = 0;
if (snap) {
if (isIntentionalDrag.current) {
offsetX = withSnap({
offset
});
} else {
springIt({
offset: lastOffset.current,
actionType: "correction"
});
return;
}
} else {
offsetX = withMomentum({
offset,
v
});
}
springIt({
offset: offsetX,
actionType: "release"
});
}, [snap, springIt, withMomentum, withSnap]);
const navigate = React.useCallback(({
index: _index,
direction,
immediate
}) => {
let targetOffset = 0;
if (_index) {
targetOffset = getCurrentOffset({
index: _index
});
} else {
if (mode === "fixed") {
const page = getCurrentIndex({
offset: OffsetX.get()
});
if (direction === "next") {
const nextPage = page + 1;
targetOffset = -nextPage * itemWidth;
} else if (direction === "prev") {
const prevPage = page - 1;
targetOffset = -prevPage * itemWidth;
}
} else {
targetOffset = getNextDynamicOffset({
offsetX: OffsetX.get(),
ranges,
dir: direction === "next" ? "left" : direction === "prev" ? "right" : null,
centered: !!centered
});
}
}
springIt({
offset: targetOffset,
immediate,
actionType: "release"
});
}, [OffsetX, centered, getCurrentIndex, getCurrentOffset, itemWidth, mode, ranges, springIt]);
const move = React.useCallback(offset => {
springIt({
offset: OffsetX.get() + offset,
actionType: "release"
});
}, [OffsetX, springIt]);
const panGesture = Gesture.Pan().onUpdate(({
translationX,
velocityX,
state
}) => {
const dir = velocityX > 0 ? "right" : velocityX < 0 ? "left" : "center";
direction.current = dir;
if (dir !== "center") {
lastValidDirection.current = dir;
}
isIntentionalDrag.current = Math.abs(translationX) >= 40;
isDragging.current = state === 4;
const offset = lastOffset.current + translationX;
drag(offset);
}).onEnd(({
velocityX,
translationX,
state
}) => {
isDragging.current = state === 4;
const offset = lastOffset.current + translationX;
release({
offset,
v: velocityX / 12
});
});
const handlePressToSlide = React.useCallback(idx => {
if (!pressToSlide || isDragging.current || isIntentionalDrag.current) {
return;
}
if (mode === "fixed") {
const prev = index.current === 0 && idx === dataLength - 1;
const next = index.current === dataLength - 1 && idx === 0;
const smaller = idx < index.current;
const bigger = idx > index.current;
if (prev) {
navigate({
direction: "prev"
});
} else if (next) {
navigate({
direction: "next"
});
} else if (smaller) {
navigate({
direction: "prev"
});
} else if (bigger) {
navigate({
direction: "next"
});
}
} else {
const currIndx = getCurrentDynamicIndex(OffsetX.get(), ranges);
if (idx < currIndx) {
navigate({
direction: "prev"
});
} else if (idx > currIndx) {
navigate({
direction: "next"
});
}
}
}, [OffsetX, dataLength, index, mode, navigate, pressToSlide, ranges]);
React.useEffect(() => {
if (shouldAnimatedStartup) {
if (mode === "dynamic") {
if (ranges.length && container.height) {
Opacity.start({
to: 1,
onRest: () => {
onReady == null ? void 0 : onReady(true);
}
});
}
} else {
Opacity.start({
to: 1,
delay: 100,
onRest: () => {
onReady == null ? void 0 : onReady(true);
}
});
}
} else {
onReady == null ? void 0 : onReady(true);
}
}, [Opacity, container.height, ranges.length, shouldAnimatedStartup]);
React.useEffect(() => {
if (mode === "dynamic" && centered) {
var _ranges$;
const alignment = centered ? "center" : "start";
springIt({
offset: -(((_ranges$ = ranges[0]) == null ? void 0 : _ranges$.range[alignment]) || 0),
actionType: "release",
immediate: true
});
}
}, [centered, mode, ranges, springIt]);
React.useEffect(() => {
const {
end
} = checkEdges({
offset: OffsetX.get()
});
if (end) {
springIt({
offset: clampOffset.MAX,
actionType: "release"
});
}
}, [clampOffset.MAX]);
React.useEffect(() => {
if (!infinite) {
navigate({
index: 0,
immediate: true
});
}
}, [infinite]);
React.useImperativeHandle(ref, () => ({
next: () => navigate({
direction: "next"
}),
previous: () => navigate({
direction: "prev"
}),
goTo: ({
index,
animated
}) => navigate({
index,
immediate: !animated
}),
move
}), [move, navigate]);
const shouldRender = React.useCallback(i => {
if (eagerLoading) {
return true;
}
return isInRange(i, {
dataLength,
viewSize: itemWidth,
visibleItems: visibleItems || Math.round(dataLength / 2),
offsetX: OffsetX.get()
});
}, [OffsetX, dataLength, eagerLoading, itemWidth, visibleItems]);
return React.createElement(GestureDetector, {
gesture: panGesture
}, React.createElement(Styled.Wrapper, {
ref: containerRef,
style: {
opacity: Opacity,
justifyContent: centered ? "center" : "flex-start",
width: containerWidth || screenWidth,
height: itemHeight || container.height || "100%",
overflow: overflowHidden ? "hidden" : undefined
}
}, data.map((props, i) => {
var _ranges$i, _ranges$i2;
return React.createElement(LazyLoad, {
key: i,
render: shouldRender(i)
}, React.createElement(Item, {
ref: itemRefs[i],
index: i,
mode: mode,
item: props,
dataLength: dataLength,
renderItem: renderItem,
infinite: infinite,
itemHeight: itemHeight,
itemWidth: mode === "fixed" ? itemWidth : ((_ranges$i = ranges[i]) == null ? void 0 : _ranges$i.width) || 0,
interpolators: interpolators || {},
dynamicOffset: ((_ranges$i2 = ranges[i]) == null ? void 0 : _ranges$i2.range[centered ? "center" : "start"]) || 0,
onPress: () => pressToSlide && handlePressToSlide(i),
offsetX: OffsetX.to(offsetX => infinite ? offsetX % wrapperWidth : offsetX),
isLazy: !eagerLoading
}));
})));
}
function ItemComponent({
offsetX,
dataLength,
index,
infinite,
itemWidth,
itemHeight,
item,
interpolators,
dynamicOffset,
mode,
isLazy,
renderItem,
onPress
}, ref) {
const Opacity = React.useMemo(() => {
return new SpringValue(isLazy ? 0 : 1, {
config: {
tension: 260,
friction: 32,
mass: 1
}
});
}, [isLazy]);
const x = displacement({
offsetX,
dataLength,
index,
itemWidth,
infinite
});
const keys = Object.entries(interpolators);
const translateX = React.useMemo(() => {
if (mode === "fixed") {
return x.to(val => val / itemWidth).to([-1, 0, 1], [-itemWidth, 0, itemWidth]);
}
return to(offsetX, x => x + dynamicOffset);
}, [dynamicOffset, itemWidth, mode, offsetX, x]);
const {
scale,
opacity
} = React.useMemo(() => {
if (itemWidth) {
return keys.reduce((acc, [key, val]) => {
acc[key] = translateX.to(val => val / itemWidth).to([-1, 0, 1], [val, 1, val], "clamp");
return acc;
}, {});
}
return {
scale: 1,
opacity: 1
};
}, [itemWidth, keys, translateX]);
React.useEffect(() => {
if (isLazy) {
Opacity.start({
to: 1
});
}
}, [isLazy]);
const memoRenderItem = React.useMemo(() => {
return renderItem({
item,
index
});
}, [index, item, renderItem]);
return React.createElement(TouchableWithoutFeedback, {
onPress: onPress
}, React.createElement(Styled.Item, {
ref: ref,
style: {
transform: [{
translateX
}, {
scale: scale || 1
}],
opacity,
width: itemWidth === 0 ? undefined : itemWidth,
height: itemHeight
}
}, React.createElement(AnimatedBox, {
style: {
width: "100%",
height: "100%",
opacity: Opacity
}
}, memoRenderItem)));
}
const Item = React.forwardRef(ItemComponent);
const ForwardReactSlipAndSlideRef = React.forwardRef(ReactSlipAndSlideComponent);
const ReactSlipAndSlide = typedMemo(ForwardReactSlipAndSlideRef);
export { ReactSlipAndSlide };