h5活动页面结构优化:

概括:

  • 修改了标签名称
  • 页面节点添加 .page
  • 结构优化,主要分为三部分:
    1. 页面主体部分
    2. loading部分 因为 刚进页面需要loading
    3. 其他需要预加载图片的部分

代码实现:

  1. <!--页面主题部分-->
  2. <section id="app">
  3. <!-- p1 start -->
  4. <ul id="p1" class="page" style="position:relative;z-index:2;">
  5. <li class="wpr">
  6. <!-- 内容区域 -->
  7. <header class="head">标题区域</header>
  8. <div class="content">内容区域</div>
  9. <footer class="foot">按钮区域</footer>
  10. </li>
  11. </ul>
  12. <!-- p2 start -->
  13. <ul id="p2" class="page" style="position:relative;z-index:2;">
  14. <li class="wpr">
  15. <!--内容区域-->
  16. <header class="head">上传区域</header>
  17. <div class="content">提示部分区域</div>
  18. <footer class="foot">按钮区域</footer>
  19. </li>
  20. </ul>
  21. <!-- p3 start -->
  22. <ul id="p3" class="page" style="position:relative;z-index:2;">
  23. <li class="wpr">
  24. <!--内容区域-->
  25. <header class="head">展示效果区域</header>
  26. <div class="content">效果切换按钮区域</div>
  27. <footer class="foot">分享保存按钮区域</footer>
  28. </li>
  29. </ul>
  30. </section>
  31. <!--loading部分-->
  32. <section class="loading mask run">
  33. <ul class="loadWpr">
  34. <li class="load">
  35. <img src="./static/img/common/loading.png" class="loadingIcon" alt="">
  36. <var></var>
  37. </li>
  38. </ul>
  39. </section>
  40. <!-- 需要预加载图片部分(包含各种弹窗,结果页的模版)-->
  41. <section class="preLoad">
  42. <!-- 错误提示框 -->
  43. <ul class="mask errMsg" style="display:none;">
  44. <li class="errMsgWpr p-r">
  45. <div class="errMsgIn p-r">
  46. <var></var>
  47. <img src="./static/pic/p4_ok.png" alt="">
  48. </div>
  49. </li>
  50. </ul>
  51. <!--相册相机选择弹窗-->
  52. <ul class="selectBox mask" style="display:none;z-index:590">
  53. <li class="owpr p-r dialog-sty">
  54. <a href="javascript:void(0)" id="cameraBtn">
  55. <img src="./static/pic/cameraBtn.png" alt="">
  56. </a>
  57. <a href="javascript:void(0)" id="galleryBtn">
  58. <img src="./static/pic/albumBtn.png" alt="">
  59. </a>
  60. </li>
  61. </ul>
  62. <!--保存成功toast部分-->
  63. <div class="saveSucToast" style="display:none">保存成功</div>
  64. <!-- input 上传图片 -->
  65. <input type="file" id="inputFile" accept="image/*" style="width:0;height:0;position:absolute;top:0;left:-91846px;" />
  66. <!-- 保存相册相机返回的图片-->
  67. <img id="bridgeImg" style="position:absolute;width:0;height:0;opacity:0.001;top:0;left:-9999px;" alt="" />
  68. <!--cofirm 弹出层-->
  69. <ul class="confirmBox mask">
  70. <li class="comfirm">
  71. <var>是否放弃正在编辑的内容</var>
  72. <img src="static/pic/confirmNo.png" alt="" class="no">
  73. <img src="static/pic/confirmYes.png" alt="" class="yes">
  74. </li>
  75. </ul>
  76. <!--图片保存弹出层-->
  77. <ul class="shareImgBox mask">
  78. <li class="shareInnerBox">
  79. <img alt="">
  80. <p>长按保存图片哦 ~</p>
  81. </li>
  82. </ul>
  83. <!--以下加载的模版图,结果页需要-->
  84. <img src="static/pic/temp3.png" id="res3" style="position:absolute;top:-999rem;right:999rem;width:100%;z-index:166;" alt="">
  85. <img src="static/pic/txt1.png" id="txt1" style="position:absolute;top:-999rem;right:999rem;width:100%;z-index:166;" alt="">
  86. <img src="static/pic/txt2.png" id="txt2" style="position:absolute;top:-999rem;right:999rem;width:100%;z-index:166;" alt="">
  87. <img src="static/pic/txt3.png" id="txt3" style="position:absolute;top:-999rem;right:999rem;width:100%;z-index:166;" alt="">
  88. <img src="static/pic/txt4.png" id="txt4" style="position:absolute;top:-999rem;right:999rem;width:100%;z-index:166;" alt="">
  89. <img src="static/pic/txt5.png" id="txt5" style="position:absolute;top:-999rem;right:999rem;width:100%;z-index:166;" alt="">
  90. <img src="static/pic/txt7.png" id="txt7" style="position:absolute;top:-999rem;right:999rem;width:100%;z-index:166;" alt="">
  91. <img src="static/pic/txt8.png" id="txt8" style="position:absolute;top:-999rem;right:999rem;width:100%;z-index:166;" alt="">
  92. <img src="static/pic/txt9.png" id="txt9" style="position:absolute;top:-999rem;right:999rem;width:100%;z-index:166;" alt="">
  93. <img src="static/pic/txt10.png" id="txt10" style="position:absolute;top:-999rem;right:999rem;width:100%;z-index:166;" alt="">
  94. <img src="static/pic/txt11.png" id="txt11" style="position:absolute;top:-999rem;right:999rem;width:100%;z-index:166;" alt="">
  95. </section>

hide节点内的”图片懒加载”实现

  • hide节点就是display:none 的节点

    规则:

  1. 项目的图片 放在 根目录下的 static/img 目录下

    1. 如: static/img/1.png
  2. 在 页面节点 下要懒加载的图片 书写格式:

    1. <img data-src="img/1.png" alt="">
  3. 图片data-src 路径中 不要写 @/static/ ,因为在动态加载图片时 require中写了

    1. <img data-src="img/1.png" alt="">

    实现原理:

    ```javascript // @/static/handlers.js

/**

  1. * 控制displaynone 节点中懒加载图片显示
  2. * @param
  3. * Node 页面节点
  4. * */
  5. static showHideNodeImg(Node) {
  6. imgs = Node.find('img');
  7. if (imgs.eq(0).data('src') && !imgs.eq(0).attr('src')) {
  8. $.each(imgs, function () {
  9. srcRes = $(this).data('src');
  10. srcRes && $(this).attr('src', require(`@/static/${srcRes}`));
  11. })
  12. }
  13. }
  1. <a name="1zWP2"></a>
  2. ## 应用:
  3. 1. 初始化页面的时候加载图片
  4. ```javascript
  5. // @/static/handlers.js
  6. /**
  7. * @param:
  8. * actPage 页面节点
  9. * */
  10. static initPage(actPage) {
  11. return new Promise((resolve) => {
  12. $('.page').css('opacity', 0).hide();
  13. // 加载当前页面图片
  14. actPage.show();
  15. Handlers.showHideNodeImg(actPage);
  16. setTimeout(() => {
  17. actPage.css('opacity', 1).css('willChange', 'auto');
  18. resolve()
  19. }, 100)
  20. })
  21. }
  1. 加载完首页后,可以加载其他后续需要的图片
    1. async function enterP1(){
    2. await Handlers.initPage($('#p1'));
    3. setTimeout(function () {
    4. Handlers.showHideNodeImg($('.preLoad'))
    5. },200)
    6. }

    节点重复解除绑定事件的优化

    优化原因和思路

  • 当前:

    在函数中给节点绑定事件,会使一个节点多次绑定相似事件,所以目前实现方式是 先解绑事件,然后重新添加

  • 优化:

    • 在节点上添加一个标识: havedEvent 值为事件类型字符串如: “click change dbclick”
    • 先判断节点是否包含指定类型事件,不包含则绑定事件

      实现

      1. /**
      2. * 判断某个节点是否已绑定指定类型事件,避免函数内绑定事件重复解绑添加事件
      3. * @param:
      4. * Node: 节点
      5. * Type: 事件类型
      6. * @return 如果有
      7. * */
      8. static isHasEvent(Node,Type){
      9. return new Promise((resolve) => {
      10. // havedEvent ,在节点上添加事件绑定的标记
      11. let havedEvent = Node.attr('havedEvent');
      12. if(havedEvent){
      13. if(havedEvent.includes(Type)){
      14. // 已绑定指定事件
      15. resolve(true);
      16. }else{
      17. resolve(false)
      18. }
      19. }else{
      20. resolve(false)
      21. }
      22. })
      23. }
      24. /**
      25. * 判断某个节点是否已绑定指定类型事件,避免函数内绑定事件重复解绑添加事件
      26. * @param:
      27. * Node: 节点
      28. * Type: 事件类型
      29. * @return 如果有
      30. * */
      31. static addEvent(Node,Type){
      32. let havedEvent = Node.attr('havedEvent');
      33. if(havedEvent && havedEvent.includes(Type)){
      34. throw new Error(`重复标记了${Type}事件类型`);
      35. }
      36. if(havedEvent && !havedEvent.includes(Type)){
      37. Node.attr('havedEvent',`${havedEvent} ${Type}`)
      38. }else{
      39. Node.attr('havedEvent',Type)
      40. }
      41. }

      应用:

      1. // 如首页按钮点击进入上传页
      2. async function enterP1() {
      3. // 先获取节点,避免多次获取同一个节点
      4. let $startBtn = $('.startBtn');
      5. // 判断节点上是否已绑定click事件,没有则绑定
      6. if(! await Handlers.isHasEvent($startBtn,'click')){
      7. // 绑定时给节点添加标识
      8. Handlers.addEvent($startBtn,'click')
      9. $startBtn.on('click', function(){
      10. // 代码实现
      11. enterP2()
      12. })
      13. }
      14. }

      获取APP信息

      概括:

  • 修改为Promise形式

    实现:

    1. static checkAppInfo() {
    2. return new Promise((resolve)=>{
    3. if (BrowserChecker.isIos()) {
    4. assignMyApp({isIos: true})
    5. } else if (BrowserChecker.isAndroid()) {
    6. assignMyApp({isAnd: true})
    7. }
    8. Bridge.appInfo(res => {
    9. if (res.app) {
    10. let o = {
    11. isInApp: true,
    12. version: res.app,
    13. inState: '-inApp'
    14. };
    15. // 如果有版本,且小于3.3.5的
    16. if (!compareVersion('3.3.5', res.app)) {
    17. o.needUpdata = true;
    18. }
    19. assignMyApp(o);
    20. assignMyApp({EventFullPath: eventBaseName + Handlers.myApp.inState});
    21. EventFullPath = Handlers.myApp.EventFullPath;
    22. console.log('在站内');
    23. }
    24. resolve();
    25. })
    26. })
    27. }

    应用:

    1. Handlers.checkAppInfo().then(()=>{
    2. console.log('站内')
    3. $('.saveInApp').css('display', 'block');
    4. $('.saveOutApp').css('display', 'none');
    5. _hmt.push(['_trackEvent', Handlers.myApp.EventFullPath, 'init', '受访pv'])
    6. });

    初始化页面:

    概述:

  • initPage 常用,迁移到了 handler.js 中

    实现:

    1. /**
    2. * @param:
    3. * actPage 页面节点
    4. * */
    5. static initPage(actPage) {
    6. return new Promise((resolve) => {
    7. $('.page').css('opacity', 0).hide();
    8. actPage.show();
    9. // 加载当前页面
    10. Handlers.showHideNodeImg(actPage);
    11. setTimeout(() => {
    12. actPage.css('opacity', 1).css('willChange', 'auto');
    13. resolve()
    14. }, 100)
    15. })
    16. }

    应用:

    1. async function enterP1() {
    2. await Handlers.initPage($('#p1'));
    3. }

图片上传到显示

优化:

  • 修改为Promise语法
  • 避免多次获取节点
  • 避免了节点解绑和重新绑定
  • 兼容处理旋转图片

代码实现:

获取图片部分

  1. let currentResolve; // 记录 相册、相机按钮点击时执行函数中的 回调函数参数
  2. // 选取照片
  3. static pickImg() {
  4. return new Promise(async resolve => {
  5. // 动态赋值最新的resolve,这样“相机相册按钮”节点执行click事件中,获取的是最新Promise中的resolve
  6. currentResolve = resolve;
  7. if (Handlers.myApp.isInApp) {
  8. cameraMenu();
  9. //避免多次获取节点
  10. let $galleryBtn = $("#galleryBtn"), $cameraBtn = $('#cameraBtn');
  11. // 避免了节点解除和重新绑定事件
  12. if(! await Handlers.isHasEvent($galleryBtn,'click')){
  13. Handlers.addEvent($galleryBtn,'click');
  14. $galleryBtn.off('click').on('click', () => {
  15. loading();
  16. cameraMenu({state: 0});
  17. const galleryParams = new EventCameraParam({type: 'imageAlbum'})
  18. _hmt.push(['_trackEvent', EventFullPath, 'Btn', '相册选取照片'])
  19. Bridge.eventCamera(galleryParams, function (res, type) {
  20. // currentResolve 最新的promise中的resolve
  21. // 站内获取图片后的操作
  22. Handlers.eventCameraCallback(res, type, currentResolve)
  23. })
  24. })
  25. }
  26. // 避免了节点解除和重新绑定事件
  27. if(! await Handlers.isHasEvent($cameraBtn,'click')){
  28. Handlers.addEvent($cameraBtn,'click');
  29. $cameraBtn.off('click').on('click', () => {
  30. loading();
  31. cameraMenu({state: 0})
  32. setTimer = setTimeout(() => {
  33. loading({state: 0});
  34. }, 1200)
  35. const cameraParams = new EventCameraParam({type: 'imageCamera'})
  36. _hmt.push(['_trackEvent', EventFullPath, 'Btn', '拍照选取照片'])
  37. Bridge.eventCamera(cameraParams, function (res, type) {
  38. // currentResolve 最新的promise中的resolve
  39. // 站内获取图片后的操作
  40. Handlers.eventCameraCallback(res, type, currentResolve)
  41. })
  42. })
  43. }
  44. } else {
  45. /*
  46. 站外input如果没有选取照片,无法捕捉onchange事件回调,不要提前加loading效果
  47. */
  48. let $inputFile = $('#inputFile') ;
  49. $inputFile[0].value = '';
  50. $inputFile.trigger('click');
  51. // 避免了节点解除和重新绑定事件
  52. if(! await Handlers.isHasEvent($inputFile,'change')){
  53. Handlers.addEvent($inputFile,'change');
  54. $('#inputFile').on('change', function () {
  55. // currentResolve 最新的promise中的resolve
  56. // 站外获取图片后的操作
  57. return Handlers.fileChanged.call(this, currentResolve)
  58. })
  59. }
  60. }
  61. })
  62. }

上面代码为什么要用一个变量(currentResolve)呢 ?

  • 当我们第二次调用 pickImg 方法时 ,产生新的Promise,新的resolve,但是 “相册相机按钮”节点已经绑定过“click,change”事件了,所以不再重新绑定。
  • 这样在事件绑定函数中传入的回调函数参数(resolve),还是第一次传入的resolve,导致第二次调用 pickImg 没有返回结果,也就是 Handler.pickImg() 后面的then不再执行
  • 如果我们每次调用将最新的resolve赋值给一个变量 currentResolve,事件回调函数执行时,将currentResolve当作回调函数的参数传下去,执行完调用currentResolve返回结果。
  • 通过上一步的赋值操作,使的 pickImg 都有返回值,在then中获取

站内获取图片后操作

  1. /*
  2. 站内 拍照、选图的回调
  3. */
  4. static eventCameraCallback(res, type, resolve){
  5. if (res.success) {
  6. let img = new Image()
  7. img.onload = function () {
  8. let imageType = this.src.split(",")[0].split(";")[0].split(":")[1].toLowerCase();
  9. let imageTypeCheck = imageType.includes("jpg") || imageType.includes("jpeg") || imageType.includes("png")
  10. if (!imgSupport.validationImageSize(this.width, this.height)) {
  11. clearTimeout(setTimer);
  12. return resolve({errMsg: '图片过小,请重选照片'})
  13. } else if (!imageTypeCheck) {
  14. clearTimeout(setTimer);
  15. return resolve({errMsg: '图片格式不符合'})
  16. }
  17. // 旋转图片的浏览器兼容处理,合理限制图片大小不操过3000 ,下文展开
  18. // Handlers.renderFileChangedImg
  19. }
  20. img.src = res.base64Image;
  21. img.onerror = function () {
  22. return resolve({errMsg: '请检查网络连接'})
  23. }
  24. } else {
  25. resolve()
  26. }
  27. }

站外获取图片后操作

  1. // 站外 选图的回调
  2. static fileChanged(resolve){
  3. loading()
  4. if (this.files.length <= 0){
  5. return resolve()
  6. }
  7. let img = new Image()
  8. img.onload = function () {
  9. if (!imgSupport.validationImageSize(this.width, this.height)) {
  10. return resolve({errMsg: '图片尺寸或比例不符合'})
  11. }
  12. // 旋转图片的浏览器兼容处理,合理限制图片大小不操过3000 ,下文展开
  13. // Handlers.renderFileChangedImg
  14. }
  15. img.src = imgSupport.inputPath2url(this.files[0])
  16. }

获取后图片旋转缩放处理

图片旋转判断实现

  1. /**
  2. * 原理是:原图的是一张 1 * 2 ,oritation为6的图片(逆时针旋转90deg)的图片,
  3. * ios高 获取的图片尺寸 width为2,height为1,
  4. * ios低和android获取的图片尺寸 width为1,height为2,实现代码如下
  5. * 用一张特殊的图片来检测当前浏览器是否对带 EXIF 信息的图片进行回正
  6. * */
  7. static detectImageAutomaticRotation() {
  8. const testAutoOrientationImageURL =
  9. 'data:image/jpeg;base64,/9j/4QAiRXhpZgAATU0AKgAAAAgAAQESAAMAAAABAAYAAAA' +
  10. 'AAAD/2wCEAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA' +
  11. 'QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE' +
  12. 'BAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAf/AABEIAAEAAgMBEQACEQEDEQH/x' +
  13. 'ABKAAEAAAAAAAAAAAAAAAAAAAALEAEAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAAAAAAAAA' +
  14. 'AAAAAEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwA/8H//2Q==';
  15. let isImageAutomaticRotation;
  16. return new Promise((resolve) => {
  17. const img = new Image();
  18. img.onload = () => {
  19. // 如果图片变成 1x2,说明浏览器对图片进行了回正
  20. isImageAutomaticRotation = img.width === 1 && img.height === 2;
  21. resolve(isImageAutomaticRotation);
  22. };
  23. img.src = testAutoOrientationImageURL;
  24. });
  25. }

图片旋转的浏览器兼容处理

  • 判断浏览器是否对旋转图片已纠正
  • 对图片进行纠正

    1. // 处理旋转图片的兼容处理 、 图片渲染为合适尺寸
    2. static renderFileChangedImg(img, distImg) {
    3. return new Promise(function (resolve) {
    4. Handlers.detectImageAutomaticRotation().then(res=>{
    5. // res 为 true 则浏览器对图片进行了回正,canvas操作的是纠正后的图片
    6. // 这里 android没进行旋转修复,是因为foodie android的jsBridge返回的图片
    7. // 获取不到旋转信息,不执行getData的回调函数
    8. if(res || BrowserChecker.isAndroid()){
    9. // Handlers.fixedImgOAS 设置图片尺寸,下文解析
    10. Handlers.fixedImgOAS(img, {
    11. maxWidth: 3000,
    12. maxHeight: 3000,
    13. }).then((canvas)=>{
    14. resolve(canvas)
    15. })
    16. }else{
    17. EXIF.getData(img, () => {
    18. var allMetaData = EXIF.getAllTags(img);
    19. var orientation = allMetaData.Orientation;
    20. // Handlers.fixedImgOAS 设置图片尺寸,下文解析
    21. Handlers.fixedImgOAS(img, {
    22. maxWidth: 3000,
    23. maxHeight: 3000,
    24. orientation: orientation
    25. }).then((canvas)=>{
    26. resolve(canvas)
    27. })
    28. })
    29. }
    30. })
    31. })
    32. }

    上面代码中 android没进行旋转修复,是因为foodie android的jsBridge返回的图片获取不到旋转信息,不执行getData的回调函数

    纠正旋转,调整图片尺寸

    ```javascript /**

    • 原名:fixedImageOritationAndSize
    • @param image 已加载的图片原生节点
    • @param options 配置 可选参数 width,height,maxWidth,maxHeight,oritation
    • @return {Promise} */ static fixedImgOAS(image,options){ return new Promise(resolve => {

      1. options = options || {};
      2. // 通过options 中的width,height,maxWidth,maxHeight,及图片的原始尺寸, 获取最终的width,height 值
      3. let imgWidth = image.naturalWidth, imgHeight = image.naturalHeight,
      4. width = options.width, height = options.height,
      5. maxWidth = options.maxWidth, maxHeight = options.maxHeight;
      6. if (width && !height) {
      7. height = (imgHeight * width / imgWidth) << 0;
      8. } else if (height && !width) {
      9. width = (imgWidth * height / imgHeight) << 0;
      10. } else {
      11. width = imgWidth;
      12. height = imgHeight;
      13. }
      14. if (maxWidth && width > maxWidth) {
      15. width = maxWidth;
      16. height = (imgHeight * width / imgWidth) << 0;
      17. }
      18. if (maxHeight && height > maxHeight) {
      19. height = maxHeight;
      20. width = (imgWidth * height / imgHeight) << 0;
      21. }
      22. var opt = {};
      23. for (var k in options) opt[k] = options[k];
      24. opt.width = width;
      25. opt.height = height;
      26. let canvas = document.createElement('canvas');
      27. let ctx = canvas.getContext('2d');
      28. ctx.save();
      29. // 设置canvas 的尺寸并旋转偏移canvas,下边附有代码
      30. transformCoordinate(canvas, ctx, width, height, options.orientation);
      31. ctx.drawImage(image,0,0,imgWidth,imgHeight,0,0,width,height);
      32. ctx.restore();
      33. resolve(canvas);

      }) }

/**

  • 根据指定的尺寸和方向变换画布
  • @param canvas
  • @param ctx
  • @param width
  • @param height
  • @param orientation */ function transformCoordinate(canvas, ctx, width, height, orientation) { switch (orientation) {
    1. case 5:
    2. case 6:
    3. case 7:
    4. case 8:
    5. canvas.width = height;
    6. canvas.height = width;
    7. break;
    8. default:
    9. canvas.width = width;
    10. canvas.height = height;
    } switch (orientation) {
    1. case 2:
    2. // horizontal flip
    3. ctx.translate(width, 0);
    4. ctx.scale(-1, 1);
    5. break;
    6. case 3:
    7. // 180 rotate left
    8. ctx.translate(width, height);
    9. ctx.rotate(Math.PI);
    10. break;
    11. case 4:
    12. // vertical flip
    13. ctx.translate(0, height);
    14. ctx.scale(1, -1);
    15. break;
    16. case 5:
    17. // vertical flip + 90 rotate right
    18. ctx.rotate(0.5 * Math.PI);
    19. ctx.scale(1, -1);
    20. break;
    21. case 6:
    22. // 90 rotate right
    23. ctx.rotate(0.5 * Math.PI);
    24. ctx.translate(0, -height);
    25. break;
    26. case 7:
    27. // horizontal flip + 90 rotate right
    28. ctx.rotate(0.5 * Math.PI);
    29. ctx.translate(width, -height);
    30. ctx.scale(-1, 1);
    31. break;
    32. case 8:
    33. // 90 rotate left
    34. ctx.rotate(-0.5 * Math.PI);
    35. ctx.translate(-width, 0);
    36. break;
    37. default:
    38. break;
    } } ```

应用:

  1. $('.reUpBtn, .selectArea').on('click',()=>{
  2. Handlers.pickImg()
  3. .then(async img => {
  4. if (!!img == false) {
  5. return loading({state: 0})
  6. } else if (img.errMsg) {
  7. return errMsg({text: img.errMsg})
  8. }
  9. // 拿到获取到的图片
  10. uploadFlag = true; // 点击上传按钮,可以上传图片了
  11. let $dragImg = $('.dragImg'),dragImg = $dragImg[0]; // dragImg 进行拖动缩放移动操作的图片
  12. // 图片附值
  13. canvas.toBlob((blob)=>{
  14. let url = URL.createObjectURL(blob);
  15. dragImg.src = url;
  16. dragImg.onload = function () {
  17. URL.revokeObjectURL(url);
  18. // 图片 拖拽操作实现,下文讲解
  19. await Handlers.ezGesture($('.showPicArea'),$dragImg,$('.selectArea'));
  20. $('.dragArea').css('opacity', 1)
  21. loading({state: 0})
  22. }
  23. })
  24. $('.dragTip').css('display','none'); // ➕号按钮隐藏
  25. })
  26. })

图片拖拽缩放操作

概述:

  • 图片加载完后,立刻居中显示
  • 去掉了配置,targetMinWidth,targetMinHeight设置为操作层的尺寸
  • EZGesture库,将containerDom(操作层)的尺寸内置在构造函数中,位置改变通过transform来实现,代替position,因为top和left的改变会触发浏览器的 reflow和 repaint 。然后整个动画过程都在不断触发浏览器的重新渲染,这个过程是很影响性能的。transform 动画由GPU控制,支持硬件加速,不重新绘制。(https://zhuanlan.zhihu.com/p/78230297
  • 原来的定位实现是相对于.page节点的,如果不小心给其他父容器设置position值,会影响实现。

    实现:

    ```javascript /**
    • 给图片绑定缩放和平移操作
    • @param $showPicArea // 操作平面层
    • @param $dragImg // 操作的图片 */ static ezGesture($showPicArea,$dragImg){ let dragImgRatio = ($dragImg.width() / $dragImg.height()).toFixed(3); //clientWidth 获取节点除边框外的尺寸,没有单位度量 let showPicAreaRatio = ($showPicArea[0].clientWidth / $showPicArea[0].clientHeight).toFixed(3); // cropGesture && cropGesture.unbindEvents(); //取消上次的监听 if(dragImgRatio > showPicAreaRatio){
      1. $dragImg.css({'width': 'auto', 'height': '100%'});
      2. setTimeout(function () {
      3. $dragImg.css({'transform':`translate(${-($dragImg.width() - $showPicArea[0].clientWidth)>>1}px,0)`});
      4. },50)
      }else{
      1. $dragImg.css({'height': 'auto', 'width': '100%'});
      2. setTimeout(function () {
      3. $dragImg.css({'transform':`translate(0,${-($dragImg.height() - $showPicArea[0].clientHeight)>>1}px)`});
      4. },50)
      } new EZGesture($showPicArea[0],$dragImg[0]); }

//EZGesture库: // 手势框 // 不要给drop图片加transition /**

  • pageX,pageY 相对于HTML文档的距离
  • clientX,clientY 返回触点相对于可见视区(visual viewport)左边沿的的X坐标。不包括任何滚动偏移.这个值会根据用户对可见视区的缩放行为而发生变化.
  • screenX,screenY 触点相对于屏幕左边沿的X坐标,不包含页面滚动的偏移量
  • element.getBoundingClientRect() 方法返回元素的大小及其相对于可见视区的位置。
  • */ (function(){ let supportTouch = (“ontouchend” in document); // let supportTouch = (‘createTouch’ in document); // 阻止事件冒泡 function preventEventPropagation(evt) {

    1. evt.preventDefault();
    2. evt.stopPropagation();
    3. return false;

    }

    /**

    • 获取节点的transform值
    • @param Node
    • @return {{translate: {x: number, y: number}, scale: number}} */ function _getTransform(Node) { let str = Node.style.transform; str = str.trim(); let arr = str.split(‘) ‘); let obj = {translate:{x:0,y:0},scale:1}; for(let i =0,j = arr.length; i <j ; i++){

      1. if(arr[i].trim().startsWith('translate')){
      2. let pos = arr[i].split('(')[1].split(',');
      3. obj.translate = {
      4. x:parseFloat(pos[0]),
      5. y:parseFloat(pos[1])
      6. }
      7. }else if(arr[i].trim().startsWith('scale')){
      8. let sca = arr[i].split('(')[1];
      9. obj.scale = parseFloat(sca)
      10. }

      } return obj }

      // 手势开始触发 function gestureTouchStart(evt) { let touches = evt.touches || evt.originalEvent.touches; let touch = touches[0]; // 获取第一个点 相对于 HTML文档左上角的位置 let offset = {

      1. 'x': touch.clientX,
      2. 'y': touch.clientY

      }; // 如果触摸点 大于等于 2指 if (touches.length >= 2) {

      1. // 获取第二点的
      2. let touch2 = touches[1];
      3. // 获取第二个点 相对于 HTML文档左上角的位置
      4. let offset2 = {
      5. 'x': touch2.clientX,
      6. 'y': touch2.clientY
      7. };
      8. this.gesturePinchStart([offset, offset2]);

      } else {

      1. this.gesturePanStart(offset);

      }

      return preventEventPropagation(evt); } // 手势移动 function gestureTouchMove(evt) { let touches = evt.touches || evt.originalEvent.touches; let touch = touches[0]; // 平移过程中第一个点在可视区域的坐标 let offset = {

      1. 'x': touch.clientX,
      2. 'y': touch.clientY

      };

      if (touches.length >= 2) {

      1. // 如果大于一个触摸点,则获取第二个点坐标
      2. let touch2 = touches[1];
      3. let offset2 = {
      4. 'x': touch2.clientX,
      5. 'y': touch2.clientY
      6. };
      7. this.gesturePinchChange([offset, offset2]);

      } else {

      1. this.gesturePanMove(offset);

      }

      return preventEventPropagation(evt); } // 手势结束 function gestureTouchEnd(evt) { this.gesturePanEnd(); this.gesturePinchEnd(); return preventEventPropagation(evt); } // 鼠标开始点击 function gestureMouseDown(evt) { let offset = {

      1. 'x': evt.clientX,
      2. 'y': evt.clientY

      }; this.gesturePanStart(offset);

      return preventEventPropagation(evt); } // 鼠标移动 function gestureMouseMove(evt) { let offset = {

      1. 'x': evt.clientX,
      2. 'y': evt.clientY

      }; this.gesturePanMove(offset); return preventEventPropagation(evt); } // 鼠标抬起 function gestureMouseUp(evt) { this.gesturePanEnd(); return preventEventPropagation(evt); } // 平移开始 offset 触摸点在页面坐标的位置 function gesturePanStart(offset) {

      // 禁止缩放过程操作 this.gesturePinchEnabled = false; // 记录开始坐标点 this.gesturePanFrom = offset;

      let transform = _getTransform(this.targetDom); this.gesturePanOrigin.x = transform.translate.x; this.gesturePanOrigin.y = transform.translate.y;

      // 开启移动操作 this.gesturePanEnabled = true;

      return false; } // 手势平移过程 function gesturePanMove(offset) { // offset 获取移动过程中点在可视区域的坐标 if (this.gesturePanEnabled) {

      1. let targetOriginX = ~~(this.gesturePanOrigin.x + offset.x - this.gesturePanFrom.x);
      2. let targetOriginY = ~~(this.gesturePanOrigin.y +offset.y - this.gesturePanFrom.y);
      3. this.targetDom.style.transform = `translate(${targetOriginX}px,${targetOriginY}px)`;

      } return false; } // 手势平移结束 function gesturePanEnd() { // $(‘.selectArea’).css(‘opacity’, 0) if (this.gesturePanEnabled) {

      1. let targetRect = this.targetDom.getBoundingClientRect();
      2. let targetOriginX = targetRect.left - this.containerRect.left;
      3. let targetOriginY = targetRect.top - this.containerRect.top;
  1. // 如果图片距离框左边有空白区域
  2. if (targetOriginX > 0) {
  3. targetOriginX = 0;
  4. } else {
  5. let targetWidth = targetRect.width;
  6. let containerWidth = this.containerRect.width;
  7. // 在图片左边隐藏的条件下,如果 右边有空白区域
  8. if ((targetOriginX + targetWidth) < containerWidth) {
  9. targetOriginX = containerWidth - targetWidth;
  10. }
  11. }
  12. if (targetOriginY > 0) {
  13. targetOriginY = 0;
  14. } else {
  15. let targetHeight = targetRect.height;
  16. let containerHeight = this.containerRect.height;
  17. if ((targetOriginY + targetHeight) < containerHeight) {
  18. targetOriginY = containerHeight - targetHeight;
  19. }
  20. }
  21. this.targetDom.style.transform = `translate(${targetOriginX}px,${targetOriginY}px)`;
  22. this.gesturePanEnabled = false;
  23. }
  24. return false;
  25. }
  26. // 手势缩放开始
  27. function gesturePinchStart(offsets) {
  28. // 禁用平移功能
  29. this.gesturePanEnabled = false;
  30. // 两个点 X轴方向的距离, Y轴方向的距离
  31. let distanceX = Math.abs(offsets[1].x - offsets[0].x);
  32. let distanceY = Math.abs(offsets[1].y - offsets[0].y);
  33. // 两个触摸点的长度值
  34. this.gesturePinchFrom = Math.sqrt(distanceX * distanceX + distanceY * distanceY);
  35. if (this.gesturePinchFrom > 0) {
  36. // 获取拖拽元素的尺寸
  37. let targetRect = this.targetDom.getBoundingClientRect();
  38. let centerX = (offsets[0].x + offsets[1].x) * 0.5 - targetRect.left;
  39. let centerY = (offsets[0].y + offsets[1].y) * 0.5 - targetRect.top;
  40. // 缩放中心点在原图中的比例
  41. this.gesturePinchOrigin.x = centerX / targetRect.width;
  42. this.gesturePinchOrigin.y = centerY / targetRect.height;
  43. this.gesturePinchSize.width = targetRect.width;
  44. this.gesturePinchSize.height = targetRect.height;
  45. this.gesturePinchEnabled = true;
  46. }
  47. return false;
  48. }
  49. // 手势缩放过程
  50. //
  51. function gesturePinchChange(offsets) {
  52. if (this.gesturePinchEnabled) {
  53. // 两个点 X轴方向的距离, Y轴方向的距离
  54. let distanceX = Math.abs(offsets[1].x - offsets[0].x);
  55. let distanceY = Math.abs(offsets[1].y - offsets[0].y);
  56. // 触摸过程中,两个触摸点的距离
  57. let gesturePinchTo = Math.sqrt(distanceX * distanceX + distanceY * distanceY);
  58. // 缩放的比例
  59. let scale = gesturePinchTo / this.gesturePinchFrom;
  60. // 设置大小
  61. let targetWidth = ~~ this.gesturePinchSize.width * scale;
  62. let targetHeight = ~~ this.gesturePinchSize.height * scale;
  63. // 获取移动过程中的中心点
  64. let centerX = ~~((offsets[0].x + offsets[1].x) * 0.5 - this.containerRect.left);
  65. let centerY = ~~((offsets[0].y + offsets[1].y) * 0.5 - this.containerRect.top);
  66. let targetOriginX = ~~((centerX - targetWidth * this.gesturePinchOrigin.x));
  67. let targetOriginY = ~~((centerY - targetHeight * this.gesturePinchOrigin.y));
  68. this.targetDom.style.width = `${targetWidth}px`;
  69. this.targetDom.style.height = `${targetHeight}px`;
  70. this.targetDom.style.transform = `translate(${targetOriginX}px,${targetOriginY}px)`;
  71. }
  72. return false;
  73. }
  74. // 手势缩放过程
  75. function gesturePinchEnd() {
  76. if (this.gesturePinchEnabled) {
  77. let targetRect = this.targetDom.getBoundingClientRect();
  78. let targetOriginX = targetRect.left - this.containerRect.left;
  79. let targetOriginY = targetRect.top - this.containerRect.top;
  80. let targetWidth = targetRect.width;
  81. let targetHeight = targetRect.height;
  82. let dragImgRatio = (targetWidth / targetHeight).toFixed(3);
  83. //clientWidth 获取节点除边框外的尺寸,没有单位度量
  84. let showPicAreaRatio = (this.containerDom.clientWidth / this.containerDom.clientHeight).toFixed(3);
  85. if(dragImgRatio > showPicAreaRatio){
  86. if(targetHeight < this.containerDom.clientHeight){
  87. targetHeight = this.containerDom.clientHeight;
  88. targetWidth = ~~(targetHeight * dragImgRatio);
  89. targetOriginX = -(targetWidth - this.containerDom.clientWidth)>>1;
  90. targetOriginY = 0;
  91. }
  92. } else {
  93. if(targetWidth < this.containerDom.clientWidth){
  94. targetWidth = this.containerDom.clientWidth;
  95. targetHeight = ~~(targetWidth / dragImgRatio);
  96. targetOriginX = 0;
  97. targetOriginY = -(targetHeight - this.containerDom.clientHeight)>>1;
  98. }
  99. }
  100. if (targetOriginX > 0) {
  101. targetOriginX = 0;
  102. } else {
  103. let containerWidth = this.containerRect.width;
  104. if ((targetOriginX + targetWidth) < containerWidth) {
  105. targetOriginX = containerWidth - targetWidth;
  106. }
  107. }
  108. if (targetOriginY > 0) {
  109. targetOriginY = 0;
  110. } else {
  111. let containerHeight = this.containerRect.height;
  112. if ((targetOriginY + targetHeight) < containerHeight) {
  113. targetOriginY = containerHeight - targetHeight;
  114. }
  115. }
  116. this.targetDom.style.width = `${targetWidth}px`;
  117. this.targetDom.style.height = `${targetHeight}px`;
  118. this.targetDom.style.transform = `translate(${targetOriginX}px,${targetOriginY}px)`;
  119. this.gesturePinchEnabled = false;
  120. }
  121. return false;
  122. }
  123. // 绑定事件
  124. function bindEvents() {
  125. let self = this;
  126. if (supportTouch) {
  127. self.containerDom.ontouchstart = function(evt){ self.gestureTouchStart(evt); return preventEventPropagation(evt); };
  128. self.containerDom.ontouchmove = function(evt){ self.gestureTouchMove(evt); return preventEventPropagation(evt); };
  129. self.containerDom.ontouchend = function(evt){ self.gestureTouchEnd(evt); return preventEventPropagation(evt); };
  130. self.containerDom.ontouchcancel = function(evt){ self.gestureTouchEnd(evt); return preventEventPropagation(evt); };
  131. } else {
  132. self.containerDom.onmousedown = function(evt){ self.gestureMouseDown(evt); return preventEventPropagation(evt); };
  133. self.containerDom.onmousemove = function(evt){ self.gestureMouseMove(evt); return preventEventPropagation(evt); };
  134. self.containerDom.onmouseup = function(evt){ self.gestureMouseUp(evt); return preventEventPropagation(evt); };
  135. self.containerDom.onmouseout = function(evt){ self.gestureMouseUp(evt); return preventEventPropagation(evt); };
  136. }
  137. }
  138. // 解绑事件
  139. function unbindEvents() {
  140. let self = this;
  141. if (supportTouch) {
  142. self.containerDom.ontouchstart = null;
  143. self.containerDom.ontouchmove = null;
  144. self.containerDom.ontouchend = null;
  145. } else {
  146. self.containerDom.onmousedown = null;
  147. self.containerDom.onmousemove = null;
  148. self.containerDom.onmouseup = null;
  149. }
  150. }
  151. // 手势类
  152. let EZGestureClass = function(containerDom, targetDom){
  153. this.containerDom = containerDom;
  154. this.targetDom = targetDom;
  155. this.gesturePanEnabled = false; // 平移开关
  156. this.gesturePanFrom = {x:0, y:0}; // 平移开始时,触摸点在浏览器可视区域的位置
  157. this.gesturePanOrigin = {x:0, y:0};// 平移开始时,原来的位置
  158. this.gesturePinchEnabled = false; // 缩放开关
  159. this.gesturePinchFrom = 0; //开始缩放,两个触摸点的距离
  160. this.gesturePinchOrigin = {x:0, y:0}; // 图片再缩放前,缩放中心点在图片中的比例
  161. this.gesturePinchSize = {width:0, height:0}; // 开始缩放时 targetDom 的尺寸
  162. this.containerRect = this.containerDom.getBoundingClientRect(); // containerDom 的尺寸
  163. this.bindEvents();
  164. }
  165. EZGestureClass.prototype.gestureTouchStart = gestureTouchStart;
  166. EZGestureClass.prototype.gestureTouchMove = gestureTouchMove;
  167. EZGestureClass.prototype.gestureTouchEnd = gestureTouchEnd;
  168. EZGestureClass.prototype.gestureMouseDown = gestureMouseDown;
  169. EZGestureClass.prototype.gestureMouseMove = gestureMouseMove;
  170. EZGestureClass.prototype.gestureMouseUp = gestureMouseUp;
  171. EZGestureClass.prototype.gesturePanStart = gesturePanStart;
  172. EZGestureClass.prototype.gesturePanMove = gesturePanMove;
  173. EZGestureClass.prototype.gesturePanEnd = gesturePanEnd;
  174. EZGestureClass.prototype.gesturePinchStart = gesturePinchStart;
  175. EZGestureClass.prototype.gesturePinchChange = gesturePinchChange;
  176. EZGestureClass.prototype.gesturePinchEnd = gesturePinchEnd;
  177. EZGestureClass.prototype.bindEvents = bindEvents;
  178. EZGestureClass.prototype.unbindEvents = unbindEvents;
  179. window.EZGesture = EZGestureClass;

})();

  1. <a name="oRJk8"></a>
  2. ## 应用:
  3. - 拿到图片后,赋值给dragImg,onload之后,执行Handlers.ezGesture
  4. ```javascript
  5. Handlers.pickImg()
  6. .then(async canvas => {
  7. // canvas 旋转和最大尺寸处理后的canvas
  8. if (!!canvas == false) {
  9. return loading({state: 0})
  10. } else if (canvas.errMsg) {
  11. return errMsg({text: canvas.errMsg})
  12. }
  13. // 点击上传按钮,可以上传图片了
  14. uploadFlag = true;
  15. let $dragImg = $('.dragImg'),dragImg = $dragImg[0];
  16. canvas.toBlob((blob)=>{
  17. let url = URL.createObjectURL(blob);
  18. dragImg.src = url;
  19. dragImg.onload = async function () {
  20. URL.revokeObjectURL(url);
  21. Handlers.ezGesture($('.selectArea'),$dragImg);
  22. $('.dragArea').css('opacity', 1)
  23. loading({state: 0})
  24. }
  25. })
  26. $('.dragTip').css('display','none');
  27. })

图片拖拽缩放后区域截取

上传服务器,返回图片