diff --git a/README.md b/README.md index 3e03c68..3fe6842 100644 --- a/README.md +++ b/README.md @@ -10,14 +10,15 @@ Demo: [https://minjieliu.github.io/react-photo-view](https://minjieliu.github.io ### 特性 -1. 支持左右切换导航、上/下滑关闭、双击放大/缩小、双指放大/缩小/平移、键盘导航/关闭、点击切换控件等 +1. 支持左右切换导航、上/下滑关闭、双击放大/缩小、双指放大/缩小/平移、键盘导航/关闭、旋转、点击切换控件等 1. 打开/关闭缩放动画 1. 自适应图像适应 +1. 长图模式 1. 支持桌面端(兼容 IE10+)/移动端 1. 轻量的体积 1. 高度的扩展性 1. 支持服务端渲染 -1. 基于 `typescript` 友好的语法提示 +1. 基于 `typescript` ## 开始使用 @@ -76,6 +77,7 @@ function ImageView() { | bannerVisible | boolean | 否 | 导航条 visible,默认 true | | introVisible | boolean | 否 | 简介 visible,默认 true | | overlayRender | (overlayProps) => React.ReactNode | 否 | 自定义渲染 | +| toolbarRender | (overlayProps) => React.ReactNode | 否 | 工具栏渲染 | | className | string | 否 | className | | maskClassName | string | 否 | 遮罩 className | | viewClassName | string | 否 | 图片容器 className | diff --git a/example/src/index.tsx b/example/src/index.tsx index 647cc14..db905fe 100644 --- a/example/src/index.tsx +++ b/example/src/index.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import styled from 'styled-components'; -import { PhotoSlider } from 'react-photo-view'; +import { PhotoProvider, PhotoSlider, PhotoConsumer } from 'react-photo-view'; import { IPhotoProvider } from 'react-photo-view/dist/PhotoProvider'; import { IPhotoConsumer } from 'react-photo-view/dist/PhotoConsumer'; import { IPhotoSliderProps } from 'react-photo-view/dist/PhotoSlider'; @@ -83,6 +83,73 @@ export const ControlledView = () => { ); }; +const FullScreenIcon = (props: React.HTMLAttributes) => { + const [fullscreen, setFullscreen] = React.useState(false); + React.useEffect(() => { + document.onfullscreenchange = () => { + setFullscreen(Boolean(document.fullscreenElement)); + }; + }, []); + return ( + + {fullscreen ? ( + + ) : ( + + )} + + ); +}; + +export const WithToolbar = () => { + function toggleFullScreen() { + if (document.fullscreenElement) { + document.exitFullscreen(); + } else { + const element = document.getElementById('PhotoView_Slider'); + if (element) { + element.requestFullscreen(); + } + } + } + return ( + { + return ( + <> + onRotate(rotate + 90)} + width="44" + height="44" + fill="white" + viewBox="0 0 768 768" + > + + + + + ); + }} + > + + {photoImages.map((item, index) => ( + + + + ))} + + + ); +}; + export function IPhotoProviderForwardProps(props: IPhotoProvider) {} export function IPhotoConsumerForwardProps(props: IPhotoConsumer) {} diff --git a/example/src/stories/Test.stories.mdx b/example/src/stories/Test.stories.mdx index 0b5ecdf..f466f23 100644 --- a/example/src/stories/Test.stories.mdx +++ b/example/src/stories/Test.stories.mdx @@ -9,7 +9,7 @@ import { Button, DefaultImage, ControlledView, - + WithToolbar, IPhotoProviderForwardProps, IPhotoConsumerForwardProps, IPhotoSliderForwardProps, @@ -91,6 +91,14 @@ import defaultPhoto from '../default-photo.svg'; +## 自定义工具栏 + + + + + + + # Props ### PhotoProvider diff --git a/package.json b/package.json index fc172cc..6349b09 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-photo-view", - "version": "0.4.5", + "version": "0.5.0", "description": "一款精致的 React 的图片预览组件", "author": "MinJieLiu", "license": "MIT", diff --git a/src/Photo.tsx b/src/Photo.tsx index 2745d12..0fb3fc6 100644 --- a/src/Photo.tsx +++ b/src/Photo.tsx @@ -11,6 +11,7 @@ export interface IPhotoProps extends React.HTMLAttributes { broken: boolean; width: number; height: number; + rotate: number; className?: string; onImageLoad: (PhotoParams, callback?: Function) => void; loadingElement?: JSX.Element; @@ -23,6 +24,7 @@ const Photo: React.FC = ({ broken, width, height, + rotate, className, onImageLoad, loadingElement, @@ -38,7 +40,7 @@ const Photo: React.FC = ({ loaded: true, naturalWidth, naturalHeight, - ...getSuitableImageSize(naturalWidth, naturalHeight), + ...getSuitableImageSize(naturalWidth, naturalHeight, rotate), }); } } diff --git a/src/PhotoSlider.less b/src/PhotoSlider.less index 0a68f47..5ebbcb4 100644 --- a/src/PhotoSlider.less +++ b/src/PhotoSlider.less @@ -69,10 +69,12 @@ } &-PhotoSlider__BannerRight { + display: flex; + align-items: center; height: 100%; } - &-PhotoSlider__Close { + &-PhotoSlider__toolbarIcon { box-sizing: border-box; padding: 10px; opacity: 0.75; diff --git a/src/PhotoSlider.tsx b/src/PhotoSlider.tsx index 49f27f5..0ff10ee 100644 --- a/src/PhotoSlider.tsx +++ b/src/PhotoSlider.tsx @@ -7,7 +7,7 @@ import Close from './components/Close'; import ArrowLeft from './components/ArrowLeft'; import ArrowRight from './components/ArrowRight'; import isTouchDevice from './utils/isTouchDevice'; -import { dataType, IPhotoProviderBase, ReachTypeEnum, ShowAnimateEnum } from './types'; +import { dataType, IPhotoProviderBase, overlayRenderProps, ReachTypeEnum, ShowAnimateEnum } from './types'; import { defaultOpacity, horizontalOffset, maxMoveOffset } from './variables'; import './PhotoSlider.less'; @@ -46,6 +46,8 @@ type PhotoSliderState = { overlayVisible: boolean; // 可下拉关闭 canPullClose: boolean; + // 旋转集合 + rotatingMap: Map; }; export default class PhotoSlider extends React.Component { @@ -83,6 +85,8 @@ export default class PhotoSlider extends React.Component(), }; } @@ -140,6 +144,14 @@ export default class PhotoSlider extends React.Component { + const { photoIndex, rotatingMap } = this.state; + rotatingMap.set(photoIndex, rotating); + this.setState({ + rotatingMap, + }); + }; + handleKeyDown = (evt: KeyboardEvent) => { const { visible } = this.props; if (visible) { @@ -298,6 +310,7 @@ export default class PhotoSlider extends React.Component e.stopPropagation()} >
- + {toolbarRender && toolbarRender(overlayParams)} +
)} @@ -391,6 +417,7 @@ export default class PhotoSlider extends React.Component ); })} @@ -411,15 +438,7 @@ export default class PhotoSlider extends React.Component{overlayIntro} )} - {overlayRender && - overlayRender({ - images, - index: photoIndex, - visible, - onClose: this.handleClose, - onIndexChange: this.handleIndexChange, - overlayVisible: currentOverlayVisible, - })} + {overlayRender && overlayRender(overlayParams)} ); } diff --git a/src/PhotoView.less b/src/PhotoView.less index 9846817..d546efc 100644 --- a/src/PhotoView.less +++ b/src/PhotoView.less @@ -45,6 +45,11 @@ } &__PhotoBox { + display: flex; + justify-content: center; + align-items: center; + width: 0; + height: 0; } &__PhotoMask { diff --git a/src/PhotoView.tsx b/src/PhotoView.tsx index f8487a2..4f74f59 100644 --- a/src/PhotoView.tsx +++ b/src/PhotoView.tsx @@ -6,7 +6,7 @@ import isTouchDevice from './utils/isTouchDevice'; import getMultipleTouchPosition from './utils/getMultipleTouchPosition'; import getPositionOnMoveOrScale from './utils/getPositionOnMoveOrScale'; import slideToPosition from './utils/slideToPosition'; -import { getReachType, getClosedHorizontal, getClosedVertical } from './utils/getCloseEdge'; +import { getReachType, getClosedEdge } from './utils/getCloseEdge'; import withContinuousTap, { TapFuncType } from './utils/withContinuousTap'; import getAnimateOrigin from './utils/getAnimateOrigin'; import { maxScale, minStartTouchOffset, minScale, scaleBuffer } from './variables'; @@ -36,6 +36,8 @@ export interface IPhotoViewProps { loadingElement?: JSX.Element; // 加载失败 Element brokenElement?: JSX.Element; + // 旋转状态 + rotate: number; // Photo 点击事件 onPhotoTap: PhotoTapFunction; @@ -132,14 +134,19 @@ export default class PhotoView extends React.Component) { + const { rotate } = this.props; + if (rotate !== prevProps.rotate) { + const { naturalWidth, naturalHeight } = this.state; + this.setState(getSuitableImageSize(naturalWidth, naturalHeight, rotate)); } + } + + componentWillUnmount() { + window.removeEventListener('touchmove', this.handleTouchMove); + window.removeEventListener('touchend', this.handleTouchEnd); + window.removeEventListener('mousemove', this.handleMouseMove); + window.removeEventListener('mouseup', this.handleMouseUp); window.removeEventListener('resize', this.handleResize); } @@ -148,10 +155,10 @@ export default class PhotoView extends React.Component { - const { onPhotoResize } = this.props; + const { onPhotoResize, rotate } = this.props; const { loaded, naturalWidth, naturalHeight } = this.state; if (loaded) { - this.setState(getSuitableImageSize(naturalWidth, naturalHeight)); + this.setState(getSuitableImageSize(naturalWidth, naturalHeight, rotate)); if (onPhotoResize) { onPhotoResize(); } @@ -173,10 +180,8 @@ export default class PhotoView extends React.Component { - const { onReachMove, isActive } = this.props; + const { onReachMove, isActive, rotate } = this.props; const { - width, - height, naturalWidth, x, y, @@ -193,6 +198,11 @@ export default class PhotoView extends React.Component { // 重置响应状态 this.initialTouchState = TouchStartEnum.Normal; - const { onReachUp, onPhotoTap, onMaskTap, isActive } = this.props; + const { onReachUp, onPhotoTap, onMaskTap, isActive, rotate } = this.props; const { width, height, @@ -397,6 +407,7 @@ export default class PhotoView extends React.Component @@ -460,7 +472,7 @@ export default class PhotoView extends React.Component void; // 覆盖物可见度 overlayVisible: boolean; + // 当前图片旋转角度 + rotate: number; + // 旋转事件 + onRotate: (rotate: number) => void; }; export interface IPhotoProviderBase { @@ -40,6 +44,8 @@ export interface IPhotoProviderBase { introVisible?: boolean; // 自定义渲染 overlayRender?: (overlayProps: overlayRenderProps) => React.ReactNode; + // 工具栏渲染 + toolbarRender?: (overlayProps: overlayRenderProps) => React.ReactNode; // className className?: string; // 遮罩 className diff --git a/src/utils/getCloseEdge.ts b/src/utils/getCloseEdge.ts index 6983b05..5c67da2 100644 --- a/src/utils/getCloseEdge.ts +++ b/src/utils/getCloseEdge.ts @@ -1,45 +1,22 @@ import { CloseEdgeEnum, ReachTypeEnum, TouchStartEnum } from '../types'; /** - * 接触左边或右边边缘 - * @param x + * 接触左边/上边 或 右边/下边边缘 + * @param position - x/y * @param scale - * @param width + * @param size - width/height + * @param innerSize - innerWidth/innerHeight * @return CloseEdgeEnum */ -export function getClosedHorizontal(x: number, scale: number, width: number): CloseEdgeEnum { - const { innerWidth } = window; - const currentWidth = width * scale; +export function getClosedEdge(position: number, scale: number, size: number, innerSize: number): CloseEdgeEnum { + const currentWidth = size * scale; // 图片超出的宽度 - const outOffsetX = (currentWidth - innerWidth) / 2; - if (currentWidth <= innerWidth) { + const outOffsetX = (currentWidth - innerSize) / 2; + if (currentWidth <= innerSize) { return CloseEdgeEnum.Small; - } else if (x > 0 && outOffsetX - x <= 0) { + } else if (position > 0 && outOffsetX - position <= 0) { return CloseEdgeEnum.Before; - } else if (x < 0 && outOffsetX + x <= 0) { - return CloseEdgeEnum.After; - } - return CloseEdgeEnum.Normal; -} - -/** - * 接触上边或下边边缘 - * @param y - * @param scale - * @param height - * @return CloseEdgeEnum - */ -export function getClosedVertical(y: number, scale: number, height: number): CloseEdgeEnum { - const { innerHeight } = window; - const currentHeight = height * scale; - // 图片超出的高度 - const outOffsetY = (currentHeight - innerHeight) / 2; - - if (currentHeight <= innerHeight) { - return CloseEdgeEnum.Small; - } else if (y > 0 && outOffsetY - y <= 0) { - return CloseEdgeEnum.Before; - } else if (y < 0 && outOffsetY + y <= 0) { + } else if (position < 0 && outOffsetX + position <= 0) { return CloseEdgeEnum.After; } return CloseEdgeEnum.Normal; diff --git a/src/utils/getSuitableImageSize.ts b/src/utils/getSuitableImageSize.ts index 74f30ba..dd898c5 100644 --- a/src/utils/getSuitableImageSize.ts +++ b/src/utils/getSuitableImageSize.ts @@ -4,6 +4,7 @@ export default function getSuitableImageSize( naturalWidth: number, naturalHeight: number, + rotate: number, ): { width: number; height: number; @@ -14,7 +15,14 @@ export default function getSuitableImageSize( let width; let height; let y = 0; - const { innerWidth, innerHeight } = window; + let { innerWidth, innerHeight } = window; + const isVertical = rotate % 180 !== 0; + + // 若图片不是水平则调换宽高 + if (isVertical) { + [innerHeight, innerWidth] = [innerWidth, innerHeight]; + } + const autoWidth = (naturalWidth / naturalHeight) * innerHeight; const autoHeight = (naturalHeight / naturalWidth) * innerWidth; @@ -32,7 +40,7 @@ export default function getSuitableImageSize( height = autoHeight; } // 长图模式 - else if (naturalHeight / naturalWidth >= 3) { + else if (naturalHeight / naturalWidth >= 3 && !isVertical) { width = innerWidth; height = autoHeight; // 默认定位到顶部区域 diff --git a/src/utils/slideToPosition.ts b/src/utils/slideToPosition.ts index d0dbcd5..60ea506 100644 --- a/src/utils/slideToPosition.ts +++ b/src/utils/slideToPosition.ts @@ -1,6 +1,6 @@ import { maxTouchTime, slideAcceleration } from '../variables'; import { CloseEdgeEnum } from '../types'; -import { getClosedHorizontal, getClosedVertical } from './getCloseEdge'; +import { getClosedEdge } from './getCloseEdge'; /** * 适应到合适的图片偏移量 @@ -13,6 +13,7 @@ export default function slideToPosition({ width, height, scale, + rotate, touchedTime, }: { x: number; @@ -22,6 +23,7 @@ export default function slideToPosition({ width: number; height: number; scale: number; + rotate: number; touchedTime: number; }): { x: number; @@ -38,8 +40,13 @@ export default function slideToPosition({ const slideTimeY = Math.abs(speedY / slideAcceleration); // 计划滑动位置 - const planX = Math.floor(x + speedX * slideTimeX); - const planY = Math.floor(y + speedY * slideTimeY); + let planX = Math.floor(x + speedX * slideTimeX); + let planY = Math.floor(y + speedY * slideTimeY); + + // 若图片不是水平则调换属性 + if (rotate % 180 !== 0) { + [width, height] = [height, width]; + } let currentX = planX; let currentY = planY; @@ -49,8 +56,8 @@ export default function slideToPosition({ const outOffsetX = (width * scale - innerWidth) / 2; const outOffsetY = (height * scale - innerHeight) / 2; - const horizontalCloseEdge = getClosedHorizontal(planX, scale, width); - const verticalCloseEdge = getClosedVertical(planY, scale, height); + const horizontalCloseEdge = getClosedEdge(planX, scale, width, innerWidth); + const verticalCloseEdge = getClosedEdge(planY, scale, height, innerHeight); // x if (horizontalCloseEdge === CloseEdgeEnum.Small) { @@ -72,7 +79,8 @@ export default function slideToPosition({ // 时间过长 if ( moveTime >= maxTouchTime && - horizontalCloseEdge === CloseEdgeEnum.Normal && verticalCloseEdge === CloseEdgeEnum.Normal + horizontalCloseEdge === CloseEdgeEnum.Normal && + verticalCloseEdge === CloseEdgeEnum.Normal ) { return { x,