react-native-snap-carousel-v4
Version:
Original project: https://github.com/meliorence/react-native-snap-carousel I made this package because I need the version 4 package to be published, so that I can run EAS Build on my expo app, previously I was pointing directly to the v4 branch on the ori
1,349 lines (1,066 loc) • 42.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = exports.Carousel = void 0;
var _react = _interopRequireDefault(require("react"));
var _reactNative = require("react-native");
var _reactAddonsShallowCompare = _interopRequireDefault(require("react-addons-shallow-compare"));
var _animations = require("../utils/animations");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _extends() { _extends = Object.assign || 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 _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
// Metro doesn't support dynamic imports - i.e. require() done in the component itself
// But at the same time the following import will fail on Snack...
// TODO: find a way to get React Native's version without having to assume the file path
// import RN_PACKAGE from '../../../react-native/package.json';
const IS_ANDROID = _reactNative.Platform.OS === 'android'; // React Native automatically handles RTL layouts; unfortunately, it's buggy with horizontal ScrollView
// See https://github.com/facebook/react-native/issues/11960
// NOTE: the following variable is not declared in the constructor
// otherwise it is undefined at init, which messes with custom indexes
const IS_RTL = _reactNative.I18nManager.isRTL;
class Carousel extends _react.default.Component {
constructor(props) {
super(props);
_defineProperty(this, "_activeItem", void 0);
_defineProperty(this, "_onScrollActiveItem", void 0);
_defineProperty(this, "_previousFirstItem", void 0);
_defineProperty(this, "_previousItemsLength", void 0);
_defineProperty(this, "_mounted", void 0);
_defineProperty(this, "_positions", void 0);
_defineProperty(this, "_currentScrollOffset", void 0);
_defineProperty(this, "_scrollEnabled", void 0);
_defineProperty(this, "_initTimeout", void 0);
_defineProperty(this, "_apparitionTimeout", void 0);
_defineProperty(this, "_hackSlideAnimationTimeout", void 0);
_defineProperty(this, "_enableAutoplayTimeout", void 0);
_defineProperty(this, "_autoplayTimeout", void 0);
_defineProperty(this, "_snapNoMomentumTimeout", void 0);
_defineProperty(this, "_androidRepositioningTimeout", void 0);
_defineProperty(this, "_autoplayInterval", void 0);
_defineProperty(this, "_scrollPos", void 0);
_defineProperty(this, "_onScrollHandler", void 0);
_defineProperty(this, "_carouselRef", null);
_defineProperty(this, "_autoplaying", void 0);
_defineProperty(this, "_autoplay", void 0);
_defineProperty(this, "_onLayoutInitDone", void 0);
this.state = {
hideCarousel: !!props.apparitionDelay,
interpolators: []
}; // this._RNVersionCode = this._getRNVersionCode();
// The following values are not stored in the state because 'setState()' is asynchronous
// and this results in an absolutely crappy behavior on Android while swiping (see #156)
const initialActiveItem = this._getFirstItem(props.firstItem);
this._activeItem = initialActiveItem;
this._onScrollActiveItem = initialActiveItem;
this._previousFirstItem = initialActiveItem;
this._previousItemsLength = initialActiveItem;
this._mounted = false;
this._positions = [];
this._currentScrollOffset = 0; // Store ScrollView's scroll position
this._scrollEnabled = props.scrollEnabled !== false;
this._getCellRendererComponent = this._getCellRendererComponent.bind(this);
this._getItemLayout = this._getItemLayout.bind(this);
this._getKeyExtractor = this._getKeyExtractor.bind(this);
this._onLayout = this._onLayout.bind(this);
this._onScroll = this._onScroll.bind(this);
this._onMomentumScrollEnd = this._onMomentumScrollEnd.bind(this);
this._onTouchStart = this._onTouchStart.bind(this);
this._onTouchEnd = this._onTouchEnd.bind(this);
this._renderItem = this._renderItem.bind(this); // WARNING: call this AFTER binding _onScroll
this._setScrollHandler(props); // Display warnings
this._displayWarnings(props);
}
componentDidMount() {
const {
apparitionDelay,
autoplay,
firstItem
} = this.props;
this._mounted = true;
this._initPositionsAndInterpolators(); // Without 'requestAnimationFrame' or a `0` timeout, images will randomly not be rendered on Android...
this._initTimeout = setTimeout(() => {
if (!this._mounted) {
return;
}
const apparitionCallback = () => {
if (apparitionDelay) {
this.setState({
hideCarousel: false
});
}
if (autoplay) {
this.startAutoplay();
}
}; // FlatList will use its own built-in prop `initialScrollIndex`
if (this._needsScrollView()) {
const _firstItem = this._getFirstItem(firstItem);
this._snapToItem(_firstItem, false, false, true); // this._hackActiveSlideAnimation(_firstItem);
}
if (apparitionDelay) {
this._apparitionTimeout = setTimeout(() => {
apparitionCallback();
}, apparitionDelay);
} else {
apparitionCallback();
}
}, 1);
}
shouldComponentUpdate(nextProps, nextState) {
if (this.props.shouldOptimizeUpdates === false) {
return true;
} else {
return (0, _reactAddonsShallowCompare.default)(this, nextProps, nextState);
}
}
componentDidUpdate(prevProps) {
const {
interpolators
} = this.state;
const {
firstItem,
scrollEnabled
} = this.props;
const itemsLength = this._getCustomDataLength(this.props);
if (!itemsLength) {
return;
}
const nextFirstItem = this._getFirstItem(firstItem, this.props);
let nextActiveItem = typeof this._activeItem !== 'undefined' ? this._activeItem : nextFirstItem;
const hasNewSize = this.props.vertical !== prevProps.vertical || this.props.vertical && prevProps.vertical && (prevProps.itemHeight !== this.props.itemHeight || prevProps.sliderHeight !== this.props.sliderHeight) || !this.props.vertical && !prevProps.vertical && (prevProps.itemWidth !== this.props.itemWidth || prevProps.sliderWidth !== this.props.sliderWidth); // Prevent issues with dynamically removed items
if (nextActiveItem > itemsLength - 1) {
nextActiveItem = itemsLength - 1;
} // Handle changing scrollEnabled independent of user -> carousel interaction
if (scrollEnabled !== prevProps.scrollEnabled) {
this._setScrollEnabled(scrollEnabled);
}
if (interpolators.length !== itemsLength || hasNewSize) {
this._activeItem = nextActiveItem;
this._previousItemsLength = itemsLength;
this._initPositionsAndInterpolators(this.props); // Handle scroll issue when dynamically removing items (see #133)
// This also fixes first item's active state on Android
// Because 'initialScrollIndex' apparently doesn't trigger scroll
if (this._previousItemsLength > itemsLength) {
this._hackActiveSlideAnimation(nextActiveItem);
}
if (hasNewSize) {
this._snapToItem(nextActiveItem, false, false, true);
}
} else if (nextFirstItem !== this._previousFirstItem && nextFirstItem !== this._activeItem) {
this._activeItem = nextFirstItem;
this._previousFirstItem = nextFirstItem;
this._snapToItem(nextFirstItem, false, true, true);
}
if (this.props.onScroll !== prevProps.onScroll) {
this._setScrollHandler(this.props);
}
}
componentWillUnmount() {
this._mounted = false;
this.stopAutoplay(); // @ts-expect-error setTimeout / clearTiemout is buggy :/
clearTimeout(this._initTimeout); // @ts-expect-error setTimeout / clearTiemout is buggy :/
clearTimeout(this._apparitionTimeout); // @ts-expect-error setTimeout / clearTiemout is buggy :/
clearTimeout(this._hackSlideAnimationTimeout); // @ts-expect-error setTimeout / clearTiemout is buggy :/
clearTimeout(this._enableAutoplayTimeout); // @ts-expect-error setTimeout / clearTiemout is buggy :/
clearTimeout(this._autoplayTimeout); // @ts-expect-error setTimeout / clearTiemout is buggy :/
clearTimeout(this._snapNoMomentumTimeout); // @ts-expect-error setTimeout / clearTiemout is buggy :/
clearTimeout(this._androidRepositioningTimeout);
}
get realIndex() {
return this._activeItem;
}
get currentIndex() {
return this._getDataIndex(this._activeItem);
}
get currentScrollPosition() {
return this._currentScrollOffset;
}
_setScrollHandler(props) {
// Native driver for scroll events
const scrollEventConfig = {
listener: this._onScroll,
useNativeDriver: true
};
this._scrollPos = new _reactNative.Animated.Value(0);
const argMapping = props.vertical ? [{
nativeEvent: {
contentOffset: {
y: this._scrollPos
}
}
}] : [{
nativeEvent: {
contentOffset: {
x: this._scrollPos
}
}
}]; // @ts-expect-error Let's ignore for now that trick
if (props.onScroll && Array.isArray(props.onScroll._argMapping)) {
// Because of a react-native issue https://github.com/facebook/react-native/issues/13294
argMapping.pop(); // @ts-expect-error Let's ignore for now that trick
const [argMap] = props.onScroll._argMapping;
if (argMap && argMap.nativeEvent && argMap.nativeEvent.contentOffset) {
// Shares the same animated value passed in props
this._scrollPos = argMap.nativeEvent.contentOffset.x || argMap.nativeEvent.contentOffset.y || this._scrollPos;
} // @ts-expect-error Let's ignore for now that trick
argMapping.push(...props.onScroll._argMapping);
}
this._onScrollHandler = _reactNative.Animated.event(argMapping, scrollEventConfig);
} // This will return a future-proof version code number compatible with semantic versioning
// Examples: 0.59.3 -> 5903 / 0.61.4 -> 6104 / 0.62.12 -> 6212 / 1.0.2 -> 10002
// _getRNVersionCode () {
// const version = RN_PACKAGE && RN_PACKAGE.version;
// if (!version) {
// return null;
// }
// const versionSplit = version.split('.');
// if (!versionSplit || !versionSplit.length) {
// return null;
// }
// return versionSplit[0] * 10000 +
// (typeof versionSplit[1] !== 'undefined' ? versionSplit[1] * 100 : 0) +
// (typeof versionSplit[2] !== 'undefined' ? versionSplit[2] * 1 : 0);
// }
_displayWarnings() {
let props = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.props;
const pluginName = 'react-native-snap-carousel';
const removedProps = ['activeAnimationType', 'activeAnimationOptions', 'enableMomentum', 'lockScrollTimeoutDuration', 'lockScrollWhileSnapping', 'onBeforeSnapToItem', 'swipeThreshold']; // if (this._RNVersionCode && this._RNVersionCode < 5800) {
// console.error(
// `${pluginName}: Version 4+ of the plugin is based on React Native props that were introduced in version 0.58. ` +
// 'Please downgrade to version 3.x or update your version of React Native.'
// );
// }
if (!props.vertical && (!props.sliderWidth || !props.itemWidth)) {
console.error(`${pluginName}: You need to specify both 'sliderWidth' and 'itemWidth' for horizontal carousels`);
}
if (props.vertical && (!props.sliderHeight || !props.itemHeight)) {
console.error(`${pluginName}: You need to specify both 'sliderHeight' and 'itemHeight' for vertical carousels`);
}
removedProps.forEach(removedProp => {
if (removedProp in props) {
console.warn(`${pluginName}: Prop ${removedProp} has been removed in version 4 of the plugin`);
}
});
}
_needsScrollView() {
const {
useScrollView
} = this.props; // Android's cell renderer is buggy and has a stange overflow
// TODO: a workaround might be to pass the custom animated styles directly to it
return IS_ANDROID ? useScrollView || !_reactNative.Animated.FlatList || this._shouldUseStackLayout() || this._shouldUseTinderLayout() : useScrollView || !_reactNative.Animated.FlatList;
}
_needsRTLAdaptations() {
const {
vertical
} = this.props;
return IS_RTL && IS_ANDROID && !vertical;
}
_enableLoop() {
const {
data,
enableSnap,
loop
} = this.props;
return enableSnap && loop && data && data.length && data.length > 1;
}
_shouldAnimateSlides() {
let props = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.props;
const {
inactiveSlideOpacity,
inactiveSlideScale,
scrollInterpolator,
slideInterpolatedStyle
} = props;
return inactiveSlideOpacity < 1 || inactiveSlideScale < 1 || !!scrollInterpolator || !!slideInterpolatedStyle || this._shouldUseShiftLayout() || this._shouldUseStackLayout() || this._shouldUseTinderLayout();
}
_shouldUseShiftLayout() {
const {
inactiveSlideShift,
layout
} = this.props;
return layout === 'default' && inactiveSlideShift !== 0;
}
_shouldUseStackLayout() {
return this.props.layout === 'stack';
}
_shouldUseTinderLayout() {
return this.props.layout === 'tinder';
}
_shouldRepositionScroll(index) {
const {
data,
enableSnap,
loopClonesPerSide
} = this.props;
const dataLength = data && data.length;
if (!enableSnap || !dataLength || !this._enableLoop() || index >= loopClonesPerSide && index < dataLength + loopClonesPerSide) {
return false;
}
return true;
}
_roundNumber(num) {
let decimals = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1;
// https://stackoverflow.com/a/41716722/
const rounder = Math.pow(10, decimals);
return Math.round((num + Number.EPSILON) * rounder) / rounder;
}
_isMultiple(x, y) {
// This prevents Javascript precision issues: https://stackoverflow.com/a/58440614/
// Required because Android viewport size can return pretty complicated decimals numbers
return Math.round(Math.round(x / y) / (1 / y)) === Math.round(x);
}
_getCustomData() {
let props = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.props;
const {
data,
loopClonesPerSide
} = props;
const dataLength = data && data.length;
if (!dataLength) {
return [];
}
if (!this._enableLoop()) {
return data;
}
let previousItems = [];
let nextItems = [];
if (loopClonesPerSide > dataLength) {
const dataMultiplier = Math.floor(loopClonesPerSide / dataLength);
const remainder = loopClonesPerSide % dataLength;
for (let i = 0; i < dataMultiplier; i++) {
previousItems.push(...data);
nextItems.push(...data);
}
previousItems.unshift(...data.slice(-remainder));
nextItems.push(...data.slice(0, remainder));
} else {
previousItems = data.slice(-loopClonesPerSide);
nextItems = data.slice(0, loopClonesPerSide);
}
return previousItems.concat(data, nextItems);
}
_getCustomDataLength() {
let props = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.props;
const {
data,
loopClonesPerSide
} = props;
const dataLength = data && data.length;
if (!dataLength) {
return 0;
}
return this._enableLoop() ? dataLength + 2 * loopClonesPerSide : dataLength;
}
_getCustomIndex(index) {
let props = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this.props;
const itemsLength = this._getCustomDataLength(props);
if (!itemsLength || typeof index === 'undefined') {
return 0;
}
return this._needsRTLAdaptations() ? itemsLength - index - 1 : index;
}
_getDataIndex(index) {
const {
data,
loopClonesPerSide
} = this.props;
const dataLength = data && data.length;
if (!this._enableLoop() || !dataLength) {
return index;
}
if (index >= dataLength + loopClonesPerSide) {
return loopClonesPerSide > dataLength ? (index - loopClonesPerSide) % dataLength : index - dataLength - loopClonesPerSide;
} else if (index < loopClonesPerSide) {
// TODO: is there a simpler way of determining the interpolated index?
if (loopClonesPerSide > dataLength) {
const baseDataIndexes = [];
const dataIndexes = [];
const dataMultiplier = Math.floor(loopClonesPerSide / dataLength);
const remainder = loopClonesPerSide % dataLength;
for (let i = 0; i < dataLength; i++) {
baseDataIndexes.push(i);
}
for (let j = 0; j < dataMultiplier; j++) {
dataIndexes.push(...baseDataIndexes);
}
dataIndexes.unshift(...baseDataIndexes.slice(-remainder));
return dataIndexes[index];
} else {
return index + dataLength - loopClonesPerSide;
}
} else {
return index - loopClonesPerSide;
}
} // Used with `snapToItem()` and 'PaginationDot'
_getPositionIndex(index) {
const {
loop,
loopClonesPerSide
} = this.props;
return loop ? index + loopClonesPerSide : index;
}
_getSnapOffsets() {
let props = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.props;
const offset = this._getItemMainDimension();
return [...Array(this._getCustomDataLength(props))].map((_, i) => {
return i * offset;
});
}
_getFirstItem(index) {
let props = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this.props;
const {
loopClonesPerSide
} = props;
const itemsLength = this._getCustomDataLength(props);
if (!itemsLength || index > itemsLength - 1 || index < 0) {
return 0;
}
return this._enableLoop() ? index + loopClonesPerSide : index;
}
_getWrappedRef() {
// Starting with RN 0.62, we should no longer call `getNode()` on the ref of an Animated component
if (this._carouselRef && (this._needsScrollView() && this._carouselRef.scrollTo || !this._needsScrollView() && this._carouselRef.scrollToOffset)) {
return this._carouselRef;
} // https://github.com/facebook/react-native/issues/10635
// https://stackoverflow.com/a/48786374/8412141
return this._carouselRef && // @ts-expect-error This is for before 0.62
this._carouselRef.getNode && // @ts-expect-error This is for before 0.62
this._carouselRef.getNode();
}
_getScrollEnabled() {
return this._scrollEnabled;
}
_setScrollEnabled() {
let scrollEnabled = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
const wrappedRef = this._getWrappedRef();
if (!wrappedRef || !wrappedRef.setNativeProps) {
return;
} // 'setNativeProps()' is used instead of 'setState()' because the latter
// really takes a toll on Android behavior when momentum is disabled
wrappedRef.setNativeProps({
scrollEnabled
});
this._scrollEnabled = scrollEnabled;
}
_getItemMainDimension() {
return this.props.vertical ? this.props.itemHeight : this.props.itemWidth;
}
_getItemScrollOffset(index) {
return this._positions && this._positions[index] && this._positions[index].start;
}
_getItemLayout(_, index) {
const itemMainDimension = this._getItemMainDimension();
return {
index,
length: itemMainDimension,
offset: itemMainDimension * index // + this._getContainerInnerMargin()
};
} // This will allow us to have a proper zIndex even with a FlatList
// https://github.com/facebook/react-native/issues/18616#issuecomment-389444165
_getCellRendererComponent(_ref) {
let {
children,
index,
style,
...props
} = _ref;
const cellStyle = [style, !IS_ANDROID ? {
zIndex: this._getCustomDataLength() - index
} : {}];
return /*#__PURE__*/_react.default.createElement(_reactNative.View, _extends({
style: cellStyle,
key: index
}, props), children);
}
_getKeyExtractor(_, index) {
return this._needsScrollView() ? `scrollview-item-${index}` : `flatlist-item-${index}`;
}
_getScrollOffset(event) {
const {
vertical
} = this.props;
return event && event.nativeEvent && event.nativeEvent.contentOffset && event.nativeEvent.contentOffset[vertical ? 'y' : 'x'] || 0;
}
_getContainerInnerMargin() {
let opposite = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
const {
activeSlideAlignment
} = this.props;
if (activeSlideAlignment === 'start' && !opposite || activeSlideAlignment === 'end' && opposite) {
return 0;
} else if (activeSlideAlignment === 'end' && !opposite || activeSlideAlignment === 'start' && opposite) {
return this.props.vertical ? this.props.sliderHeight - this.props.itemHeight : this.props.sliderWidth - this.props.itemWidth;
} else {
return this.props.vertical ? (this.props.sliderHeight - this.props.itemHeight) / 2 : (this.props.sliderWidth - this.props.itemWidth) / 2;
}
}
_getActiveSlideOffset() {
const {
activeSlideOffset
} = this.props;
const itemMainDimension = this._getItemMainDimension();
const minOffset = 10; // Make sure activeSlideOffset never prevents the active area from being at least 10 px wide
return itemMainDimension / 2 - activeSlideOffset >= minOffset ? activeSlideOffset : minOffset;
}
_getActiveItem(offset) {
const itemMainDimension = this._getItemMainDimension();
const center = offset + itemMainDimension / 2;
const activeSlideOffset = this._getActiveSlideOffset();
const lastIndex = this._positions.length - 1;
let itemIndex;
if (offset <= 0) {
return 0;
}
if (this._positions[lastIndex] && offset >= this._positions[lastIndex].start) {
return lastIndex;
}
for (let i = 0; i < this._positions.length; i++) {
const {
start,
end
} = this._positions[i];
if (center + activeSlideOffset >= start && center - activeSlideOffset <= end) {
itemIndex = i;
break;
}
}
return itemIndex || 0;
}
_getSlideInterpolatedStyle(index, animatedValue) {
const {
layoutCardOffset,
slideInterpolatedStyle
} = this.props;
if (slideInterpolatedStyle) {
return slideInterpolatedStyle(index, animatedValue, this.props);
} else if (this._shouldUseTinderLayout()) {
return (0, _animations.tinderAnimatedStyles)(index, animatedValue, this.props, layoutCardOffset);
} else if (this._shouldUseStackLayout()) {
return (0, _animations.stackAnimatedStyles)(index, animatedValue, this.props, layoutCardOffset);
} else if (this._shouldUseShiftLayout()) {
return (0, _animations.shiftAnimatedStyles)(index, animatedValue, this.props);
} else {
return (0, _animations.defaultAnimatedStyles)(index, animatedValue, this.props);
}
}
_initPositionsAndInterpolators() {
let props = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.props;
const {
data,
scrollInterpolator
} = props;
const itemMainDimension = this._getItemMainDimension();
if (!data || !data.length) {
return;
}
const interpolators = [];
this._positions = [];
this._getCustomData(props).forEach((_itemData, index) => {
const _index = this._getCustomIndex(index, props);
let animatedValue;
this._positions[index] = {
start: index * itemMainDimension,
end: index * itemMainDimension + itemMainDimension
};
if (!this._shouldAnimateSlides(props) || !this._scrollPos) {
animatedValue = new _reactNative.Animated.Value(1);
} else {
let interpolator;
if (scrollInterpolator) {
interpolator = scrollInterpolator(_index, props);
} else if (this._shouldUseStackLayout()) {
interpolator = (0, _animations.stackScrollInterpolator)(_index, props);
} else if (this._shouldUseTinderLayout()) {
interpolator = (0, _animations.tinderScrollInterpolator)(_index, props);
}
if (!interpolator || !interpolator.inputRange || !interpolator.outputRange) {
interpolator = (0, _animations.defaultScrollInterpolator)(_index, props);
}
animatedValue = this._scrollPos.interpolate({ ...interpolator,
extrapolate: 'clamp'
});
}
interpolators.push(animatedValue);
});
this.setState({
interpolators
});
}
_hackActiveSlideAnimation(index) {
let scrollValue = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1;
const offset = this._getItemScrollOffset(index);
if (!this._mounted || !this._carouselRef || typeof offset === 'undefined') {
return;
}
const multiplier = this._currentScrollOffset === 0 ? 1 : -1;
const scrollDelta = scrollValue * multiplier;
this._scrollTo({
offset: offset + scrollDelta,
animated: false
}); // @ts-expect-error setTimeout / clearTiemout is buggy :/
clearTimeout(this._hackSlideAnimationTimeout);
this._hackSlideAnimationTimeout = setTimeout(() => {
this._scrollTo({
offset,
animated: false
});
}, 1); // works randomly when set to '0'
}
_repositionScroll(index) {
let animated = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
const {
data,
loopClonesPerSide
} = this.props;
const dataLength = data && data.length;
if (typeof index === 'undefined' || !this._shouldRepositionScroll(index)) {
return;
}
let repositionTo = index;
if (index >= dataLength + loopClonesPerSide) {
repositionTo = index - dataLength;
} else if (index < loopClonesPerSide) {
repositionTo = index + dataLength;
}
this._snapToItem(repositionTo, animated, false);
}
_scrollTo(_ref2) {
let {
offset,
index,
animated = true
} = _ref2;
const {
vertical
} = this.props;
const wrappedRef = this._getWrappedRef();
if (!this._mounted || !wrappedRef || typeof offset === 'undefined' && typeof index === 'undefined') {
return;
}
let scrollToOffset;
if (typeof index !== 'undefined') {
scrollToOffset = this._getItemScrollOffset(index);
} else {
scrollToOffset = offset;
}
if (typeof scrollToOffset === 'undefined') {
return;
}
const options = this._needsScrollView() ? {
x: vertical ? 0 : offset,
y: vertical ? offset : 0,
animated
} : {
offset,
animated
};
if (this._needsScrollView()) {
wrappedRef.scrollTo(options);
} else {
wrappedRef.scrollToOffset(options);
}
}
_onTouchStart(event) {
const {
onTouchStart
} = this.props; // `onTouchStart` is fired even when `scrollEnabled` is set to `false`
if (this._getScrollEnabled() !== false && this._autoplaying) {
this.pauseAutoPlay();
}
onTouchStart && onTouchStart(event);
}
_onTouchEnd(event) {
const {
onTouchEnd
} = this.props;
if (this._getScrollEnabled() !== false && this._autoplay && !this._autoplaying) {
// This event is buggy on Android, so a fallback is provided in _onMomentumScrollEnd()
this.startAutoplay();
}
onTouchEnd && onTouchEnd(event);
}
_onScroll(event) {
const {
onScroll,
onScrollIndexChanged
} = this.props;
const scrollOffset = event ? this._getScrollOffset(event) : this._currentScrollOffset;
const nextActiveItem = this._getActiveItem(scrollOffset);
this._currentScrollOffset = scrollOffset;
if (nextActiveItem !== this._onScrollActiveItem) {
this._onScrollActiveItem = nextActiveItem;
onScrollIndexChanged && onScrollIndexChanged(this._getDataIndex(nextActiveItem));
}
if (typeof onScroll === 'function' && event) {
onScroll(event);
}
}
_onMomentumScrollEnd(event) {
const {
autoplayDelay,
onMomentumScrollEnd,
onSnapToItem
} = this.props;
const scrollOffset = event ? this._getScrollOffset(event) : this._currentScrollOffset;
const nextActiveItem = this._getActiveItem(scrollOffset);
const hasSnapped = this._isMultiple(scrollOffset, this.props.vertical ? this.props.itemHeight : this.props.itemWidth); // WARNING: everything in this condition will probably need to be called on _snapToItem as well because:
// 1. `onMomentumScrollEnd` won't be called if the scroll isn't animated
// 2. `onMomentumScrollEnd` won't be called at all on Android when scrolling programmatically
if (nextActiveItem !== this._activeItem) {
this._activeItem = nextActiveItem;
onSnapToItem && onSnapToItem(this._getDataIndex(nextActiveItem));
if (hasSnapped) {
this._repositionScroll(nextActiveItem);
}
}
onMomentumScrollEnd && onMomentumScrollEnd(event); // The touchEnd event is buggy on Android, so this will serve as a fallback whenever needed
// https://github.com/facebook/react-native/issues/9439
if (IS_ANDROID && this._autoplay && !this._autoplaying) {
// @ts-expect-error setTimeout / clearTiemout is buggy :/
clearTimeout(this._enableAutoplayTimeout);
this._enableAutoplayTimeout = setTimeout(() => {
this.startAutoplay();
}, autoplayDelay);
}
}
_onLayout(event) {
const {
onLayout
} = this.props; // Prevent unneeded actions during the first 'onLayout' (triggered on init)
if (this._onLayoutInitDone) {
this._initPositionsAndInterpolators();
this._snapToItem(this._activeItem, false, false, true);
} else {
this._onLayoutInitDone = true;
}
onLayout && onLayout(event);
}
_snapToItem(index) {
let animated = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
let fireCallback = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
let forceScrollTo = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
const {
onSnapToItem
} = this.props;
const itemsLength = this._getCustomDataLength();
const wrappedRef = this._getWrappedRef();
if (!itemsLength || !wrappedRef) {
return;
}
if (!index || index < 0) {
index = 0;
} else if (itemsLength > 0 && index >= itemsLength) {
index = itemsLength - 1;
}
if (index === this._activeItem && !forceScrollTo) {
return;
}
const offset = this._getItemScrollOffset(index);
if (offset === undefined) {
return;
}
this._scrollTo({
offset,
animated
}); // On both platforms, `onMomentumScrollEnd` won't be triggered if the scroll isn't animated
// so we need to trigger the callback manually
// On Android `onMomentumScrollEnd` won't be triggered when scrolling programmatically
// Therefore everything critical needs to be manually called here as well, even though the timing might be off
const requiresManualTrigger = !animated || IS_ANDROID;
if (requiresManualTrigger) {
this._activeItem = index;
if (fireCallback) {
onSnapToItem && onSnapToItem(this._getDataIndex(index));
} // Repositioning on Android
if (IS_ANDROID && this._shouldRepositionScroll(index)) {
if (animated) {
this._androidRepositioningTimeout = setTimeout(() => {
// Without scroll animation, the behavior is completely buggy...
this._repositionScroll(index, true);
}, 400); // Approximate scroll duration on Android
} else {
this._repositionScroll(index);
}
}
}
}
startAutoplay() {
const {
autoplayInterval,
autoplayDelay
} = this.props;
this._autoplay = true;
if (this._autoplaying) {
return;
} // @ts-expect-error setTimeout / clearTiemout is buggy :/
clearTimeout(this._autoplayTimeout);
this._autoplayTimeout = setTimeout(() => {
this._autoplaying = true;
this._autoplayInterval = setInterval(() => {
if (this._autoplaying) {
this.snapToNext();
}
}, autoplayInterval);
}, autoplayDelay);
}
pauseAutoPlay() {
this._autoplaying = false; // @ts-expect-error setTimeout / clearTiemout is buggy :/
clearTimeout(this._autoplayTimeout); // @ts-expect-error setTimeout / clearTiemout is buggy :/
clearTimeout(this._enableAutoplayTimeout); // @ts-expect-error setTimeout / clearTiemout is buggy :/
clearInterval(this._autoplayInterval);
}
stopAutoplay() {
this._autoplay = false;
this.pauseAutoPlay();
}
snapToItem(index) {
let animated = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
let fireCallback = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
if (!index || index < 0) {
index = 0;
}
const positionIndex = this._getPositionIndex(index);
if (positionIndex === this._activeItem) {
return;
}
this._snapToItem(positionIndex, animated, fireCallback);
}
snapToNext() {
let animated = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
let fireCallback = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
const itemsLength = this._getCustomDataLength();
let newIndex = this._activeItem + 1;
if (newIndex > itemsLength - 1) {
newIndex = 0;
}
this._snapToItem(newIndex, animated, fireCallback);
}
snapToPrev() {
let animated = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
let fireCallback = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
const itemsLength = this._getCustomDataLength();
let newIndex = this._activeItem - 1;
if (newIndex < 0) {
newIndex = itemsLength - 1;
}
this._snapToItem(newIndex, animated, fireCallback);
} // https://github.com/facebook/react-native/issues/1831#issuecomment-231069668
triggerRenderingHack() {
let offset = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 1;
this._hackActiveSlideAnimation(this._activeItem, offset);
}
_renderItem(_ref3) {
let {
item,
index
} = _ref3;
const {
interpolators
} = this.state;
const {
keyExtractor,
slideStyle
} = this.props;
const animatedValue = interpolators && interpolators[index];
if (typeof animatedValue === 'undefined') {
return null;
}
const animate = this._shouldAnimateSlides();
const Component = animate ? _reactNative.Animated.View : _reactNative.View;
const animatedStyle = animate ? this._getSlideInterpolatedStyle(index, animatedValue) : {};
const dataIndex = this._getDataIndex(index);
const mainDimension = this.props.vertical ? {
height: this.props.itemHeight
} : {
width: this.props.itemWidth
};
const specificProps = this._needsScrollView() ? {
key: keyExtractor ? keyExtractor(item, index) : this._getKeyExtractor(item, index)
} : {};
return /*#__PURE__*/_react.default.createElement(Component, _extends({
style: [mainDimension, slideStyle, animatedStyle],
pointerEvents: "box-none"
}, specificProps), this.props.vertical ? this.props.renderItem({
item,
index,
dataIndex
}, {
scrollPosition: this._scrollPos,
carouselRef: this._carouselRef,
vertical: this.props.vertical,
sliderHeight: this.props.sliderHeight,
itemHeight: this.props.itemHeight
}) : this.props.renderItem({
item,
index,
dataIndex
}, {
scrollPosition: this._scrollPos,
carouselRef: this._carouselRef,
vertical: !!this.props.vertical,
sliderWidth: this.props.sliderWidth,
itemWidth: this.props.itemWidth
}));
}
_getComponentOverridableProps() {
const {
hideCarousel
} = this.state;
const {
loopClonesPerSide
} = this.props;
const visibleItems = Math.ceil(this.props.vertical ? this.props.sliderHeight / this.props.itemHeight : this.props.sliderWidth / this.props.itemWidth) + 1;
const initialNumPerSide = this._enableLoop() ? loopClonesPerSide : 2;
const initialNumToRender = visibleItems + initialNumPerSide * 2;
const maxToRenderPerBatch = initialNumToRender + initialNumPerSide * 2;
const windowSize = maxToRenderPerBatch;
const specificProps = !this._needsScrollView() ? {
initialNumToRender,
maxToRenderPerBatch,
windowSize // updateCellsBatchingPeriod
} : {};
return { ...specificProps,
automaticallyAdjustContentInsets: false,
decelerationRate: 'fast',
directionalLockEnabled: true,
disableScrollViewPanResponder: false,
// If set to `true`, touch events will be triggered too easily
inverted: this._needsRTLAdaptations(),
overScrollMode: 'never',
pinchGestureEnabled: false,
pointerEvents: hideCarousel ? 'none' : 'auto',
// removeClippedSubviews: !this._needsScrollView(),
// renderToHardwareTextureAndroid: true,
scrollsToTop: false,
showsHorizontalScrollIndicator: false,
showsVerticalScrollIndicator: false
};
}
_getComponentStaticProps() {
const {
hideCarousel
} = this.state;
const {
activeSlideAlignment,
CellRendererComponent,
containerCustomStyle,
contentContainerCustomStyle,
firstItem,
getItemLayout,
keyExtractor,
style,
useExperimentalSnap
} = this.props;
const containerStyle = [// { overflow: 'hidden' },
containerCustomStyle || style || {}, hideCarousel ? {
opacity: 0
} : {}, this.props.vertical ? {
height: this.props.sliderHeight,
flexDirection: 'column'
} : // LTR hack; see https://github.com/facebook/react-native/issues/11960
// and https://github.com/facebook/react-native/issues/13100#issuecomment-328986423
{
width: this.props.sliderWidth,
flexDirection: this._needsRTLAdaptations() ? 'row-reverse' : 'row'
}];
const innerMarginStyle = this.props.vertical ? {
paddingTop: this._getContainerInnerMargin(),
paddingBottom: this._getContainerInnerMargin(true)
} : {
paddingLeft: this._getContainerInnerMargin(),
paddingRight: this._getContainerInnerMargin(true)
};
const contentContainerStyle = [!useExperimentalSnap ? innerMarginStyle : {}, contentContainerCustomStyle || {}]; // WARNING: `snapToAlignment` won't work as intended because of the following:
// https://github.com/facebook/react-native/blob/d0871d0a9a373e1d3ac35da46c85c0d0e793116d/React/Views/ScrollView/RCTScrollView.m#L751-L755
// - Snap points will be off
// - Slide animations will be off
// - Last items won't be set as active (no `onSnapToItem` callback)
// Recommended only with large slides and `activeSlideAlignment` set to `start` for the time being
const snapProps = useExperimentalSnap ? {
// disableIntervalMomentum: true, // Slide ± one item at a time
snapToAlignment: activeSlideAlignment,
snapToInterval: this._getItemMainDimension()
} : {
snapToOffsets: this._getSnapOffsets()
}; // Flatlist specifics
const specificProps = !this._needsScrollView() ? {
CellRendererComponent: CellRendererComponent || this._getCellRendererComponent,
getItemLayout: getItemLayout || this._getItemLayout,
initialScrollIndex: this._getFirstItem(firstItem),
keyExtractor: keyExtractor || this._getKeyExtractor,
numColumns: 1,
renderItem: this._renderItem
} : {};
return { ...specificProps,
...snapProps,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ref: c => {
this._carouselRef = c;
},
contentContainerStyle: contentContainerStyle,
data: this._getCustomData(),
horizontal: !this.props.vertical,
scrollEventThrottle: 1,
style: containerStyle,
onLayout: this._onLayout,
onMomentumScrollEnd: this._onMomentumScrollEnd,
onScroll: this._onScrollHandler,
onTouchStart: this._onTouchStart,
onTouchEnd: this._onTouchEnd
};
}
render() {
const {
data,
renderItem,
useScrollView
} = this.props;
if (!data || !renderItem) {
return null;
}
const props = { ...this._getComponentOverridableProps(),
...this.props,
...this._getComponentStaticProps()
};
const ScrollViewComponent = typeof useScrollView === 'function' ? useScrollView : _reactNative.Animated.ScrollView;
return this._needsScrollView() || !_reactNative.Animated.FlatList ? /*#__PURE__*/_react.default.createElement(ScrollViewComponent, props, this._getCustomData().map((item, index) => {
return this._renderItem({
item,
index
});
})) :
/*#__PURE__*/
// @ts-expect-error Seems complicated to make TS 100% happy, while sharing that many things between
// flatlist && scrollview implementation. I'll prob try to rewrite parts of the logic to overcome that.
_react.default.createElement(_reactNative.Animated.FlatList, props);
}
}
exports.Carousel = Carousel;
_defineProperty(Carousel, "defaultProps", {
activeSlideAlignment: 'center',
activeSlideOffset: 20,
apparitionDelay: 0,
autoplay: false,
autoplayDelay: 1000,
autoplayInterval: 3000,
callbackOffsetMargin: 5,
containerCustomStyle: {},
contentContainerCustomStyle: {},
enableSnap: true,
firstItem: 0,
hasParallaxImages: false,
inactiveSlideOpacity: 0.7,
inactiveSlideScale: 0.9,
inactiveSlideShift: 0,
layout: 'default',
loop: false,
loopClonesPerSide: 3,
scrollEnabled: true,
slideStyle: {},
shouldOptimizeUpdates: true,
useExperimentalSnap: false,
useScrollView: !_reactNative.Animated.FlatList
});
var _default = Carousel;
exports.default = _default;
//# sourceMappingURL=Carousel.js.map