PhotoSlider.tsx 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. import React from 'react';
  2. import PhotoView from './PhotoView';
  3. import SlideWrap from './components/SlideWrap';
  4. import Backdrop from './components/Backdrop';
  5. import { dataType } from './types';
  6. import { maxMoveOffset, defaultOpacity, horizontalOffset } from './variables';
  7. interface IPhotoSliderProps {
  8. // 图片列表
  9. images: (string | dataType)[];
  10. // 图片当前索引
  11. index?: number;
  12. // 可见
  13. visible: boolean;
  14. // 关闭事件
  15. onClose: Function;
  16. // 索引改变回调
  17. onIndexChange?: Function;
  18. // 自定义容器
  19. overlay?: React.ReactNode;
  20. // className
  21. className?: string;
  22. // 遮罩 className
  23. maskClassName?: string;
  24. // 图片容器 className
  25. viewClassName?: string;
  26. // 图片 className
  27. imageClassName?: string;
  28. // 自定义 loading
  29. loadingElement?: JSX.Element;
  30. // 加载失败 Element
  31. brokenElement?: JSX.Element;
  32. }
  33. type PhotoSliderState = {
  34. // 偏移量
  35. translateX: number;
  36. // 图片当前的 index
  37. photoIndex: number;
  38. // 图片处于触摸的状态
  39. touched: boolean;
  40. // Reach 开始时 x 坐标
  41. lastClientX: number | undefined;
  42. // Reach 开始时 y 坐标
  43. lastClientY: number | undefined;
  44. // 背景透明度
  45. backdropOpacity: number;
  46. // 缩放度
  47. photoScale: number;
  48. };
  49. export default class PhotoSlider extends React.Component<
  50. IPhotoSliderProps,
  51. PhotoSliderState
  52. > {
  53. static displayName = 'PhotoSlider';
  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. };
  77. }
  78. componentDidMount() {
  79. const { index = 0 } = this.props;
  80. this.setState({
  81. translateX: index * -(window.innerWidth + horizontalOffset),
  82. photoIndex: index,
  83. });
  84. window.addEventListener('resize', this.handleResize);
  85. }
  86. componentWillUnmount() {
  87. window.removeEventListener('resize', this.handleResize);
  88. }
  89. handleResize = () => {
  90. const { innerWidth } = window;
  91. this.setState(({ photoIndex }) => {
  92. return {
  93. translateX: -(innerWidth + horizontalOffset) * photoIndex,
  94. lastClientX: undefined,
  95. lastClientY: undefined,
  96. };
  97. });
  98. }
  99. handleReachVerticalMove = (clientX, clientY) => {
  100. this.setState(({ lastClientY, backdropOpacity }) => {
  101. if (lastClientY === undefined) {
  102. return {
  103. touched: true,
  104. lastClientY: clientY,
  105. backdropOpacity,
  106. photoScale: 1,
  107. };
  108. }
  109. const offsetClientY = Math.abs(clientY - lastClientY);
  110. return {
  111. touched: true,
  112. lastClientY,
  113. backdropOpacity: Math.max(
  114. Math.min(defaultOpacity, defaultOpacity - offsetClientY / 100 / 2),
  115. 0,
  116. ),
  117. photoScale: Math.max(Math.min(1, 1 - offsetClientY / 100 / 10), 0.6),
  118. };
  119. });
  120. }
  121. handleReachHorizontalMove = (clientX) => {
  122. const { innerWidth } = window;
  123. this.setState(({ lastClientX, translateX, photoIndex }) => {
  124. if (lastClientX === undefined) {
  125. return {
  126. touched: true,
  127. lastClientX: clientX,
  128. translateX,
  129. };
  130. }
  131. const offsetClientX = clientX - lastClientX;
  132. return {
  133. touched: true,
  134. lastClientX: lastClientX,
  135. translateX: -(innerWidth + horizontalOffset) * photoIndex + offsetClientX,
  136. };
  137. });
  138. }
  139. handleReachUp = (clientX, clientY) => {
  140. const { innerWidth, innerHeight } = window;
  141. const { images, onIndexChange, onClose } = this.props;
  142. const { lastClientX = clientX, lastClientY = clientY, photoIndex } = this.state;
  143. const offsetClientX = clientX - lastClientX;
  144. const offsetClientY = clientY - lastClientY;
  145. const singlePageWidth = innerWidth + horizontalOffset;
  146. // 当前偏移
  147. let currentTranslateX = -singlePageWidth * photoIndex;
  148. let currentPhotoIndex = photoIndex;
  149. if (Math.abs(offsetClientY) > innerHeight * 0.14) {
  150. onClose();
  151. // 下一张
  152. } else if (offsetClientX < -maxMoveOffset && photoIndex < images.length - 1) {
  153. currentPhotoIndex = photoIndex + 1;
  154. currentTranslateX = -singlePageWidth * currentPhotoIndex;
  155. if (onIndexChange) {
  156. onIndexChange(currentPhotoIndex);
  157. }
  158. // 上一张
  159. } else if (offsetClientX > maxMoveOffset && photoIndex > 0) {
  160. currentPhotoIndex = photoIndex - 1;
  161. currentTranslateX = -singlePageWidth * currentPhotoIndex;
  162. if (onIndexChange) {
  163. onIndexChange(currentPhotoIndex);
  164. }
  165. }
  166. this.setState({
  167. touched: false,
  168. translateX: currentTranslateX,
  169. photoIndex: currentPhotoIndex,
  170. lastClientX: undefined,
  171. lastClientY: undefined,
  172. backdropOpacity: defaultOpacity,
  173. photoScale: 1,
  174. });
  175. }
  176. render() {
  177. const {
  178. images,
  179. visible,
  180. overlay,
  181. className,
  182. maskClassName,
  183. viewClassName,
  184. imageClassName,
  185. loadingElement,
  186. brokenElement,
  187. } = this.props;
  188. const {
  189. translateX,
  190. touched,
  191. photoIndex,
  192. backdropOpacity,
  193. photoScale,
  194. } = this.state;
  195. const imageLength = images.length;
  196. const transform = `translate3d(${translateX}px, 0px, 0)`;
  197. if (visible) {
  198. const { innerWidth } = window;
  199. return (
  200. <SlideWrap className={className}>
  201. <Backdrop
  202. className={maskClassName}
  203. style={{ background: `rgba(0, 0, 0, ${backdropOpacity})` }}
  204. />
  205. {images
  206. .slice( // 加载相邻三张
  207. Math.max(photoIndex - 1, 0),
  208. Math.min(photoIndex + 2, imageLength + 1)
  209. )
  210. .map((item: string | dataType, index) => {
  211. const isStrItem = typeof item === 'string';
  212. // 截取之前的索引位置
  213. const realIndex = photoIndex === 0
  214. ? photoIndex + index
  215. : photoIndex - 1 + index;
  216. return (
  217. <PhotoView
  218. key={
  219. isStrItem
  220. ? (item as string) + realIndex
  221. : (item as dataType).dataKey
  222. }
  223. src={isStrItem ? (item as string) : (item as dataType).src}
  224. onReachTopMove={this.handleReachVerticalMove}
  225. onReachBottomMove={this.handleReachVerticalMove}
  226. onReachRightMove={
  227. realIndex < imageLength - 1
  228. ? this.handleReachHorizontalMove
  229. : undefined
  230. }
  231. onReachLeftMove={
  232. realIndex > 0 ? this.handleReachHorizontalMove : undefined
  233. }
  234. onReachUp={this.handleReachUp}
  235. photoScale={photoIndex === realIndex ? photoScale : 1}
  236. wrapClassName={viewClassName}
  237. className={imageClassName}
  238. style={{
  239. left: `${(innerWidth + horizontalOffset) * realIndex}px`,
  240. WebkitTransform: transform,
  241. transform,
  242. transition: touched
  243. ? undefined
  244. : 'transform 0.6s cubic-bezier(0.25, 0.8, 0.25, 1)',
  245. }}
  246. loadingElement={loadingElement}
  247. brokenElement={brokenElement}
  248. />
  249. );
  250. })}
  251. {overlay}
  252. </SlideWrap>
  253. );
  254. }
  255. return null;
  256. }
  257. }