1. 滚动插件的使用缘由
在项目开发中,图片滚动的应用场景特别多。有很多设计依靠滚动来实现。那么此时我们可以选择不同的滚动插件达到相同的效果。但是,外部插件却是是比较优秀,但是同时因为功能性代码太多,根据我们 组的开发情况,我们基本杜绝了外部插件,除非特殊情况必须使用。
而我编写的这个插件,可以实现目前设计中的各种滚动场景,支持自定义滚动距离,只要你是以下结构,你就可以直接将该滚动插件套用。当然,不是说一定要url>li 结构,而是只要是这种列表形式的dom结构都可以。
PC
Mobile
2. 需求驱动开发
我的页面并不是只有一个滚动,而是有很多个滚动效果。因此,并不能将代码写死在具体某一个dom(或者class名称)上。毕竟,在不同的结构中,class名称不可能一模一样。也不能因为滚动,就添加额外的class名称来控制。最简单的就是按照需要,提取公共的参数。
- parent: 滚动列表的父容器,
- children: 具体滚动列表集合,
- scrollStep:每次滑动的宽度,
- currentIndex:当前滚动显示的索引,
- childCls: child列表的class名称,
- isInfiniteScroll:是否需要支持无限滚动,
- paginationMethods:该插件根据需求支持3钟分页按钮,该方法可以自定义需要哪些,不局一个分页,
- paginationStep:每一页显示的个数(总共有4个card,默认4页,值设置为2,那么页数为2页),
- customInitDom:当进行无限循环,每页有多个card, 当最后一个只有1个card,需要将剩下的card使用占位符占位,该值表示使用什么节点进行占位,默认为li
tip: 我们利用CSS transform 来实现移动。
3. 基本结构
下面呈现了该组件的基本结构,其实就是一个 slider 组件类,然后new 了一个组件出来。 我们还在组件内部,进行初始化了一系列的私有变量,每一个变量已近进行了详细注释。看到这里,可能对变量不是很清楚使用,这个没关系,我们后续每隔方法中会使用到。 但是注意 prototype上的 originListDoms 变量,它的作用是:当界面进行大小拖拽时,界面处于PC/Mobile的样式之间来回切换,如果mobile和pc一页显示的数量不一致,例如上面示例图中的情况,那么就需要存储原有的dom,这个参数就是用于存储改值的。
// 组件
function Slider(parent, children, scrollStep, currentIndex, childCls, isInfiniteScroll, paginationMethods, paginationStep, customInitDom) {
// dom 相关参数初始化
paginationStep = paginationStep || 1; // 默认每页1个card
var parent = parent;
var childCount = Math.ceil(children.length / paginationStep); // 多少个card
var scrollStep = scrollStep; // 每次滑动的距离
var currentIndex = currentIndex; // 当前选中的index(分页显示使用或者 正常滑动的索引)
var infiniteCurrentIndex = currentIndex; // 无限滚动的索引(无限滚动时,分页的索引使用currentIndex)
// 位置相关参数初始化
var startPosition, // 滑动开始的位置
endPosition, // 滑动结束位置
deltaX, // 横向滑动距离
deltaY, // 纵向滑动距离
isTouchStartFirst = 1, // 是否第一次touch滚动
isScroll; // isScroll 为0时,表示纵向滑动,1为横向滑动
var isScrollProgress = false; // 是否在滑动中,用于计算滑动时不能点击card
// 如果滑动元素有a标签,滑动中禁止点击
var hrefAs = parent.find('a'); // 计算滑动中,将card link禁用
var isPc = window.iSPc();
this.initMobileOrPcSlider = function() {
}
}
Slider.prototype.originListDoms = {};
//实例化Slider
var slider = new Slider(gallery, children, scrollStep, 0, childCls, isInfiniteScroll, paginationMethods, paginationStep, customInitDom);
slider.initMobileOrPcSlider();
4. 组件初始化
下面进行了伪代码的编写,我们首先进行了事件 dom等一系列的初始化,为什么呢?为了当自适应的时候,pc/mobile样式切换时进行,重置之前所有的参数,方便后续初始化。
为什么需要将 手机滚动事件、pc滚动事件也一起注册,同样是为了应付PC 调整尺寸大小时,为了平板尺寸大小时,能够进行滑动。
this.initMobileOrPcSlider = function() {
// 清除所有的事件,dom初始化等
gallery.siblings('.gallery-pagination').remove();
gallery.siblings('.gallery-pagination-circle').remove();
gallery.css('marginLeft', '0px');
gallery.css('transform', ' translate3d(0px, 0px, 0px)');
// 2. 先卸载移动事件,避免resize时,重复注册
gallery.off('touchstart');
gallery.off('mousedown');
// 是如果无限滚动处,进行dom处理
isInfiniteScroll && infiniteScrollDomInit();
// 注册动画滚动结束事件
initTransitionend();
// 注册手机滑动效果
registerMobileScroll();
// 注册PC 滑动效果
registerPCScroll();
// 动态添加小圆点
if (paginationMethods) {
//初始化自定义的小圆点
getCustomPagination(paginationMethods);
} else {
// 根据pc mobile 初始化小圆点或者分页按钮
isPc ? initPCGalleryPagination() : initMobileGalleryPagination();}
}
preventHrefLink();
处理card上的a标签,滑动不能点击
var preventHrefLink = function() {
hrefAs.on('click', function(e) {
if (isScrollProgress) {
e.preventDefault();
return false;
}
});
};
好的,那么接下来,我们就开始按照上面的伪代码,丰满每一个小的步骤。
4.1 无限循环处理
无限滚动,实际利用了一个视觉的障眼法。按照以下步骤实现:
- 将第一张,拷贝到最后一个位置,最后一个张内容,拷贝到第一个位置。例如下面的卡片。
- dom准备好后,将滚动区域向前移动一个card距离(每次滚动的的间距)
- 注册滚动结束时的事件。滚动可以向前滚动,可以向后滚动。如果向前滚动到最前面的一张(card3备份,索引为0),那么将其索引设置为3(card3原图)。并在滚动结束时滚动到card3原图。因为滚动的时候使用了动画,那么在滚动到card3原图时,不使用动画,直接跳转,视觉上和card3备份图内容一致,感觉不到任何差异,就完成了无缝切换的效果。当然,滚动到最后一张图也是类似操作,当最后一张card1复制 滚动结束时,将索引设置为Card1,并消除动画,移动到card1原图,实现无缝切换。
到此,无限滚动处理完成,相关代码如下:
/**
* 无限循环gallery, 初始化时,将第一个添加到最后,将最后一个添加到第一个
*/
var infiniteScrollDomInit = function() {
var children = parent.find(childCls);
// 获取第一页元素和最后一页元素
var preToEndDom = children.slice(0, paginationStep);
var endToPreDom = children.slice(paginationStep * (childCount - 1), paginationStep * (childCount - 1) + 2);
// endToPreDom 如果不够一页内容,则使用li填充
var endPageCount = endToPreDom.length;
if (endPageCount < paginationStep) {
// 最后一页数量只有一条,但是每一页需要显示paginationStep,则现在父组件末尾添加一些空的占位符
for (var i = endPageCount; i < paginationStep; i++ ) {
customInitDom = customInitDom || '<li></li>';
parent.append($(customInitDom));
}
endToPreDom = parent.find(childCls).slice(paginationStep * (childCount - 1), paginationStep * (childCount - 1) + 2);
}
// 添加到最前面和最后面
parent.prepend($(endToPreDom.clone()));
parent.append($(preToEndDom.clone()));
parent.css('marginLeft', -(scrollStep) + getUnit());
// 更新a标签(添加的dom也需要追加)
hrefAs = parent.find('a');
}
/**
* 当动画滚动结束后,将isScrollProgress设置为false,表示滚动结束
* - 当滚动到最前面或者最后面,初始化index索引,并将其滚动到与其内容相同的card上
*/
var initTransitionend = function() {
parent.on('transitionend', function() {
// 移动完成,将表示设置为false
isScrollProgress = false;
// 如果滑动到最最后面,索引修改为 第一个
if (infiniteCurrentIndex == childCount) {
infiniteCurrentIndex = 0;
move(infiniteCurrentIndex, 0);
}
// 如果滑动到最前面,索引修改为 最后一个
if (infiniteCurrentIndex < 0) {
infiniteCurrentIndex = childCount - 1;
move(infiniteCurrentIndex, 0);
}
})
}
4.2 给slider注册滑动事件
现在,我们需要给Slider注册上滚动事件,让我们手动滑动时,能够进行滚动。当然,如果你是希望自动播放,那么你可以通过setInterval等相关的操作实现。
我们上面在定义参数时,isScrollPrgress
就是表示,是否在滚动中。滚动中就不进行第二次滚动触发。不管是PC/Mobile都是一样的。
我们还需要注意一点:滚动时,如果滚动的角度小(例如是纵向滑动,就不应该滚动),那么避免滚动。
- Mobile 滑动效果,通过 touchstart, touchmove, touched来实现。
// -------手机滑动效果 start
parent.on('touchstart', function(e) {
// 如果在移动中,不再进行下一次移动
if (isScrollProgress) {
return;
}
var touch = e.originalEvent.targetTouches[0];
startPosition = {
x: touch.clientX,
y: touch.clientY
}
isTouchStartFirst = 1;
parent.on('touchmove', function(e) {
isScrollProgress = true;
var touch = e.targetTouches[0];
endPosition = {
x: touch.clientX,
y: touch.clientY
};
deltaX = endPosition.x - startPosition.x;
deltaY = endPosition.y - startPosition.y;
// 只有刚开始的touchstart,才去判断滑动的方向
if(isTouchStartFirst === 1){
isScroll = (Math.abs(deltaX) * 1.3- Math.abs(deltaY)) > 0 ? 1 : 0;
}
isTouchStartFirst++;
// isScrolling为0时,表示纵向滑动,1为横向滑动
if (isScroll === 1) {
e.preventDefault();
if (deltaX !== 0 && isInfiniteScroll) {
mouseMoveTransation(deltaX);
}
}
});
parent.on('touchend', function(){
if ((Math.abs(deltaY) > 10 && Math.abs(deltaX) < 10) || isScroll === 0) {
return;
}
if(deltaX < 0) {
movePre();
} else if(deltaX > 0) {
moveNext();
}
parent.off('touchmove');
parent.off('touchend');
});
});
// --------Mobile滑动效果 end
- PC 滑动效果,通过 mousedown, mousemove, mouseup来实现。
parent.on('mousedown', function (ev) {
// 如果在移动中,不再进行下一次移动
if (isScrollProgress) {
return;
}
ev.preventDefault();
ev.stopPropagation();
ev.cancelable = false;
startPosition = {
x: ev.pageX
}
$("body").on('mousemove', function(e) {
isScrollProgress = true;
endPosition = {
x: e.pageX,
};
deltaX = endPosition.x - startPosition.x;
if (deltaX !== 0 && isInfiniteScroll) {
mouseMoveTransation(deltaX);
}
});
$("body").on('mouseup', function() {
if(deltaX < 0) {
movePre();
} else if(deltaX > 0) {
moveNext();
}
$("body").off('mousemove');
$("body").off('mouseup');
});
});
4.3 根据拖拽移动
加入我们开始滑动,但是鼠标左右拖拽,那么此时将card随着鼠标的位置进行移动,这样会显得我们的slider比较活跃。当然,该操作是在 mousemove/touchmove时触发的。
var mouseMoveTransation = function(mouseMoveWidth) {
mouseMoveWidth = isPc ? ( mouseMoveWidth / parent.width() * 100) : mouseMoveWidth / 100;
var transtationWidth = tranlateX + mouseMoveWidth;
parent.css('transform', 'translate3d(' + transtationWidth + getUnit() + ', 0px, 0px)');
parent.css('transitionDuration', '0s');
}
4.4 移动处理
上面的准备操作依据完成了,movePre
和moveNext
还是一个空壳。 那么下面开始直接实现移动效果。
每次移动后,进行了分页刷新 reloadPagination,这里先知道就可以了,分页的方法放在最后。
/**
* 计算滚动距离
* @param{*}current 当前索引
*/
vargetScrollWidth = function(current) {
returnscrollStep * current;
}
vargetUnit = function() {
// var unit = isPc ? '%' : '%';
return'%';
}
/**
* 移动一个图片
* @param{*}current 当前选中页面
* @param{*}transitionDuration 滑动消费时间
*/
varmove = function(current, transitionDuration) {
tranlateX = -getScrollWidth(current);
parent.css('transform', 'translate3d(' + tranlateX + getUnit() + ', 0px, 0px)');
varnewTransitionDuration = (transitionDuration === undefined || transitionDuration === null) ? 0.6 : transitionDuration;
parent.css('transitionDuration', (parseFloat(newTransitionDuration) * paginationStep) + 's');
reloadPagination();
};
上面的方法,是移动一个图片,通过css的属性实现的。对应无限滚动和普通滚动都是一致的。那么接下来我们就处理movePre
和moveNext
方法。
/**
* 滑动方式左移动
*/
var movePre = function() {
isInfiniteScroll ? infiniteMovePre() : finiteMovePre();
};
/**
* 滑动方式右移动
*/
var moveNext = function() {
isInfiniteScroll ? infiniteMoveNext() : finiteMoveNext();
}
普通滑动事件处理
/**
* 向左边移动
*/
var finiteMovePre = function() {
if (currentIndex >= childCount - 1) {
isScrollProgress = false;
return;
}
currentIndex++;
move(currentIndex);
}
/**
* 向右边移动
*/
var finiteMoveNext = function() {
if (currentIndex <= 0) {
isScrollProgress = false;
return;
}
currentIndex--;
move(currentIndex);
}
无限滚动滑动事件处理
/**
* 无限循环向左移动
* curentIndex 用于显示pagination,因此保持更新
*/
var infiniteMovePre = function() {
// 处理pagination 的index,逻辑保持不变
if (currentIndex >= childCount - 1) {
currentIndex = 0;
} else {
currentIndex++;
}
infiniteCurrentIndex++;
move(infiniteCurrentIndex);
}
/**
* 无限循环向右边移动
* curentIndex 用于显示pagination,因此保持更新
*/
var infiniteMoveNext = function() {
if (currentIndex <= 0) {
currentIndex = childCount - 1;
} else {
currentIndex--;
}
infiniteCurrentIndex--;
move(infiniteCurrentIndex);
}
按钮方式点击分页
/**
* 通过按钮左移动
*/
var clickPreMove = function() {
if (!isScrollProgress) {
isScrollProgress = true;
movePre();
}
};
/**
* 通过按钮右移动
*/
var clickNextMove = function() {
if (!isScrollProgress) {
isScrollProgress = true;
moveNext();
}
}
4.5 分页处理
这里,我选择了2中分页方式,一种小圆点方式,一种是按钮方式。小圆点方式可以用于PC,也可以用于Mobile。而按钮建议用于PC.
小圆点分页
var initMobileGalleryPagination = function() {
var pagination = '<div class="gallery-pagination-circle">${items}</div>', items = '';
for(var i = 0; i < childCount; i++) {
let spanDom = i === 0
? '<span class="select-span" indexValue="' + i + '" ></span>'
: '<span indexValue="' + i + '"></span>';
items += spanDom;
}
pagination = pagination.replace('${items}', items);
parent.after(pagination);
// pagination 注册点击事件
var paginationDom = parent.next()[0];
if (paginationDom) {
$(paginationDom).on('click', 'span', function(e) {
var index = $(e.target).attr('indexValue');
currentIndex = index;
infiniteCurrentIndex = index;
move(index);
})
}
}
按钮分页
var initPreAndNextPagination = function() {
var prePagination = $('<span class="pre-pagination-btn"></span>');
var nextPagination = $('<span class="next-pagination-btn"></span>');
prePagination.on('click', clickNextMove);
nextPagination.on('click', clickPreMove);
// 在父级的父级身上添加按钮
var wrapper = parent.parent();
wrapper.parent().css('position', 'relative');
wrapper.siblings('.pre-pagination-btn').remove()
wrapper.siblings('.next-pagination-btn').remove();
wrapper.after(nextPagination);
wrapper.after(prePagination);
}
**通过名字,自定义分页 **
/**
* 自定义分页:分页有很多种,当想自定义不同类型的分页,可以传递方法名称
*
* @param {*} paginationArr 自定义方法名称字符串 [initMobileGalleryPagination, initPCGalleryPagination, initPreAndNextPagination]
*/
var getCustomPagination = function(paginationArr) {
var originPagiantions = {
initMobileGalleryPagination: initMobileGalleryPagination,
initPCGalleryPagination: initPCGalleryPagination,
initPreAndNextPagination: initPreAndNextPagination,
};
paginationArr.map(function(method) {
originPagiantions[method] && originPagiantions[method]();
});
}
不同分页方式分页reload 该方法在move函数中使用到的,每次通过说移动结束,需要手动更新分页显示。
var reloadPagination = function() {
var paginationDom = parent.next();
// PC 页码方式分页
var currentPage = paginationDom.find('.current-page');
if (currentPage.length > 0) {
currentPage.text(currentIndex + 1);
}
// 底部Circle分页 点击分页后,更新页码
if (paginationDom.hasClass('gallery-pagination-circle')) {
var circleSpans = paginationDom.children('span');
circleSpans.removeClass('select-span');
circleSpans.eq(currentIndex).addClass('select-span');
}
}
5. 窗口调整Resize初始化
用户在使用的过程中,肯定会遇到拖拽,那么在拖拽的时候,我们不仅仅是css进行适应,还应该有JS注册事件。在上面我们已经在初始化中清除了所有的事件,方便resize时,重新初始化。但是我们可能做得更好,在进行resize时,如果是PC样式,那么就不在重复渲染。
我们公共resize和节流函数进行配合达到这个目的。
定义全局变量,通过 windowIsResize ,我们可以直到窗口是否移动。
window.windowIsResize = false;
; (function ($) {
// 上一次记录
var preWindowWidth = $(window).width();
var preDevice = iSPc();
$(window).resize(throttle(function () {
// resize 获取
var currentWindowWidth = $(window).width();
var currentDdevice = iSPc();
// 宽度相等或者设备相等,直接设置为false
if (currentWindowWidth === preWindowWidth || currentDdevice === preDevice) {
window.windowIsResize = false;
} else {
window.windowIsResize = true;
}
// 更新
preWindowWidth = currentWindowWidth;
preDevice = currentDdevice;
}, 200));
})($);
通过设备宽度判断,是否重新渲染slider
;(function($, document) {
resizeGallery();
$(window).resize(throttle(function() {
// 宽度变化且设备变化,重新初始化gallery
if (window.windowIsResize) {
var slider= new Slider(gallery, children, scrollStep, 0, childCls, isInfiniteScroll, paginationMethods, paginationStep, customInitDom);
slider.initMobileOrPcGallery();
}
}, 200));
})($);
至此,Slider组件已经完成,肯定会存在一些小的问题,大家事件了可以告知我~~
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!