diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1d16602fe..7287ca974 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,17 @@
+## v3.2.0
+* Refactor callback handling. **Make sure to use the new prop `callbackOffsetMargin` if you experience missed callbacks.**
+* Make item's scale and opacity animation follow scroll value (thanks [@hammadj](https://github.com/hammadj))
+* `Pagination` component: make dots tappable with new props `tappableDots` and `carouselRef` (see the [example](https://github.com/archriss/react-native-snap-carousel/blob/master/example/src/index.js))
+* Fix state and scroll issues when the currently active item is being dynamically removed
+* Improve snap feeling when momentum is disabled (default)
+* Add prop `callbackOffsetMargin`
+* Remove props `animationFunc`, `animationOptions`, `scrollEndDragDebounceValue`, `snapOnAndroid`, and `useNativeOnScroll`
+
## v3.1.0
* `Pagination` component: add new props for advanced customization
## v3.0.0
-###WARNING
+### WARNING
* **Do not use this version as some temporary code was pushed to `npm` by mistake. Make sure to use version `3.1.0` instead.**
### Breaking changes
* Plugin is now built on top of `FlatList`, which allows for huge performance optimizations. From now on, items must be rendered using props `data` and `renderItem`.
diff --git a/README.md b/README.md
index dbcecf84b..0f1c9f97d 100644
--- a/README.md
+++ b/README.md
@@ -164,17 +164,15 @@ Prop | Description | Type | Default
Prop | Description | Type | Default
------ | ------ | ------ | ------
-`activeSlideOffset` | From slider's center, minimum slide distance to be scrolled before being set to active | Number | `25`
+`activeSlideOffset` | From slider's center, minimum slide distance to be scrolled before being set to active. | Number | `20`
`apparitionDelay` | `FlatList`'s init is a real mess, with lots of unneeded flickers and slides movement. This prop controls the delay during which the carousel will be hidden when mounted. | Number | `250`
+`callbackOffsetMargin` | Scroll events might not be triggered often enough to get a precise measure and, therefore, to provide a reliable callback. This usually is an Android issue, which might be linked to the version of React Native you're using (see ["Unreliable callbacks"](#unreliable-callbacks)). To work around this, you can define a small margin that will increase the "sweet spot"'s width. The default value should cover most cases, but **you will want to increase it if you experience missed callbacks**. | Number | `5`
`enableMomentum` | See [momentum](#momentum) | Boolean | `false`
`enableSnap` | If enabled, releasing the touch will scroll to the center of the nearest/active item | Boolean | `true`
`firstItem` | Index of the first item to display | Number | `0`
`hasParallaxImages` | Whether the carousel contains `` components or not. Required for specific data to be passed to children. | Boolean | `false`
-`scrollEndDragDebounceValue` | **When momentum is disabled**, this prop defines the timeframe during which multiple callback calls should be "grouped" into a single one. This debounce also helps smoothing the snap effect by providing a bit of inertia when touch is released. **Note that this will delay callback's execution.** | Number | `50` for iOS, `150` for Android
`shouldOptimizeUpdates` | Whether to implement a `shouldComponentUpdate` strategy to minimize updates | Boolean | `true`
-`snapOnAndroid` | Snapping on android is sometimes choppy, especially when swiping quickly, so you can disable it | Boolean | `true`
`swipeThreshold` | Delta x when swiping to trigger the snap | Number | `20`
-`useNativeOnScroll` | Move `onScroll` events to the native thread in order to prevent the tiny lag associated with RN's JS bridge. **Activate this if you have a `transform` and/or `opacity` animation that needs to follow carousel's scroll position closely**. More info in [this post](https://facebook.github.io/react-native/blog/2017/02/14/using-native-driver-for-animated.html). Note that it will be activated if `hasParallaxImages` is set to `true` and/or if `scrollEventThrottle` is set to less than `16`. | Boolean | `false`
`vertical` | Layout slides vertically instead of horizontally | Boolean | `false`
### Autoplay
@@ -190,8 +188,6 @@ Prop | Description | Type | Default
Prop | Description | Type | Default
------ | ------ | ------ | ------
`activeSlideAlignment` | Determine active slide's alignment relative to the carousel. Possible values are: `'start'`, `'center'` and `'end'`. | String | `'center'`
-`animationFunc` | Animated animation to use; you must provide the name of the method. Note that it will only be applied to the scale animation since opacity's animation type will always be set to `timing` (no one wants the opacity to 'bounce' around) | String | `timing`
-`animationOptions` | Animation options to be merged with the default ones. Can be used without `animationFunc`. Note that opacity's easing will be kept linear. | Object | `{ duration: 600, easing: Easing.elastic(1) }`
`containerCustomStyle` | Optional styles for Scrollview's global wrapper | View Style Object | `{}`
`contentContainerCustomStyle` | Optional styles for Scrollview's items container | View Style Object | `{}`
`inactiveSlideOpacity` | Value of the opacity effect applied to inactive slides | Number | `0.7`
@@ -321,6 +317,14 @@ const styles = Stylesheet.create({
```
+### Carousel's stretched height
+
+Since `` is, ultimately, based on ``, it inherits [its default styles](https://github.com/facebook/react-native/blob/master/Libraries/Components/ScrollView/ScrollView.js#L864) and particularly `{ flexGrow: 1 }`. This means that, by default, **the carousel container will stretch to fill up all available space**.
+
+If this is not what you're after, you can prevent this behavior by passing `{ flexGrow: 0 }` to prop `containerCustomStyle`.
+
+Alternatively, you can either use this prop to pass a custom height to the container, or wrap the carousel in a `` with a fixed height.
+
### Understanding styles
Here is a screenshot that should help you understand how each of the above variables is used.
@@ -400,6 +404,10 @@ export class MyCarousel extends Component {
[This plugin](https://github.com/shichongrui/react-native-on-layout) can also prove useful.
+### Native-powered animations
+
+Scroll events have been moved to the native thread in order to prevent the tiny lag associated with React Native's JavaScript bridge. This is really useful when displaying a `transform` and/or `opacity` animation that needs to follow carousel's scroll position closely. You can find more info in [this post from Facebook](https://facebook.github.io/react-native/blog/2017/02/14/using-native-driver-for-animated.html).
+
## Known issues
### React Native version
@@ -427,9 +435,13 @@ We're trying to work around these issues, but the result is not always as smooth
### Unreliable callbacks
-When `enableMomentum` is disabled, providing a reliable callback is really tricky since no `scrollEnd` event has been exposed yet for the `ScrollView` component. We can only rely on the `scrollEndDrag` event, which comes with a huge bunch of issues. See [#34](https://github.com/archriss/react-native-snap-carousel/issues/34) for more information.
+When `enableMomentum` is disabled (default behavior), providing a reliable callback is really tricky since no `scrollEnd` event has been exposed yet for the `ScrollView` component. We can only rely on the `scrollEndDrag` event, which comes with a huge bunch of issues. See [#34](https://github.com/archriss/react-native-snap-carousel/issues/34) for more information.
+
+Version 2.3.0 tackled these issues with all sorts of flags and hacks. But you could still be facing the following one: **when you build a debug version of your app without enabling JS remote debugging, timers may desynchronize and cause a complete callback mess**. Try to either enable remote debugging or build a production version of your app, and everything should get back to normal.
+
+Callback handling has been completely revamped in version 3.2.0, in a less hacky and more reliable way. There is one issue though: callbacks now rely on scroll events. Usually, this is not a problem since the plugin features a native-powered scroll. **But there has been [a regression in React Native 0.46.x](https://github.com/facebook/react-native/issues/15769), that has been fixed in version 0.48.2.**
-Version 2.3.0 tackled these issues with a bunch of flags and hacks. But you could still be facing the following one: **when you build a debug version of your app without enabling JS remote debugging**, timers will desynchronize and callbacks will be a complete mess. Try to either enable remote debugging or build a production version of your app, and everything should get back to normal.
+If you're using an in-between version, you're in for some trouble since events won't be fired frequently enough (particularly on Android). **We've added a prop `callbackOffsetMargin` to help with this situation.**
### Error with Jest
@@ -447,6 +459,7 @@ As such, this feature should be considered experimental since it might break wit
## TODO
+- [ ] Implement a custom `PanResponder` for better control over carousel's callbacks and overall feeling
- [ ] Implement 'loop' mode
- [ ] Handle changing major props on-the-fly
- [ ] Handle autoplay properly when updating children's length
diff --git a/example/package.json b/example/package.json
index f0c5dc1cf..0e88a4302 100644
--- a/example/package.json
+++ b/example/package.json
@@ -7,11 +7,11 @@
},
"dependencies": {
"react": "16.0.0-alpha.12",
- "react-native": "~0.47.2",
+ "react-native": "~0.48.3",
"react-native-linear-gradient": "^2.3.0",
"react-native-snap-carousel": "file:../"
},
"devDependencies": {
- "babel-preset-react-native": "3.0.1"
+ "babel-preset-react-native": "3.0.2"
}
}
diff --git a/package.json b/package.json
index 4f5940cf9..36e013669 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "react-native-snap-carousel",
- "version": "3.1.0",
+ "version": "3.2.0",
"description": "Swiper component for React Native with previews, snapping effect, parallax images, performant handling of huge numbers of items, and RTL support. Compatible with Android & iOS.",
"main": "src/index.js",
"repository": {
@@ -31,7 +31,6 @@
"author": "Archriss (github.com/archriss)",
"license": "ISC",
"dependencies": {
- "lodash.debounce": "4.0.8",
"prop-types": "^15.5.10",
"react-addons-shallow-compare": "15.6.0"
},
diff --git a/src/carousel/Carousel.js b/src/carousel/Carousel.js
index 49f879fff..11a9bfb81 100644
--- a/src/carousel/Carousel.js
+++ b/src/carousel/Carousel.js
@@ -2,7 +2,6 @@ import React, { Component } from 'react';
import { View, FlatList, Animated, Platform, I18nManager, ViewPropTypes } from 'react-native';
import PropTypes from 'prop-types';
import shallowCompare from 'react-addons-shallow-compare';
-import _debounce from 'lodash.debounce';
const IS_IOS = Platform.OS === 'ios';
@@ -32,6 +31,7 @@ export default class Carousel extends Component {
autoplay: PropTypes.bool,
autoplayDelay: PropTypes.number,
autoplayInterval: PropTypes.number,
+ callbackOffsetMargin: PropTypes.number,
containerCustomStyle: ViewPropTypes ? ViewPropTypes.style : View.propTypes.style,
contentContainerCustomStyle: ViewPropTypes ? ViewPropTypes.style : View.propTypes.style,
enableMomentum: PropTypes.bool,
@@ -40,23 +40,21 @@ export default class Carousel extends Component {
hasParallaxImages: PropTypes.bool,
inactiveSlideOpacity: PropTypes.number,
inactiveSlideScale: PropTypes.number,
- scrollEndDragDebounceValue: PropTypes.number,
slideStyle: Animated.View.propTypes.style,
shouldOptimizeUpdates: PropTypes.bool,
- snapOnAndroid: PropTypes.bool,
swipeThreshold: PropTypes.number,
- useNativeOnScroll: PropTypes.bool,
vertical: PropTypes.bool,
onSnapToItem: PropTypes.func
};
static defaultProps = {
activeSlideAlignment: 'center',
- activeSlideOffset: 25,
+ activeSlideOffset: 20,
apparitionDelay: 250,
autoplay: false,
autoplayDelay: 5000,
autoplayInterval: 3000,
+ callbackOffsetMargin: 5,
containerCustomStyle: {},
contentContainerCustomStyle: {},
enableMomentum: false,
@@ -65,12 +63,9 @@ export default class Carousel extends Component {
hasParallaxImages: false,
inactiveSlideOpacity: 0.7,
inactiveSlideScale: 0.9,
- scrollEndDragDebounceValue: IS_IOS ? 50 : 150,
slideStyle: {},
shouldOptimizeUpdates: true,
- snapOnAndroid: true,
swipeThreshold: 20,
- useNativeOnScroll: false,
vertical: false
}
@@ -89,51 +84,37 @@ export default class Carousel extends Component {
this._positions = [];
this._currentContentOffset = 0; // store ScrollView's scroll position
- this._hasFiredEdgeItemCallback = true; // deal with overscroll and callback
this._canFireCallback = false; // used only when `enableMomentum` is set to `false`
- this._isShortSnapping = false; // used only when `enableMomentum` is set to `false`
+ this._callbackScrollOffset = null; // used only when `enableMomentum` is set to `false`
- this._initInterpolators = this._initInterpolators.bind(this);
this._getItemLayout = this._getItemLayout.bind(this);
+ this._initPositionsAndInterpolators = this._initPositionsAndInterpolators.bind(this);
this._renderItem = this._renderItem.bind(this);
+ this._onSnap = this._onSnap.bind(this);
+
+ this._onLayout = this._onLayout.bind(this);
this._onScroll = this._onScroll.bind(this);
- this._onScrollEnd = this._snapEnabled || props.autoplay ? this._onScrollEnd.bind(this) : undefined;
- this._onScrollBeginDrag = this._snapEnabled ? this._onScrollBeginDrag.bind(this) : undefined;
+ this._onScrollBeginDrag = props.enableSnap ? this._onScrollBeginDrag.bind(this) : undefined;
+ this._onScrollEnd = props.enableSnap || props.autoplay ? this._onScrollEnd.bind(this) : undefined;
this._onScrollEndDrag = !props.enableMomentum ? this._onScrollEndDrag.bind(this) : undefined;
this._onMomentumScrollEnd = props.enableMomentum ? this._onMomentumScrollEnd.bind(this) : undefined;
this._onTouchStart = this._onTouchStart.bind(this);
this._onTouchRelease = this._onTouchRelease.bind(this);
- this._onLayout = this._onLayout.bind(this);
- this._onSnap = this._onSnap.bind(this);
// Native driver for scroll events
- if (this._shouldUseNativeOnScroll(props)) {
- const scrollEventConfig = {
- listener: this._onScroll,
- useNativeDriver: true
- };
- this._scrollPos = new Animated.Value(0);
- this._onScrollHandler = props.vertical ?
- Animated.event(
- [{ nativeEvent: { contentOffset: { y: this._scrollPos } } }],
- scrollEventConfig
- ) : Animated.event(
- [{ nativeEvent: { contentOffset: { x: this._scrollPos } } }],
- scrollEventConfig
- );
- } else {
- this._onScrollHandler = this._onScroll;
- }
-
- // Debounce `_onScrollEndDrag` execution
- // This aims at improving snap feeling and callback reliability
- this._onScrollEndDragDebounced = !props.scrollEndDragDebounceValue ?
- this._onScrollEndDragDebounced.bind(this) :
- _debounce(
- this._onScrollEndDragDebounced,
- props.scrollEndDragDebounceValue,
- { leading: false, trailing: true }
- ).bind(this);
+ const scrollEventConfig = {
+ listener: this._onScroll,
+ useNativeDriver: true
+ };
+ this._scrollPos = new Animated.Value(0);
+ this._onScrollHandler = props.vertical ?
+ Animated.event(
+ [{ nativeEvent: { contentOffset: { y: this._scrollPos } } }],
+ scrollEventConfig
+ ) : Animated.event(
+ [{ nativeEvent: { contentOffset: { x: this._scrollPos } } }],
+ scrollEventConfig
+ );
// This bool aims at fixing an iOS bug due to scrollTo that triggers onMomentumScrollEnd.
// onMomentumScrollEnd fires this._snapScroll, thus creating an infinite loop.
@@ -155,23 +136,18 @@ export default class Carousel extends Component {
}
componentDidMount () {
- const { firstItem, autoplay, apparitionDelay } = this.props;
- const _firstItem = this._getFirstItem(firstItem);
-
- this._initInterpolators(this.props);
-
- setTimeout(() => {
- this.snapToItem(_firstItem, false, false, true);
+ const { apparitionDelay } = this.props;
- if (autoplay) {
- this.startAutoplay();
- }
- }, 0);
+ this._initPositionsAndInterpolators();
- // hide FlatList's awful init
- setTimeout(() => {
- this.setState({ hideCarousel: false });
- }, apparitionDelay);
+ if (apparitionDelay) {
+ // Hide FlatList's awful init
+ this._apparitionTimeout = setTimeout(() => {
+ this._didMountDelayedInit();
+ }, apparitionDelay);
+ } else {
+ this._didMountDelayedInit();
+ }
}
shouldComponentUpdate (nextProps, nextState) {
@@ -184,7 +160,7 @@ export default class Carousel extends Component {
componentWillReceiveProps (nextProps) {
const { activeItem, interpolators, previousFirstItem, previousItemsLength } = this.state;
- const { data, firstItem, itemHeight, itemWidth, sliderHeight, sliderWidth, vertical } = nextProps;
+ const { data, firstItem, itemHeight, itemWidth, sliderHeight, sliderWidth } = nextProps;
const itemsLength = data.length;
@@ -200,7 +176,7 @@ export default class Carousel extends Component {
const hasNewItemWidth = itemWidth && itemWidth !== this.props.itemWidth;
const hasNewItemHeight = itemHeight && itemHeight !== this.props.itemHeight;
- // Prevent issue with dynamically removed items
+ // Prevent issues with dynamically removed items
if (nextActiveItem > itemsLength - 1) {
nextActiveItem = itemsLength - 1;
}
@@ -211,22 +187,16 @@ export default class Carousel extends Component {
activeItem: nextActiveItem,
previousItemsLength: itemsLength
}, () => {
- this._positions = [];
- this._calcCardPositions(nextProps);
- this._initInterpolators(nextProps);
-
- // Handle state and scroll issue when dynamically removing items (see #133)
- const itemRemoved = previousItemsLength > itemsLength;
-
- if (itemRemoved) {
- // trigger scroll to hack item's active animation
- this._flatlist.scrollToOffset({
- offset: this._positions[nextActiveItem].start + 1,
- horizontal: !vertical,
- animated: false
- });
- this.snapToItem(nextActiveItem, false, true);
- } else if (hasNewSliderWidth || hasNewSliderHeight || hasNewItemWidth ||
+ this._initPositionsAndInterpolators(nextProps);
+
+ // 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 (previousItemsLength > itemsLength) {
+ this._hackActiveSlideAnimation(nextActiveItem);
+ }
+
+ if (hasNewSliderWidth || hasNewSliderHeight || hasNewItemWidth ||
hasNewItemHeight || (IS_RTL && !nextProps.vertical)) {
this.snapToItem(nextActiveItem, false, false);
}
@@ -243,17 +213,14 @@ export default class Carousel extends Component {
componentWillUnmount () {
this.stopAutoplay();
+ clearTimeout(this._apparitionTimeout);
+ clearTimeout(this._hackSlideAnimationTimeout);
clearTimeout(this._enableAutoplayTimeout);
clearTimeout(this._autoplayTimeout);
clearTimeout(this._snapNoMomentumTimeout);
clearTimeout(this._scrollToTimeout);
}
- get _snapEnabled () {
- const { enableSnap, snapOnAndroid } = this.props;
- return enableSnap && (IS_IOS || snapOnAndroid);
- }
-
get currentIndex () {
return this.state.activeItem;
}
@@ -267,13 +234,6 @@ export default class Carousel extends Component {
return inactiveSlideOpacity < 1 || inactiveSlideScale < 1;
}
- _shouldUseNativeOnScroll (props = this.props) {
- return props.useNativeOnScroll ||
- props.hasParallaxImages ||
- props.scrollEventThrottle < 16 ||
- this._shouldAnimateSlides(props);
- }
-
_getCustomIndex (index, props = this.props) {
const itemsLength = props.data && props.data.length;
@@ -296,36 +256,42 @@ export default class Carousel extends Component {
return index;
}
- _calcCardPositions (props = this.props) {
- const { data, itemWidth, itemHeight, vertical } = props;
- const sizeRef = vertical ? itemHeight : itemWidth;
+ _didMountDelayedInit () {
+ const { firstItem, autoplay } = this.props;
+ const _firstItem = this._getFirstItem(firstItem);
- data.forEach((itemData, index) => {
- const _index = this._getCustomIndex(index, props);
- this._positions[index] = {
- start: _index * sizeRef,
- end: _index * sizeRef + sizeRef
- };
- });
+ this.snapToItem(_firstItem, false, false, true);
+ this._hackActiveSlideAnimation(_firstItem, 'start');
+ this.setState({ hideCarousel: false });
+
+ if (autoplay) {
+ this.startAutoplay();
+ }
}
- _initInterpolators (props = this.props) {
+ _initPositionsAndInterpolators (props = this.props) {
const { data, itemWidth, itemHeight, vertical } = props;
-
const sizeRef = vertical ? itemHeight : itemWidth;
+
let interpolators = [];
+ this._positions = [];
data.forEach((itemData, index) => {
+ const _index = this._getCustomIndex(index, props);
const start = (index - 1) * sizeRef;
const middle = index * sizeRef;
const end = (index + 1) * sizeRef;
-
const value = this._shouldAnimateSlides(props) ? this._scrollPos.interpolate({
inputRange: [start, middle, end],
outputRange: [0, 1, 0],
extrapolate: 'clamp'
}) : 1;
+ this._positions[index] = {
+ start: _index * sizeRef,
+ end: _index * sizeRef + sizeRef
+ };
+
interpolators.push({
opacity: value,
scale: value
@@ -335,29 +301,53 @@ export default class Carousel extends Component {
this.setState({ interpolators });
}
- _getScrollOffset (event) {
- const { vertical } = this.props;
- return (event && event.nativeEvent && event.nativeEvent.contentOffset &&
- event.nativeEvent.contentOffset[vertical ? 'y' : 'x']) || 0;
+ _hackActiveSlideAnimation (index, goTo) {
+ const { data, vertical } = this.props;
+
+ if (IS_IOS || !this._flatlist || !this._positions[index]) {
+ return;
+ }
+
+ const itemsLength = data && data.length;
+ const direction = goTo || itemsLength === 1 ? 'start' : 'end';
+ const offset = this._positions[index].start;
+ const commonOptions = {
+ horizontal: !vertical,
+ animated: false
+ };
+
+ this._flatlist.scrollToOffset({
+ offset: offset + (direction === 'start' ? -1 : 1),
+ ...commonOptions
+ });
+
+ this._hackSlideAnimationTimeout = setTimeout(() => {
+ this._flatlist.scrollToOffset({
+ offset: offset,
+ ...commonOptions
+ });
+ }, 50); // works randomly when set to '0'
}
- _getActiveItem (offset) {
- const { activeSlideOffset } = this.props;
- const center = this._getCenter(offset);
+ _getKeyExtractor (item, index) {
+ return `carousel-item-${index}`;
+ }
- for (let i = 0; i < this._positions.length; i++) {
- const { start, end } = this._positions[i];
- if (center + activeSlideOffset >= start && center - activeSlideOffset <= end) {
- return i;
- }
- }
+ _getItemLayout (data, index) {
+ const { itemWidth, itemHeight, vertical } = this.props;
+ const itemSize = vertical ? itemHeight : itemWidth;
- const lastIndex = this._positions.length - 1;
- if (this._positions[lastIndex] && center - activeSlideOffset > this._positions[lastIndex].end) {
- return lastIndex;
- }
+ return {
+ length: itemSize,
+ offset: itemSize * index,
+ index
+ };
+ }
- return 0;
+ _getScrollOffset (event) {
+ const { vertical } = this.props;
+ return (event && event.nativeEvent && event.nativeEvent.contentOffset &&
+ event.nativeEvent.contentOffset[vertical ? 'y' : 'x']) || 0;
}
_getContainerInnerMargin (opposite = false) {
@@ -374,45 +364,41 @@ export default class Carousel extends Component {
}
}
- _getCenter (offset) {
+ _getViewportOffet () {
const { sliderWidth, sliderHeight, itemWidth, itemHeight, vertical, activeSlideAlignment } = this.props;
- let viewportOffset;
if (activeSlideAlignment === 'start') {
- viewportOffset = vertical ? itemHeight / 2 : itemWidth / 2;
+ return vertical ? itemHeight / 2 : itemWidth / 2;
} else if (activeSlideAlignment === 'end') {
- viewportOffset = vertical ?
+ return vertical ?
sliderHeight - (itemHeight / 2) :
sliderWidth - (itemWidth / 2);
} else {
- viewportOffset = vertical ? sliderHeight / 2 : sliderWidth / 2;
+ return vertical ? sliderHeight / 2 : sliderWidth / 2;
}
-
- return offset + viewportOffset - (this._getContainerInnerMargin() * (IS_RTL ? -1 : 1));
}
- _getKeyExtractor (item, index) {
- return `carousel-item-${index}`;
+ _getCenter (offset) {
+ return offset +
+ this._getViewportOffet() -
+ (this._getContainerInnerMargin() * (IS_RTL ? -1 : 1));
}
- _getItemLayout (data, index) {
- const { itemWidth, itemHeight, vertical } = this.props;
- const itemSize = vertical ? itemHeight : itemWidth;
+ _getActiveItem (offset) {
+ const { activeSlideOffset, swipeThreshold } = this.props;
+ const center = this._getCenter(offset);
+ const centerOffset = activeSlideOffset || swipeThreshold;
- return {
- length: itemSize,
- offset: itemSize * index,
- index
- };
- }
+ for (let i = 0; i < this._positions.length; i++) {
+ const { start, end } = this._positions[i];
+ if (center + centerOffset >= start && center - centerOffset <= end) {
+ return i;
+ }
+ }
- _getItemOffset (index) {
- // 'viewPosition' doesn't work for the first item
- // It is always aligned to the left
- // Unfortunately, 'viewOffset' doesn't work on Android ATM
- if ((!IS_RTL && index === 0) ||
- (IS_RTL && index === this.props.data.length - 1)) {
- return this._getContainerInnerMargin();
+ const lastIndex = this._positions.length - 1;
+ if (this._positions[lastIndex] && center - centerOffset > this._positions[lastIndex].end) {
+ return lastIndex;
}
return 0;
@@ -420,40 +406,21 @@ export default class Carousel extends Component {
_onScroll (event) {
const { activeItem } = this.state;
- const { data, enableMomentum, onScroll } = this.props;
+ const { enableMomentum, onScroll, callbackOffsetMargin } = this.props;
const scrollOffset = this._getScrollOffset(event);
const nextActiveItem = this._getActiveItem(scrollOffset);
- const itemsLength = data.length;
this._currentContentOffset = scrollOffset;
if (enableMomentum) {
clearTimeout(this._snapNoMomentumTimeout);
- }
-
- if (activeItem !== nextActiveItem) {
- if (activeItem === 0 || activeItem === itemsLength - 1) {
- this._hasFiredEdgeItemCallback = false;
- }
-
- // WARNING: `setState()` is asynchronous
- this.setState({ activeItem: nextActiveItem }, () => {
- // When "short snapping", we can rely on the "activeItem/nextActiveItem" comparison
- if (!enableMomentum && this._canFireCallback && this._isShortSnapping) {
- this._isShortSnapping = false;
- this._onSnap(nextActiveItem);
- }
- });
- }
-
- // When scrolling, we need to check that we are not "short snapping",
- // that the new slide is different from the very first one,
- // that we are scrolling to the relevant slide,
- // and that callback can be fired
- if (!enableMomentum && this._canFireCallback && !this._isShortSnapping &&
- (this._scrollStartActive !== nextActiveItem || !this._hasFiredEdgeItemCallback) &&
- this._itemToSnapTo === nextActiveItem) {
+ } else if (this._canFireCallback &&
+ activeItem !== nextActiveItem &&
+ nextActiveItem === this._itemToSnapTo &&
+ (scrollOffset >= this._callbackScrollOffset - callbackOffsetMargin ||
+ scrollOffset <= this._callbackScrollOffset + callbackOffsetMargin)) {
+ this._canFireCallback = false;
this.setState({ activeItem: nextActiveItem }, () => {
this._onSnap(nextActiveItem);
});
@@ -489,26 +456,21 @@ export default class Carousel extends Component {
_onScrollEndDrag (event) {
const { onScrollEndDrag } = this.props;
- // event.persist(); // See https://stackoverflow.com/a/24679479
- this._onScrollEndDragDebounced();
+ if (this._flatlist) {
+ this._onScrollEnd && this._onScrollEnd();
+ }
if (onScrollEndDrag) {
onScrollEndDrag(event);
}
}
- _onScrollEndDragDebounced (event) {
- if (this._flatlist && this._onScrollEnd) {
- this._onScrollEnd();
- }
- }
-
// Used when `enableMomentum` is ENABLED
_onMomentumScrollEnd (event) {
const { onMomentumScrollEnd } = this.props;
- if (this._flatlist && this._onScrollEnd) {
- this._onScrollEnd();
+ if (this._flatlist) {
+ this._onScrollEnd && this._onScrollEnd();
}
if (onMomentumScrollEnd) {
@@ -517,7 +479,7 @@ export default class Carousel extends Component {
}
_onScrollEnd (event) {
- const { autoplay } = this.props;
+ const { autoplay, enableSnap } = this.props;
if (this._ignoreNextMomentum) {
// iOS fix
@@ -528,9 +490,8 @@ export default class Carousel extends Component {
this._scrollEndOffset = this._currentContentOffset;
this._scrollEndActive = this._getActiveItem(this._scrollEndOffset);
- if (this._snapEnabled) {
- const delta = this._scrollEndOffset - this._scrollStartOffset;
- this._snapScroll(delta);
+ if (enableSnap) {
+ this._snapScroll(this._scrollEndOffset - this._scrollStartOffset);
}
if (autoplay) {
@@ -560,7 +521,7 @@ export default class Carousel extends Component {
_onLayout (event) {
const { onLayout } = this.props;
- this._calcCardPositions();
+ this._initPositionsAndInterpolators();
this.snapToItem(this.currentIndex, false, false);
if (onLayout) {
@@ -578,15 +539,9 @@ export default class Carousel extends Component {
}
if (this._scrollStartActive !== this._scrollEndActive) {
- // Flag necessary in order to fire the callback
- // at the right time in `_onScroll()`
- this._isShortSnapping = false;
-
// Snap to the new active item
this.snapToItem(this._scrollEndActive);
} else {
- this._isShortSnapping = true;
-
// Snap depending on delta
if (delta > 0) {
if (delta > swipeThreshold) {
@@ -616,21 +571,10 @@ export default class Carousel extends Component {
}
_onSnap (index) {
- const { data, enableMomentum, onSnapToItem } = this.props;
- const itemsLength = data.length;
+ const { onSnapToItem } = this.props;
if (this._flatlist) {
- if (enableMomentum) {
- onSnapToItem && onSnapToItem(index);
- } else if (this._canFireCallback) {
- this._canFireCallback = false;
-
- if (index === 0 || index === itemsLength - 1) {
- this._hasFiredEdgeItemCallback = true;
- }
-
- onSnapToItem && onSnapToItem(index);
- }
+ onSnapToItem && onSnapToItem(index);
}
}
@@ -659,7 +603,7 @@ export default class Carousel extends Component {
snapToItem (index, animated = true, fireCallback = true, initial = false) {
const { previousActiveItem } = this.state;
- const { data, enableMomentum, scrollEndDragDebounceValue } = this.props;
+ const { data, enableMomentum, onSnapToItem } = this.props;
const itemsLength = data.length;
if (!itemsLength) {
@@ -672,72 +616,49 @@ export default class Carousel extends Component {
if (itemsLength > 0 && index >= itemsLength) {
index = itemsLength - 1;
- this._isShortSnapping = false; // prevent issue #105
- if (this._scrollStartActive === itemsLength - 1 && this._hasFiredEdgeItemCallback) {
- fireCallback = false;
- }
} else if (index < 0) {
index = 0;
- this._isShortSnapping = false; // prevent issue #105
- if (this._scrollStartActive === 0 && this._hasFiredEdgeItemCallback) {
- fireCallback = false;
- }
} else if (enableMomentum && index === previousActiveItem) {
fireCallback = false;
}
// Make sure the component hasn't been unmounted
- if (this._flatlist) {
- if (enableMomentum) {
- this.setState({ previousActiveItem: index });
- // Callback can be fired here when relying on 'onMomentumScrollEnd'
- if (fireCallback) {
- this._onSnap(index);
- }
- } else {
- // `_onScrollEndDragDebounced()` might occur when "peaking" to another item
- // Therefore we need to make sure that callback is fired when scrolling
- // back to the right one
- this._itemToSnapTo = index;
-
- // Callback needs to be fired while scrolling when relying on 'onScrollEndDrag'
- // Thus we need a flag on top of the debounce function to call it only once
- this._canFireCallback = this.props.onSnapToItem && fireCallback;
-
- // If user has scrolled to an edge item before the end of `scrollEndDragDebounceValue`
- // `onScroll()` won't be triggered and callback is not going to be fired
- // So we check if scroll position has been updated after a small delay and,
- // if not, it's safe to assume that callback should be called
- const scrollPosition = this._currentContentOffset;
- clearTimeout(this._scrollToTimeout);
- this._scrollToTimeout = setTimeout(() => {
- if (scrollPosition === this._currentContentOffset && this._canFireCallback) {
- this._onSnap(index);
- }
- }, Math.max(500, scrollEndDragDebounceValue + 50));
+ if (!this._flatlist) {
+ return;
+ }
+
+ if (enableMomentum) {
+ this.setState({ previousActiveItem: index });
+ // Callback can be fired here when relying on 'onMomentumScrollEnd'
+ if (fireCallback) {
+ this._onSnap(index);
}
+ } else {
+ // 'scrollEndDrag' might be fired when "peaking" to another item. We need to
+ // make sure that callback is fired when scrolling back to the right one.
+ this._itemToSnapTo = index;
+
+ // Callback needs to be fired while scrolling when relying on 'scrollEndDrag'.
+ // Thus we need a flag to make sure that it's going to be called only once.
+ if (onSnapToItem && fireCallback) {
+ this._canFireCallback = true;
+ this._callbackScrollOffset = this._positions[index] && this._positions[index].start;
+ }
+ }
- // Unfortunately, 'viewPosition' is quite buggy at the moment
- // Moreover, 'viewOffset' just doesn't work on Android
- // const viewOffset = this._getItemOffset(index);
- // let viewPosition = 0.5;
- // if (activeSlideAlignment === 'start') {
- // viewPosition = IS_RTL ? 1 : 0;
- // } else if (activeSlideAlignment === 'end') {
- // viewPosition = IS_RTL ? 0 : 1;
- // }
-
- this._flatlist.scrollToIndex({
- index,
- viewPosition: 0,
- viewOffset: 0,
- animated
- });
+ this._flatlist.scrollToIndex({
+ index,
+ viewPosition: 0,
+ viewOffset: 0,
+ animated
+ });
- // iOS fix, check the note in the constructor
- if (!initial && IS_IOS) {
- this._ignoreNextMomentum = true;
- }
+ // Android hack since `onScroll` sometimes seems to not be triggered
+ this._hackActiveSlideAnimation(index);
+
+ // iOS fix, check the note in the constructor
+ if (!initial && IS_IOS && enableMomentum) {
+ this._ignoreNextMomentum = true;
}
}
@@ -826,9 +747,9 @@ export default class Carousel extends Component {
itemHeight,
keyExtractor,
renderItem,
- scrollEventThrottle,
sliderWidth,
sliderHeight,
+ style,
vertical
} = this.props;
@@ -836,11 +757,8 @@ export default class Carousel extends Component {
return false;
}
- const nativePoweredScroll = this._shouldUseNativeOnScroll();
- const Component = nativePoweredScroll ? AnimatedFlatList : FlatList;
-
- const style = [
- containerCustomStyle || {},
+ const containerStyle = [
+ containerCustomStyle || style || {},
hideCarousel ? { opacity: 0 } : {},
vertical ?
{ height: sliderHeight, flexDirection: 'column' } :
@@ -857,24 +775,25 @@ export default class Carousel extends Component {
paddingRight: this._getContainerInnerMargin(true)
}
];
-
const visibleItems = Math.ceil(vertical ?
sliderHeight / itemHeight :
sliderWidth / itemWidth) + 1;
return (
- { if (c) { this._flatlist = nativePoweredScroll ? c._component : c; } }}
+ ref={(c) => { if (c) { this._flatlist = c._component; } }}
data={data}
renderItem={this._renderItem}
// extraData={this.state}
@@ -882,10 +801,10 @@ export default class Carousel extends Component {
keyExtractor={keyExtractor || this._getKeyExtractor}
initialScrollIndex={firstItem || undefined}
numColumns={1}
- style={style}
+ style={containerStyle}
contentContainerStyle={contentContainerStyle}
horizontal={!vertical}
- scrollEventThrottle={nativePoweredScroll ? 1 : (scrollEventThrottle || 16)}
+ scrollEventThrottle={1}
onScroll={this._onScrollHandler}
onScrollBeginDrag={this._onScrollBeginDrag}
onScrollEndDrag={this._onScrollEndDrag}
diff --git a/src/parallaximage/README.md b/src/parallaximage/README.md
index cd18ca7d5..52e905e69 100644
--- a/src/parallaximage/README.md
+++ b/src/parallaximage/README.md
@@ -19,9 +19,7 @@ All [`` props](https://facebook.github.io/react-native/docs/image.html#
## Usage
-The first thing you need to do is to **set `hasParallaxImages` to `true` on your ``**. This has two consequences:
-- migrating scroll events to the native driver for top-notch performances
-- your custom `renderItem` function now has access to a second argument that must be passed to the ``.
+The first thing you need to do is to **set `hasParallaxImages` to `true` for your ``**. This will make a new argument available in your `renderItem()` function, which must then be passed to the ``.
Here is an example that shows how to connect images to your carousel (note the `parallaxProps` argument).