From 64b3b031b4e2194d5e71de3fffc410678de54621 Mon Sep 17 00:00:00 2001 From: Liangyu Date: Tue, 5 Sep 2017 20:38:06 +0800 Subject: [PATCH] Refactor AlbumView, fix a bug in TransformView, #52 --- components/AlbumView/AlbumSheet.js | 298 ++++++++++++ components/AlbumView/AlbumView.deprecated.js | 468 +++++++++++++++++++ components/AlbumView/AlbumView.js | 403 +++------------- components/TabView/TabButton.js | 2 + components/TransformView/TransformView.js | 39 +- docs/cn/AlbumView.md | 3 +- example/views/AlbumViewExample.js | 37 +- package.json | 2 +- 8 files changed, 894 insertions(+), 358 deletions(-) create mode 100644 components/AlbumView/AlbumSheet.js create mode 100644 components/AlbumView/AlbumView.deprecated.js diff --git a/components/AlbumView/AlbumSheet.js b/components/AlbumView/AlbumSheet.js new file mode 100644 index 0000000..c97359e --- /dev/null +++ b/components/AlbumView/AlbumSheet.js @@ -0,0 +1,298 @@ +// AlbumSheet.js + +'use strict'; + +import React, {Component} from "react"; +import PropTypes from 'prop-types'; +import {StyleSheet, View, Image, Animated} from 'react-native'; +import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource'; + +import Theme from 'teaset/themes/Theme'; +import TransformView from '../TransformView/TransformView'; + +export default class AlbumSheet extends TransformView { + + static propTypes = { + ...TransformView.propTypes, + image: PropTypes.oneOfType([Image.propTypes.source, PropTypes.element]).isRequired, + thumb: Image.propTypes.source, + defaultPosition: PropTypes.oneOf(['center', 'left', 'right']), + space: PropTypes.number, + load: PropTypes.bool, + onWillLoadImage: PropTypes.func, + onLoadImageSuccess: PropTypes.func, //(width, height) + onLoadImageFailure: PropTypes.func, //(error) + }; + + static defaultProps = { + ...TransformView.defaultProps, + maxScale: 3, + minScale: 1, + defaultPosition: 'center', + space: 20, + load: true, + }; + + constructor(props) { + super(props); + Object.assign(this.state, { + position: props.defaultPosition, + imageLoaded: false, + thumbLoaded: false, + actualWidth: 0, + actualHeight: 0, + fitWidth: 0, + fitHeight: 0, + viewWidth: 0, + viewHeight: 0, + }); + } + + componentDidMount() { + this.loadImage(this.props); + } + + componentWillReceiveProps(nextProps) { + if (nextProps.image != this.props.image || nextProps.load != this.props.load) { + this.loadImage(nextProps); + } + } + + loadImage(props) { + let {image, thumb, load, onWillLoadImage, onLoadImageSuccess, onLoadImageFailure} = props; + let {imageLoaded, thumbLoaded} = this.state; + + if (!load) return; + + if (React.isValidElement(image)) { + let {width, height} = this.getElementSize(props.image); + this.imageSizeChange(width, height); + this.setState({imageLoaded: true}); + } else { + if (thumb && !thumbLoaded) { + this.getImageSize(thumb, (width, height) => { + if (!this.state.imageLoaded) { + this.imageSizeChange(width, height); + } + this.setState({thumbLoaded: true}); + }); + } + if (image && !imageLoaded) { + onWillLoadImage && onWillLoadImage(); + this.getImageSize(image, (width, height) => { + this.imageSizeChange(width, height); + this.setState({imageLoaded: true}); + onLoadImageSuccess && onLoadImageSuccess(width, height); + }, error => { + onLoadImageFailure && onLoadImageFailure(error); + }); + } + } + } + + getImageSize(source, success, failure) { + if (typeof source === 'number') { + let {width, height} = resolveAssetSource(source); + success && success(width, height); + } else if (source && typeof source === 'object' && source.uri) { + //This func will doanload and cache image + Image.getSize(source.uri, + (width, height) => success && success(width, height), + (error) => failure && failure(error) + ); + } else { + failure && failure('source error'); + } + } + + getElementSize(element) { + let width = 0, htight = 0; + if (React.isValidElement(props.image)) { + let style = StyleSheet.flatten(props.image.props.style); + if (style.width === null || style.width === undefined + || style.height === null || style.height === undefined) { + console.error('You need to specify the width and height style when the image is a element'); + } else { + width = style.width; + height = style.height; + } + } + return {width, htight}; + } + + getFitSize(actualWidth, actualHeight, viewWidth, viewHeight) { + let fitWidth = 0, fitHeight = 0; + + if (actualWidth && actualHeight) { + fitWidth = viewWidth; + fitHeight = actualHeight * fitWidth / actualWidth; + if (fitHeight > viewHeight) { + fitHeight = viewHeight; + fitWidth = actualWidth * fitHeight / actualHeight; + } + } else if (actualWidth) { + fitWidth = viewWidth; + } else if (actualHeight) { + fitHeight = viewHeight; + } + + return {fitWidth, fitHeight}; + } + + getScrollValue() { + let {space} = this.props; + let {fitWidth, viewWidth, scale} = this.state; + let scaleWidth = fitWidth * scale._value; //image scale width + let exceedWidth = scaleWidth > viewWidth ? scaleWidth - viewWidth : 0; + let leftX = -(viewWidth + space + exceedWidth / 2); //scroll to left position + let rightX = viewWidth + space + exceedWidth / 2; //scroll to right position + let centerLeftX = -exceedWidth / 2; //scroll from left to center position + let centerRightX = exceedWidth / 2; //scroll from right to center position + return { + leftX, + rightX, + centerLeftX, + centerRightX, + }; + } + + scrollTo(toPosition, animated = true, valueCallback = null) { + let {position, translateX, translateY} = this.state; + let {leftX, rightX, centerLeftX, centerRightX} = this.getScrollValue(); + + let valueX = 0; + if (toPosition === 'left') valueX = leftX; + else if (toPosition === 'right') valueX = rightX; + else { + if (position == 'left') valueX = centerLeftX; + else valueX = centerRightX; + } + + let valueY = translateY._value; + let {y, height} = this.contentLayout; + if (height > this.viewLayout.height) { + if (y > 0) { + valueY = translateY._value - y; + } + else if ((y + height) < this.viewLayout.height) { + valueY = translateY._value + (this.viewLayout.height - (y + height)); + } + } + + if (valueCallback) { + valueCallback(translateX, valueX); + valueCallback(translateY, valueY); + } else { + if (animated) { + Animated.parallel([ + Animated.spring(translateX, { + toValue: valueX, + friction: 9, + }), + Animated.spring(translateY, { + toValue: valueY, + friction: 9, + }), + ]).start(); + } else { + translateX.setValue(valueX); + translateY.setValue(valueY); + } + } + + this.setState({position: toPosition}); + } + + scrollX(x, animated = true) { + let {position, translateX} = this.state; + let {leftX, rightX} = this.getScrollValue(); + + let toValue = 0; + if (position === 'left') toValue = leftX; + else if (position === 'right') toValue = rightX; + toValue += x; + + if (animated) { + Animated.spring(translateX, { + toValue: toValue, + friction: 9, + }).start(); + } else { + translateX.setValue(toValue); + } + } + + restoreImage() { + let {space} = this.props; + let {position, viewWidth, translateX, translateY, scale} = this.state; + + scale.setValue(1); + translateY.setValue(0); + switch (position) { + case 'left': translateX.setValue(-(viewWidth + space)); break; + case 'right': translateX.setValue(viewWidth + space); break; + default: translateX.setValue(0); + } + } + + layoutChange(width, height) { + let {actualWidth, actualHeight, fitWidth, fitHeight, viewWidth, viewHeight} = this.state; + let needRestoreImage = false; + viewWidth = width; + viewHeight = height; + if (actualWidth && actualHeight) { + let fitSize = this.getFitSize(actualWidth, actualHeight, viewWidth, viewHeight); + fitWidth = fitSize.fitWidth; + fitHeight = fitSize.fitHeight; + needRestoreImage = true; + } + this.setState({actualWidth, actualHeight, fitWidth, fitHeight, viewWidth, viewHeight}, () => { + needRestoreImage && this.restoreImage(); + }); + } + + imageSizeChange(width, height) { + let {actualWidth, actualHeight, fitWidth, fitHeight, viewWidth, viewHeight} = this.state; + let needRestoreImage = false; + actualWidth = width; + actualHeight = height; + if (viewWidth && viewHeight) { + let fitSize = this.getFitSize(actualWidth, actualHeight, viewWidth, viewHeight); + fitWidth = fitSize.fitWidth; + fitHeight = fitSize.fitHeight; + needRestoreImage = true; + } + this.setState({actualWidth, actualHeight, fitWidth, fitHeight, viewWidth, viewHeight}, () => { + needRestoreImage && this.restoreImage(); + }); + } + + buildProps() { + let {style, image, thumb, load, children, onLayout, ...others} = this.props; + let {position, imageLoaded, thumbLoaded, fitWidth, fitHeight} = this.state; + + style = [{backgroundColor: 'rgba(0, 0, 0, 0)'}].concat(style); + + let childrenStyle = {width: fitWidth, height: fitHeight}; + if (React.isValidElement(image)) { + children = React.cloneElement(image, {style: childrenStyle}); + } else { + if (imageLoaded || !thumb) { + children = ; + } else { + children = ; + } + } + + let saveOnLayout = onLayout; + onLayout = e => { + let {width, height} = e.nativeEvent.layout; + this.layoutChange(width, height); + saveOnLayout && saveOnLayout(e); + }; + + this.props = {style, image, thumb, load, children, onLayout, ...others}; + super.buildProps(); + } + +} diff --git a/components/AlbumView/AlbumView.deprecated.js b/components/AlbumView/AlbumView.deprecated.js new file mode 100644 index 0000000..d6a3421 --- /dev/null +++ b/components/AlbumView/AlbumView.deprecated.js @@ -0,0 +1,468 @@ +// AlbumView.js + +'use strict'; + +import React, {Component} from "react"; +import PropTypes from 'prop-types'; +import {StyleSheet, View, Image, Animated} from 'react-native'; +import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource'; + +import Theme from 'teaset/themes/Theme'; +import TransformView from '../TransformView/TransformView'; +import CarouselControl from '../Carousel/CarouselControl'; + +export default class AlbumView extends Component { + + static propTypes = { + ...View.propTypes, + images: PropTypes.arrayOf(Image.propTypes.source).isRequired, + thumbs: PropTypes.arrayOf(Image.propTypes.source), + defaultIndex: PropTypes.number, + index: PropTypes.number, + maxScale: PropTypes.number, + space: PropTypes.number, + control: PropTypes.oneOfType([PropTypes.bool, PropTypes.element]), + onChange: PropTypes.func, //(index, oldIndex) + onPress: PropTypes.func, //(index, event) + onLongPress: PropTypes.func, //(index, event) + onWillLoadImage: PropTypes.func, //(index) + onLoadImageSuccess: PropTypes.func, //(index, width, height) + onLoadImageFailure: PropTypes.func, //(index, error) + }; + + static defaultProps = { + ...View.defaultProps, + defaultIndex: 0, + maxScale: 3, + space: 20, + control: false, + }; + + static Control = CarouselControl; + + constructor(props) { + super(props); + let index = props.index || props.index === 0 ? props.index : props.defaultIndex; + this.state = { + layout: {x: 0, y: 0, width: 0, height: 0}, + index: index, + imageInfos: this.initImageInfos(props.images), + leftIndex: index - 1, + rightIndex: index + 1, + leftTranslateX: new Animated.Value(0), + rightTranslateX: new Animated.Value(0), + directionFactor: 0, //-1 prev 1 next + }; + } + + componentDidMount() { + this.preloadImage(this.state.index); + } + + componentWillReceiveProps(nextProps) { + let {imageInfos, index} = this.state; + if ((nextProps.index || nextProps.index === 0) && nextProps.index != this.props.index) { + index = nextProps.index; + } + if (nextProps.images.length != this.props.images.length) { + imageInfos = this.initImageInfos(nextProps.images); + } + this.preloadImage(index); + this.setState({index, imageInfos}, () => this.checkLeftRight()); + } + + initImageInfos(images) { + let layouts = []; + for (let i = 0; i < images.length; ++i) { + layouts.push({ + loadStatus: 0, //0: none 1: thumb loaded 2: image loaded (-1: load failure) + width: 1, + height: 1, + translateY: 0, + scale: 1, + }); + } + return layouts; + } + + getImageSize(source, success, failure) { + if (typeof source === 'number') { + let {width, height} = resolveAssetSource(source); + success && success(width, height); + } else if (source && typeof source === 'object' && source.uri) { + Image.getSize(source.uri, + (width, height) => success && success(width, height), + (error) => failure && failure(error) + ); + } else { + failure && failure('source error'); + } + } + + getImageSizeInfo(index) { + let {layout} = this.state; + let imageInfo = this.state.imageInfos[index]; + + let initWidth = layout.width; + let initHeight = imageInfo.height * initWidth / imageInfo.width; + if (initHeight > layout.height) { + initHeight = layout.height; + initWidth = imageInfo.width * initHeight / imageInfo.height; + } + + return { + width: imageInfo.width, + height: imageInfo.height, + initWidth, + initHeight, + scaleWidth: initWidth * imageInfo.scale, + scaleHeight: initHeight * imageInfo.scale, + } + } + + loadImage(index) { + let {images, thumbs, onWillLoadImage, onLoadImageSuccess, onLoadImageFailure} = this.props; + let {imageInfos} = this.state; + let imageInfo = imageInfos[index]; + + if (index < 0 || index >= images.length) { + return; + } + + if (imageInfo.loadStatus === 0 && thumbs instanceof Array && thumbs.length > index) { + this.getImageSize(thumbs[index], (width, height) => { + if (imageInfo.loadStatus === 0) { + imageInfo.loadStatus = 1; + imageInfo.width = width; + imageInfo.height = height; + this.setState({imageInfos}); + } + }); + } + if (imageInfo.loadStatus !== 2) { + onWillLoadImage && onWillLoadImage(index); + this.getImageSize(images[index], (width, height) => { + imageInfo.loadStatus = 2; + imageInfo.width = width; + imageInfo.height = height; + this.setState({imageInfos}); + onLoadImageSuccess && onLoadImageSuccess(index, width, height); + }, error => { + onLoadImageFailure && onLoadImageFailure(index, error); + }); + } + } + + preloadImage(index) { + this.loadImage(index); + this.loadImage(index - 1); + this.loadImage(index + 1); + } + + checkStopScroll(noDelay) { + if (!this.scrollParam) return; + + let {onChange} = this.props; + let {newIndex, directionFactor, lrAnimated, lrValue, tvValue} = this.scrollParam; + this.scrollParam = null; + + lrAnimated.stopAnimation(); + this.refs.transformView.state.translateX.stopAnimation(); + lrAnimated.setValue(lrValue); + this.refs.transformView.state.translateX.setValue(tvValue); + + this.preloadImage(newIndex); + let {index} = this.state; + this.setState({index: newIndex, directionFactor}, () => { + onChange && onChange(newIndex, index); + noDelay ? this.checkLeftRight() : setTimeout(() => this.checkLeftRight(), 200); + }); + } + + scrollToImage(newIndex) { + let {images, space} = this.props; + let {index} = this.state; + let {width} = this.state.layout; + + if (newIndex < 0 || newIndex >= images.length || newIndex == index) { + return; + } + + let directionFactor = newIndex < index ? -1 : 1; + let lrAnimated = newIndex < index ? this.state.leftTranslateX : this.state.rightTranslateX; + let lrValue = -(width + space) * directionFactor; + let distance = lrValue - lrAnimated._value; + let tvValue = this.refs.transformView.state.translateX._value + distance; + + this.scrollParam = { + newIndex, + directionFactor, + lrAnimated, + lrValue, + tvValue, + }; + + Animated.parallel([ + Animated.spring(lrAnimated, { + toValue: lrValue, + friction: 9, + }), + Animated.spring(this.refs.transformView.state.translateX, { + toValue: tvValue, + friction: 9, + }), + ]).start(e => this.checkStopScroll(false)); + // let the animation stop faster + lrAnimated.addListener(e => { + if (Math.abs(e.value - lrValue) <= 1) { + lrAnimated.stopAnimation(); + this.refs.transformView.state.translateX.stopAnimation(); + lrAnimated.removeAllListeners(); + } + }); + + } + + // for CarouselControl, no scroll + scrollToPage(newIndex) { + let {images, onChange} = this.props; + let {index} = this.state; + + if (newIndex < 0 || newIndex >= images.length || newIndex == index) { + return; + } + + let directionFactor = newIndex < index ? -1 : 1; + this.preloadImage(newIndex); + this.setState({index: newIndex, directionFactor}, () => { + onChange && onChange(newIndex, index); + setTimeout(() => this.checkLeftRight(), 200); + }); + } + + checkLeftRight() { + let {index, imageInfos, leftIndex, rightIndex, leftTranslateX, rightTranslateX, directionFactor} = this.state; + + if (leftIndex != index - 1 || rightIndex != index + 1) { + let {width} = this.state.layout; + let {scaleWidth} = this.getImageSizeInfo(index); + let imageInfo = imageInfos[index]; + let tx = scaleWidth > width ? (scaleWidth - width) / 2 * directionFactor : 0; + this.refs.transformView.state.translateX.setValue(tx); + this.refs.transformView.state.translateY.setValue(imageInfo.translateY); + this.refs.transformView.state.scale.setValue(imageInfo.scale); + this.saveScale = imageInfo.scale; + + leftTranslateX.setValue(0); + rightTranslateX.setValue(0); + this.setState({leftIndex: index - 1, rightIndex: index + 1}); + } + } + + onTransforming(translateX, translateY, scale) { + let saveScale = this.saveScale; + this.saveScale = scale; + if (scale < 1 || (saveScale && scale < saveScale)) { + return; + } + + let {x, y, width, height} = this.refs.transformView.contentLayout; + let ltx = translateX, rtx = translateX; + if (width > this.state.layout.width) { + ltx = x; + rtx = x + (width - this.state.layout.width); + } + + this.state.leftTranslateX.setValue(ltx); + this.state.rightTranslateX.setValue(rtx); + } + + onDidTransform(translateX, translateY, scale) { + this.saveScale = scale; + let {index, imageInfos} = this.state; + imageInfos[index].translateY = translateY; + imageInfos[index].scale = scale; + this.setState({imageInfos}); + } + + onWillMagnetic(translateX, translateY, scale, newX, newY, newScale) { + let {images, space} = this.props; + let {index} = this.state; + + let {x, y, width, height} = this.refs.transformView.contentLayout; + let ltx = translateX, rtx = translateX; + if (width > this.state.layout.width) { + ltx = x; + rtx = x + (width - this.state.layout.width); + } + let triggerWidth = this.state.layout.width / 3; + + if (scale < 1) { + return true; + } else if ((ltx < triggerWidth && rtx > -triggerWidth) + || (ltx >= triggerWidth && index === 0) + || (rtx <= -triggerWidth && index === images.length - 1)) { + // scroll to current image + let distance = newX - translateX; + let newltx = ltx + distance; + let newrtx = rtx + distance; + Animated.parallel([ + Animated.spring(this.state.leftTranslateX, { + toValue: newltx, + friction: 9, + }), + Animated.spring(this.state.rightTranslateX, { + toValue: newrtx, + friction: 9, + }), + ]).start(); + return true; + } + + this.scrollToImage(ltx >= triggerWidth ? index - 1 : index + 1); + + return false; + } + + renderLeftImage() { + let {images, thumbs, space} = this.props; + let {leftIndex, imageInfos, leftTranslateX} = this.state; + if (leftIndex < 0 || leftIndex >= images.length) return null; + + let {loadStatus, translateY, scale} = imageInfos[leftIndex]; + let {width, height} = this.state.layout; + let {scaleWidth} = this.getImageSizeInfo(leftIndex); + let cy = height / 2; + let top = -cy * scale + translateY + cy; + let viewStyle = { + position: 'absolute', + left: -(width + space), + top: 0, + width, + height, + }; + let imageStyle = { + position: 'absolute', + top: top, + right: 0, + width: scaleWidth > width ? scaleWidth : width, + height: height * scale, + transform: [{translateX: leftTranslateX}], + }; + let imageSource; + switch (loadStatus) { + case 1: imageSource = thumbs[leftIndex]; break; + case 2: imageSource = images[leftIndex]; break; + default: imageSource = null; + } + + return ( + + + + ); + } + + renderRightImage() { + let {images, thumbs, space} = this.props; + let {rightIndex, imageInfos, rightTranslateX} = this.state; + if (rightIndex < 0 || rightIndex >= images.length) return null; + + let {loadStatus, translateY, scale} = imageInfos[rightIndex]; + let {width, height} = this.state.layout; + let {scaleWidth} = this.getImageSizeInfo(rightIndex); + let cy = height / 2; + let top = -cy * scale + translateY + cy; + let viewStyle = { + position: 'absolute', + left: width + space, + top: 0, + width, + height, + }; + let imageStyle = { + position: 'absolute', + top: top, + left: 0, + width: scaleWidth > width ? scaleWidth : width, + height: height * scale, + transform: [{translateX: rightTranslateX}], + }; + let imageSource; + switch (loadStatus) { + case 1: imageSource = thumbs[rightIndex]; break; + case 2: imageSource = images[rightIndex]; break; + default: imageSource = null; + } + + return ( + + + + ); + } + + renderImage() { + let {images, thumbs, space, maxScale, onPress, onLongPress} = this.props; + let {index, imageInfos} = this.state; + let {loadStatus, width, height} = imageInfos[index]; + + let {initWidth, initHeight} = this.getImageSizeInfo(index); + let imageSource; + switch (loadStatus) { + case 1: imageSource = thumbs[index]; break; + case 2: imageSource = images[index]; break; + default: imageSource = null; + } + return ( + this.checkStopScroll(true)} + onTransforming={(translateX, translateY, scale) => this.onTransforming(translateX, translateY, scale)} + onDidTransform={(translateX, translateY, scale) => this.onDidTransform(translateX, translateY, scale)} + onWillMagnetic={(translateX, translateY, scale, newX, newY, newScale) => this.onWillMagnetic(translateX, translateY, scale, newX, newY, newScale)} + onPress={e => onPress && onPress(index, e)} + onLongPress={e => onLongPress && onLongPress(index, e)} + ref='transformView' + > + this.checkLeftRight()} + key={index} + /> + + ); + } + + render() { + let {images, thumbs, defaultIndex, index, maxScale, space, control, children, onLayout, ...others} = this.props; + + if (React.isValidElement(control)) { + control = React.cloneElement(control, {index: this.state.index, total: images.length, carousel: this}); + } else if (control) { + control = + } + + return ( + { + this.setState({layout: e.nativeEvent.layout}); + onLayout && onLayout(e); + }} + {...others} + > + {this.renderImage()} + {this.renderLeftImage()} + {this.renderRightImage()} + {control} + + ); + } + +} diff --git a/components/AlbumView/AlbumView.js b/components/AlbumView/AlbumView.js index aea9761..753c429 100644 --- a/components/AlbumView/AlbumView.js +++ b/components/AlbumView/AlbumView.js @@ -8,20 +8,21 @@ import {StyleSheet, View, Image, Animated} from 'react-native'; import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource'; import Theme from 'teaset/themes/Theme'; -import TransformView from '../TransformView/TransformView'; +import AlbumSheet from './AlbumSheet'; import CarouselControl from '../Carousel/CarouselControl'; export default class AlbumView extends Component { static propTypes = { ...View.propTypes, - images: PropTypes.arrayOf(Image.propTypes.source).isRequired, + images: PropTypes.arrayOf(PropTypes.oneOfType([Image.propTypes.source, PropTypes.element])).isRequired, thumbs: PropTypes.arrayOf(Image.propTypes.source), defaultIndex: PropTypes.number, index: PropTypes.number, maxScale: PropTypes.number, space: PropTypes.number, control: PropTypes.oneOfType([PropTypes.bool, PropTypes.element]), + onWillChange: PropTypes.func, //(index, oldIndex) onChange: PropTypes.func, //(index, oldIndex) onPress: PropTypes.func, //(index, event) onLongPress: PropTypes.func, //(index, event) @@ -38,224 +39,68 @@ export default class AlbumView extends Component { control: false, }; + static Sheet = AlbumSheet; static Control = CarouselControl; constructor(props) { super(props); + this.animateActions = []; + this.layout = {x: 0, y: 0, width: 0, height: 0}; let index = props.index || props.index === 0 ? props.index : props.defaultIndex; this.state = { - layout: {x: 0, y: 0, width: 0, height: 0}, index: index, - imageInfos: this.initImageInfos(props.images), - leftIndex: index - 1, - rightIndex: index + 1, - leftTranslateX: new Animated.Value(0), - rightTranslateX: new Animated.Value(0), - directionFactor: 0, //-1 prev 1 next }; } - componentDidMount() { - this.preloadImage(this.state.index); - } - componentWillReceiveProps(nextProps) { - let {imageInfos, index} = this.state; if ((nextProps.index || nextProps.index === 0) && nextProps.index != this.props.index) { - index = nextProps.index; - } - if (nextProps.images.length != this.props.images.length) { - imageInfos = this.initImageInfos(nextProps.images); - } - this.preloadImage(index); - this.setState({index, imageInfos}, () => this.checkLeftRight()); - } - - initImageInfos(images) { - let layouts = []; - for (let i = 0; i < images.length; ++i) { - layouts.push({ - loadStatus: 0, //0: none 1: thumb loaded 2: image loaded (-1: load failure) - width: 1, - height: 1, - translateY: 0, - scale: 1, - }); - } - return layouts; - } - - getImageSize(source, success, failure) { - if (typeof source === 'number') { - let {width, height} = resolveAssetSource(source); - success && success(width, height); - } else if (source && typeof source === 'object' && source.uri) { - Image.getSize(source.uri, - (width, height) => success && success(width, height), - (error) => failure && failure(error) - ); - } else { - failure && failure('source error'); + this.changeIndex(nextProps.index); } } - getImageSizeInfo(index) { - let {layout} = this.state; - let imageInfo = this.state.imageInfos[index]; - - let initWidth = layout.width; - let initHeight = imageInfo.height * initWidth / imageInfo.width; - if (initHeight > layout.height) { - initHeight = layout.height; - initWidth = imageInfo.width * initHeight / imageInfo.height; - } - - return { - width: imageInfo.width, - height: imageInfo.height, - initWidth, - initHeight, - scaleWidth: initWidth * imageInfo.scale, - scaleHeight: initHeight * imageInfo.scale, - } + needLoad(index) { + return index >= this.state.index - 1 && index <= this.state.index + 1; } - loadImage(index) { - let {images, thumbs, onWillLoadImage, onLoadImageSuccess, onLoadImageFailure} = this.props; - let {imageInfos} = this.state; - let imageInfo = imageInfos[index]; - - if (index < 0 || index >= images.length) { - return; - } - - if (imageInfo.loadStatus === 0 && thumbs instanceof Array && thumbs.length > index) { - this.getImageSize(thumbs[index], (width, height) => { - if (imageInfo.loadStatus === 0) { - imageInfo.loadStatus = 1; - imageInfo.width = width; - imageInfo.height = height; - this.setState({imageInfos}); - } - }); - } - if (imageInfo.loadStatus !== 2) { - onWillLoadImage && onWillLoadImage(index); - this.getImageSize(images[index], (width, height) => { - imageInfo.loadStatus = 2; - imageInfo.width = width; - imageInfo.height = height; - this.setState({imageInfos}); - onLoadImageSuccess && onLoadImageSuccess(index, width, height); - }, error => { - onLoadImageFailure && onLoadImageFailure(index, error); - }); - } - } - - preloadImage(index) { - this.loadImage(index); - this.loadImage(index - 1); - this.loadImage(index + 1); - } - - checkStopScroll(noDelay) { - if (!this.scrollParam) return; + changeIndex(newIndex) { + let {index} = this.state; + if (newIndex == index) return; - let {onChange} = this.props; - let {newIndex, directionFactor, lrAnimated, lrValue, tvValue} = this.scrollParam; - this.scrollParam = null; + this.props.onWillChange && this.props.onWillChange(index, newIndex); + this.setState({index: newIndex}); - lrAnimated.stopAnimation(); - this.refs.transformView.state.translateX.stopAnimation(); - lrAnimated.setValue(lrValue); - this.refs.transformView.state.translateX.setValue(tvValue); + let sheet = this.refs['sheet' + index]; + let nextSheet = this.refs['sheet' + newIndex]; + let toPosition = newIndex > index ? 'left' : 'right'; - this.preloadImage(newIndex); - this.setState({index: newIndex, directionFactor}, () => { - onChange && onChange(newIndex, index); - noDelay ? this.checkLeftRight() : setTimeout(() => this.checkLeftRight(), 200); + this.animateActions = []; + sheet && sheet.scrollTo(toPosition, true, (variable, toValue) => { + this.animateActions.push({variable, toValue}) }); - } - - scrollToImage(newIndex) { - let {images, space} = this.props; - let {index} = this.state; - let {width} = this.state.layout; - - if (newIndex < 0 || newIndex >= images.length || newIndex == index) { - return; - } - - let directionFactor = newIndex < index ? -1 : 1; - let lrAnimated = newIndex < index ? this.state.leftTranslateX : this.state.rightTranslateX; - let lrValue = -(width + space) * directionFactor; - let distance = lrValue - lrAnimated._value; - let tvValue = this.refs.transformView.state.translateX._value + distance; - - this.scrollParam = { - newIndex, - directionFactor, - lrAnimated, - lrValue, - tvValue, - }; + nextSheet && nextSheet.scrollTo('center', true, (variable, toValue) => { + this.animateActions.push({variable, toValue}) + }); + if (this.animateActions.length === 0) return; - Animated.parallel([ - Animated.spring(lrAnimated, { - toValue: lrValue, - friction: 9, - }), - Animated.spring(this.refs.transformView.state.translateX, { - toValue: tvValue, - friction: 9, - }), - ]).start(e => this.checkStopScroll(false)); - // let the animation stop faster - lrAnimated.addListener(e => { - if (Math.abs(e.value - lrValue) <= 1) { - lrAnimated.stopAnimation(); - this.refs.transformView.state.translateX.stopAnimation(); - lrAnimated.removeAllListeners(); - } + Animated.parallel(this.animateActions.map((item, index) => + Animated.spring(item.variable, {toValue: item.toValue, friction: 9}) + )).start(e => { + this.props.onChange && this.props.onChange(newIndex, index); }); } - // for CarouselControl, no scroll - scrollToPage(newIndex) { - let {images, onChange} = this.props; - let {index} = this.state; - - if (newIndex < 0 || newIndex >= images.length || newIndex == index) { - return; - } - - let directionFactor = newIndex < index ? -1 : 1; - this.preloadImage(newIndex); - this.setState({index: newIndex, directionFactor}, () => { - onChange && onChange(newIndex, index); - setTimeout(() => this.checkLeftRight(), 200); + checkStopScroll() { + this.animateActions.map((item, index) => { + item.variable.stopAnimation(); + item.variable.setValue(item.toValue); }); + this.animateActions = []; } - checkLeftRight() { - let {index, imageInfos, leftIndex, rightIndex, leftTranslateX, rightTranslateX, directionFactor} = this.state; - - if (leftIndex != index - 1 || rightIndex != index + 1) { - let {width} = this.state.layout; - let {scaleWidth} = this.getImageSizeInfo(index); - let imageInfo = imageInfos[index]; - let tx = scaleWidth > width ? (scaleWidth - width) / 2 * directionFactor : 0; - this.refs.transformView.state.translateX.setValue(tx); - this.refs.transformView.state.translateY.setValue(imageInfo.translateY); - this.refs.transformView.state.scale.setValue(imageInfo.scale); - this.saveScale = imageInfo.scale; - - leftTranslateX.setValue(0); - rightTranslateX.setValue(0); - this.setState({leftIndex: index - 1, rightIndex: index + 1}); - } + // for CarouselControl + scrollToPage(newIndex) { + this.changeIndex(newIndex); } onTransforming(translateX, translateY, scale) { @@ -265,182 +110,82 @@ export default class AlbumView extends Component { return; } - let {x, y, width, height} = this.refs.transformView.contentLayout; + let {images} = this.props; + let {index} = this.state; + let {x, y, width, height} = this.refs['sheet' + index].contentLayout; let ltx = translateX, rtx = translateX; - if (width > this.state.layout.width) { + if (width > this.layout.width) { ltx = x; - rtx = x + (width - this.state.layout.width); + rtx = x + (width - this.layout.width); } - this.state.leftTranslateX.setValue(ltx); - this.state.rightTranslateX.setValue(rtx); - } - - onDidTransform(translateX, translateY, scale) { - this.saveScale = scale; - let {index, imageInfos} = this.state; - imageInfos[index].translateY = translateY; - imageInfos[index].scale = scale; - this.setState({imageInfos}); + index > 0 && this.refs['sheet' + (index - 1)].scrollX(ltx, false); + index < (images.length - 1) && this.refs['sheet' + (index + 1)].scrollX(rtx, false); } onWillMagnetic(translateX, translateY, scale, newX, newY, newScale) { - let {images, space} = this.props; + let {images} = this.props; let {index} = this.state; - let {x, y, width, height} = this.refs.transformView.contentLayout; + let {x, y, width, height} = this.refs['sheet' + index].contentLayout; let ltx = translateX, rtx = translateX; - if (width > this.state.layout.width) { + if (width > this.layout.width) { ltx = x; - rtx = x + (width - this.state.layout.width); + rtx = x + (width - this.layout.width); } - let triggerWidth = this.state.layout.width / 3; + let triggerWidth = this.layout.width / 3; if (scale < 1) { return true; } else if ((ltx < triggerWidth && rtx > -triggerWidth) || (ltx >= triggerWidth && index === 0) || (rtx <= -triggerWidth && index === images.length - 1)) { - // scroll to current image - let distance = newX - translateX; - let newltx = ltx + distance; - let newrtx = rtx + distance; - Animated.parallel([ - Animated.spring(this.state.leftTranslateX, { - toValue: newltx, - friction: 9, - }), - Animated.spring(this.state.rightTranslateX, { - toValue: newrtx, - friction: 9, - }), - ]).start(); + index > 0 && this.refs['sheet' + (index - 1)].scrollX(0, true); + index < (images.length - 1) && this.refs['sheet' + (index + 1)].scrollX(0, true); return true; } - this.scrollToImage(ltx >= triggerWidth ? index - 1 : index + 1); + this.changeIndex(ltx >= triggerWidth ? index - 1 : index + 1); return false; } - renderLeftImage() { - let {images, thumbs, space} = this.props; - let {leftIndex, imageInfos, leftTranslateX} = this.state; - if (leftIndex < 0 || leftIndex >= images.length) return null; - - let {loadStatus, translateY, scale} = imageInfos[leftIndex]; - let {width, height} = this.state.layout; - let {scaleWidth} = this.getImageSizeInfo(leftIndex); - let cy = height / 2; - let top = -cy * scale + translateY + cy; - let viewStyle = { - position: 'absolute', - left: -(width + space), - top: 0, - width, - height, - }; - let imageStyle = { - position: 'absolute', - top: top, - right: 0, - width: scaleWidth > width ? scaleWidth : width, - height: height * scale, - transform: [{translateX: leftTranslateX}], - }; - let imageSource; - switch (loadStatus) { - case 1: imageSource = thumbs[leftIndex]; break; - case 2: imageSource = images[leftIndex]; break; - default: imageSource = null; - } + renderImage(index) { + let {images, thumbs, maxScale, space, onPress, onLongPress, onWillLoadImage, onLoadImageSuccess, onLoadImageFailure} = this.props; - return ( - - - - ); - } - - renderRightImage() { - let {images, thumbs, space} = this.props; - let {rightIndex, imageInfos, rightTranslateX} = this.state; - if (rightIndex < 0 || rightIndex >= images.length) return null; - - let {loadStatus, translateY, scale} = imageInfos[rightIndex]; - let {width, height} = this.state.layout; - let {scaleWidth} = this.getImageSizeInfo(rightIndex); - let cy = height / 2; - let top = -cy * scale + translateY + cy; - let viewStyle = { - position: 'absolute', - left: width + space, - top: 0, - width, - height, - }; - let imageStyle = { - position: 'absolute', - top: top, - left: 0, - width: scaleWidth > width ? scaleWidth : width, - height: height * scale, - transform: [{translateX: rightTranslateX}], - }; - let imageSource; - switch (loadStatus) { - case 1: imageSource = thumbs[rightIndex]; break; - case 2: imageSource = images[rightIndex]; break; - default: imageSource = null; - } + let position = 'center'; + if (index < this.state.index) position = 'left'; + else if (index > this.state.index) position = 'right'; return ( - - - - ); - } - - renderImage() { - let {images, thumbs, space, maxScale, onPress, onLongPress} = this.props; - let {index, imageInfos} = this.state; - let {loadStatus, width, height} = imageInfos[index]; - - let {initWidth, initHeight} = this.getImageSizeInfo(index); - let imageSource; - switch (loadStatus) { - case 1: imageSource = thumbs[index]; break; - case 2: imageSource = images[index]; break; - default: imageSource = null; - } - return ( - this.checkStopScroll(true)} + image={images[index]} + thumb={thumbs instanceof Array && thumbs.length > index ? thumbs[index] : null} + defaultPosition={position} + space={space} + load={index >= this.state.index - 1 && index <= this.state.index + 1} + onWillTransform={() => this.checkStopScroll()} onTransforming={(translateX, translateY, scale) => this.onTransforming(translateX, translateY, scale)} - onDidTransform={(translateX, translateY, scale) => this.onDidTransform(translateX, translateY, scale)} onWillMagnetic={(translateX, translateY, scale, newX, newY, newScale) => this.onWillMagnetic(translateX, translateY, scale, newX, newY, newScale)} onPress={e => onPress && onPress(index, e)} onLongPress={e => onLongPress && onLongPress(index, e)} - ref='transformView' - > - this.checkLeftRight()} - key={index} - /> - + onWillLoadImage={() => onWillLoadImage && onWillLoadImage(index)} + onLoadImageSuccess={(width, height) => onLoadImageSuccess && onLoadImageSuccess(index, width, height)} + onLoadImageFailure={error => onLoadImageFailure && onLoadImageFailure(index, error)} + ref={'sheet' + index} + key={'sheet' + index} + /> ); } render() { - let {images, thumbs, defaultIndex, index, maxScale, space, control, children, onLayout, ...others} = this.props; + let {images, thumbs, defaultIndex, index, maxScale, space, control, children, onLayout, onWillChange, onChange, onPress, onLongPress, onWillLoadImage, onLoadImageSuccess, onLoadImageFailure, ...others} = this.props; if (React.isValidElement(control)) { control = React.cloneElement(control, {index: this.state.index, total: images.length, carousel: this}); @@ -451,14 +196,12 @@ export default class AlbumView extends Component { return ( { - this.setState({layout: e.nativeEvent.layout}); + this.layout = e.nativeEvent.layout; onLayout && onLayout(e); }} {...others} > - {this.renderImage()} - {this.renderLeftImage()} - {this.renderRightImage()} + {images.map((item, index) => this.renderImage(index))} {control} ); diff --git a/components/TabView/TabButton.js b/components/TabView/TabButton.js index 124ca87..ea9862e 100644 --- a/components/TabView/TabButton.js +++ b/components/TabView/TabButton.js @@ -42,11 +42,13 @@ export default class TabButton extends Component { let textStyle; if (active) { textStyle = [{ + backgroundColor: 'rgba(0, 0, 0, 0)', color: Theme.tvBarBtnActiveTitleColor, fontSize: Theme.tvBarBtnActiveTextFontSize, }].concat(titleStyle).concat(activeTitleStyle); } else { textStyle = [{ + backgroundColor: 'rgba(0, 0, 0, 0)', color: Theme.tvBarBtnTitleColor, fontSize: Theme.tvBarBtnTextFontSize, }].concat(titleStyle); diff --git a/components/TransformView/TransformView.js b/components/TransformView/TransformView.js index c93fcd5..adde835 100644 --- a/components/TransformView/TransformView.js +++ b/components/TransformView/TransformView.js @@ -105,8 +105,6 @@ export default class TransformView extends Component { } onPanResponderMove(e, gestureState) { - this.removeLongPressTimer(); - this.touchMoved = true; this.handleTouches(e.nativeEvent.touches, (dx, dy, scaleRate) => { let {tension, onTransforming} = this.props; let {translateX, translateY, scale} = this.state; @@ -120,8 +118,11 @@ export default class TransformView extends Component { } this.dxSum += dx; this.dySum += dy; + let adx = Math.abs(this.dxSum), ady = Math.abs(this.dySum), asr = Math.abs(scaleRate - 1); + if (!this.touchMoved && adx < 6 && ady < 6 && asr < 0.01) { + return; + } if (e.nativeEvent.touches.length == 1 && this.lockDirection === 'none') { - let adx = Math.abs(this.dxSum), ady = Math.abs(this.dySum); if (adx > ady && height <= this.viewLayout.height) { this.lockDirection = 'y'; } else if (adx < ady && width <= this.viewLayout.width) { @@ -144,6 +145,8 @@ export default class TransformView extends Component { scale.setValue(scale._value * scaleRate); } + this.removeLongPressTimer(); + this.touchMoved = true; onTransforming && onTransforming(translateX._value, translateY._value, scale._value); }); } @@ -208,20 +211,22 @@ export default class TransformView extends Component { scaleRate = maxScale / scale._value; } - let scalePointX = (prevTouches[1].locationX + prevTouches[0].locationX) / 2; - let scalePointY = (prevTouches[1].locationY + prevTouches[0].locationY) / 2; - let {x, y, width, height} = this.contentLayout; - //view center point position - let viewCenterX = x + width / 2; - let viewCenterY = y + height / 2; - //the scale point with the center of the view as the origin - let spBeforScaleX = scalePointX - viewCenterX; - let spBeforScaleY = scalePointY - viewCenterY; - let spAfterScaleX = spBeforScaleX * scaleRate; - let spAfterScaleY = spBeforScaleY * scaleRate; - //So that the scale point does not seem to move - dx += spBeforScaleX - spAfterScaleX; - dy += spBeforScaleY - spAfterScaleY; + // unused code + // let {x, y, width, height} = this.contentLayout; + // let scalePointX = (prevTouches[1].locationX + prevTouches[0].locationX) / 2 - x; + // let scalePointY = (prevTouches[1].locationY + prevTouches[0].locationY) / 2 - y; + // //view center point position + // let centerX = width / 2; + // let centerY = height / 2; + // //the scale point with the center of the view as the origin + // let spBeforScaleX = scalePointX - centerX; + // let spBeforScaleY = scalePointY - centerY; + // let spAfterScaleX = spBeforScaleX * scaleRate; + // let spAfterScaleY = spBeforScaleY * scaleRate; + // //So that the scale point does not seem to move + // dx += spBeforScaleX - spAfterScaleX; + // dy += spBeforScaleY - spAfterScaleY; + onHandleCompleted(dx, dy, scaleRate); } else { onHandleCompleted(dx, dy, 1); diff --git a/docs/cn/AlbumView.md b/docs/cn/AlbumView.md index db31c60..67d8e35 100644 --- a/docs/cn/AlbumView.md +++ b/docs/cn/AlbumView.md @@ -17,7 +17,8 @@ AlbumView 组件定义一个相册视图, 支持多图左右切换显示,支 | Event Name | Returns | Notes | |---|---|---| | [View events...](https://facebook.github.io/react-native/docs/view.html) | | AlbumView 组件继承 View 组件的全部事件。 -| onChange | index, oldIndex | 改变当前页面时调用, index 为改变后页面索引值, oldIndex 为改变前页面索引值。 +| onWillChange | index, newIndex | 改变当前页面前时调用, index 为当前页面索引值, newIndex 为将要改变的页面索引值。 +| onChange | index, oldIndex | 改变当前页面完成后调用, index 为改变后页面索引值, oldIndex 为改变前页面索引值。 | onPress | index, event | 单击事件, 触摸结束时调用。 | onLongPress | index, event | 长按事件, 按压组件超过 500ms 时调用。 | onWillLoadImage | index | 加载图片前调用。 diff --git a/example/views/AlbumViewExample.js b/example/views/AlbumViewExample.js index d2b044d..b9474c7 100644 --- a/example/views/AlbumViewExample.js +++ b/example/views/AlbumViewExample.js @@ -5,7 +5,7 @@ import React, {Component} from 'react'; import {View, Image, TouchableOpacity, StatusBar} from 'react-native'; -import {Theme, NavigationPage, AlbumView, Overlay} from 'teaset'; +import {Theme, NavigationPage, AlbumView, Overlay, Button} from 'teaset'; export default class AlbumViewExample extends NavigationPage { @@ -61,14 +61,16 @@ export default class AlbumViewExample extends NavigationPage { renderPage() { return ( - - {this.thumbs.map((item, index) => ( - - this.onImagePress(index)}> - - - - ))} + + + {this.thumbs.map((item, index) => ( + + this.onImagePress(index)}> + + + + ))} + ); } @@ -80,4 +82,21 @@ export default class AlbumViewExample extends NavigationPage { {uri: 'https://b-ssl.duitang.com/uploads/item/201207/23/20120723200118_acfUi.thumb.700_0.jpeg'}, {uri: 'http://img.warting.com/allimg/2017/0308/exsaicsvc5w-92.jpg'}, {uri: 'http://img.warting.com/allimg/2017/0308/o4ovnsq2uqj-96.jpg'}, + +import AlbumSheet from 'teaset/components/AlbumView/AlbumSheet'; + + + + + + +