Skip to content

Feat/carousel page width #513

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Aug 28, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion demo/src/screens/MainScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,12 @@ export default class MainScreen extends Component {
const keys = _.keys(data);

return (
<Carousel migrate onChangePage={this.onChangePage} ref={carousel => (this.carousel = carousel)}>
<Carousel
migrate
ref={carousel => (this.carousel = carousel)}
containerStyle={{flex: 1}}
onChangePage={this.onChangePage}
>
{_.map(data, (section, key) => {
return (
<View key={key} style={[styles.page, pageStyle]}>
Expand Down
2 changes: 1 addition & 1 deletion demo/src/screens/componentScreenScreens/ModalScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export default class ModalScreen extends Component {
color={Colors.dark10}
size={15}
/>
<Carousel migrate onChangePage={currentPage => this.setState({currentPage})}>
<Carousel migrate onChangePage={currentPage => this.setState({currentPage})} containerStyle={{flex: 1}}>
<View bg-green50 flex style={styles.page}>
<Modal.TopBar
title='modal title'
Expand Down
1 change: 1 addition & 0 deletions demo/src/screens/componentScreens/ActionBarScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export default class ActionBarScreen extends Component {
/>
<Carousel
migrate
containerStyle={{flex: 1}}
onChangePage={currentPage => this.setState({currentPage})}
initialPage={this.state.currentPage}
>
Expand Down
37 changes: 29 additions & 8 deletions demo/src/screens/componentScreens/CarouselScreen.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React, {Component} from 'react';
import {StyleSheet} from 'react-native';
import {View, Text, Carousel} from 'react-native-ui-lib'; // eslint-disable-line
import {Constants, View, Text, Carousel} from 'react-native-ui-lib'; // eslint-disable-line


const INITIAL_PAGE = 0;
const WIDTH = Constants.screenWidth - 120;

class CarouselScreen extends Component {
state = {
Expand All @@ -14,14 +15,31 @@ class CarouselScreen extends Component {
this.setState({currentPage: index});
}

onPagePress = (index) => {
this.carousel.goToPage(index, true);
}

render() {
return (
<View flex>
<Carousel migrate loop onChangePage={(index => this.onChangePage(index))} /* initialPage={INITIAL_PAGE} */>
<Page bg-cyan50>
<Text text30 margin-20>Carousel</Text>
<Carousel
migrate
ref={r => this.carousel = r}
// loop
onChangePage={(index => this.onChangePage(index))}
pageWidth={WIDTH}
// itemSpacings={20}
// initialPage={INITIAL_PAGE}
containerStyle={{height: 200/* , flex: 1 */}}
pageControlPosition={'under'}
pageControlProps={{onPagePress: this.onPagePress}}
// showCounter
>
<Page bg-red50>
<Text margin-15>PAGE 0</Text>
</Page>
<Page bg-red50>
<Page bg-yellow20>
<Text margin-15>PAGE 1</Text>
</Page>
<Page bg-purple50>
Expand All @@ -30,17 +48,18 @@ class CarouselScreen extends Component {
<Page bg-green50>
<Text margin-15>PAGE 3</Text>
</Page>
<Page bg-yellow20>
<Page bg-cyan50>
<Text margin-15>PAGE 4</Text>
</Page>
<Page bg-purple20>
<Text margin-15>PAGE 5</Text>
</Page>
<Page bg-blue70>
<Page bg-blue60>
<Text margin-15>PAGE 6</Text>
</Page>
</Carousel>
<View center style={{...StyleSheet.absoluteFillObject}} pointerEvents="none">

<View margin-20 center /*style={{...StyleSheet.absoluteFillObject}} */ pointerEvents="none">
<Text text10>{this.state.currentPage}</Text>
</View>
</View>
Expand All @@ -58,7 +77,9 @@ const Page = ({children, ...others}) => {

const styles = StyleSheet.create({
page: {
flex: 1
flex: 1,
borderWidth: 1,
borderRadius: 4
}
});

Expand Down
9 changes: 0 additions & 9 deletions src/components/carousel/CarouselPresenter.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,3 @@ export function isOutOfBounds(offset, props, pageWidth) {

return !_.inRange(offset, minLimit, maxLimit);
}

// TODO: need to support more cases of page width in loop mode
export function calcCarouselWidth(props) {
const {pageWidth, loop} = props;

let length = getChildrenLength(props);
length = loop ? length + 2 : length;
return pageWidth * length;
}
6 changes: 0 additions & 6 deletions src/components/carousel/__tests__/CarouselPresenter.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,4 @@ describe('Carousel presenter', () => {
expect(uut.isOutOfBounds(481, {migrate: true, children: [{}, {}, {}]}, 120)).toBe(true);
expect(uut.isOutOfBounds(1875, {migrate: true, children: [{}, {}, {}, {}]}, 375)).toBe(true);
});

it('should calcCarouselWidth', () => {
expect(uut.calcCarouselWidth({migrate: true, pageWidth: 70, children: [{}, {}, {}]})).toBe(210);
expect(uut.calcCarouselWidth({migrate: true, pageWidth: 50, children: [{}, {}, {}]})).toBe(150);
expect(uut.calcCarouselWidth({migrate: true, pageWidth: 150, loop: true, children: [{}, {}, {}]})).toBe(750);
});
});
147 changes: 120 additions & 27 deletions src/components/carousel/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,19 @@ import PropTypes from 'prop-types';
import React from 'react';
import {ScrollView, StyleSheet} from 'react-native';
import {Constants} from '../../helpers';
import {Colors} from '../../style';
import {BaseComponent} from '../../commons';
import View from '../view';
import Text from '../text';
import PageControl from '../pageControl';
import * as presenter from './CarouselPresenter';


const PAGE_CONTROL_POSITIONS = {
OVER: 'over',
UNDER: 'under'
}

/**
* @description: Carousel for scrolling pages horizontally
* @gif: https://media.giphy.com/media/l0HU7f8gjpRlMRhKw/giphy.gif, https://media.giphy.com/media/3oFzmcjX9OhpyckhcQ/giphy.gif
Expand All @@ -25,9 +33,13 @@ export default class Carousel extends BaseComponent {
*/
initialPage: PropTypes.number,
/**
* the page width (all pages should have the same width)
* the page width (all pages should have the same width). Does not work if passing 'loop' prop
*/
pageWidth: PropTypes.number,
/**
* the spacing between the items
*/
itemSpacings: PropTypes.number,
/**
* if true, will have infinite scroll
*/
Expand All @@ -44,20 +56,39 @@ export default class Carousel extends BaseComponent {
* the carousel style
*/
containerStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]),
/**
* PageControl component props
*/
pageControlProps: PropTypes.shape(PageControl.propTypes),
/**
* The position of the PageControl component ['over', 'under'], otherwise it won't display
*/
pageControlPosition: PropTypes.oneOf(Object.values(PAGE_CONTROL_POSITIONS)),
/**
* whether to show a page counter (will not work with pageWidths)
*/
showCounter: PropTypes.bool,
/**
* the counter's text style
*/
counterTextStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array])
};

static defaultProps = {
initialPage: 0
initialPage: 0,
itemSpacings: 12
};

static pageControlPositions = PAGE_CONTROL_POSITIONS;

constructor(props) {
super(props);

this.carousel = React.createRef();
const defaultPageWidth = props.pageWidth || Constants.screenWidth;
const defaultPageWidth = props.loop ? Constants.screenWidth : (props.pageWidth + props.itemSpacings || Constants.screenWidth);

this.state = {
currentPage: props.initialPage,
currentPage: this.shouldUsePageWidth() ? this.getCalcIndex(props.initialPage) : props.initialPage,
currentStandingPage: props.initialPage,
pageWidth: defaultPageWidth,
initialOffset: {x: presenter.calcOffset(props, {currentPage: props.initialPage, pageWidth: defaultPageWidth})}
Expand All @@ -73,7 +104,7 @@ export default class Carousel extends BaseComponent {
}

onOrientationChanged = () => {
if (!this.props.pageWidth) {
if (!this.props.pageWidth || this.props.loop) {
this.setState({pageWidth: Constants.screenWidth});
this.goToPage(this.state.currentPage, true);
}
Expand All @@ -84,7 +115,8 @@ export default class Carousel extends BaseComponent {
}

updateOffset = (animated = false) => {
const x = presenter.calcOffset(this.props, this.state);
const centerOffset = Constants.isIOS && this.shouldUsePageWidth() ? (Constants.screenWidth - this.state.pageWidth) / 2 : 0;
const x = presenter.calcOffset(this.props, this.state) - centerOffset;

if (this.carousel) {
this.carousel.current.scrollTo({x, animated});
Expand All @@ -111,6 +143,11 @@ export default class Carousel extends BaseComponent {
return index;
}

shouldUsePageWidth() {
const {loop, pageWidth} = this.props;
return !loop && pageWidth;
}

onContentSizeChange = () => {
// this is to handle initial scroll position (content offset)
if (Constants.isAndroid) {
Expand Down Expand Up @@ -151,8 +188,10 @@ export default class Carousel extends BaseComponent {
}

renderChild = (child, key) => {
const paddingLeft = this.shouldUsePageWidth() ? this.props.itemSpacings : undefined;

return (
<View style={{width: this.state.pageWidth}} key={key}>
<View style={{width: this.state.pageWidth, paddingLeft}} key={key}>
{child}
</View>
);
Expand All @@ -165,7 +204,7 @@ export default class Carousel extends BaseComponent {
const childrenArray = React.Children.map(children, (child, index) => {
return this.renderChild(child, `${index}`);
});

if (loop) {
childrenArray.unshift(this.renderChild(children[length - 1], `${length - 1}-clone`));
childrenArray.push(this.renderChild(children[0], `${0}-clone`));
Expand All @@ -174,32 +213,86 @@ export default class Carousel extends BaseComponent {
return childrenArray;
}

renderPageControl() {
const {pageControlPosition, pageControlProps} = this.props;

if (pageControlPosition) {
const pagesCount = presenter.getChildrenLength(this.props);
const containerStyle = pageControlPosition === PAGE_CONTROL_POSITIONS.UNDER ?
{marginVertical: 16} : {position: 'absolute', bottom: 16, alignSelf: 'center'};

return (
<PageControl
size={6}
containerStyle={containerStyle}
inactiveColor={Colors.dark60}
color={Colors.dark20}
{...pageControlProps}
numOfPages={pagesCount}
currentPage={this.getCalcIndex(this.state.currentPage)}
/>
);
}
}

renderCounter() {
const {pageWidth, showCounter, counterTextStyle} = this.props;
const {currentPage} = this.state;
const pagesCount = presenter.getChildrenLength(this.props);

if (showCounter && !pageWidth) {
return (
<View center style={this.styles.counter}>
<Text dark80 text90 style={[{fontWeight: 'bold'}, counterTextStyle]}>{currentPage + 1}/{pagesCount}</Text>
</View>
);
}
}

render() {
const {containerStyle, ...others} = this.props;
const {initialOffset} = this.state;
const {containerStyle, itemSpacings, initialPage, ...others} = this.props;
const {initialOffset, pageWidth} = this.state;

const scrollContainerStyle = this.shouldUsePageWidth() ? {paddingRight: itemSpacings} : undefined;
const spacings = pageWidth === Constants.screenWidth ? 0 : itemSpacings;
const initialBreak = pageWidth - (Constants.screenWidth - pageWidth - spacings) / 2;
const snapToOffsets = _.times(presenter.getChildrenLength(this.props), (index) => initialBreak + index * pageWidth);

return (
<ScrollView
{...others}
ref={this.carousel}
style={[containerStyle, {flexGrow: 1}]}
horizontal
showsHorizontalScrollIndicator={false}
pagingEnabled
onScroll={this.onScroll}
scrollEventThrottle={200}
contentOffset={initialOffset}
onContentSizeChange={this.onContentSizeChange}
onMomentumScrollEnd={this.onMomentumScrollEnd}
>
{this.renderChildren()}
</ScrollView>
<View style={containerStyle}>
<ScrollView
{...others}
ref={this.carousel}
contentContainerStyle={scrollContainerStyle}
horizontal
showsHorizontalScrollIndicator={false}
snapToOffsets={snapToOffsets}
decelerationRate="fast"
contentOffset={initialOffset} // iOS only
scrollEventThrottle={200}
onContentSizeChange={this.onContentSizeChange}
onScroll={this.onScroll}
onMomentumScrollEnd={this.onMomentumScrollEnd}
>
{this.renderChildren()}
</ScrollView>
{this.renderPageControl()}
{this.renderCounter()}
</View>
);
}
}

function createStyles() {
return StyleSheet.create({

counter: {
paddingHorizontal: 8,
paddingVertical: 3, // height: 24,
borderRadius: 20,
backgroundColor: Colors.rgba(Colors.black, 0.6),
position: 'absolute',
top: 12,
right: 12
}
});
}