PhotoSlider.tsx 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  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 } from './variables';
  7. export 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. }
  29. type PhotoSliderState = {
  30. // 偏移量
  31. translateX: number;
  32. // 图片当前的 index
  33. photoIndex: number;
  34. // 图片处于触摸的状态
  35. touched: boolean;
  36. // Reach 开始时 x 坐标
  37. lastPageX: number | undefined;
  38. // Reach 开始时 y 坐标
  39. lastPageY: number | undefined;
  40. // 背景透明度
  41. backdropOpacity: number;
  42. // 缩放度
  43. photoScale: number;
  44. };
  45. export default class PhotoSlider extends React.Component<
  46. IPhotoSliderProps,
  47. PhotoSliderState
  48. > {
  49. static displayName = 'PhotoSlider';
  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 * nextProps.index,
  58. };
  59. }
  60. return null;
  61. }
  62. constructor(props) {
  63. super(props);
  64. const { index = 0 } = props;
  65. this.state = {
  66. translateX: index * -window.innerWidth,
  67. photoIndex: index,
  68. touched: false,
  69. lastPageX: undefined,
  70. lastPageY: undefined,
  71. backdropOpacity: defaultOpacity,
  72. photoScale: 1,
  73. };
  74. }
  75. componentDidMount() {
  76. window.addEventListener('resize', this.handleResize);
  77. }
  78. componentWillUnmount() {
  79. window.removeEventListener('resize', this.handleResize);
  80. }
  81. handleResize = () => {
  82. const { innerWidth } = window;
  83. this.setState(({ photoIndex }) => {
  84. return {
  85. translateX: -innerWidth * photoIndex,
  86. lastPageX: undefined,
  87. lastPageY: undefined,
  88. };
  89. });
  90. }
  91. handleReachVerticalMove = (pageX, pageY) => {
  92. this.setState(({ lastPageY, backdropOpacity }) => {
  93. if (lastPageY === undefined) {
  94. return {
  95. touched: true,
  96. lastPageY: pageY,
  97. backdropOpacity,
  98. photoScale: 1,
  99. };
  100. }
  101. const offsetPageY = Math.abs(pageY - lastPageY);
  102. return {
  103. touched: true,
  104. lastPageY,
  105. backdropOpacity: Math.max(
  106. Math.min(defaultOpacity, defaultOpacity - offsetPageY / 100 / 2),
  107. defaultOpacity / 6,
  108. ),
  109. photoScale: Math.max(Math.min(1, 1 - offsetPageY / 100 / 10), 0.6),
  110. };
  111. });
  112. }
  113. handleReachHorizontalMove = (pageX) => {
  114. const { innerWidth } = window;
  115. this.setState(({ lastPageX, translateX, photoIndex }) => {
  116. if (lastPageX === undefined) {
  117. return {
  118. touched: true,
  119. lastPageX: pageX,
  120. translateX,
  121. };
  122. }
  123. const offsetPageX = pageX - lastPageX;
  124. return {
  125. touched: true,
  126. lastPageX: lastPageX,
  127. translateX: -innerWidth * photoIndex + offsetPageX,
  128. };
  129. });
  130. }
  131. handleReachUp = (pageX, pageY) => {
  132. const { innerWidth, innerHeight } = window;
  133. const { images, onIndexChange, onClose } = this.props;
  134. const { lastPageX = pageX, lastPageY = pageY, photoIndex } = this.state;
  135. const offsetPageX = pageX - lastPageX;
  136. const offsetPageY = pageY - lastPageY;
  137. if (Math.abs(offsetPageY) > innerHeight * 0.14) {
  138. onClose();
  139. return;
  140. }
  141. // 当前偏移
  142. let currentTranslateX = -innerWidth * photoIndex;
  143. let currentPhotoIndex = photoIndex;
  144. // 下一张
  145. if (offsetPageX < -maxMoveOffset && photoIndex < images.length - 1) {
  146. currentPhotoIndex = photoIndex + 1;
  147. currentTranslateX = -innerWidth * currentPhotoIndex;
  148. if (onIndexChange) {
  149. onIndexChange(currentPhotoIndex);
  150. }
  151. // 上一张
  152. } else if (offsetPageX > maxMoveOffset && photoIndex > 0) {
  153. currentPhotoIndex = photoIndex - 1;
  154. currentTranslateX = -innerWidth * currentPhotoIndex;
  155. if (onIndexChange) {
  156. onIndexChange(currentPhotoIndex);
  157. }
  158. }
  159. this.setState({
  160. touched: false,
  161. translateX: currentTranslateX,
  162. photoIndex: currentPhotoIndex,
  163. lastPageX: undefined,
  164. lastPageY: undefined,
  165. backdropOpacity: defaultOpacity,
  166. photoScale: 1,
  167. });
  168. }
  169. render() {
  170. const { innerWidth } = window;
  171. const {
  172. images,
  173. visible,
  174. overlay,
  175. className,
  176. maskClassName,
  177. viewClassName,
  178. imageClassName,
  179. } = this.props;
  180. const {
  181. translateX,
  182. touched,
  183. photoIndex,
  184. backdropOpacity,
  185. photoScale,
  186. } = this.state;
  187. const transform = `translate3d(${translateX}px, 0px, 0)`;
  188. if (visible) {
  189. return (
  190. <SlideWrap className={className}>
  191. <Backdrop
  192. className={maskClassName}
  193. style={{ background: `rgba(0, 0, 0, ${backdropOpacity})` }}
  194. />
  195. {images
  196. .map((item: string | dataType, index) => {
  197. const isStrItem = typeof item === 'string';
  198. return (
  199. <PhotoView
  200. key={
  201. isStrItem
  202. ? (item as string) + index
  203. : (item as dataType).dataKey
  204. }
  205. src={isStrItem ? (item as string) : (item as dataType).src}
  206. onReachTopMove={this.handleReachVerticalMove}
  207. onReachBottomMove={this.handleReachVerticalMove}
  208. onReachRightMove={
  209. index < images.length - 1
  210. ? this.handleReachHorizontalMove
  211. : undefined
  212. }
  213. onReachLeftMove={
  214. index > 0 ? this.handleReachHorizontalMove : undefined
  215. }
  216. onReachUp={this.handleReachUp}
  217. photoScale={photoIndex === index ? photoScale : 1}
  218. wrapClassName={viewClassName}
  219. className={imageClassName}
  220. style={{
  221. left: `${innerWidth * index}px`,
  222. WebkitTransform: transform,
  223. transform,
  224. transition: touched
  225. ? undefined
  226. : 'transform 0.6s cubic-bezier(0.25, 0.8, 0.25, 1)',
  227. }}
  228. />
  229. );
  230. })}
  231. {overlay}
  232. </SlideWrap>
  233. );
  234. }
  235. return null;
  236. }
  237. }