PhotoSlider.tsx 9.6 KB

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