ionic中滑动缩放的焦点图特效实现

在之前项目整理中,实现过这样一个特效:当用手指滑动时,焦点图随着滑动的距离而成比例的缩放的效果,常见于一些App上,主要是用于展示信息的卡片,相关技术栈版本,ionic1, angular1, 这里再来说一下:技术栈不同,但实现的思路是想通的,仅供开发参考。

效果预览

这里写图片描述

这里由于gif生成工具生成的图片很大,没有展示全部的功能,demo上可以切换不同类型的杂志以及一些切换的特效,并且如果是在线的图片,代码内还进行了懒加载处理。

github演示地址

View层布局

<ion-view class="carousel" cache-view="false">
  <ion-header-bar class="bar-stable">
    <button ng-click="back()">
      <i class="ion-arrow-left-c"></i> Back
    </button>
    <h1 class="title">缩放Banner</h1>
  </ion-header-bar>
  <ion-content scroll="false" class="no-scroll-bar" overflow-scroll="true" data-tap-disable="true" scrollbar-y="false"
               has-bouncing="false">
    <!-- 轮播部分 -->
    <div class="magazine-slider" ng-class="{'cur':overRate}">
      <div carousel-slider data-vol-list="volList"></div>
    </div>
    <!-- loading部分 -->
    <ion-spinner class="center-center loading-bubbles" icon="bubbles" ng-if="isLoading"></ion-spinner>
  </ion-content>
  <!-- 杂志层 -->
  <div class="magazine-wrap">
    <div class="magazine-cate" ng-class="{cur:spread}" on-tap="spread=!spread">
      <span class="magazine-cate-title">杂志·{{curMag}}</span>
      <!-- 两个三角 -->
      <span class="magazine-arr-wrap" ng-class="{cur:spread}">
                <span class="ion-ios-arrow-down"></span>
                <span class="ion-ios-arrow-up"></span>
            </span>
    </div>
    <!-- 下拉菜单 -->
    <div class="magazine-cate-cont" ng-class="{cur:spread,'bd-t':spread}">
      <ion-scroll has-bouncing="true" scrollbar-y="true" direction="y" style="width: 100%; height: 100%"
                  overflow-scroll="false" class="scroll-content">
        <ul>
          <li ng-repeat="item in magList track by $index" on-tap="fn.switchVols($index,item)">
            {{item.magName}}
          </li>
        </ul>
      </ion-scroll>
    </div>
  </div>
  <!-- 灰层 -->
  <div class="gray-layer" ng-if="spread" ng-click="fn.hideGray()"></div>
</ion-view>

Controller逻辑

.controller('MarouselCtrl', [
    '$scope',
    '$timeout',
    'appUtils',
    'CarouselData',
    function ($scope, $timeout, appUtils, CarouselData) {
      /* 初始化数据模型 */
      $scope.back = appUtils.back;
      $scope.magList = []; // 杂志列表
      $scope.volList = []; // 期列表数据
      $scope.curMag = '最新'; // 默认杂志 : 最新
      $scope.isLoading = true; // loading 默认 true
      var fn = $scope.fn = {};

      /* 进入视图,收起 */
      $scope.$on('$ionicView.beforeEnter', function () {
        $scope.spread = false; // 默认不展开
      });

      pageInit();

      /* 页面初始化 */
      function pageInit() {
        loadingHide(); // loading 效果
        getMagList(); // 获取杂志列表
        getLatest(); // 获取最新杂志
      }

      /* 图片质量较大 添加延迟隐藏方法 */
      function loadingHide() {
        var t = $timeout(function () {
          $scope.isLoading = false;
          $timeout.cancel(t); // 去除延定时器
        }, 300);
      }

      /* 获取杂志列表 */
      function getMagList() {
        var json = {
          "magName": "最新"
        };
        $scope.magList.push(json);
        $scope.magList = $scope.magList.concat(CarouselData.magList);
      }

      /* 获取最新期 */
      function getLatest() {
        $scope.volList = CarouselData.latest; // 最新期数据
      }

      /* 根据杂志code获取该杂志的期 */
      function getVolByMagCode(magCode) {
        switch (magCode) {
          case "ECON":
            $scope.volList = CarouselData.ecoList;
            break;
          case "BIOL":
            $scope.volList = CarouselData.bioList;
            break;
          case "COMP":
            $scope.volList = CarouselData.comList;
            break;
          default:
            console.log("not match");
            $scope.volList = CarouselData.latest; // 分配给最新期
        }
      }

      /* 隐藏灰层 */
      fn.hideGray = function() {
        $scope.spread = false;
      }

      /* 杂志的点击 */
      fn.switchVols = function (index, item) {
        $scope.spread = false; // 默认收起
        $scope.isLoading = $scope.curMag !== item.magName; // 表示切换了
        // 没有loading ,不去请求数据
        if (!$scope.isLoading) return;
        // 有loading, 设置效果
        loadingHide();
        // 点击第一个获取最新数据
        if (!index) {
          $scope.curMag = '最新';
          return getLatest();
        }
        getVolByMagCode(item.magCode);
        $scope.curMag = item.magName;
      }
    }
  ]);

Directive指令

.directive('carouselSlider', function (appUtils, $compile, $timeout) {
      return {
        restrict: 'A',
        scope: {
          volList: '=',
          charShow: '='
        },
        template: '<div class="slider-wrap"></div>',
        link: function (scope) {
          // 用于挂载在外部的变量, 用于处理屏幕变化的变量
          scope.outWatcher = {};

          // 所有设置函数
          function setUp() {
            // 针对宽高比的判断
            // 进行轮播图的 dom 生成操作
            var $ = angular.element; // jqLite 对象
            var slideBox = scope.outWatcher.slideBox = document.querySelector('.slider-wrap'); // 获取轮播盒子对象
            var sliderInner = scope.outWatcher.sliderInner = document.createElement('ul');
            sliderInner.className = 'slider-wrap-inner';
            slideBox.appendChild(sliderInner);

            // 杂志点击的回调 可用于其他逻辑处理
            scope.magClick = function (title) {
              console.log(title);
            };

            // 缩放的动画
            function scale(obj, rate) {
              if (!obj) return;
              obj.style.transform = "scale(" + rate + ")";
              obj.style.webkitTransform = "scale(" + rate + ")";
            }

            // 获取数据
            function getData(list, callback) {
              // 通过获得的数据,生成节点操作
              for (var i = 0; i < list.length; i++) {
                var li = document.createElement('li');
                var img = document.createElement('img');
                var imgWrap = document.createElement('div');
                imgWrap.className = 'img-wrap';
                img.src = 'images/transparent.gif';
                // 首先先加载前三张图片的地址,其他的作懒加载处理
                if (i < 3) {
                  img.setAttribute('style', 'background-image:url(' + list[i].coverimg + ')');
                }
                imgWrap.appendChild(img);
                li.appendChild(imgWrap);
                li.setAttribute('on-tap', 'magClick("' + list[i].title + '")'); // 绑定事件
                $(sliderInner).append($(li));
              }
              var htmlObj = $compile($(sliderInner).html())(scope); // 对html 进行重新编译
              $(sliderInner).html(''); // 清空
              $(sliderInner).append(htmlObj); // 追加
              var lis = sliderInner.querySelectorAll('li'); // 得到当前的所有li对象
              var imgs = scope.outWatcher.imgs = []; // 用于存放图像包裹节点
              // 图像包裹节点数组, 初始化样式
              for (var k = 0; k < lis.length; k++) {
                if (!k) {
                  imgs.push(lis[0].querySelector('.img-wrap')); // 第一个只 push 进去 ,不 设置样式
                  continue;
                }
                var item = lis[k].querySelector('.img-wrap');
                scale(item, 252 / 291); // 样式初始化缩放
                imgs.push(item); // 并push
              }
              callback && angular.isFunction(callback) && callback(imgs, list); // 将数据通过callback带走
            }

            // 针对杂志切换,数据同时切换
            scope.$watch('volList', function (now) {
              if (now && now.length) {
                sliderInner.innerHTML = ''; // 先清空内容
                // 使用$timeout来解决宽度问题,重新渲染dom.
                $timeout(function(){
                  getData(now, function (imgs, now) {
                    var m = new MobileMove(); // 重新new
                    m.setSwipe(slideBox, sliderInner, imgs, now);
                  });
                });
              }
            });
          }

          // 监听屏幕变化事件, 随时构造对象
          window.onresize = function() {
            var m = new MobileMove(); // 重新new
            m.setSwipe(scope.outWatcher.slideBox, scope.outWatcher.sliderInner, scope.outWatcher.imgs, scope.volList);
          }

          // 页面加载完成后执行
          var contentLoaded = scope.$watch('$viewContentLoaded', function() {
            setUp(); // 全面设置
            contentLoaded(); // 取消 watch
          });
        }
      };
    })

模拟的假数据

.factory('CarouselData', [
      function () {
        return {
          // 杂志列表
          magList: [
            {
              "magName": "经济学",
              "magCode": "ECON"
            },
            {
              "magName": "生物技术",
              "magCode": "BIOL"
            },
            {
              "magName": "计算机科技",
              "magCode": "COMP"
            }
          ],
          // 最新杂志
          latest: [
            {
              "coverimg": "images/carousel/latest01.jpg",
              "title": "民间文学",
              "magCode": "LITE",
            },
            {
              "coverimg": "images/carousel/latest02.jpg",
              "title": "视觉传播",
              "magCode": "COAR"
            },
            {
              "coverimg": "images/carousel/latest03.jpg",
              "title": "光量子器件及通信",
              "magCode": "COMM"
            },
            {
              "coverimg": "images/carousel/latest04.jpg",
              "title": "营养管理",
              "magCode": "FOOD"
            },
            {
              "coverimg": "images/carousel/latest05.jpg",
              "title": "晶体材料",
              "magCode": "MATE"
            }
          ],
          // 经济学杂志
          ecoList:[
            {
              "coverimg": "images/carousel/econ01.jpg",
              "title": "经济增长",
              "magCode": "ECON"
            },
            {
              "coverimg": "images/carousel/econ02.jpg",
              "title": "绿色经济",
              "magCode": "ECON"
            },
            {
              "coverimg": "images/carousel/econ03.jpg",
              "title": "网络经济",
              "magCode": "ECON"
            },
            {
              "coverimg": "images/carousel/econ04.jpg",
              "title": "蓝海战略",
              "magCode": "ECON"
            },
            {
              "coverimg": "images/carousel/econ05.jpg",
              "title": "电子商务市场",
              "magCode": "ECON"
            }
          ],
          // 生物技术
          bioList:[
            {
              "coverimg": "images/carousel/biol01.jpg",
              "title": "农业生物技术",
              "magCode": "BIOL"
            },
            {
              "coverimg": "images/carousel/biol02.jpg",
              "title": "生物能源技术",
              "magCode": "BIOL"
            },
            {
              "coverimg": "images/carousel/biol03.jpg",
              "title": "特色农业生物技术",
              "magCode": "BIOL"
            },
            {
              "coverimg": "images/carousel/biol04.jpg",
              "title": "基因组育种",
              "magCode": "BIOL"
            },
            {
              "coverimg": "images/carousel/biol05.jpg",
              "title": "食品生物技术",
              "magCode": "BIOL"
            }
          ],
          // 计算机
          comList:[
            {
              "coverimg": "images/carousel/comp01.jpg",
              "title": "计算语言学",
              "magCode": "COMP"
            },
            {
              "coverimg": "images/carousel/comp02.jpg",
              "title": "机器学习",
              "magCode": "COMP"
            },
            {
              "coverimg": "images/carousel/comp03.jpg",
              "title": "云计算",
              "magCode": "COMP"
            },
            {
              "coverimg": "images/carousel/comp04.jpg",
              "title": "互联网创新应用",
              "magCode": "COMP"
            },
            {
              "coverimg": "images/carousel/comp05.jpg",
              "title": "移动计算",
              "magCode": "COMP"
            }
          ]
        }
      }
    ]);

用于特效的底层脚本封装

(function (window) {
  var MobileMove = function () {
  };
  MobileMove.prototype = {
    addTransition: function (obj, time) {
      obj.style.transition = "all " + time + "s ease";
      obj.style.webkitTransition = "all " + time + "s ease";
    },
    removeTransition: function (obj) {
      obj.style.transition = "none";
      obj.style.webkitTransition = "none";
    },
    changeTranslateX: function (obj, x) {
      obj.style.transform = "translateX(" + x + "px)";
      obj.style.webkitTransform = "translateX(" + x + "px)";
    },
    transitionEnd: function (obj, callback) {
      /*当是对象的时候绑定事件*/
      if (typeof obj === 'object') {
        obj.addEventListener('transitionEnd', function (e) {
          callback && callback(e);
        }, false);
        obj.addEventListener('webkitTransitionEnd', function (e) {
          callback && callback(e);
        }, false);
      }
    },
    /* 模仿的tap事件 */
    tap: function (obj, callback) {
      /* 点击事件 超过200ms */
      if (typeof  obj !== 'object') return false;
      var startTime = 0,
        isMove = false; // 来标记我们是否移动过
      obj.addEventListener('touchstart', function () {
        startTime = Date.now(); // 取当前时间
      }, false);
      obj.addEventListener('touchmove', function () {
        isMove = true;
      }, false);
      window.addEventListener('touchend', function (e) {
        /* 响应时间小于200ms 并且没有滑动过 */
        if (Date.now() - startTime < 200 && !isMove) {
          callback && callback.apply(obj, e);
        }
        startTime = 0;
        isMove = false;
      }, false);
    },
    /* 缩放动画 */
    scale: function (obj, rate) {
      if (!obj) return;
      obj.style.transform = "scale(" + rate + ")";
      obj.style.webkitTransform = "scale(" + rate + ")";
    },
    /* 缩放动画的还原 */
    scaleBack: function (obj,index) {
      var that = this;
      if (!obj) return;
      for(var i=0;i<obj.length;i++){
        if(i === index){
          that.scale(obj[index],1); // 当前缩放为1
          continue;
        }
        that.scale(obj[i],252/291); // 其他缩放回归默认值
      }
    },
    setSwipe: function (obj, obj_move, imgs, list) {
      if (typeof  obj !== 'object') return false;
      var that = this;
      var num = imgs.length; // 获取当前节点数
      var startX = 0; // 开始你的X的位置
      var endX = 0; // 停止滑动的时候的X的位置
      var distanceX = 0; // 是改变的距离
      var _distanceX = 0; // 算比率时用到
      var index = 0; // 滑动到第几张图片
      var super_width = obj.clientWidth; // 最大的盒子 ,相当于最外面的宽度 或者和 window.innerWidth 相同.
      var width = super_width * (291 / 375); // 图片每次移动的宽度 , 临界距离 291 是 img-wrap 所占宽度 (根据设计图来的比例)

      that.removeTransition(obj_move); // 初始去除过度
      that.changeTranslateX(obj_move, 0); // 初始化X距离

      // 针对事件的监听
      obj.addEventListener('touchstart', function (e) {
        e.preventDefault();
        startX = e.touches[0].clientX;
        if(index < imgs.length -2){
          imgs[index+2].querySelector('img').setAttribute('style', 'background-image:url(' + list[index+2].coverimg + ')');
        }
      }, false);

      obj.addEventListener('touchmove', function (e) {
        e.preventDefault();
        endX = e.touches[0].clientX;
        distanceX = startX - endX; // 获取移动距离

        // distanceX > 0 滑动方向 true => 左滑 无需考虑 0
        _distanceX = distanceX > 0 && distanceX > width ? width : distanceX; // 移动距离>宽度时 ? 移动距离=宽度
        _distanceX = !(distanceX > 0) && distanceX < -width ? -width : distanceX; // 移动距离<-宽度时 ? 移动距离=-宽度
        var rate = Math.abs(_distanceX / width); // 缩放比率

        if (!(distanceX > 0) && !index || distanceX > 0 && index === num - 1) {
          // DO NOTHING 此处做过滤
        } else {
          if (distanceX > 0) {
            // 左滑时缩放
            that.scale(imgs[index], 1 - (1 - 252 / 291) * rate); // 当前的缩小  252/291 或者 344/382 这个是缩放比
            // 下一个放大
            that.scale(imgs[index + 1], (252 / 291 + (1 - 252 / 291) * rate) <1 ? (252 / 291 + (1 - 252 / 291) * rate) : 1); 
          } else {
            // 右滑时缩放
            that.scale(imgs[index], 1 - (1 - 252 / 291) * rate); // 当前的缩小
            that.scale(imgs[index - 1], 252 / 291 + (1 - 252 / 291) * rate); // 上一个放大
          }
        }
        that.removeTransition(obj_move); // 去除过渡
        that.changeTranslateX(obj_move, -index * width - distanceX); // 同步盒子移动
      }, false);
      obj.addEventListener('touchend', function (e) {
        e.preventDefault();
        // 移动结束还原缩放
        if (!(distanceX > 0) && !index || distanceX > 0 && index === num - 1) {
          that.scale(imgs[index], 1);
        }
        /* 满足1/3的时候滑动一次 */
        if (Math.abs(distanceX) > 1 / 3 * width && endX) {
          // 进行 index 加工过滤
          index = distanceX > 0 ? ++index : --index; // 根据方向判断中间值
          index = index <= 0 ? 0 : index;  // 判断第一个时
          index = index >= num - 1 ? num - 1 : index;  // 判断最后一个时
          that.addTransition(obj_move, 0.2); // 加上过渡效果
          that.changeTranslateX(obj_move, -index * width); // 滑动
        } else {
          // 当不满足1/3的时候吸附回去
          that.addTransition(obj_move, 0.2); // 加上过渡效果
          that.changeTranslateX(obj_move, -index * width);
        }
        that.scaleBack(imgs,index); // 恢复原始缩放比

        // 每次滑动结束 , 恢复初始值
        startX = 0;
        endX = 0;
        distanceX = 0;
      }, false);
    }
  };
  // 暴露对象
  window.MobileMove = MobileMove;
})(window);

总结

合理的特效依赖合理的布局设定以及合理的数据结构,上述demo用到了很多移动端事件和相关的动画处理,这里不一一赘述,重要的是提供一种解决问题的思路。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wang's Blog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值