1. 程式人生 > >ionic中滑動縮放的焦點圖特效實現

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用到了很多移動端事件和相關的動畫處理,這裡不一一贅述,重要的是提供一種解決問題的思路。