PhotoSlider.tsx 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. import React from 'react';
  2. import PhotoView from './PhotoView';
  3. import SlideWrap from './components/SlideWrap';
  4. import Backdrop from './components/Backdrop';
  5. import { Close, Counter, BannerWrap, BannerRight } from './components/BannerWrap';
  6. import FooterWrap from './components/FooterWrap';
  7. import isMobile from './utils/isMobile';
  8. import { dataType, IPhotoProviderBase } from './types';
  9. import { defaultOpacity, horizontalOffset, maxMoveOffset } from './variables';
  10. interface IPhotoSliderProps extends IPhotoProviderBase {
  11. // 图片列表
  12. images: dataType[];
  13. // 图片当前索引
  14. index?: number;
  15. // 可见
  16. visible: boolean;
  17. // 关闭事件
  18. onClose: (evt?: React.MouseEvent | React.TouchEvent) => void;
  19. // 索引改变回调
  20. onIndexChange?: Function;
  21. }
  22. type PhotoSliderState = {
  23. // 偏移量
  24. translateX: number;
  25. // 图片当前的 index
  26. photoIndex: number;
  27. // 图片处于触摸的状态
  28. touched: boolean;
  29. // Reach 开始时 x 坐标
  30. lastClientX: number | undefined;
  31. // Reach 开始时 y 坐标
  32. lastClientY: number | undefined;
  33. // 背景透明度
  34. backdropOpacity: number;
  35. // 缩放度
  36. photoScale: number;
  37. // 覆盖物可见度
  38. overlayVisible: boolean;
  39. };
  40. export default class PhotoSlider extends React.Component<
  41. IPhotoSliderProps,
  42. PhotoSliderState
  43. > {
  44. static displayName = 'PhotoSlider';
  45. static defaultProps = {
  46. maskClosable: true,
  47. bannerVisible: true,
  48. introVisible: true,
  49. };
  50. static getDerivedStateFromProps(nextProps, prevState) {
  51. if (
  52. nextProps.index !== undefined &&
  53. nextProps.index !== prevState.photoIndex
  54. ) {
  55. return {
  56. photoIndex: nextProps.index,
  57. translateX: -(window.innerWidth + horizontalOffset) * nextProps.index,
  58. };
  59. }
  60. return null;
  61. }
  62. constructor(props) {
  63. super(props);
  64. this.state = {
  65. translateX: 0,
  66. photoIndex: 0,
  67. touched: false,
  68. lastClientX: undefined,
  69. lastClientY: undefined,
  70. backdropOpacity: defaultOpacity,
  71. photoScale: 1,
  72. overlayVisible: true,
  73. };
  74. }
  75. componentDidMount() {
  76. const { index = 0 } = this.props;
  77. this.setState({
  78. translateX: index * -(window.innerWidth + horizontalOffset),
  79. photoIndex: index,
  80. });
  81. window.addEventListener('resize', this.handleResize);
  82. }
  83. componentWillUnmount() {
  84. window.removeEventListener('resize', this.handleResize);
  85. }
  86. handlePhotoTap = () => {
  87. this.setState(prevState => ({
  88. overlayVisible: !prevState.overlayVisible,
  89. }));
  90. }
  91. handlePhotoMaskTap = () => {
  92. const { maskClosable, onClose } = this.props;
  93. if (maskClosable) {
  94. onClose();
  95. this.setState({
  96. overlayVisible: true,
  97. });
  98. }
  99. }
  100. handleResize = () => {
  101. const { innerWidth } = window;
  102. this.setState(({ photoIndex }) => {
  103. return {
  104. translateX: -(innerWidth + horizontalOffset) * photoIndex,
  105. lastClientX: undefined,
  106. lastClientY: undefined,
  107. };
  108. });
  109. }
  110. handleReachVerticalMove = (clientX, clientY) => {
  111. this.setState(({ lastClientY, backdropOpacity }) => {
  112. if (lastClientY === undefined) {
  113. return {
  114. touched: true,
  115. lastClientY: clientY,
  116. backdropOpacity,
  117. photoScale: 1,
  118. };
  119. }
  120. const offsetClientY = Math.abs(clientY - lastClientY);
  121. return {
  122. touched: true,
  123. lastClientY,
  124. backdropOpacity: Math.max(
  125. Math.min(defaultOpacity, defaultOpacity - offsetClientY / 100 / 2),
  126. 0,
  127. ),
  128. photoScale: Math.max(Math.min(1, 1 - offsetClientY / 100 / 10), 0.6),
  129. };
  130. });
  131. }
  132. handleReachHorizontalMove = (clientX) => {
  133. const { innerWidth } = window;
  134. this.setState(({ lastClientX, translateX, photoIndex }) => {
  135. if (lastClientX === undefined) {
  136. return {
  137. touched: true,
  138. lastClientX: clientX,
  139. translateX,
  140. };
  141. }
  142. const offsetClientX = clientX - lastClientX;
  143. return {
  144. touched: true,
  145. lastClientX: lastClientX,
  146. translateX: -(innerWidth + horizontalOffset) * photoIndex + offsetClientX,
  147. };
  148. });
  149. }
  150. handleReachUp = (clientX: number, clientY: number) => {
  151. const { innerWidth, innerHeight } = window;
  152. const { images, onIndexChange, onClose } = this.props;
  153. const {
  154. lastClientX = clientX,
  155. lastClientY = clientY,
  156. photoIndex,
  157. overlayVisible,
  158. } = this.state;
  159. const offsetClientX = clientX - lastClientX;
  160. const offsetClientY = clientY - lastClientY;
  161. const singlePageWidth = innerWidth + horizontalOffset;
  162. // 当前偏移
  163. let currentTranslateX = -singlePageWidth * photoIndex;
  164. let currentPhotoIndex = photoIndex;
  165. let isChangeVisible = false;
  166. if (Math.abs(offsetClientY) > innerHeight * 0.14) {
  167. isChangeVisible = true;
  168. onClose();
  169. // 下一张
  170. } else if (offsetClientX < -maxMoveOffset && photoIndex < images.length - 1) {
  171. currentPhotoIndex = photoIndex + 1;
  172. currentTranslateX = -singlePageWidth * currentPhotoIndex;
  173. if (onIndexChange) {
  174. onIndexChange(currentPhotoIndex);
  175. }
  176. // 上一张
  177. } else if (offsetClientX > maxMoveOffset && photoIndex > 0) {
  178. currentPhotoIndex = photoIndex - 1;
  179. currentTranslateX = -singlePageWidth * currentPhotoIndex;
  180. if (onIndexChange) {
  181. onIndexChange(currentPhotoIndex);
  182. }
  183. }
  184. this.setState({
  185. touched: false,
  186. translateX: currentTranslateX,
  187. photoIndex: currentPhotoIndex,
  188. lastClientX: undefined,
  189. lastClientY: undefined,
  190. backdropOpacity: defaultOpacity,
  191. photoScale: 1,
  192. overlayVisible: isChangeVisible ? true : overlayVisible,
  193. });
  194. }
  195. render() {
  196. const {
  197. images,
  198. visible,
  199. overlay,
  200. className,
  201. maskClassName,
  202. viewClassName,
  203. imageClassName,
  204. onClose,
  205. bannerVisible,
  206. introVisible,
  207. loadingElement,
  208. brokenElement,
  209. } = this.props;
  210. const {
  211. translateX,
  212. touched,
  213. photoIndex,
  214. backdropOpacity,
  215. photoScale,
  216. overlayVisible,
  217. } = this.state;
  218. const imageLength = images.length;
  219. const transform = `translate3d(${translateX}px, 0px, 0)`;
  220. // Overlay
  221. const overlayIntro = imageLength ? images[photoIndex].intro : undefined;
  222. const overlayStyle = { opacity: +overlayVisible };
  223. if (visible) {
  224. const { innerWidth } = window;
  225. return (
  226. <SlideWrap className={className}>
  227. <Backdrop
  228. className={maskClassName}
  229. style={{ background: `rgba(0, 0, 0, ${backdropOpacity})` }}
  230. />
  231. {bannerVisible ? (
  232. <BannerWrap style={overlayStyle}>
  233. <Counter>{photoIndex + 1} / {imageLength}</Counter>
  234. <BannerRight>
  235. <Close
  236. onTouchEnd={isMobile ? onClose : undefined}
  237. onClick={isMobile ? undefined : onClose}
  238. />
  239. </BannerRight>
  240. </BannerWrap>
  241. ) : undefined}
  242. {images
  243. .slice( // 加载相邻三张
  244. Math.max(photoIndex - 1, 0),
  245. Math.min(photoIndex + 2, imageLength + 1)
  246. )
  247. .map((item: dataType, index) => {
  248. // 截取之前的索引位置
  249. const realIndex = photoIndex === 0
  250. ? photoIndex + index
  251. : photoIndex - 1 + index;
  252. return (
  253. <PhotoView
  254. key={item.key || realIndex}
  255. src={item.src}
  256. onReachTopMove={this.handleReachVerticalMove}
  257. onReachBottomMove={this.handleReachVerticalMove}
  258. onReachRightMove={
  259. realIndex < imageLength - 1
  260. ? this.handleReachHorizontalMove
  261. : undefined
  262. }
  263. onReachLeftMove={
  264. realIndex > 0 ? this.handleReachHorizontalMove : undefined
  265. }
  266. onReachUp={this.handleReachUp}
  267. onPhotoTap={this.handlePhotoTap}
  268. onMaskTap={this.handlePhotoMaskTap}
  269. photoScale={photoIndex === realIndex ? photoScale : 1}
  270. wrapClassName={viewClassName}
  271. className={imageClassName}
  272. style={{
  273. left: `${(innerWidth + horizontalOffset) * realIndex}px`,
  274. WebkitTransform: transform,
  275. transform,
  276. transition: touched
  277. ? undefined
  278. : 'transform 0.6s cubic-bezier(0.25, 0.8, 0.25, 1)',
  279. }}
  280. loadingElement={loadingElement}
  281. brokenElement={brokenElement}
  282. />
  283. );
  284. })}
  285. {introVisible && overlayIntro ? (
  286. <FooterWrap style={overlayStyle}>{overlayIntro}</FooterWrap>
  287. ) : undefined}
  288. {overlay}
  289. </SlideWrap>
  290. );
  291. }
  292. return null;
  293. }
  294. }