Skip to content

Commit

Permalink
Merge pull request #53 from archriss/rtl-support
Browse files Browse the repository at this point in the history
Add RTL support and fix minor issues
  • Loading branch information
bd-arc authored Mar 29, 2017
2 parents 94e5829 + 67694fc commit a677bd2
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 40 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## v2.1.0
* Add RTL support
* Keep current active item when adding slides dynamically
* Prevent invalid `firstItem` number
* Add prop `activeSlideOffset`

## v2.0.3

* Prevent error when carousel has only one child (thanks [@kevinvandijk](https://github.com/kevinvandijk) !)
Expand Down
36 changes: 27 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# react-native-snap-carousel
Swiper component for React Native with **previews** and **snapping effect**. Compatible with Android & iOS.
Swiper component for React Native with **previews**, **snapping effect** and **RTL support**. Compatible with Android & iOS.
Pull requests are very welcome!

## Table of contents
Expand All @@ -10,6 +10,7 @@ Pull requests are very welcome!
1. [Props](#props)
1. [Methods](#methods)
1. [Properties](#properties)
1. [RTL support](#rtl-support)
1. [Example](#example)
1. [Tips and tricks](#tips-and-tricks)
1. [TODO](#todo)
Expand All @@ -18,14 +19,15 @@ Pull requests are very welcome!
## Showcase

You can try these examples live in **Archriss' showcase app** on [Android](https://play.google.com/store/apps/details?id=fr.archriss.demo.app) and [iOS](https://itunes.apple.com/lu/app/archriss-presentation-mobile/id1180954376?mt=8).
This app is going to be updated on a regular basis.

> Since it has been asked multiple times, please note that **we do not plan on Open-Sourcing the code of our showcase app**. Still, we've put together [an example](#example) for you to play with, and you can find some insight about our map implementation [in this comment](https://github.com/archriss/react-native-snap-carousel/issues/11#issuecomment-265147385).

![react-native-snap-carousel](http://i.imgur.com/Fope3uj.gif)
![react-native-snap-carousel](https://media.giphy.com/media/3o6ZsU9gWWrvYtogow/giphy.gif)
![react-native-snap-carousel](https://media.giphy.com/media/3o7TKUAlvi1tYLFCTK/giphy.gif)

> Since it has been asked multiple times, please note that **we do not plan on Open-Sourcing the code of our showcase app**. Still, we've put together [an example](#example) for you to play with, and you can find some insight about our map implementation [in this comment](https://github.com/archriss/react-native-snap-carousel/issues/11#issuecomment-265147385).
App currently uses version 1.4.0 of the plugin. Especially, this means that you should expect **slider's layout to break with RTL devices** (see #38) since support was added in version 2.1.0.

## Breaking change
Since version 2.0.0, items are now **direct children of the <Carousel> component**. As a result, props `items` and `renderItem` have been removed.

Expand All @@ -41,7 +43,7 @@ import Carousel from 'react-native-snap-carousel';
// Example with different children
render () {
<Carousel
ref={'carousel'}
ref={(carousel) => { this._carousel = carousel; }}
sliderWidth={sliderWidth}
itemWidth={itemWidth}
>
Expand All @@ -62,7 +64,7 @@ import Carousel from 'react-native-snap-carousel';
});

<Carousel
ref={'carousel'}
ref={(carousel) => { this._carousel = carousel; }}
sliderWidth={sliderWidth}
itemWidth={itemWidth}
>
Expand All @@ -79,6 +81,7 @@ Prop | Description | Type | Default
------ | ------ | ------ | ------
**itemWidth** | Width in pixels of your slides, **must be the same for all of them** | Number | **Required**
**sliderWidth** | Width in pixels of your slider | Number | **Required**
activeSlideOffset | From slider's center, minimum slide distance to be scrolled before being set to active | Number | `25`
animationFunc | Animated animation to use. Provide the name of the method | String | `timing`
animationOptions | Animation options to be merged with the default ones. Can be used w/ animationFunc | Object | `{ easing: Easing.elastic(1) }`
autoplay | Trigger autoplay on mount | Boolean | `false`
Expand Down Expand Up @@ -107,7 +110,7 @@ In order to use the following methods, you need to create a reference to the car
```javascript
<Carousel
// other props
ref={(carousel) => { this._carousel = carousel; } }
ref={(carousel) => { this._carousel = carousel; }}
/>

// methods can then be called this way
Expand Down Expand Up @@ -135,8 +138,22 @@ onPress={() => { this.refs.carousel.snapToNext(); }}

## Properties

> You need a reference to the carousel's instance (see [above](#reference-to-the-component) if needed).
* `currentIndex` Current active item (`int`, starts at 0)

## RTL support

### Experimental feature

Since version 2.1.0, the plugin is compatible with RTL layouts. Our implementation relies on miscellaneous hacks that work around a [React Native bug](https://github.com/facebook/react-native/issues/11960) with horizontal `ScrollView`.

As such, this feature should be considered experimental since it might break with newer versions of React Native.

### Known issue

There is one kown issue with RTL layouts: during init, the last slide will shortly be seen. You can work around this by delaying slider's visibility with a small timer (FYI, version 0.43.0 of React Native [introduced a `display` style prop](https://github.com/facebook/react-native/commit/4d69f4b2d1cf4f2e8265fe5758f28086f1b67500) that could either be set to `flex` or `none`).

## Example
You can find the following example in the [/example](https://github.com/archriss/react-native-snap-carousel/tree/master/example) folder.

Expand Down Expand Up @@ -178,12 +195,13 @@ const styles = Stylesheet.create({
## TODO
- [ ] Handle autoplay properly when updating children's length
- [ ] Implement 'loop' mode
- [ ] Implement 'preload' mode
- [ ] Add vertical implementation
- [ ] Handle changing props on-the-fly
- [ ] Handle device orientation event
- [ ] Add vertical implementation
- [ ] Handle autoplay properly when updating children's length
- [x] Add RTL support
- [x] Improve momemtum handling
- [x] Improve snap on Android
- [x] Handle passing 1 item only
Expand Down
112 changes: 82 additions & 30 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import React, { Component, PropTypes } from 'react';
import { ScrollView, Animated, Platform, Easing } from 'react-native';
import { ScrollView, Animated, Platform, Easing, I18nManager } from 'react-native';
import shallowCompare from 'react-addons-shallow-compare';

// React Native automatically handles RTL layouts; unfortunately, it's buggy with horizontal ScrollView
// See https://github.com/facebook/react-native/issues/11960
// Handling it requires a bunch of hacks
// NOTE: the following variable is not declared in the constructor
// otherwise it is undefined at init, which messes with custom indexes
const IS_RTL = I18nManager.isRTL;

export default class Carousel extends Component {

static propTypes = {
Expand All @@ -16,6 +23,11 @@ export default class Carousel extends Component {
*/
sliderWidth: PropTypes.number.isRequired,
/**
* From slider's center, minimum slide distance
* to be scrolled before being set to active
*/
activeSlideOffset: PropTypes.number,
/**
* Animated animation to use. Provide the name
* of the method, defaults to timing
*/
Expand Down Expand Up @@ -94,6 +106,7 @@ export default class Carousel extends Component {
};

static defaultProps = {
activeSlideOffset: 25,
animationFunc: 'timing',
animationOptions: {
easing: Easing.elastic(1)
Expand All @@ -117,7 +130,7 @@ export default class Carousel extends Component {
constructor (props) {
super(props);
this.state = {
activeItem: props.firstItem,
activeItem: this._getFirstItem(props.firstItem),
interpolators: []
};
this._positions = [];
Expand All @@ -135,14 +148,17 @@ export default class Carousel extends Component {

componentDidMount () {
const { firstItem, autoplay } = this.props;
const _firstItem = this._getFirstItem(firstItem);

this._initInterpolators(this.props);

setTimeout(() => {
this.snapToItem(firstItem, false, false, true);
this.snapToItem(_firstItem, false, false, true);

if (autoplay) {
this.startAutoplay();
}
}, 0);
if (autoplay) {
this.startAutoplay();
}
}

shouldComponentUpdate (nextProps, nextState) {
Expand All @@ -154,15 +170,21 @@ export default class Carousel extends Component {
}

componentWillReceiveProps (nextProps) {
const { interpolators } = this.state;
const { activeItem, interpolators } = this.state;
const { firstItem } = nextProps;
const _firstItem = this._getFirstItem(firstItem, nextProps);
const childrenLength = React.Children.count(nextProps.children);
const newActiveItem = activeItem || activeItem === 0 ? activeItem : _firstItem;

if (childrenLength && interpolators.length !== childrenLength) {
this._positions = [];
this._calcCardPositions(nextProps);
this._initInterpolators(nextProps);
this.setState({ activeItem: firstItem });
this.setState({ activeItem: newActiveItem });

if (IS_RTL) {
this.snapToItem(newActiveItem, false, false, true);
}
}
}

Expand All @@ -176,37 +198,61 @@ export default class Carousel extends Component {
return enableSnap && (Platform.OS === 'ios' || snapOnAndroid);
}

get _nextItem () {
const { activeItem } = this.state;
get currentIndex () {
return this.state.activeItem;
}

return this._positions[activeItem + 1] ? activeItem + 1 : 0;
_getCustomIndex (index, props = this.props) {
const itemsLength = this._children(props).length;

if (!itemsLength || (!index && index !== 0)) {
return 0;
}

return IS_RTL ?
itemsLength - index - 1 :
index;
}

_getFirstItem (index, props = this.props) {
const itemsLength = this._children(props).length;

if (index > itemsLength - 1 || index < 0) {
return 0;
}

return index;
}

_calcCardPositions (props = this.props) {
const { itemWidth } = props;

this._children(props).map((item, index) => {
const _index = this._getCustomIndex(index, props);
this._positions[index] = {
start: index * itemWidth,
end: index * itemWidth + itemWidth
start: _index * itemWidth,
end: _index * itemWidth + itemWidth
};
});
}

_initInterpolators (props = this.props) {
const { firstItem } = props;
const _firstItem = this._getFirstItem(firstItem, props);
let interpolators = [];

this._children(props).map((item, index) => {
interpolators.push(new Animated.Value(index === firstItem ? 1 : 0));
interpolators.push(new Animated.Value(index === _firstItem ? 1 : 0));
});
this.setState({ interpolators });
}

_getActiveItem (centerX, offset = 25) {
_getActiveItem (centerX) {
const { activeSlideOffset } = this.props;

for (let i = 0; i < this._positions.length; i++) {
const { start, end } = this._positions[i];
if (centerX + offset >= start && centerX - offset <= end) {
if (centerX + activeSlideOffset >= start && centerX - activeSlideOffset <= end) {
return i;
}
}
Expand Down Expand Up @@ -311,13 +357,21 @@ export default class Carousel extends Component {
// Snap depending on delta
if (deltaX > 0) {
if (deltaX > swipeThreshold) {
this.snapToItem(this._scrollStartActive + 1);
if (IS_RTL) {
this.snapToItem(this._scrollStartActive - 1);
} else {
this.snapToItem(this._scrollStartActive + 1);
}
} else {
this.snapToItem(this._scrollEndActive);
}
} else if (deltaX < 0) {
if (deltaX < -swipeThreshold) {
this.snapToItem(this._scrollStartActive - 1);
if (IS_RTL) {
this.snapToItem(this._scrollStartActive + 1);
} else {
this.snapToItem(this._scrollStartActive - 1);
}
} else {
this.snapToItem(this._scrollEndActive);
}
Expand All @@ -328,10 +382,6 @@ export default class Carousel extends Component {
}
}

get currentIndex () {
return this.state.activeItem;
}

startAutoplay (instantly = false) {
const { autoplayInterval, autoplayDelay } = this.props;

Expand All @@ -344,7 +394,7 @@ export default class Carousel extends Component {
this._autoplayInterval =
setInterval(() => {
if (this._autoplaying) {
this.snapToItem(this._nextItem);
this.snapToNext();
}
}, autoplayInterval);
}, instantly ? 0 : autoplayDelay);
Expand Down Expand Up @@ -373,8 +423,8 @@ export default class Carousel extends Component {
const snapX = itemsLength && this._positions[index].start;

// Make sure the component hasn't been unmounted
if (this.refs.scrollview) {
this.refs.scrollview.scrollTo({x: snapX, y: 0, animated});
if (this._scrollview) {
this._scrollview.scrollTo({ x: snapX, y: 0, animated });
this.props.onSnapToItem && fireCallback && this.props.onSnapToItem(index);

// iOS fix, check the note in the constructor
Expand Down Expand Up @@ -405,7 +455,7 @@ export default class Carousel extends Component {
}

_children (props = this.props) {
return React.Children.map(props.children, (child) => child);
return React.Children.toArray(props.children);
}

_childSlides () {
Expand Down Expand Up @@ -445,20 +495,22 @@ export default class Carousel extends Component {

const containerSideMargin = (sliderWidth - itemWidth) / 2;
const style = [
containerCustomStyle || {},
{ paddingHorizontal: Platform.OS === 'ios' ? containerSideMargin : 0 },
containerCustomStyle || {}
// LTR hack; see https://github.com/facebook/react-native/issues/11960
{ flexDirection: IS_RTL ? 'row-reverse' : 'row' }
];
const contentContainerStyle = [
{ paddingHorizontal: Platform.OS === 'android' ? containerSideMargin : 0 },
contentContainerCustomStyle || {}
contentContainerCustomStyle || {},
{ paddingHorizontal: Platform.OS === 'android' ? containerSideMargin : 0 }
];

return (
<ScrollView
decelerationRate={0.9}
style={style}
contentContainerStyle={contentContainerStyle}
ref={'scrollview'}
ref={(scrollview) => { this._scrollview = scrollview; }}
horizontal={true}
onScrollBeginDrag={this._onScrollBegin}
onMomentumScrollEnd={enableMomentum ? this._onScrollEnd : undefined}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-native-snap-carousel",
"version": "2.0.3",
"version": "2.1.0",
"description": "Swiper component for React Native with previews and snapping effect. Compatible with Android & iOS.",
"main": "index.js",
"repository": {
Expand Down

0 comments on commit a677bd2

Please sign in to comment.