写一个H5图片预览组件

http://imweb.io/topic/5bbc264b6477d81e668cc930

最近将一个照片墙从小程序迁移到了h5上,h5使用React开发。

这里需要实现的一点是点击照片墙上的小图时预览大图,小程序中提供了API: wx.previewImage预览图片,非常方便。但没有找到比较满意的React实现,于是仿小程序写了一个PhotoPreview组件。

效果预览 (移动端访问)

组件功能

首先分析一下组件功能啦~

模态框

最基本的是一个模态框,单击照片时显示,再次单击时隐藏。

手势部分
  • 双指缩放图片
  • 单指移动图片
  • 左右滑动切换图片

实现细节

props

hidePreview: Function 控制模态框显隐的方法

urls: Array 所有将要预览的图片链接

initIndex: Number 初始预览的图片下标

模态框

模态框部分比较常见,为了减少模态框受父组件的影响,这里使用了Portal,将其直接添加到body下。

import React from 'react'
import { createPortal } from 'react-dom'; import './index.css' export default class PhotoPreview extends React.PureComponent { constructor(props) { super(props); const { hidePreview } = this.props; this.root = document.createElement('div'); // 创建一个容器放置模态框 this.root.classList.add('preview-modal-wrapper'); // 设置一些样式 this.root.addEventListener('click', hidePreview); document.body.appendChild(this.root); // 将容器添加到body下 this.root.addEventListener('touchmove', this.preventTouchMove); } preventTouchMove = e => e.preventDefault(); // 阻止模态框上的touchMove事件影响到下方的元素 componentWillUnmount() { // 模态框销毁后移除事件和外层容器 this.root.removeEventListener('touchmove', this.preventTouchMove); document.body.removeChild(this.root); } render() { return createPortal( // 创建一个Portal,将模态框添加到我们新创建的this.root容器上 ( <div> {/* TODO.. */} </div> ), this.root); } } 
手势部分

工具:AlloyFinger

这里借助了一个手势库AlloyFinger帮助捕获一些手势事件。

主要用到的事件如下:

  • onPinch(e) 双指缩放时触发,e.zoom为缩放倍数
  • onMultipointStart(e) 多点触摸时触发
  • onPressMove(e) 手指按下并移动时触发,e.deltaX, e.deltaY为两个方向上移动的距离
  • onTouchEnd(e) 触摸停止时触发
<AlloyFinger
  onPinch={this.onPinch}
  onPressMove={this.onPressMove} onMultipointStart={this.onMultipointStart} onTouchEnd={this.onTouchEnd} > <div className="img-wrapper"> {/* TODO.. */} </div> </AlloyFinger> 

根据上面分析的功能,考虑用transform属性的scale和translate来控制图片随手势的变化。

<div 
  className="img-wrapper" > <img src={urls[curIndex]} ref={this.imgRef} style={{ // eslint-disable-next-line transform: `scale(${scale}) translate(${translate.x}px, ${translate.y}px)`, }} /> </div> 

接下来就要根据功能一一定制各种手势下的行为了:

双指缩放

这里比较简单,直接使用onPinch获得的zoom去改变this.state.scale。

需要注意的是这里的zoom是相对于每一次缩放手势开始时的放大倍数,因此需要监听onMultipointStart事件,在开始缩放时记录下原始的scale值。

onMultipointStart() {
  this.setState({
    base: this.state.scale,
  })
}

onPinch(evt) {
  const nextScale = evt.zoom * this.state.base;
  this.setState({ scale: nextScale < 1 ? 1 : nextScale, // 禁止小于原尺寸 }); evt.preventDefault(); } 
单指移动图片

移动比较简单,根据onPressMove获得的deltaX, deltaY改变图片translate属性就可以了,另外在onTouchEnd判断一下图片有没有被移出屏幕,我们要保持图片最大程度地填充屏幕空间。

onPressMove(evt) {
  // transform里scale放在translate前面,手指移动的距离要除以scale
  let transX = this.state.translate.x + evt.deltaX / this.state.scale;
  let transY = this.state.translate.y + evt.deltaY / this.state.scale; if (this.state.scale <= 1) { // 缩放倍数小于1时使y方向上的移动失效 transY = 0; } this.setState({ translate: { x: transX, y: transY, }, }); evt.preventDefault(); } onTouchEnd() { const {left, right, top, bottom, width, height} = this.imgRef.current.getBoundingClientRect(); let translate = {}; // 保持图片在屏幕中央 if (width < screenWidth) { translate.x = 0; } else if (left > 0) { translate.x = (width - screenWidth) * 0.5 / this.state.scale; } else if (right < screenWidth) { translate.x = (screenWidth - width) * 0.5 / this.state.scale; } else { translate.x = this.state.translate.x; } if (height < screenHeight) { translate.y = 0; } else if (top > 0) { translate.y = (height - screenHeight) * 0.5 / this.state.scale; } else if (bottom < screenHeight) { translate.y = (screenHeight - height) * 0.5 / this.state.scale; } else { translate.y = this.state.translate.y; } this.setState({ translate, }); } 
左右滑动切换图片

这大概就是大魔王吧...思考了几种实现的方式,最终使用的方法是这样的:

提前加载前后两张图片,并在onPressMove时同步更改左右两张图片的位置,那么当前图片的左右两侧有空隙时,前后的图片就可以显示出来。这里用了shiftBeforeshiftAfter来记录前后两张图的偏移。

<AlloyFinger
  onPinch={this.onPinch} onPressMove={this.onPressMove} onMultipointStart={this.onMultipointStart} onTouchEnd={this.onTouchEnd} > {/*当前图片....*/} </AlloyFinger> <div className="img-wrapper" style={{ transform: `translate(${shiftBefore - screenWidth}px, -50%)`, position: 'absolute', top: '50%', }} > <img src={urls[curIndex-1]} /> </div> <div className="img-wrapper" style={{ transform: `translate(${shiftAfter + screenWidth}px, -50%)`, position: 'absolute', top: '50%', }} > <img src={urls[curIndex+1]} /> </div> 

接下来就需要在onPressMove的时候同步修改shiftBefore和shiftAfter两个state了:

onPressMove(evt) {
  let transX = this.state.translate.x + evt.deltaX / this.state.scale;
  let transY = this.state.translate.y + evt.deltaY / this.state.scale; let shiftAfter, shiftBefore; if (this.state.scale <= 1) { // 图片没有缩放时shiftAfter, shiftBefore和图片的translate相同 shiftAfter = transX > 0 ? 0 : transX; shiftBefore = transX < 0 ? 0 : transX; transY = 0; } else { // 图片被放大则将图片边缘与屏幕边缘比较 const {left, right} = this.imgRef.current.getBoundingClientRect(); shiftAfter = right < screenWidth ? right - screenWidth : 0; shiftBefore = left > 0 ? left : 0; } this.setState({ translate: { x: transX, y: transY, }, shiftBefore, shiftAfter, }); evt.preventDefault(); } 

另外,在onTouchEnd时判断当前手指移动的距离是否足够大,判断是否切换到下一张图片。

若切换图片,完成下一张图片滑动到屏幕中央的动画后,替换当前图片、前一张和后一张图片的src。

onTouchEnd() {
  const {shiftAfter, shiftBefore, curIndex} = this.state;
  if (Math.abs(shiftAfter) < screenWidth * 0.1 && Math.abs(shiftBefore) < screenWidth * 0.1) { // 不切换图片 // 同上计算translate // ... this.setState({ translate, shiftAfter: 0, shiftBefore: 0, }) } else { // 切换下一张图 const prevImage = (shiftAfter === 0); let nextIndex = prevImage ? curIndex - 1 : curIndex + 1; if (nextIndex < 0) { nextIndex = 0; } else if (nextIndex >= this.props.urls.length) { nextIndex = this.props.urls.length - 1; } const self = this; // 这里是下一张图滑到页面中的效果 // 用了setTimeout不断改变translate值,应该可以优化 function moveToCenter() { if (prevImage && screenWidth - Math.abs(self.state.shiftBefore) < 20 || !prevImage && screenWidth - Math.abs(self.state.shiftAfter) < 20) { // 滑动动画完成,改变当前图片的下标curIndex self.setState({ curIndex: nextIndex, shiftAfter: 0, shiftBefore: 0, translate: { x: 0, y: 0, }, scale: 1, base: 1, }); } else { // 继续滑动动画 setTimeout(moveToCenter, 10); self.setState({ translate: { x: self.state.translate.x + 20 * (prevImage ? 1 : -1), y: self.state.translate.y, }, shiftBefore: self.state.shiftBefore + 20 * (prevImage ? 1 : -1), shiftAfter: self.state.shiftAfter + 20 * (prevImage ? 1 : -1), }); } } moveToCenter(); } } 

总结

在上面几步之后基本就实现了一个基础的图片预览组件,比较复杂的还是图片位置的计算吧,以及还需要增加一些优化来使得动作更加流畅。另外,现在的做法预加载了当前图片前后的两张图片,可以考虑增加更多的图片预加载,使得切换时更加流畅。

转载于:https://www.cnblogs.com/rubyxie/articles/9792133.html

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值