在之前项目整理中,实现过这样一个特效:当用手指滑动时,焦点图随着滑动的距离而成比例的缩放的效果,常见于一些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用到了很多移动端事件和相关的动画处理,这里不一一赘述,重要的是提供一种解决问题的思路。