@daeun2351/react-native-gallery-swiper
Version:
An easy and simple to use React Native component to render a high performant and easily customizable image gallery with common gestures like pan, pinch and double tap. Supporting both iOS and Android. Free and made possible along with costly maintenance
477 lines (440 loc) • 15.4 kB
JavaScript
import PageList from "@daeun2351/react-native-page-list";
import PropTypes from "prop-types";
import React, { PureComponent } from "react";
import { Dimensions, View, ViewPropTypes } from "react-native";
import { createResponder } from "react-native-easy-guesture-responder";
import ImageTransformer from "@daeun2351/react-native-image-transformer";
const DEFAULT_FLAT_LIST_PROPS = {
windowSize: 3,
};
const height = Dimensions.get("screen").height;
const width = Dimensions.get("screen").width;
export default class GallerySwiper extends PureComponent {
static propTypes = {
...View.propTypes,
images: PropTypes.arrayOf(PropTypes.object).isRequired,
initialPage: PropTypes.number,
resizeMode: PropTypes.string,
initialNumToRender: PropTypes.number,
scrollViewStyle: ViewPropTypes ? ViewPropTypes.style : View.propTypes.style,
pageMargin: PropTypes.number,
sensitiveScroll: PropTypes.bool,
backgroundColor: PropTypes.string,
renderOverlay: PropTypes.func,
onPageSelected: PropTypes.func,
onPageScrollStateChanged: PropTypes.func,
onPageScroll: PropTypes.func,
onPinchTransforming: PropTypes.func,
onPinchStartReached: PropTypes.func,
onPinchEndReached: PropTypes.func,
onDoubleTapStartReached: PropTypes.func,
onDoubleTapEndReached: PropTypes.func,
onDoubleTapConfirmed: PropTypes.func,
onSingleTapConfirmed: PropTypes.func,
onGalleryStateChanged: PropTypes.func,
onLongPress: PropTypes.func,
onViewTransformed: PropTypes.func,
onTransformGestureReleased: PropTypes.func,
onSwipeUpReleased: PropTypes.func,
onSwipeDownReleased: PropTypes.func,
onEndReached: PropTypes.func,
onEndReachedThreshold: PropTypes.number,
enableScale: PropTypes.bool,
maxScale: PropTypes.number,
enableTranslate: PropTypes.bool,
enableResistance: PropTypes.bool,
resistantStrHorizontal: PropTypes.oneOfType([
PropTypes.func,
PropTypes.number,
PropTypes.string,
]),
resistantStrVertical: PropTypes.oneOfType([
PropTypes.func,
PropTypes.number,
PropTypes.string,
]),
maxOverScrollDistance: PropTypes.number,
removeClippedSubviews: PropTypes.bool,
imageComponent: PropTypes.func,
errorComponent: PropTypes.func,
flatListProps: PropTypes.object,
refPage: PropTypes.func,
pageTransitionThreshold: PropTypes.number,
estimatedItemSize: PropTypes.number,
drawDistance: PropTypes.number,
enableFlashList: PropTypes.bool,
};
static defaultProps = {
style: {
flex: 1,
backgroundColor: "#000",
},
initialNumToRender: 7,
imageComponent: undefined,
scrollViewStyle: {},
flatListProps: DEFAULT_FLAT_LIST_PROPS,
onEndReachedThreshold: 0.5,
pageTransitionThreshold: 1 / 3,
drawDistance: width * 7,
enableFlashList: false,
};
imageRefs = new Map();
activeResponder = undefined;
firstMove = true;
currentPage = 0;
pageCount = 0;
gestureResponder = undefined;
constructor(props) {
super(props);
this.renderPage = this.renderPage.bind(this);
this.onPageSelected = this.onPageSelected.bind(this);
this.onPageScrollStateChanged = this.onPageScrollStateChanged.bind(this);
this.getViewPagerInstance = this.getViewPagerInstance.bind(this);
this.getCurrentImageTransformer =
this.getCurrentImageTransformer.bind(this);
this.getImageTransformer = this.getImageTransformer.bind(this);
this.getViewPagerInstance = this.getViewPagerInstance.bind(this);
this.activeImageResponder = this.activeImageResponder.bind(this);
let onResponderReleaseOrTerminate = (evt, gestureState) => {
if (this.activeResponder) {
if (
this.activeResponder === this.viewPagerResponder &&
!this.shouldScrollViewPager(evt, gestureState) &&
Math.abs(gestureState.vx) > 0.5
) {
this.activeResponder.onEnd(evt, gestureState, true);
this.getViewPagerInstance().flingToPage(
this.currentPage,
gestureState.vx
);
} else {
this.activeResponder.onEnd(evt, gestureState);
}
this.activeResponder = null;
}
this.firstMove = true;
this.props.onGalleryStateChanged &&
this.props.onGalleryStateChanged(true);
};
this.gestureResponder = createResponder({
onStartShouldSetResponderCapture: (evt, gestureState) => true,
onStartShouldSetResponder: (evt, gestureState) => true,
onResponderGrant: this.activeImageResponder,
onResponderMove: (evt, gestureState) => {
if (this.firstMove) {
this.firstMove = false;
if (this.shouldScrollViewPager(evt, gestureState)) {
this.activeViewPagerResponder(evt, gestureState);
}
this.props.onGalleryStateChanged &&
this.props.onGalleryStateChanged(false);
}
if (this.activeResponder === this.viewPagerResponder) {
const dx = gestureState.moveX - gestureState.previousMoveX;
const offset =
this.getViewPagerInstance().getScrollOffsetFromCurrentPage();
if (
dx > 0 &&
offset > 0 &&
!this.shouldScrollViewPager(evt, gestureState)
) {
if (dx > offset) {
// active image responder
this.getViewPagerInstance().scrollByOffset(offset);
gestureState.moveX -= offset;
this.activeImageResponder(evt, gestureState);
}
} else if (
dx < 0 &&
offset < 0 &&
!this.shouldScrollViewPager(evt, gestureState)
) {
if (dx < offset) {
// active image responder
this.getViewPagerInstance().scrollByOffset(offset);
gestureState.moveX -= offset;
this.activeImageResponder(evt, gestureState);
}
}
}
this.activeResponder.onMove(evt, gestureState);
},
onResponderRelease: onResponderReleaseOrTerminate,
onResponderTerminate: onResponderReleaseOrTerminate,
// Do not allow parent view to intercept gesture
onResponderTerminationRequest: (evt, gestureState) => false,
onResponderDoubleTapConfirmed: (evt, gestureState) => {
this.props.onDoubleTapConfirmed &&
this.props.onDoubleTapConfirmed(this.currentPage);
},
onResponderSingleTapConfirmed: (evt, gestureState) => {
this.props.onSingleTapConfirmed &&
this.props.onSingleTapConfirmed(this.currentPage);
},
});
this.viewPagerResponder = {
onStart: (evt, gestureState) => {
this.getViewPagerInstance().onResponderGrant(evt, gestureState);
},
onMove: (evt, gestureState) => {
this.getViewPagerInstance().onResponderMove(evt, gestureState);
},
onEnd: (evt, gestureState, disableSettle) => {
this.getViewPagerInstance().onResponderRelease(
evt,
gestureState,
disableSettle
);
},
};
this.imageResponder = {
onStart: (evt, gestureState) => {
const currentImageTransformer = this.getCurrentImageTransformer();
currentImageTransformer &&
currentImageTransformer.onResponderGrant(evt, gestureState);
if (this.props.onLongPress) {
this._longPressTimeout = setTimeout(() => {
this.props.onLongPress(gestureState, this.currentPage);
}, 600);
}
},
onMove: (evt, gestureState) => {
const currentImageTransformer = this.getCurrentImageTransformer();
if (currentImageTransformer) {
this._galleryViewPager &&
this._galleryViewPager.setFade(
1 -
(Math.abs(currentImageTransformer.state.translateY) / height) *
1
);
currentImageTransformer &&
currentImageTransformer.onResponderMove(evt, gestureState);
}
clearTimeout(this._longPressTimeout);
},
onEnd: (evt, gestureState) => {
this._galleryViewPager && this._galleryViewPager.resetFade();
const currentImageTransformer = this.getCurrentImageTransformer();
currentImageTransformer &&
currentImageTransformer.onResponderRelease(evt, gestureState);
clearTimeout(this._longPressTimeout);
},
};
}
componentDidMount() {
this._isMounted = true;
}
componentWillUnmount() {
this._isMounted = false;
}
shouldScrollViewPager(evt, gestureState) {
if (gestureState.numberActiveTouches > 1) {
return false;
}
const viewTransformer = this.getCurrentImageTransformer();
if (!viewTransformer) {
return false;
}
const space = viewTransformer.getAvailableTranslateSpace();
const dx = gestureState.moveX - gestureState.previousMoveX;
if (dx > 0 && space.left <= 0 && this.currentPage > 0) {
return true;
}
if (dx < 0 && space.right <= 0 && this.currentPage < this.pageCount - 1) {
return true;
}
return false;
}
activeImageResponder(evt, gestureState) {
if (this.activeResponder !== this.imageResponder) {
if (this.activeResponder === this.viewPagerResponder) {
// pass true to disable ViewPager settle
this.viewPagerResponder.onEnd(evt, gestureState, true);
}
this.activeResponder = this.imageResponder;
this.imageResponder.onStart(evt, gestureState);
}
}
activeViewPagerResponder(evt, gestureState) {
if (this.activeResponder !== this.viewPagerResponder) {
if (this.activeResponder === this.imageResponder) {
this.imageResponder.onEnd(evt, gestureState);
}
this.activeResponder = this.viewPagerResponder;
this.viewPagerResponder.onStart(evt, gestureState);
}
}
getImageTransformer(page) {
if (page >= 0 && page < this.pageCount) {
let ref = this.imageRefs.get(page);
if (ref) {
return ref.getViewTransformerInstance();
}
}
}
getCurrentImageTransformer() {
return this.getImageTransformer(this.currentPage);
}
getViewPagerInstance() {
return this._galleryViewPager;
}
onPageSelected(page) {
this.currentPage = page;
this.props.onPageSelected && this.props.onPageSelected(page);
if (
this.props.onEndReached &&
page + 1 > this.props.onEndReachedThreshold * this.props.images.length
) {
this.props.onEndReached && this.props.onEndReached();
}
}
onPageScrollStateChanged(state) {
if (state === "idle") {
this.resetHistoryImageTransform();
}
this.props.onPageScrollStateChanged &&
this.props.onPageScrollStateChanged(state);
}
renderPage(pageData, pageId) {
const {
onViewTransformed,
onPinchTransforming,
onPinchStartReached,
onPinchEndReached,
onTransformGestureReleased,
onSwipeUpReleased,
onSwipeDownReleased,
onDoubleTapStartReached,
onDoubleTapEndReached,
resizeMode,
enableResistance,
enableScale,
maxScale,
enableTranslate,
resistantStrHorizontal,
resistantStrVertical,
maxOverScrollDistance,
errorComponent,
imageComponent,
} = this.props;
return (
<ImageTransformer
onViewTransformed={(transform) => {
onViewTransformed && onViewTransformed(transform, pageId);
}}
onPinchTransforming={(transform) => {
onPinchTransforming && onPinchTransforming(transform, pageId);
}}
onPinchStartReached={(transform) => {
onPinchStartReached && onPinchStartReached(transform, pageId);
}}
onPinchEndReached={(transform) => {
onPinchEndReached && onPinchEndReached(transform, pageId);
}}
onTransformGestureReleased={(transform) => {
// need the "return" here because the
// return value is checked in ViewTransformer
return (
onTransformGestureReleased &&
onTransformGestureReleased(transform, pageId)
);
}}
onSwipeUpReleased={(transform) => {
onSwipeUpReleased && onSwipeUpReleased(transform, pageId);
}}
onSwipeDownReleased={(transform) => {
onSwipeDownReleased && onSwipeDownReleased(transform, pageId);
}}
onDoubleTapStartReached={(transform) => {
onDoubleTapStartReached && onDoubleTapStartReached(transform, pageId);
}}
onDoubleTapEndReached={(transform) => {
onDoubleTapEndReached && onDoubleTapEndReached(transform, pageId);
}}
ref={(ref) => {
this.imageRefs.set(pageId, ref);
}}
key={"innerImage#" + pageId}
errorComponent={errorComponent}
imageComponent={imageComponent}
image={pageData}
index={pageId}
enableScale={enableScale}
maxScale={maxScale}
enableTranslate={enableTranslate}
enableResistance={enableResistance}
resistantStrHorizontal={resistantStrHorizontal}
resistantStrVertical={resistantStrVertical}
maxOverScrollDistance={maxOverScrollDistance}
resizeMode={resizeMode}
/>
);
}
resetHistoryImageTransform() {
let transformer = this.getImageTransformer(this.currentPage + 1);
if (transformer) {
transformer.forceUpdateTransform({
scale: 1,
translateX: 0,
translateY: 0,
});
}
transformer = this.getImageTransformer(this.currentPage - 1);
if (transformer) {
transformer.forceUpdateTransform({
scale: 1,
translateX: 0,
translateY: 0,
});
}
}
flingToPage({ index, velocityX }) {
this._galleryViewPager &&
this._galleryViewPager.flingToPage(index, velocityX);
}
scrollToPage({ index, immediate }) {
this._galleryViewPager &&
this._galleryViewPager.scrollToPage(index, immediate);
}
render() {
let gestureResponder = this.gestureResponder;
let images = this.props.images;
if (!images) {
images = [];
}
this.pageCount = images.length;
if (this.pageCount <= 0) {
gestureResponder = {};
}
const flatListProps = {
...DEFAULT_FLAT_LIST_PROPS,
...this.props.flatListProps,
};
return (
<PageList
{...this.props}
flatListProps={flatListProps}
ref={(component) => {
this._galleryViewPager = component;
this.props.refPage && this.props.refPage(component);
}}
scrollViewStyle={this.props.scrollViewStyle}
scrollEnabled={false}
renderPage={this.renderPage}
pageDataArray={images}
{...gestureResponder}
sensitiveScroll={this.props.sensitiveScroll}
renderOverlay={this.props.renderOverlay}
onPageSelected={this.onPageSelected}
onPageScrollStateChanged={this.onPageScrollStateChanged}
onPageScroll={this.props.onPageScroll}
removeClippedSubviews={this.props.removeClippedSubviews}
backgroundColor={this.props.backgroundColor}
pageTransitionThreshold={this.props.pageTransitionThreshold}
estimatedItemSize={this.props.estimatedItemSize}
drawDistance={this.props.drawDistance}
enableFlashList={this.props.enableFlashList}
/>
);
}
}