uniapp使用 movable-area movable-view 实现按双指中心位置缩放及拖拽功能

发布于:2024-07-05 ⋅ 阅读:(20) ⋅ 点赞:(0)

原理

使用 transformOrigin: ${state.x}px ${state.y}px 0
重新设置偏移中心点

待解决问题

缩放后进行拖拽会使计算的中心点位置与双指中心位置存在偏差,如果网友有解决这个问题,请贴代码到我的评论区,谢谢。

直接贴出代码

这里有关pdf的代码可以自行去掉

<template>
  <div style="width: 100%; height: 100%; position: relative; overflow-y: auto">
    <movable-area id="pdf-view" ref="pdfView" class="content-box" :scale-area="true">
      <movable-view
        :out-of-bounds="true"
        :style="{
          transformOrigin: `${state.x}px ${state.y}px 0`,
        }"
        class="movableView"
        direction="all"
        :scale="true"
        scale-min="1"
        damping="1"
        ref="movableViewRef"
        friction="2"
        :x="0"
        :y="0"
        :scale-max="4"
        :animation="false"
        @scale="pdfScaleChange"
        @touchstart="touchStart"
        @touchmove="touchMove"
        @touchend="touchEnd"
        @change="onScroll"
        @click="onViewClick"
      >
        <div
          :style="{
            width: '15px',
            height: '15px',
            background: 'gold',
            position: 'absolute',
            left: state.x + 'px',
            top: state.y + 'px',
            transform: `translate(-50%,-50%)`,
            zIndex: 100,
          }"
        >
        </div>
        <div ref="pdfViewContainer">
          <div
            v-for="pageNumber in state.pdfPages"
            :key="pageNumber"
            :ref="(el) => (pageRefs[pageNumber - 1] = el)"
            @touchstart="onPdfClick($event, pageNumber)"
          ></div>
        </div>
      </movable-view>
    </movable-area>
  </div>

  <je-loading v-show="loading" />
</template>
<script setup>
  //解决 structuredClone
  // https://developer.mozilla.org/en-US/docs/Web/API/structuredClone#browser_compatibility
  // https://gitcode.com/zloirock/core-js/overview?utm_source=csdn_github_accelerator
  import structuredClone from 'core-js-pure/actual/structured-clone';
  // 解决 TypeError: key.split(...).at is not a function
  // https://github.com/wojtekmaj/react-pdf/issues/1465
  import 'core-js/features/array/at';

  window.structuredClone = structuredClone;
  // if (!Array.prototype.at) {
  //   Array.prototype.at = function (index) {
  //     if (index < 0) {
  //       index = this.length + index;
  //     }
  //     if (index >= 0 && index < this.length) {
  //       return this[index];
  //     }
  //     return undefined;
  //   };
  // }
  import * as pdfjsWorker from 'pdfjs-dist/lib/pdf.worker.js';
  // 解决  pdfjsWorker 未定义
  window.pdfjsWorker = pdfjsWorker;
  import 'pdfjs-dist/web/pdf_viewer.css';
  import * as PDF from 'pdfjs-dist';
  // import * as PDF from 'pdfjs-dist/build/pdf.js';

  import { useRoute } from 'vue-router';
  import { ref, reactive, onMounted, nextTick, defineProps } from 'vue';
  import { showFailToast } from 'vant';
  import { onPageScroll } from '@dcloudio/uni-app';

  const route = useRoute();
  const props = defineProps({
    src: {
      type: String,
      default: '',
    },
  });
  const pdfViewContainer = ref(null);
  const movableViewRef = ref(null);
  const pdfView = ref(null);
  const pageRefs = ref([]);
  const loading = ref(false);
  const state = reactive({
    // 总页数
    pdfPages: 1,
    pdfPageList: [], //有效页码列表
    // 页面缩放
    pdfScale: 1,
    x: 0,
    y: 0,
    translateX: 0,
    translateY: 0,
    sCenterX: 0,
    sCenterY: 0,
    startTouches: [], // 初始触摸列表
    endTouches: [], // 结束触摸列表
    scrollTop: 0,
    scrollPage: 1,
    currentHeight: 0,
    currentWidth: 0,
    xData: 0,
  });
  const cData = reactive({
    move: {},
    click: {},
  });

  let pdfDoc = null;
  const onPdfClick = (e, page) => {
    // state.scrollTop = e.clientY;
    state.scrollPage = page;
    state.currentHeight = pageRefs.value[state.scrollPage - 1].clientHeight;
    state.currentWidth = pageRefs.value[state.scrollPage - 1].clientWidth;
    // console.log('height', pageRefs.value[state.scrollPage - 1]);
  };
  const pdfScaleChange = (e) => {
    console.log('放大', e);
    state.pdfScale = e.detail.scale;
    state.xData = e.detail.x;
    state.scrollTop = e.detail.y;
    getTranslate();
  };
  const touchStart = (event) => {
    state.startTouches = event.touches;
  };

  const onViewClick = (event) => {
    cData.click = event.touches[0];
  };
  const touchMove = (event) => {
    cData.move = event.touches[0];
    state.endTouches = event.touches;
    getTranslate();
    // 判断是否为双指缩放
    if (state.endTouches.length === 2) {
      let beforeDistance = calculateCenter(state.startTouches[0], state.startTouches[1]);

      // 监听移动,更新中心点坐标
      let moveTouch1 = state.startTouches.find((t) => t.identifier === event.touches[0].identifier);
      let moveTouch2 = state.startTouches.find((t) => t.identifier === event.touches[1].identifier);
      if (moveTouch1 && moveTouch2) {
        let centerX = (moveTouch1.clientX + moveTouch2.clientX) / 2;
        let centerY = (moveTouch1.clientY + moveTouch2.clientY) / 2;
        state.sCenterX = centerX;
        state.sCenterY = centerY;

        state.y = centerY - state.translateY;

        state.x = centerX - state.translateX;

        // 边界判断
        if (state.x < 0) {
          state.x = 0;
        } else if (state.x > state.currentWidth) {
          state.x = state.currentWidth;
        }
        let offsetHeight = movableViewRef.value.$el.offsetHeight;
        if (state.y < 0) {
          state.y = 0;
        } else if (state.y > offsetHeight) {
          state.y = offsetHeight;
        }
      }
    }
  };
  const touchEnd = (event) => {
    if (event.touches.length < 2) {
      state.startTouches = [];
      state.endTouches = [];
    }
  };
  const calculateCenter = (startTouches, endTouches) => {
    let xDistance = startTouches?.clientX - endTouches?.clientX;
    let yDistance = startTouches?.clientY - endTouches?.clientY;
    return Math.sqrt(xDistance * xDistance + yDistance * yDistance);
  };
  onPageScroll((e) => {
    // 页面滚动时会触发
    // this.scrollTop = e.scrollTop; // 更新滚动位置
    // console.log('页面滚动', e.scrollTop);
  });
  // 获取偏移量
  const getTranslate = () => {
    let styleM = movableViewRef.value.$el.style;
    let transform = styleM.transform;
    // 获取y轴偏移量
    let y = transform.match(/translateY\(([^)]+)\)/)[1];
    // 去除px
    y = y.replace('px', '');
    // state.translateY = Math.abs(Number(y) || 0);
    state.translateY = Number(y);
    let x = transform.match(/translateX\(([^)]+)\)/)[1];
    x = x.replace('px', '');
    // state.translateX = Math.abs(Number(x) || 0);

    state.translateX = Number(x);
  };
  // 设置中心点
  const setCenter = () => {
    let offsetHeight = movableViewRef.value.$el.offsetHeight;
    state.x = state.sCenterX - state.translateX;
    state.y = state.sCenterY - state.translateY;
    if (state.x < 0) {
      state.x = 0;
    } else if (state.x > state.currentWidth) {
      state.x = state.currentWidth;
    }
    if (state.y < 0) {
      state.y = 0;
    } else if (state.y > offsetHeight) {
      state.y = offsetHeight;
    }
  };
  const onScroll = (e) => {
    // 页面滚动时会触发
    state.xData = e.detail.x;
    state.scrollTop = e.detail.y;
    getTranslate();
    setCenter();
  };

  async function loadFile(url) {
    // {
    //   url,
    //     cMapUrl: 'https://cdn.jsdelivr.net/npm/pdfjs-dist@2.16.105/cmaps/',
    //   cMapPacked: true,
    // }
    loading.value = true;
    // 设置配置选项 手势缩放
    PDF?.DefaultViewerConfig?.set({
      handToolOnDblClick: true,
      mouseWheelScale: true,
    });
    let arrayBufferPDF;
    //
    // if (navigator.userAgent.indexOf('QQ')) {
    //   const pdfData = await fetch(url);
    //   arrayBufferPDF = await pdfData.arrayBuffer();
    // }
    // 解决部分机型浏览器 undefined is not an object(evaluating 'response.body.getReader')
    // https://www.qingcong.tech/technology/javascript/a-pdfjs-bug-in-qq.html#%E8%A7%A3%E5%86%B3%E5%8A%9E%E6%B3%95
    fetch(url).then(async (pdfData) => {
      console.log('pdfData', pdfData);
      if (!pdfData.ok) {
        loading.value = false;
        showFailToast({
          message: '预览地址不存在或已失效',
          duration: 0,
        });
        // window.JE.alert('预览地址不存在', 'error');

        return;
      }
      arrayBufferPDF = await pdfData.arrayBuffer();

      const loadingTask = arrayBufferPDF
        ? PDF.getDocument({ data: arrayBufferPDF })
        : PDF.getDocument(url);
      loadingTask.promise.then((pdf) => {
        pdfDoc = pdf;
        // 获取pdf文件总页数
        state.pdfPages = pdf.numPages;
        nextTick(() => {
          for (let i = 0; i < state.pdfPages; i++) {
            renderPage(i + 1); // 从第一页开始渲染
          }
        });
      });
    });
  }

  function renderPage(num) {
    pdfDoc.getPage(num).then((page) => {
      // 获取当前页面对应的DOM容器元素
      const container = pageRefs.value[num - 1];

      // 创建一个新的canvas元素
      const canvas = document.createElement('canvas');
      // 获取canvas的2D渲染上下文
      const ctx = canvas.getContext('2d');

      // 获取设备像素比
      let devicePixelRatio = window.devicePixelRatio || 1;
      // 获取画布的backing store ratio
      let backingStoreRatio =
        ctx.webkitBackingStorePixelRatio ||
        ctx.mozBackingStorePixelRatio ||
        ctx.msBackingStorePixelRatio ||
        ctx.oBackingStorePixelRatio ||
        ctx.backingStorePixelRatio ||
        1;

      // 获取pdfViewContainer元素的宽度
      const pdfWrapperElWidth =
        pdfViewContainer.value.clientWidth ||
        pdfViewContainer.value.offsetWidth ||
        pdfViewContainer.value.style.width;

      // 获取PDF页面的初始视口,缩放比例为1
      const intialisedViewport = page.getViewport({ scale: 1 });
      // 计算缩放比例,使PDF页面宽度与容器宽度一致
      const scale = pdfWrapperElWidth / intialisedViewport.width;
      // 计算设备像素比与backing store ratio的比值
      let ratio = devicePixelRatio / backingStoreRatio;
      // 根据缩放比例获取PDF页面的视口
      const viewport = page.getViewport({ scale });

      // 设置canvas的宽度为容器宽度乘以ratio,确保高分辨率下的清晰度
      canvas.width = pdfWrapperElWidth * ratio;
      // 设置canvas的高度为视口高度乘以ratio,确保高分辨率下的清晰度
      canvas.height = viewport.height * ratio;
      // 设置canvas的样式宽度为100%,与容器宽度一致
      canvas.style.width = '100%';
      // 设置canvas的样式高度为auto,根据宽度自适应
      canvas.style.height = 'auto';

      // 缩放画布的渲染上下文,根据ratio进行缩放,确保在高分辨率下绘制的清晰度
      ctx.scale(ratio, ratio);
      const renderContext = {
        canvasContext: ctx,
        viewport,
      };
      // 设置页面容器的高度为视口高度
      container.style.height = `${viewport.height}px`;
      page
        .render(renderContext)
        .promise.then(() => {
          state.pdfPageList.push(num);
          // 如果 container 存在 canvas元素 覆盖canvas元素
          container?.firstChild && container.removeChild(container.firstChild);
          container && container.appendChild(canvas);
        })
        .finally(() => {
          if (num === state.pdfPages) {
            loading.value = false;
          }
        });
    });
  }

  onMounted(() => {
    const file = route.query.file && JSON.parse(decodeURIComponent(route.query.file));
    const { relName, previewUrl } = file || {};
    if (relName) {
      // 设置 uniapp 当前页面标题
      uni.setNavigationBarTitle({
        title: relName,
      });
    }
    if (previewUrl) {
      loadFile(previewUrl);
      // nextTick(() => {
      //   initPinchZoom();
      // });
    } else {
      showFailToast({
        message: '预览地址不存在',
        duration: 0,
      });
    }
  });
</script>
<style scoped lang="less">
  uni-page-body {
    overflow-y: scroll;
  }

  .content-box {
    width: 100%;
    height: 100%;
    overflow: hidden;
    position: relative;
    // overflow-y: auto;
  }

  .movableView {
    border: 1px solid #ccc;
    box-sizing: border-box;
  }

  :deep(uni-movable-view) {
    width: 100%;
    height: auto;
    // overflow-y: auto;
  }
</style>