2022强力之作:一款超精致的图片预览组件

394次阅读  |  发布于2年以前

我刚接触前端这个行业的时候就有一个想法,那就是写一个超炫酷的图片预览画廊。还记得当时用美图看看看,那轻快的速度和交互很是令人着迷。

该组件在几年前已经发布不完全版,后面断断续续的维护,总感觉差了点什么。今年春节没休息,全搭在上面进行开发,现在总算是完美实现!先看看效果:

缩略图完美渐变:

指定位置放大:

减速滚动:

什么是 react-photo-view

react-photo-view 拥有无与伦比的预览交互体验:从打开图像开始,每一帧的动画、细节和交互都经过了精心设计与反复调试,媲美原生图片预览的效果。

pnpm i react-photo-view

概览:

import { PhotoProvider, PhotoView } from 'react-photo-view';
import 'react-photo-view/dist/react-photo-view.css';

export default function MyComponent() {
  return (
    <PhotoProvider>
      <PhotoView src="/1.jpg">
        <img src="/1-thumbnail.jpg" alt="" />
      </PhotoView>
    </PhotoProvider>
  );
}

为什么要单独开发它?

当然想实现它的执念也算一个方面,但根本原因是在 React 强大的生态中根本找不到一个好用的图片预览方案。当时奉行拿来主义,在网上找了一圈基于 React 放大预览组件库,结果令我有点意外,图片放大预览的库的数量明显比不上轮播组件库。更令人窒息的是这些少得可怜的组件库中,其中一大半都是基于 PhotoSwipe 这个开源库进行的二次封装。除此之外,能用于实际生产的预览组件库……好像没有(也可能是我找不到),这种情况不仅体现在 React 库上,其他框架 Vue 乃至是原生的相关库都是如此。

当然 PhotoSwipe 也不是不能用,但原生操作 DOM 的写法在 React 中格格不入,其体积也是在 gzip 12KB 之上了,显得有点臃肿了,便有了这个大胆的想法。

它有多优秀?

它拥有非常完善的细节与特性:

还导出了支持 ES2017 以上的 JS,可以做到 6KB Gzipped。在如此的体积上加上非常多的体验细节实属不容易,更多的功能可以通过非常容易的自定义渲染来实现,这与 React 理念完美契合,从而可以避免内置一些非刚需的功能。

流行库对比

以下表格统计了大部分场景所需功能,展示 react-photo-viewPhotoSwiperc-image(ant-design) 对比:

react-photo-view PhotoSwipe rc-image
MINIFIED 19KB 47KB 40KB
MINIFIED + GZIPPED 7.3KB 12KB 14KB
基础预览 支持 支持 支持
切换预览 支持 支持 不支持
移动端 支持 支持 不支持
缩略图完美渐变 支持 支持 不支持
缩略图裁切动画 支持 支持(需手动指定) 不支持
自适应图像尺寸 支持 不支持(需手动指定) 支持
fallback 支持 不支持 支持
鼠标滚轮缩放 支持 不支持 (缺少位置)
弹簧物理滚动 支持 支持 不支持
动画参数调整 支持 支持 不支持
易用 API 支持 不支持 支持
TypeScript 支持 不支持 支持
键盘导航 支持 支持 支持
自定义元素 支持 存在 XSS 风险 不支持
受控组件 支持 支持 支持
循环预览 支持 支持 不支持
图片旋转 支持 不支持 支持
自定义工具栏 支持 支持 不支持
原生全屏打开 自定义扩展 支持 不支持

友好的文档

还有什么比文档更重要了,为此,我还准备了一个超漂亮的文档(目前只有中文,以后有时间在翻译吧~)

https://react-photo-view.vercel.app/ 文末**查看原文可预览**

实现历程

图片跟随手指滚动

onTouchStart 时记录当前触发位置状态,在 onTouchMove 时让其跟随手指移动,onTouchEnd 解除跟随就可以简单实现。

触边位置反馈使图片切换都是需要慢慢琢磨细节:在 onTouchStart 之后移动如果立即让图片跟随手指移动的话会带来许多误操作,比如本想让他切换图片却走了上下滑动的逻辑。这时候就需要一个 20px 的移动缓冲来预判手指移动方向。

指定图片位置进行放大

使用 transform: scale(value) 可以实现对图片的缩放,但是都是对图片中心进行放大,缩放的结果可能不是想要的。起初打算用 transform-origin 来实现,想法是美好的,虽然第一次在指定的位置能够进行放大。倘若缩小的位置不是原来的位置就会产生混乱跳动,很显然这个方式不行。

后来思来想去睡不着,在睡梦中发现了灵感:便于计算理解,我们设图片中心点为 0任何指定位置的放大缩小,即改变图片中心的位置。比如图片宽度 200,中心点位置为 100,基于最左侧位置放大一倍。现在图片宽度 400,那么中心点的位置应为 200。那么总结公式如下:

const centerClientX = innerWidth / 2;
// 坐标偏移转换
const lastPositionX = centerClientX + lastX;
// 缩放偏移
const offsetScale = nextScale / scale;
// 最终偏移位置
const originX = clientX - (clientX - lastPositionX) * offsetScale - centerClientX;

这种模式计算能承担各种位置响应,比如双指缩放、双指滚动+缩放、边缘计算等等。

双指之间的距离

这里需要初中时直角三角勾股定理:

Math.sqrt((nextClientX - clientX) ** 2 + (nextClientY - clientY) ** 2);

模拟滚动操作

之前的版本使用 transition 实现,通过手指滑动开始结束的时间差,计算出初始速度,估摸着用 transition 模拟出一个距离让眼睛看起来有滚动效果 。但这种方式体验始终差很多。后面结合高中物理公式模拟出滚动效果:

加速运动:

空气阻力:

CρS 都是常数,干脆都搞成一个量好了。至于怎么出这个量大小……试出来的 这样就只与 v 平方成正比了。

另外因为和运动方向相反,取个 v 的方向即 Math.sign(-v)

function scrollMove(
  initialSpeed: number,
  callback: (spatial: number) => boolean,
) {
  // 加速度
  const acceleration = -0.002;
  // 阻力
  const resistance = 0.0002;

  let v = initialSpeed;
  let s = 0;
  let lastTime: number | undefined = undefined;
  let frameId = 0;

  const calcMove = (now: number) => {
    if (!lastTime) {
      lastTime = now;
    }
    const dt = now - lastTime;
    const direction = Math.sign(initialSpeed);
    const a = direction * acceleration;
    const f = Math.sign(-v) * v ** 2 * resistance;
    const ds = v * dt + ((a + f) * dt ** 2) / 2;
    v = v + (a + f) * dt;

    s = s + ds;
    // move to s
    lastTime = now;

    if (direction * v <= 0) {
      cancelAnimationFrame(frameId);
      return;
    }

    if (callback(s)) {
      frameId = requestAnimationFrame(calcMove);
      return;
    }
    cancelAnimationFrame(frameId);
  };
  frameId = requestAnimationFrame(calcMove);
}

缩略图裁切

PhotoSwipe 支持缩略图裁切,不过需要手动指定图片宽高和 data-cropped,相当麻烦。react-photo-view 通过读取缩略图 getComputedStyle(element).objectFit 来获取当前裁切参数。实现自动裁切效果。

兼容性处理

因为每张图片都是一个合成层,这会消耗相当多的内存。IOS 上对于内存有相当大的限制,如果图片在放大的情况一直使用 scale,那么在 Safari 上会显得非常模糊。现在通过每次在运动完成后,都改变图片的宽高为指定的值,然后重设 scale 为 1,这种方式应该本身需要达到的效果吧。

其他

PhotoSwipe 的作者是居住在基辅的乌克兰人,他逃离了基辅,现在他和他家人在乌克兰西部很安全,也希望在战争结束后他能重新振作起来。

结语

我在 react-photo-view 的细节上面花费了相当的精力,如果喜欢的话可以帮忙点个 Star 就是对我的支持,谢谢!

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8