ionic中滑動縮放的焦點圖特效實現
阿新 • • 發佈:2019-01-02
在之前專案整理中,實現過這樣一個特效:當用手指滑動時,焦點圖隨著滑動的距離而成比例的縮放的效果,常見於一些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用到了很多移動端事件和相關的動畫處理,這裡不一一贅述,重要的是提供一種解決問題的思路。