diff --git a/components/carousel/PropsType.tsx b/components/carousel/PropsType.tsx new file mode 100644 index 00000000..823f7f26 --- /dev/null +++ b/components/carousel/PropsType.tsx @@ -0,0 +1,37 @@ +import { ReactNode } from 'react' +import { ScrollViewProps, StyleProp, ViewStyle } from 'react-native' +import { CarouselStyle } from './style/index' + +export interface CarouselProps extends ScrollViewProps { + accessibilityLabel?: string + autoplay?: boolean + autoplayInterval?: number + afterChange?: (index: number) => void + children: ReactNode + dots?: boolean + dotActiveStyle?: StyleProp + dotStyle?: StyleProp + infinite?: boolean + pageStyle?: StyleProp + pagination?: (props: PaginationProps) => ReactNode + selectedIndex?: number + style?: StyleProp + styles?: Partial + vertical?: boolean +} + +export interface PaginationProps { + current: number + count: number + dotStyle?: StyleProp + dotActiveStyle?: StyleProp + styles: Partial + vertical?: boolean +} + +export interface CarouselForwardedRef { + scrollToStart: () => void + scrollToEnd: () => void + scrollNextPage: () => void + goTo: (index: number, animated?: boolean) => void +} diff --git a/components/carousel/index.tsx b/components/carousel/index.tsx index 53387c87..73546b92 100644 --- a/components/carousel/index.tsx +++ b/components/carousel/index.tsx @@ -1,68 +1,31 @@ import React from 'react' import { - LayoutRectangle, + GestureResponderEvent, + LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent, Platform, ScrollView, - ScrollViewProps, - StyleProp, View, - ViewStyle, } from 'react-native' import devWarning from '../_util/devWarning' -import { WithTheme, WithThemeStyles } from '../style' -import CarouselStyles, { CarouselStyle } from './style/index' - -export interface CarouselPropsType - extends WithThemeStyles, - ScrollViewProps { - accessibilityLabel?: string - pageStyle?: ViewStyle - children?: React.ReactNode - - selectedIndex?: number - dots?: boolean - vertical?: boolean - autoplay?: boolean - autoplayInterval?: number - infinite?: boolean -} - -export interface CarouselProps extends CarouselPropsType { - style?: StyleProp - dotStyle?: StyleProp - dotActiveStyle?: StyleProp - pagination?: (props: PaginationProps) => React.ReactNode - afterChange?: (index: number) => void -} +import { WithTheme } from '../style' +import { CarouselProps, PaginationProps } from './PropsType' +import CarouselStyles from './style/index' interface NativeScrollPoint { x: number y: number } -interface TargetedEvent { - target: number -} export interface CarouselState { width: number height: number selectedIndex: number afterSelectedIndex: number - isScrolling: boolean offset: NativeScrollPoint } -export interface PaginationProps { - vertical?: boolean - current: number - count: number - styles: ReturnType - dotStyle?: StyleProp - dotActiveStyle?: StyleProp -} - const defaultPagination = (props: PaginationProps) => { const { styles, current, vertical, count, dotStyle, dotActiveStyle } = props const positionStyle = vertical ? 'paginationY' : 'paginationX' @@ -89,7 +52,7 @@ const defaultPagination = (props: PaginationProps) => { ) } class Carousel extends React.PureComponent { - static defaultProps: CarouselProps = { + static defaultProps = { accessibilityLabel: 'Carousel', pageStyle: {}, @@ -116,7 +79,6 @@ class Carousel extends React.PureComponent { this.state = { width: 0, height: 0, - isScrolling: false, selectedIndex: index, afterSelectedIndex: -1, offset: { x: 0, y: 0 }, @@ -132,13 +94,11 @@ class Carousel extends React.PureComponent { const { width, height } = this.state if (autoplay !== this.props.autoplay) { if (autoplay) { - this.autoplay() + this.autoplay(autoplay) } else { - this.autoplayTimer && clearTimeout(this.autoplayTimer) + this.clearTimeout() } } - // selectedIndex only take effect once - // ... if ( children && @@ -153,7 +113,6 @@ class Carousel extends React.PureComponent { : { x: width * (infinite ? 1 : 0), y: 0 } this.setState( { - isScrolling: false, afterSelectedIndex: -1, selectedIndex: 0, offset: offset, @@ -165,84 +124,108 @@ class Carousel extends React.PureComponent { ) } - private autoplayTimer: ReturnType - private scrollEndTimter: ReturnType + private autoplayTimer: ReturnType | undefined + private isScrolling: boolean | undefined componentWillUnmount() { - this.autoplayTimer && clearTimeout(this.autoplayTimer) - this.scrollEndTimter && clearTimeout(this.scrollEndTimter) - } - - onScrollBegin = (e: NativeSyntheticEvent) => { - this.setState( - { - isScrolling: true, - }, - () => { - if (this.props.onScrollBeginDrag) { - this.props.onScrollBeginDrag(e) - } - }, - ) + this.clearTimeout() } - onScrollEnd = (e: NativeSyntheticEvent) => { - e.persist?.() - // android/web hack - if (!e.nativeEvent.contentOffset) { - //@ts-ignore - const { position } = e.nativeEvent - e.nativeEvent.contentOffset = { - x: this.props.vertical ? 0 : position * this.state.width, - y: this.props.vertical ? position * this.state.height : 0, - } + /** + * Plathform: iOS & android + * 手势介入时: onScrollBeginDrag -> onScrollEndDrag + * **/ + private onScrollBeginDrag = (e: NativeSyntheticEvent) => { + this.isScrolling = true + + if (this.props.onScrollBeginDrag) { + this.props.onScrollBeginDrag(e) } - this.autoplay() - clearTimeout(this.scrollEndTimter) - this.scrollEndTimter = setTimeout(() => { - this.updateIndex(e.nativeEvent.contentOffset) - - if (this.props.onMomentumScrollEnd) { - this.props.onMomentumScrollEnd(e) - } - }, 50) //idle time } + private onScrollEndDrag = (e: NativeSyntheticEvent) => { + this.isScrolling = false + // fix: drag page in Perfect fit + this.onScrollAnimationEnd( + JSON.parse(JSON.stringify(e.nativeEvent.contentOffset)), + ) - onScrollEndDrag = (e: NativeSyntheticEvent) => { - e.persist?.() - const { offset, selectedIndex } = this.state - const previousOffset = offset - const newOffset = e.nativeEvent.contentOffset - if ( - (this.props.vertical - ? previousOffset.y === newOffset.y - : previousOffset.x === newOffset.x) && - (selectedIndex === 0 || selectedIndex === this.count - 1) - ) { - this.setState({ - isScrolling: false, - }) - } if (this.props.onScrollEndDrag) { this.props.onScrollEndDrag(e) } } - onTouchStartForWeb = () => { - this.setState({ isScrolling: true }) + /** + * Plathform: web + * 手势介入时: onTouchStart -> onScroll…onScroll(只要动了就会触发) -> onTouchEnd -> onScroll(动画结束时触发) + * autoplay: [onScroll...onScroll] -> onScroll(动画结束时触发) + * **/ + private onTouchStartForWeb = (e: GestureResponderEvent) => { + this.isScrolling = true + if (this.props.onTouchStart) { + this.props.onTouchStart(e) + } + } + private onTouchEndForWeb = (e: GestureResponderEvent) => { + this.isScrolling = false + if (this.props.onTouchEnd) { + this.props.onTouchEnd(e) + } } - onTouchEndForWeb = () => { - this.autoplay() + private onScroll = (e: NativeSyntheticEvent) => { + // Simulate infinite pages + if (this.props.infinite) { + const contentOffset = JSON.parse( + JSON.stringify(e.nativeEvent.contentOffset), + ) + const { width, height } = this.state + + const offset = this.props.vertical ? 'y' : 'x' + const maxOffset = + (this.props.vertical ? height : width) * (this.count + 1) + + if (contentOffset[offset] <= 0) { + contentOffset[offset] = 0 + this.updateIndex(contentOffset) + } else if (contentOffset[offset] >= maxOffset) { + contentOffset[offset] = maxOffset + this.updateIndex(contentOffset) + } + } + + this.onScrollAnimationEnd( + JSON.parse(JSON.stringify(e.nativeEvent.contentOffset)), + ) + + if (this.props.onScroll) { + this.props.onScroll(e) + } + } + /** + * 所有scroll事件结束时触发 + * **/ + private onScrollAnimationEnd = (currentOffset: NativeScrollPoint) => { + const { x, y } = currentOffset + const { width, height } = this.state + // 🌟 fix: `onMomentumScrollEnd` & `onScrollAnimationEnd` not support for web & android 🌟 + const isScrollAnimationEnd = + !this.isScrolling && + (this.props.vertical ? y / height : x / width) % 1 === 0 + + if (isScrollAnimationEnd) { + this.updateIndex(currentOffset) + this.autoplay() + } } - onScrollForWeb = (e: any) => { - this.onScrollEnd(JSON.parse(JSON.stringify(e))) + private clearTimeout = () => { + if (this.autoplayTimer) { + clearTimeout(this.autoplayTimer) + this.autoplayTimer = undefined + } } - onLayout = ( - e: NativeSyntheticEvent, - ) => { + private onLayout = (e: LayoutChangeEvent) => { const { selectedIndex, infinite, vertical } = this.props const scrollIndex = (this.count > 1 && Math.min(selectedIndex as number, this.count - 1)) || 0 @@ -263,8 +246,8 @@ class Carousel extends React.PureComponent { offset, }, () => { - // web - this.scrollview?.current?.scrollTo({ ...offset, animated: false }) + // web & android + this.scrollview?.current?.scrollTo({ ...offset, animated: true }) this.autoplay() }, ) @@ -351,8 +334,8 @@ class Carousel extends React.PureComponent { } scrollNextPage = () => { - const { selectedIndex, isScrolling, width, height } = this.state - if (isScrolling || this.count < 2) { + const { selectedIndex, width, height } = this.state + if (this.isScrolling || this.count < 2) { return } const diff = selectedIndex + 1 + (this.props.infinite ? 1 : 0) @@ -361,22 +344,6 @@ class Carousel extends React.PureComponent { ? { x: 0, y: diff * height } : { x: diff * width, y: 0 }, ) - - this.setState( - { - isScrolling: true, - }, - () => { - if (Platform.OS !== 'ios') { - this.onScrollEnd({ - nativeEvent: { - // @ts-ignore - position: diff, - }, - }) - } - }, - ) } /** @@ -441,48 +408,37 @@ class Carousel extends React.PureComponent { ) } - private autoplay = () => { - this.setState({ isScrolling: false }, () => { - const { children, autoplay, autoplayInterval, infinite } = this.props - const { selectedIndex } = this.state - if (!Array.isArray(children) || !autoplay) { - return - } - clearTimeout(this.autoplayTimer) - this.autoplayTimer = setTimeout(() => { - if (!infinite && selectedIndex + 1 === this.count - 1) { - return - } - this.scrollNextPage() - }, autoplayInterval) - }) + private autoplay = (autoplay = this.props.autoplay) => { + const { children, autoplayInterval } = this.props + if (!Array.isArray(children) || !autoplay) { + return + } + this.clearTimeout() + this.autoplayTimer = setTimeout(() => { + this.scrollNextPage() + }, autoplayInterval) } private renderScroll = (pages: React.ReactNode) => { return ( + onScroll={this.onScroll} + onTouchStart={this.onTouchStartForWeb} + onTouchEnd={this.onTouchEndForWeb}> {pages} )