diff --git a/CHANGELOG.md b/CHANGELOG.md index 3116093f5..2cc97dac2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ +## v2.0.0 + +* Items are now direct children of the component, which makes it easier to use (thanks [@Jonarod](https://github.com/Jonarod) !) +* Props `items` and `renderItem` have been removed + ## v1.6.1 -* Due to some touch events being buggy, rework methods so the children will receive touch events on Android. +* Due to some touch events being buggy, rework methods so the children will receive touch events on Android ## v1.6.0 diff --git a/README.md b/README.md index 38c17e7ee..baae772a1 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,9 @@ This app is going to be updated on a regular basis. ## Usage +### Breaking change +Since version 2.0.0, items are now **direct children of the component**. As a result, props `items` and `renderItem` have been removed. + ``` $ npm install --save react-native-snap-carousel ``` @@ -34,20 +37,36 @@ $ npm install --save react-native-snap-carousel ```javascript import Carousel from 'react-native-snap-carousel'; - _renderItem (data, index) { - return ( - ... - ); + // Example with different children + render () { + + + + + } + // Example of appending the same component multiple times while looping through an array of data render () { + const slides = this.state.entries.map((entry, index) => { + return ( + + { entry.title } + + ); + }); + + > + { slides } + } ``` @@ -57,27 +76,25 @@ import Carousel from 'react-native-snap-carousel'; Prop | Description | Type | Default ------ | ------ | ------ | ------ -items | Array of items to loop on | Array | Required -sliderWidth | The width in pixels of your slider | Number | Required -itemWidth | Width in pixels of your items | Number | Required -renderItem | Function returning a react element. The entry data is the 1st parameter, its index is the 2nd | Function | Required -shouldOptimizeUpdates | whether to implement a `shouldComponentUpdate` strategy to minimize updates | Boolean | `true` -slideStyle | Style of each item's container | Number | Required -swipeThreshold | Delta x when swiping to trigger the snap | Number | `20` -animationFunc | Animated animation to use. Provide the name of the method | String | `Timing` +**itemWidth** | Width in pixels of your items | Number | **Required** +**sliderWidth** | The width in pixels of your slider | Number | **Required** +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) }` -firstItem | Index of the first item to display | Number | `0` autoplay | Trigger autoplay on mount | Boolean | `false` -autoplayInterval | Delay in ms until navigating to the next item | `3000` autoplayDelay | Delay before enabling autoplay on startup & after releasing the touch | Number | `5000` +autoplayInterval | Delay in ms until navigating to the next item | `3000` +containerCustomStyle | Optional styles for Scrollview's global wrapper | ScrollView Style Object | `{}` +contentContainerCustomStyle | Optional styles for Scrollview's items container | ScrollView Style Object | `{}` +enableMomentum | See [momentum](#momentum) | Boolean | `false` enableSnap | If enabled, releasing the touch will scroll to the center of the nearest/active item | Number | `true` -enableMomentum | See [momentum](#momentum) | Boolean | `true` -snapOnAndroid | Snapping on android is kinda choppy, especially when swiping quickly so you can disable it | Boolean | `false` -containerCustomStyle | Optional styles for Scrollview's global wrapper | Number | `null` -contentContainerCustomStyle | Optional styles for Scrollview's items container | Number | `null` -inactiveSlideScale | Value of the 'scale' transform applied to inactive slides | Number | `0.9` +firstItem | Index of the first item to display | Number | `0` inactiveSlideOpacity | Value of the opacity effect applied to inactive slides | Number | `1` -onSnapToItem(slideIndex, itemData) | Callback fired when navigating to an item | Function | `undefined` +inactiveSlideScale | Value of the 'scale' transform applied to inactive slides | Number | `0.9` +shouldOptimizeUpdates | whether to implement a `shouldComponentUpdate` strategy to minimize updates | Boolean | `true` +slideStyle | Optional style for each item's container (the one whose scale and opacity are animated) | Animated View Style Object | {} +snapOnAndroid | Snapping on android is kinda choppy, especially when swiping quickly so you can disable it | Boolean | `true` +swipeThreshold | Delta x when swiping to trigger the snap | Number | `20` +onSnapToItem(slideIndex) | Callback fired when navigating to an item | Function | `undefined` ## Methods @@ -109,8 +126,9 @@ You can adjust this value to your needs thanks to [this prop](https://facebook.g If you need some **extra horizontal margin** between slides (besides the one resulting from the scale effect), you should add it as `paddingHorizontal` on the slide container. Make sure to take this into account when calculating item's width. ```javascript +const sliderWidth = Dimensions.get('window').width * 0.75; const slideWidth = 250; -const horizontalMargin = 40; +const horizontalMargin = 20; const itemWidth = slideWidth + horizontalMargin * 2; const styles = Stylesheet.create({ @@ -121,16 +139,19 @@ const styles = Stylesheet.create({ }; +> + ... + ``` ## TODO -- [ ] Add 'loop' mode -- [ ] Add 'preload' mode +- [ ] Known issue: updating children's length doesn't play well with autoplay +- [ ] Implement 'loop' mode +- [ ] Implement 'preload' mode - [ ] Handle changing props on-the-fly - [ ] Handle device orientation event - [ ] Add vertical implementation diff --git a/example/package.json b/example/package.json index 356359b75..aa755dad0 100644 --- a/example/package.json +++ b/example/package.json @@ -1,6 +1,6 @@ { "name": "example", - "version": "0.0.1", + "version": "0.2.0", "private": true, "scripts": { "start": "node node_modules/react-native/local-cli/cli.js start", @@ -9,7 +9,7 @@ "dependencies": { "react": "15.4.1", "react-native": "0.38.0", - "react-native-snap-carousel": "1.6.1" + "react-native-snap-carousel": "2.0.0" }, "jest": { "preset": "react-native" diff --git a/example/src/components/SliderEntry.js b/example/src/components/SliderEntry.js index de975c59f..bfd4ee936 100644 --- a/example/src/components/SliderEntry.js +++ b/example/src/components/SliderEntry.js @@ -7,27 +7,33 @@ export default class SliderEntry extends Component { static propTypes = { title: PropTypes.string.isRequired, subtitle: PropTypes.string, - illustration: PropTypes.string + illustration: PropTypes.string, + even: PropTypes.bool }; render () { - const { title, subtitle, illustration } = this.props; + const { title, subtitle, illustration, even } = this.props; + + const uppercaseTitle = title ? ( + { title.toUpperCase() } + ) : false; return ( { alert(`You've clicked '${title}'`); }} > - + + - - { title.toUpperCase() } - { subtitle } + + { uppercaseTitle } + { subtitle } ); diff --git a/example/src/index.js b/example/src/index.js index e7a63194f..c9d90477d 100644 --- a/example/src/index.js +++ b/example/src/index.js @@ -3,73 +3,87 @@ import { View, ScrollView, Text, StatusBar } from 'react-native'; import Carousel from 'react-native-snap-carousel'; import { sliderWidth, itemWidth } from 'example/src/styles/SliderEntry.style'; import SliderEntry from 'example/src/components/SliderEntry'; -import mainStyles from 'example/src/styles/index.style'; -import sliderStyles from 'example/src/styles/Slider.style'; +import styles from 'example/src/styles/index.style'; import { ENTRIES1, ENTRIES2 } from 'example/src/static/entries'; export default class example extends Component { - _renderItem (entry) { - return ( - - ); + getSlides (entries) { + if (!entries) { + return false; + } + + return entries.map((entry, index) => { + return ( + + ); + }); } get example1 () { return ( + > + { this.getSlides(ENTRIES1) } + ); } get example2 () { return ( + > + { this.getSlides(ENTRIES2) } + ); } render () { return ( - + - - - + + + - - Example 1 + + Example 1 + Momentum | Scale | Opacity { this.example1 } - Example 2 (no momentum) + Example 2 + Autoplay | No momentum { this.example2 } diff --git a/example/src/styles/Slider.style.js b/example/src/styles/Slider.style.js deleted file mode 100644 index 6565ec769..000000000 --- a/example/src/styles/Slider.style.js +++ /dev/null @@ -1,15 +0,0 @@ -import { StyleSheet } from 'react-native'; -import { itemHorizontalMargin, itemWidth } from 'example/src/styles/SliderEntry.style'; - -export default StyleSheet.create({ - slider: { - marginBottom: 35 - }, - sliderContainer: { - }, - slide: { - flexDirection: 'column', - width: itemWidth, - paddingHorizontal: itemHorizontalMargin - } -}); diff --git a/example/src/styles/SliderEntry.style.js b/example/src/styles/SliderEntry.style.js index 799bc1907..7e9333204 100644 --- a/example/src/styles/SliderEntry.style.js +++ b/example/src/styles/SliderEntry.style.js @@ -15,18 +15,24 @@ export const sliderWidth = viewportWidth; export const itemHorizontalMargin = wp(2); export const itemWidth = slideWidth + itemHorizontalMargin * 2; -const entryBorderRadius = 6; +const entryBorderRadius = 8; export default StyleSheet.create({ slideInnerContainer: { - height: slideHeight + width: itemWidth, + height: slideHeight, + paddingHorizontal: itemHorizontalMargin, + paddingBottom: 18 // needed for shadow }, imageContainer: { flex: 1, - backgroundColor: colors.gray, + backgroundColor: 'white', borderTopLeftRadius: entryBorderRadius, borderTopRightRadius: entryBorderRadius }, + imageContainerEven: { + backgroundColor: colors.black + }, image: { ...StyleSheet.absoluteFillObject, resizeMode: 'cover', @@ -34,24 +40,46 @@ export default StyleSheet.create({ borderTopLeftRadius: entryBorderRadius, borderTopRightRadius: entryBorderRadius }, + // image's border radius is buggy on ios; let's hack it! + radiusMask: { + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + height: entryBorderRadius, + backgroundColor: 'white' + }, + radiusMaskEven: { + backgroundColor: colors.black + }, textContainer: { justifyContent: 'center', - paddingVertical: 20, + paddingTop: 20 - entryBorderRadius, + paddingBottom: 20, paddingHorizontal: 16, backgroundColor: 'white', borderBottomLeftRadius: entryBorderRadius, borderBottomRightRadius: entryBorderRadius }, + textContainerEven: { + backgroundColor: colors.black + }, title: { color: colors.black, fontSize: 13, fontWeight: 'bold', letterSpacing: 0.5 }, + titleEven: { + color: 'white' + }, subtitle: { marginTop: 6, color: colors.gray, fontSize: 12, fontStyle: 'italic' + }, + subtitleEven: { + color: 'rgba(255, 255, 255, 0.7)' } }); diff --git a/example/src/styles/index.style.js b/example/src/styles/index.style.js index a9464aa7a..449238370 100644 --- a/example/src/styles/index.style.js +++ b/example/src/styles/index.style.js @@ -29,10 +29,25 @@ export default StyleSheet.create({ paddingTop: 50 }, title: { - marginVertical: 15, + marginTop: 15, backgroundColor: 'transparent', - color: 'rgba(255, 255, 255, 0.85)', - fontSize: 24, + color: 'rgba(255, 255, 255, 0.9)', + fontSize: 22, + fontWeight: 'bold', textAlign: 'center' + }, + subtitle: { + marginTop: 5, + marginBottom: 15, + backgroundColor: 'transparent', + color: 'rgba(255, 255, 255, 0.75)', + fontSize: 16, + fontStyle: 'italic', + textAlign: 'center' + }, + slider: { + marginBottom: 30 + }, + sliderContainer: { } }); diff --git a/index.js b/index.js index 48754c2b0..646d2c813 100644 --- a/index.js +++ b/index.js @@ -7,95 +7,86 @@ export default class Carousel extends Component { static propTypes = { ...ScrollView.propTypes, /** - * Supply items to loop on - */ - items: PropTypes.array.isRequired, + * Width in pixels of your elements + */ + itemWidth: PropTypes.number.isRequired, /** * Width in pixels of your slider according * to your styles */ sliderWidth: PropTypes.number.isRequired, /** - * Width in pixels of your elements - */ - itemWidth: PropTypes.number.isRequired, + * Animated animation to use. Provide the name + * of the method, defaults to timing + */ + animationFunc: PropTypes.string, /** - * Function returning a react element. The entry - * data is the 1st parameter, its index is the 2nd - */ - renderItem: PropTypes.func.isRequired, + * Animation options to be merged with the + * default ones. Can be used w/ animationFunc + */ + animationOptions: PropTypes.object, /** - * Style of each item's container - */ - slideStyle: Animated.View.propTypes.style, + * Trigger autoplay + */ + autoplay: PropTypes.bool, /** - * whether to implement a `shouldComponentUpdate` - * strategy to minimize updates - */ - shouldOptimizeUpdates: PropTypes.bool, + * Delay before enabling autoplay on startup and + * after releasing the touch + */ + autoplayDelay: PropTypes.number, + /** + * Delay until navigating to the next item + */ + autoplayInterval: PropTypes.number, /** * Global wrapper's style */ - containerCustomStyle: Animated.View.propTypes.style, + containerCustomStyle: ScrollView.propTypes.style, /** * Content container's style */ - contentContainerCustomStyle: Animated.View.propTypes.style, - /** - * Delta x when swiping to trigger the snap - */ - swipeThreshold: PropTypes.number, + contentContainerCustomStyle: ScrollView.propTypes.style, /** - * Animated animation to use. Provide the name - * of the method, defaults to timing - */ - animationFunc: PropTypes.string, - /** - * Animation options to be merged with the - * default ones. Can be used w/ animationFunc - */ - animationOptions: PropTypes.object, - /** - * Scale factor of the inactive slides - */ - inactiveSlideScale: PropTypes.number, + * If enabled, snapping will be triggered once + * the ScrollView stops moving, not when the + * user releases his finger + */ + enableMomentum: PropTypes.bool, /** - * Opacity value of the inactive slides - */ - inactiveSlideOpacity: PropTypes.number, + * If enabled, releasing the touch will scroll + * to the center of the nearest/active item + */ + enableSnap: PropTypes.bool, /** - * Index of the first item to display - */ + * Index of the first item to display + */ firstItem: PropTypes.number, /** - * Trigger autoplay - */ - autoplay: PropTypes.bool, + * Opacity value of the inactive slides + */ + inactiveSlideOpacity: PropTypes.number, /** - * Delay until navigating to the next item - */ - autoplayInterval: PropTypes.number, + * Scale factor of the inactive slides + */ + inactiveSlideScale: PropTypes.number, /** - * Delay before enabling autoplay on startup and - * after releasing the touch + * Style of each item's container */ - autoplayDelay: PropTypes.number, + slideStyle: Animated.View.propTypes.style, /** - * If enabled, releasing the touch will scroll - * to the center of the nearest/active item + * whether to implement a `shouldComponentUpdate` + * strategy to minimize updates */ - enableSnap: PropTypes.bool, - /** - * If enabled, snapping will be triggered once - * the ScrollView stops moving, not when the - * user releases his finger - */ - enableMomentum: PropTypes.bool, + shouldOptimizeUpdates: PropTypes.bool, /** * Snapping on android is kinda choppy, especially * when swiping quickly so you can disable it */ snapOnAndroid: PropTypes.bool, + /** + * Delta x when swiping to trigger the snap + */ + swipeThreshold: PropTypes.number, /** * Fired when snapping to an item */ @@ -103,24 +94,24 @@ export default class Carousel extends Component { }; static defaultProps = { - shouldOptimizeUpdates: true, - autoplay: false, - autoplayInterval: 3000, - autoplayDelay: 5000, - firstItem: 0, - enableSnap: true, - enableMomentum: true, - snapOnAndroid: false, - swipeThreshold: 20, animationFunc: 'timing', animationOptions: { easing: Easing.elastic(1) }, - slideStyle: {}, - containerCustomStyle: null, - contentContainerCustomStyle: null, + autoplay: false, + autoplayDelay: 5000, + autoplayInterval: 3000, + containerCustomStyle: {}, + contentContainerCustomStyle: {}, + enableMomentum: false, + enableSnap: true, + firstItem: 0, + inactiveSlideOpacity: 1, inactiveSlideScale: 0.9, - inactiveSlideOpacity: 1 + slideStyle: {}, + shouldOptimizeUpdates: true, + snapOnAndroid: true, + swipeThreshold: 20 } constructor (props) { @@ -162,9 +153,10 @@ export default class Carousel extends Component { } componentWillReceiveProps (nextProps) { - const { items, firstItem } = nextProps; + const { firstItem } = nextProps; + const { interpolators } = this.state; - if (items.length !== this.props.items.length) { + if (interpolators.length !== nextProps.children.length) { this._positions = []; this._calcCardPositions(nextProps); this._initInterpolators(nextProps); @@ -189,9 +181,9 @@ export default class Carousel extends Component { } _calcCardPositions (props = this.props) { - const { items, itemWidth } = props; + const { itemWidth } = props; - items.forEach((item, index) => { + this.props.children.map((item, index) => { this._positions[index] = { start: index * itemWidth }; @@ -200,10 +192,10 @@ export default class Carousel extends Component { } _initInterpolators (props = this.props) { - const { items, firstItem } = props; + const { firstItem } = props; let interpolators = []; - items.forEach((item, index) => { + this.props.children.map((item, index) => { interpolators.push(new Animated.Value(index === firstItem ? 1 : 0)); }); this.setState({ interpolators }); @@ -336,37 +328,6 @@ export default class Carousel extends Component { } } - get items () { - const { items, renderItem, slideStyle, inactiveSlideScale, inactiveSlideOpacity } = this.props; - if (!this.state.interpolators || !this.state.interpolators.length) { - return false; - } - - return items.map((entry, index) => { - const animatedValue = this.state.interpolators[index]; - return ( - - { renderItem(entry, index) } - - ); - }); - } - get currentIndex () { return this.state.activeItem; } @@ -410,7 +371,7 @@ export default class Carousel extends Component { // Make sure the component hasn't been unmounted if (this.refs.scrollview) { this.refs.scrollview.scrollTo({x: snapX, y: 0, animated}); - this.props.onSnapToItem && fireCallback && this.props.onSnapToItem(index, this.props.items[index]); + this.props.onSnapToItem && fireCallback && this.props.onSnapToItem(index); // iOS fix, check the note in the constructor if (!initial && Platform.OS === 'ios') { @@ -439,6 +400,42 @@ export default class Carousel extends Component { this.snapToItem(newIndex, animated); } + _children (children = this.props.children) { + return React.Children.map(children, (child) => child); + } + + _childSlides () { + const { slideStyle, inactiveSlideScale, inactiveSlideOpacity } = this.props; + + if (!this.state.interpolators || !this.state.interpolators.length) { + return false; + }; + + return this._children().map((child, index) => { + const animatedValue = this.state.interpolators[index]; + + if (!animatedValue) { + return false; + } + + return ( + + { child } + + ); + }); + } + render () { const { sliderWidth, itemWidth, containerCustomStyle, contentContainerCustomStyle, enableMomentum } = this.props; @@ -468,7 +465,7 @@ export default class Carousel extends Component { scrollEventThrottle={50} {...this.props} > - { this.items } + { this._childSlides() } ); } diff --git a/package.json b/package.json index 769abf9ec..7a5d06946 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "react-native-snap-carousel", - "version": "1.6.1", - "description": "Simple carousel component with snapping effect on Android & iOS for React Native", + "version": "2.0.0", + "description": "Swiper component for React Native with previews and snapping effect. Compatible with Android & iOS.", "main": "index.js", "repository": { "type": "git",