V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
liumingyi1
V2EX  ›  React

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

  •  6
     
  •   liumingyi1 · 116 天前 · 4529 次点击
    这是一个创建于 116 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

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

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

    缩略图完美渐变:

    1.gif

    指定位置放大:

    2.gif

    减速滚动:

    3.gif

    什么是 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 之上了,显得有点臃肿了,便有了这个大胆的想法。

    它有多优秀?

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

    • 支持触摸手势,拖动 /平移 /物理效果滑动,双指指定位置放大 /缩小
    • 全方面动画衔接,打开 /关闭 /回弹 /触边,顺其自然的交互效果
    • 图像自适应,以一个合适的最初呈现大小,并根据调整自适应
    • 支持自定义如 <video /> 或任意 HTML 元素的预览
    • 键盘导航,完美适配桌面端
    • 支持自定义节点扩展,轻松实现全屏预览、旋转控制、图片介绍以及更多功能
    • 基于 typescript7KB Gzipped,支持服务端渲染
    • 简单易用的 API,上手零成本

    还导出了支持 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/

    4.png

    实现历程

    图片跟随手指滚动

    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 模拟出一个距离让眼睛看起来有滚动效果 😂。但这种方式体验始终差很多。后面结合高中物理公式模拟出滚动效果:

    5.png

    加速运动:

    6.png

    空气阻力:

    7.png

    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 来获取当前裁切参数。实现自动裁切效果。

    4.gif

    兼容性处理

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

    其他

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

    结语

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

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

    谢谢!

    34 条回复    2022-03-12 17:02:54 +08:00
    keppelfei
        1
    keppelfei  
       116 天前
    thx!
    抽个时间试试。
    hua123s
        2
    hua123s  
       116 天前 via iPhone
    前年看到过你的,试着写过类似的效果,没你功能多。https://powerfulyang.com/gallery
    TimPeake
        3
    TimPeake  
       116 天前
    掘金看到你的贴子
    vsitebon
        4
    vsitebon  
       115 天前
    正好有需求,很棒!
    cuit4017
        5
    cuit4017  
       115 天前 via Android
    很棒 多谢分享
    BeijingBaby
        6
    BeijingBaby  
       115 天前
    赞,很少有人能静下心做一款好工具好产品了
    andrew2558
        7
    andrew2558  
       115 天前
    banliyaya
        8
    banliyaya  
       115 天前
    发现一个 bug ,不知道算不算兼容问题,暂无其他电脑测试。
    macbook pro m1 ,chrome:98.0.4758.109 arm 64 ,当使用触摸板下滑图片放到最大时,再缩小,图片会出现色块拼接的闪烁效果(我也不知道怎么形容,录屏转换成 gif 有点大 且很卡,就不上传 gif 了)
    ![某一帧]( https://s3.bmp.ovh/imgs/2022/03/3b5f638946d74e3e.png)
    a90120411
        9
    a90120411  
       115 天前
    做的很好,支持一下!
    NCry
        10
    NCry  
       115 天前
    稍微体验了下,真棒
    enchilada2020
        11
    enchilada2020  
       115 天前 via Android
    棒 学习一下
    v2exblog
        12
    v2exblog  
       115 天前
    作者太牛了赶紧 star
    xlsepiphone
        13
    xlsepiphone  
       115 天前 via Android
    对运动这块挺感兴趣
    danhua
        14
    danhua  
       115 天前
    赞一个,已 start 。之前遇到过类似需求,也是找了好久的库。
    yu7er
        15
    yu7er  
       115 天前
    哈哈哈,自己的项目用了你的组件,感谢
    FightPig
        16
    FightPig  
       115 天前
    好东西,不过现在不怎么用 react
    ebushicao
        17
    ebushicao  
       115 天前
    可以的,star 了
    cmdOptionKana
        18
    cmdOptionKana  
       115 天前
    同九义,何汝秀!
    LittleControl
        19
    LittleControl  
       115 天前
    受教了, 已 star
    FreeEx
        20
    FreeEx  
       115 天前
    star 了,以后有需求了会尝试使用
    nzbin
        21
    nzbin  
       115 天前
    顺便推一款纯 JS 图片预览插件
    https://github.com/nzbin/photoviewer
    ragnaroks
        22
    ragnaroks  
       115 天前
    我有个疑问,对于不存在样式复用的单功能组件,单独将样式导出是基于什么原因?
    liumingyi1
        23
    liumingyi1  
    OP
       115 天前
    @ragnaroks 便于自定义主题之类的
    kidonng
        24
    kidonng  
       115 天前 via Android
    PhotoSwipe 那段文字有心了👍
    dreamerblue
        25
    dreamerblue  
       115 天前
    赞,感觉很不错!如果以后有计划开发 Vue 版本就更好了
    fumichael
        26
    fumichael  
       115 天前
    把 github 换成 gitee ,原来已经 star 好久了
    codingBug
        27
    codingBug  
       115 天前 via Android
    不错不错
    zololiu
        28
    zololiu  
       114 天前
    感谢!
    Terry05
        29
    Terry05  
       114 天前
    厉害了,加油精细化
    yazoox
        30
    yazoox  
       114 天前
    牛 B 啊。是什么协议的。能够用于商业项目中去么?
    respect11
        31
    respect11  
       114 天前
    这评论看着怎么不对呢
    liumingyi1
        32
    liumingyi1  
    OP
       113 天前
    @yazoox 可以的
    hkyshefavor
        33
    hkyshefavor  
       107 天前
    不错,已 star
    cyberpoint
        34
    cyberpoint  
       106 天前
    @yazoox 我二月份的需求已经用了,没想到在这里遇到了作者。感谢。
    关于   ·   帮助文档   ·   API   ·   FAQ   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   1255 人在线   最高记录 5497   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 240ms · UTC 23:40 · PVG 07:40 · LAX 16:40 · JFK 19:40
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.