前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >写一个H5图片预览组件

写一个H5图片预览组件

作者头像
IMWeb前端团队
发布2019-12-03 17:59:10
1.4K0
发布2019-12-03 17:59:10
举报
文章被收录于专栏:IMWeb前端团队IMWeb前端团队

本文作者:IMWeb llunnn 原文出处:IMWeb社区 未经同意,禁止转载

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

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

效果预览 (移动端访问)

组件功能

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

模态框

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

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

实现细节

props

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

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

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

模态框

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

代码语言:javascript
复制
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) 触摸停止时触发
代码语言:javascript
复制
<AlloyFinger
  onPinch={this.onPinch}
  onPressMove={this.onPressMove}
  onMultipointStart={this.onMultipointStart}
  onTouchEnd={this.onTouchEnd}
 >
  <div className="img-wrapper">
    {/* TODO.. */}
  </div>
</AlloyFinger>

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

代码语言:javascript
复制
<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值。

代码语言:javascript
复制
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判断一下图片有没有被移出屏幕,我们要保持图片最大程度地填充屏幕空间。

代码语言:javascript
复制
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来记录前后两张图的偏移。

代码语言:javascript
复制
<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了:

代码语言:javascript
复制
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。

代码语言:javascript
复制
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();
  }
}

总结

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

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018-10-08 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 组件功能
    • 模态框
      • 手势部分
      • 实现细节
        • props
          • 模态框
            • 手势部分
              • 双指缩放
              • 单指移动图片
              • 左右滑动切换图片
          • 总结
          相关产品与服务
          容器服务
          腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档