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

2022-03-02 23:25:49 +08:00
 liumingyi1

文档:https://react-photo-view.vercel.app/

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

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

缩略图完美渐变:

指定位置放大:

减速滚动:

什么是 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

https://github.com/MinJieLiu/react-photo-view

谢谢!

5571 次点击
所在节点    React
34 条回复
keppelfei
2022-03-02 23:32:35 +08:00
thx!
抽个时间试试。
hua123s
2022-03-02 23:58:22 +08:00
前年看到过你的,试着写过类似的效果,没你功能多。https://powerfulyang.com/gallery
TimPeake
2022-03-03 00:35:15 +08:00
掘金看到你的贴子
vsitebon
2022-03-03 08:09:55 +08:00
正好有需求,很棒!
cuit4017
2022-03-03 08:13:08 +08:00
很棒 多谢分享
BeijingBaby
2022-03-03 08:23:25 +08:00
赞,很少有人能静下心做一款好工具好产品了
andrew2558
2022-03-03 08:41:26 +08:00
banliyaya
2022-03-03 09:03:27 +08:00
发现一个 bug ,不知道算不算兼容问题,暂无其他电脑测试。
macbook pro m1 ,chrome:98.0.4758.109 arm 64 ,当使用触摸板下滑图片放到最大时,再缩小,图片会出现色块拼接的闪烁效果(我也不知道怎么形容,录屏转换成 gif 有点大 且很卡,就不上传 gif 了)
![某一帧]( https://s3.bmp.ovh/imgs/2022/03/3b5f638946d74e3e.png)
a90120411
2022-03-03 09:40:47 +08:00
做的很好,支持一下!
NCry
2022-03-03 09:42:28 +08:00
稍微体验了下,真棒
enchilada2020
2022-03-03 09:46:15 +08:00
棒 学习一下
0x0208v0
2022-03-03 09:54:45 +08:00
作者太牛了赶紧 star
xlsepiphone
2022-03-03 09:59:46 +08:00
对运动这块挺感兴趣
danhua
2022-03-03 10:05:06 +08:00
赞一个,已 start 。之前遇到过类似需求,也是找了好久的库。
yu7er
2022-03-03 10:08:56 +08:00
哈哈哈,自己的项目用了你的组件,感谢
FightPig
2022-03-03 11:42:12 +08:00
好东西,不过现在不怎么用 react
ebushicao
2022-03-03 11:42:21 +08:00
可以的,star 了
cmdOptionKana
2022-03-03 11:43:15 +08:00
同九义,何汝秀!
LittleControl
2022-03-03 15:40:53 +08:00
受教了, 已 star
FreeEx
2022-03-03 15:56:13 +08:00
star 了,以后有需求了会尝试使用

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/837557

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX