基本思路
使用 setInterval 把一个动作分解开, 分成步骤来完成.
<div class="box">div><style> .box { position: absolute; left: 0; top: 0; width: 200px; height: 200px; background-color: red; }style><script> var box = document.querySelector('.box'); var timerId = setInterval(function () { var left = box.offsetLeft; left += 5; box.style.left = left + 'px'; }, 20);script>
停止动画
满足一定条件清除定时器即可
当元素向右移动 500px 就停止
... // html css 和上面相同.<script> var box = document.querySelector('.box'); var timerId = setInterval(function () { var left = box.offsetLeft; if (left < 500) { left += 5; box.style.left = left + 'px'; } else { // 元素到达指定位置, 就停止 clearInterval(timerId); } }, 20);script>
封装动画函数
可以把刚才的代码封装到专门的函数中.
animateRight 向右移动
animateLeft 向左移动
同理, 也可以实现向上移动和向下移动(只是操作的元素坐标不同).
同理, 还可以实现任意方向移动(水平和垂直两个坐标叠加移动).
// 水平向右移动// elem 表示要移动的元素. (必须要带有定位)// distance 表示要移动的距离. // 15ms 作为周期, 意味着大概 1s 是 66 帧function animateRight(elem, distance) { var srcPos = elem.offsetLeft; var destPos = srcPos + distance; var timerId = setInterval(function () { var curPos = elem.offsetLeft; if (curPos >= destPos) { // 到达指定位置了, 不需要再移动了 clearInterval(timerId); } else { elem.style.left = curPos + 1 + 'px'; } }, 15);}// 水平向左移动// 和 animateRight 相比, 三个地方有差别:function animateLeft(elem, distance) { var srcPos = elem.offsetLeft; // 差别1 var destPos = srcPos - distance; var timerId = setInterval(function () { var curPos = elem.offsetLeft; // 差别2 if (curPos <= destPos) { // 到达指定位置了, 不需要再移动了 clearInterval(timerId); } else { // 差别3 elem.style.left = curPos - 1 + 'px'; } }, 15);}
这样后面使用的时候就方便了.
<button id='btn1'>box1走起button><button id='btn2'>box2走起button><div class="box1">box1div><div class="box2">box2div><style> .box1 { position: absolute; left: 0; top: 100px; width: 200px; height: 200px; background-color: red; } .box2 { position: absolute; left: 400px; top: 400px; width: 200px; height: 200px; background-color: red; }style><script> var btn1 = document.querySelector('#btn1'); var box1 = document.querySelector('.box1'); btn1.onclick = function () { animateRight(box1, 500); } var btn2 = document.querySelector('#btn2'); var box2 = document.querySelector('.box2'); btn2.onclick = function () { animateLeft(box2, 200); }script>
一点问题: 此时发现, 快速点击按钮的时候, 会给元素设定多个定时器, 导致多个动画叠加, 元素移动很快.
修改方案1: 在设定新定时器判定之前是否有定时器, 如果之前有就不设定新的定时器
function animateRight(elem, distance) { var srcPos = elem.offsetLeft; var destPos = srcPos + distance; // 先判定之前是否有定时器. if (elem.timerId) { return; } // 再设定新的定时器 elem.timerId = setInterval(function () { var curPos = elem.offsetLeft; if (curPos >= destPos) { // 到达指定位置了, 不需要再移动了 clearInterval(elem.timerId); elem.timerId = null; } else { elem.style.left = curPos + 1 + 'px'; } }, 15);}
修改方案2: 在设定新定时器时直接取消旧定时器, 然后设置新的.
function animateRight(elem, distance) { var srcPos = elem.offsetLeft; var destPos = srcPos + distance; // 先清空元素原有的定时器(把定时器的身份标识保存到 elem 中) clearInterval(elem.timerId); // 再设定新的定时器 elem.timerId = setInterval(function () { var curPos = elem.offsetLeft; if (curPos >= destPos) { // 到达指定位置了, 不需要再移动了 clearInterval(elem.timerId); elem.timerId = null; } else { elem.style.left = curPos + 1 + 'px'; } }, 15);}
两种策略都很有用, 需要根据实际情况决定使用哪种方式更合理.
注意: 当前动画函数的参数为移动的距离. 实际使用的时候也可能需要参数为目标位置.
缓动动画
平时见到的比较多的动画效果, 就是缓动动画. 开始移动速度快, 后来越来越慢.
公式: 移动的步长 = (目标位置 - 现在位置) / 10. 定时器的周期建议是 15ms (此时意味着 FPS 为 66 ).
function animateRight(elem, distance) { var srcPos = elem.offsetLeft; var destPos = srcPos + distance; if (elem.timerId) { return; } elem.timerId = setInterval(function () { var curPos = elem.offsetLeft; if (curPos >= destPos) { clearInterval(elem.timerId); elem.timerId = null; } else { // 如果没有向上取整, 移动的距离不再精确, 误差 <10px var step = Math.ceil((destPos - curPos) / 10); elem.style.left = curPos + step + 'px'; } }, 15);}
注意,
计算公式中的除数不一定是 10. 可以通过该参数灵活调整动画的速度. (数值越大, 则动画越慢)
上面的步长需要取整, 否则元素到达的位置不一定精确.
取整的时候要注意, 如果步长结果是正值, 则需要向上取整; 如果是负值, 则需要向下取整. 总之就是往绝对值大的方向取整.
给动画函数添加回调
很多时候我们需要当动画结束时能再执行一些动作. 此时就可以给上面的动画函数增加一个回调函数作为参数.
function animateRight(elem, distance, callback) { var srcPos = elem.offsetLeft; var destPos = srcPos + distance; if (elem.timerId) { return; } elem.timerId = setInterval(function () { var curPos = elem.offsetLeft; if (curPos >= destPos) { clearInterval(elem.timerId); elem.timerId = null; // 动画结束时调用回调 if (callback) { callback(); } } else { var step = Math.ceil((destPos - curPos) / 20); elem.style.left = curPos + step + 'px'; } }, 15);}
此时可以借助这个回调完成一些功能. 例如动画结束后提示 "动画结束"
点击开始动画
代码示例: 下拉菜单
预期效果
1) 初始情况下, 只显示一个导航
2) 点击查看详情, 则从上向下弹出一个菜单, 弹出完毕时将 "查看详情" 修改为 "隐藏详情"
实现布局&样式
查看详情
前端JavaC++
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.nav {
position: relative;
width: 100%;
height: 50px;
font-size: 20px;
line-height: 50px;
color: #fff;
background-color: #333;
padding-left: 20px;
}
.item {
width: 100px;
height: 50px;
text-align: center;
cursor: pointer;
}
.nav ul {
position: absolute;
left: 20px;
/* 50px 为显示菜单; -100px 为隐藏菜单 */
/* top: 50px; */
top: -100px;
z-index: -1;
width: 100px;
height: 150px;
background-color: purple;
list-style: none;
}
.nav ul>li {
height: 50px;
padding-left: 0.5em;
cursor: pointer;
}
.nav ul>li:hover {
background-color: orange;
}
实现垂直动画函数
代码思路和之前的 animateLeft / animateRight 一致. 只不过把上下移动的功能集成在一个函数中了.
注意:
要想实现上下都能移动, 需要在判定元素是否到达目标位置时, 使用 == 判定(而不是 <= >=)
向下移动时, distance 是正值, 需要使用 Math.ceil 向上取整; 向上移动时, distance 是负值, 需要使用 Math.floor 向下取整. (往绝对值大的方向取整). 不取整就会导致最终元素的位置出现误差.
// 动画函数, 垂直方向移动
// distance 为正, 表示向下移动. distance 为负, 表示向上移动
function animateY(elem, distance, callback) {
// 1. 计算目标位置
var destPos = elem.offsetTop + distance;
// 2. 判定当前是否已经有定时器, 如果已经有定时器, 就不再搞新的
if (elem.timerId) {
return;
}
// 3. 设置新定时器
elem.timerId = setInterval(function () {
var curPos = elem.offsetTop;
// 要想实现上下都能移动, 此处需要使用 ==.
// 否则就需要拆成两个函数, 分别是 >= 和 <=
if (curPos == destPos) {
// 4. 元素到达目标位置, 拆除定时器, 并调用回调函数
clearInterval(elem.timerId);
elem.timerId = null;
if (callback) {
callback();
}
} else {
// 5. 元素未到达目标位置
// a) 先计算要移动的步长(缓动动画的公式)
var step = (destPos - curPos) / 10;
// b) 根据 step 的正负, 决定是向上取整还是向下取整(往绝对值大的方向取整)
step = step > 0 ? Math.ceil(step) : Math.floor(step);
// c) 根据步长, 修改元素位置
elem.style.top = curPos + step + 'px';
}
}, 15);
}
调用垂直动画函数
// 给下拉菜单添加上动画效果
var item = document.querySelector('.item');
var ul = document.querySelector('.nav>ul');
item.addEventListener('click', function () {
if (this.innerHTML == '查看详情') {
// 弹出菜单
animateY(ul, 150, function () {
item.innerHTML = '隐藏详情';
});
} else {
// 收缩菜单
animateY(ul, -150, function () {
item.innerHTML = '查看详情';
});
}
});
代码示例: 滚动到顶部
点击 "回到顶部" 按钮, 带有动画效果的滚动到页面最顶端.
核心函数 window.scroll (参数为要滚动的目标位置的 x, y 坐标)
实现基本布局
回到顶部
body {
height: 2000px;
}
button {
position: fixed;
bottom: 20px;
right: 20px;
}
.section {
height: 400px;
width: 800px;
background-color: red;
margin: 10px auto;
}
实现点击按钮滚动
使用 window.scroll 函数进行滚动
var button = document.querySelector('button');
button.addEventListener('click', function () {
window.scroll(0, 0);
});
这种方式滚动就是一下就滚上去了. 接下来需要添加动画效果
实现滚动动画函数
基于上面的 animateY 函数进行调整.
function animateY(elem, distance, callback) {
var destPos = document.documentElement.scrollTop + distance;
clearInterval(elem.timerId);
elem.timerId = setInterval(function () {
// [1] 通过 scrollTop 获取到当前滚动的位置
var curPos = document.documentElement.scrollTop;
if (curPos == destPos) {
clearInterval(elem.timerId);
elem.timerId = null;
if (callback) {
callback();
}
} else {
var step = (destPos - curPos) / 10;
step = step > 0 ? Math.ceil(step) : Math.floor(step);
// [2] 使用 window.scroll 来滚动页面
window.scroll(0, curPos + step);
}
}, 15);
}
调用动画函数
var button = document.querySelector('button');
button.addEventListener('click', function () {
// window.scroll(0, 0);
animateY(window, -document.documentElement.scrollTop);
});
代码示例: 轮播图
功能
鼠标经过轮播图, 显示左右按钮, 鼠标离开则隐藏.
点击小圆圈, 切换到指定的轮播图.
点击右侧按钮, 则往右显示一张轮播图; 点击左侧按钮, 则往左显示一张轮播图.
鼠标离开轮播图, 每隔一定时间自动播放下一张. 鼠标经过轮播图, 轮播图取消自动轮播.
轮播图播放时下方的小圆圈自动随之变化.
实现布局&样式
>
<
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.carousel {
/* 保证子元素的定位在父元素之内. 子绝父相 */
position: relative;
width: 520px;
height: 280px;
margin: 100px auto;
/* 调试阶段可以不加这个 */
overflow: hidden;
}
.carousel ul {
/* 一定要给 ul 加定位, 否则无法移动 */
position: absolute;
left: 0;
top: 0;
list-style: none;
width: 1000%;
}
.carousel ul>li {
float: left;
}
.carousel .left-arrow,
.carousel .right-arrow {
/* 尺寸位置 */
position: absolute;
width: 25px;
height: 40px;
top: 50%;
margin-top: -20px;
/* 字体颜色 */
font-size: 22px;
line-height: 40px;
color: #fff;
background-color: rgba(0, 0, 0, 0.4);
}
.carousel .left-arrow {
left: 0;
border-top-right-radius: 20px;
border-bottom-right-radius: 20px;
text-align: left;
}
.carousel .right-arrow {
right: 0;
border-top-left-radius: 20px;
border-bottom-left-radius: 20px;
text-align: right;
}
.carousel ol {
position: absolute;
/* 宽度最好不要写死. 能更好适应内部的 li 的个数 */
/* width: 90px; */
height: 18px;
bottom: 14px;
left: 50%;
margin-left: -45px;
background-color: rgba(255, 255, 255, 0.3);
border-radius: 9px;
list-style: none;
}
.carousel ol>li {
float: left;
width: 10px;
height: 10px;
margin: 4px;
background-color: #fff;
border-radius: 50%;
}
.carousel .current {
background-color: #ff5000;
}
初始代码
使用 load 事件包裹代码, 保证代码在页面加载完毕后再执行.
并获取到需要的元素
window.addEventListener('load', function () {
var carousel = document.querySelector('.carousel');
var ol = document.querySelector('.carousel ol');
var ul = document.querySelector('.carousel ul');
var leftArrow = document.querySelector('.left-arrow');
var rightArrow = document.querySelector('.right-arrow');
// 表示当前显示的轮播图是第几张
// 用来控制小圆点, 并实现无缝切换.
var currentIndex = 0;
// 记录图片宽度
var imageWidth = ul.firstElementChild.offsetWidth;
// 记录图片数目
var imageCount = ul.children.length;
// TODO 下面写后续代码.
});
实现左右箭头隐藏/显示
1) 先通过样式让箭头默认隐藏
.carousel .left-arrow,
.carousel .right-arrow {
/* 隐藏 */
display: none;
}
2) 给 carousel 加上鼠标经过事件, 控制箭头显示
// 1. 实现左右箭头的显示/隐藏
function showArrow() {
// 绑定鼠标事件
carousel.addEventListener('mouseenter', function () {
leftArrow.style.display = 'block';
rightArrow.style.display = 'block';
});
carousel.addEventListener('mouseleave', function () {
leftArrow.style.display = 'none';
rightArrow.style.display = 'none';
});
}
showArrow();
动态生成圆点数目
根据实际轮播图的数目, 动态生成圆点.
1) 先把 ol 里的 li 都清空
2) 动态生成 li
// 2. 动态生成小圆点
function generatePoints() {
for (var i = 0; i < ul.children.length; i++) {
var li = document.createElement('li');
ol.appendChild(li);
}
ol.children[0].className = 'current';
}
generatePoints();
实现小圆点切换选中状态
点击小圆点, 能够切换小圆点的选中状态
注意, 要把其他的圆点的选中状态去掉
// 3. 实现小圆点切换
function switchCurrent() {
// 基于事件冒泡的机制, 不必给每个 li 都绑定监听器
ol.addEventListener('click', function (e) {
if (e.target == ol) {
return;
}
// 清除原来圆点的选中状态 (全局变量 currentIndex 记录了被选中的圆点)
for (var i = 0; i < ol.children.length; i++) {
ol.children[i].className = '';
}
// 给被点击的圆点加上选中状态
e.target.className = 'current';
});
}
switchCurrent();
实现小圆点切换图片
1) 准备好动画函数.
distance 为正数, 则往右移动; 为负数则往左移动.
// 4. 水平动画函数
function animateX(elem, distance, callback) {
// 获取目标位置
var destPos = elem.offsetLeft + distance;
// 判断之前是否已经有定时器. 如果有了就不运行新的了
if (elem.timerId) {
return;
}
// 设置定时器完成动画
elem.timerId = setInterval(function () {
// 获取当前位置
var curPos = elem.offsetLeft;
if (curPos == destPos) {
// 已经到达目标, 拆除定时器, 并调用回调
clearInterval(elem.timerId);
elem.timerId = null;
if (callback) {
callback();
}
} else {
// 还没到达目标, 移动位置
var step = (destPos - curPos) / 10;
step = step > 0 ? Math.ceil(step) : Math.floor(step);
elem.style.left = curPos + step + 'px';
}
}, 15);
}
2) 修改刚才的 switchCurrent 调用动画函数.
注意:
移动的是 ul, 而不是 li 本身.
移动距离 = (上次被选中下标 - 当前被选中下标) * 图片宽度
为了知道当前被点击的圆点的下标, 需要在创建圆点时加入自定制属性.
如果不能正确移动, 一定先检查 ul 是否加了
position
先给 ol>li 加上 data-index 属性
// generatePoints 的 for 循环中, 加入一行代码
li.setAttribute('data-index', i);
再在点击时获取属性
function switchCurrent() {
// 基于事件冒泡的机制, 不必给每个 li 都绑定监听器
ol.addEventListener('click', function (e) {
if (e.target == ol) {
return;
}
// 清除原来圆点的选中状态
for (var i = 0; i < ol.children.length; i++) {
ol.children[i].className = '';
}
// 给被点击的圆点加上选中状态
e.target.className = 'current';
// [实现移动轮播图片] 移动距离 = (上次被选中下标 - 当前被选中下标) * 图片宽度
// 上次的被选中圆点下标
var lastIndex = currentIndex;
// 现在的被选中圆点下标
currentIndex = e.target.getAttribute('data-index');
// 移动 ul.
var ul = document.querySelector('.carousel>ul');
animateX(ul, (lastIndex - currentIndex) * ul.firstElementChild.offsetWidth);
});
}
switchCurrent();
实现点击左右箭头切换图片
给左右箭头加上鼠标点击事件即可.
// 5. 实现点击按钮切换图片
function switchImage() {
rightArrow.addEventListener('click', nextImage);
leftArrow.addEventListener('click', prevImage);
}
switchImage();
nextImage 实现
function nextImage() {
var lastIndex = currentIndex++;
if (currentIndex >= imageCount) {
currentIndex = 0;
}
// 移动图片
animateX(ul, -imageWidth);
// 设置小圆点的选中状态
ol.children[lastIndex].className = '';
ol.children[currentIndex].className = 'current';
}
prevImage 实现
function prevImage() {
var lastIndex = currentIndex--;
if (currentIndex < 0) {
currentIndex = imageCount - 1;
}
// 移动图片
animateX(ul, imageWidth);
// 设置小圆点的选中状态
ol.children[lastIndex].className = '';
ol.children[currentIndex].className = 'current';
}
注意1: 当前还不能循环滚动. 当滚动到最后一张图片的时候, 继续滚动就会出现空白.
注意2: 当前还不能点的太快. 如果一个动画还没播放完快速又点击切换, 会出现圆点和图不匹配的 bug [后面解决]
实现无缝滚动
实现思路:
把第一张图片复制一份, 放到最后.
当图片滚动到最后一张的时候(带动画效果), 让 ul 快速跳到最左侧(不带动画). (先播放动画, 后移动位置)
如果是向前滚动, 发现已经滚动到第一张图, 就先把 left 设置到最后一个位置, 再播放动画. (先移动位置, 后播放动画)
这里不好想, 一定要画图
1) 使用代码把图片克隆过来, 插入到 ul 最后
function switchImage() {
// [实现无缝滚动]
// 先把第一张图复制一份, 插入到最后
// 也把最后一张图复制一份, 插入到最前
var firstImage = ul.firstElementChild.cloneNode(true);
ul.appendChild(firstImage);
rightArrow.addEventListener('click', nextImage);
leftArrow.addEventListener('click', prevImage);
}
2) 在响应左右箭头点击事件中, 加入无缝滚动的判断
修改 nextImage
function nextImage() {
var lastIndex = currentIndex++;
if (currentIndex >= imageCount) {
currentIndex = 0;
}
// 移动图片
animateX(ul, -imageWidth, function () {
// [实现无缝滚动]
// 当发现当前下标是最后一张图的时候, 直接快速移动 ul 到最右侧
// 先播放完动画, 再移动位置. 不放到回调函数内部不能保证和动画的先后顺序
if (currentIndex == 0) {
ul.style.left = 0;
}
});
// 设置小圆点的选中状态
ol.children[lastIndex].className = '';
ol.children[currentIndex].className = 'current';
}
修改 prevImage
function prevImage() {
var lastIndex = currentIndex--;
if (currentIndex < 0) {
currentIndex = imageCount - 1;
// [实现无缝滚动]
// 当发现当前下标是第一张图的时候, 直接快速移动 ul 到最左侧
// 先移动位置, 后播放动画
ul.style.left = -(imageCount * imageWidth) + 'px';
}
// 移动图片
animateX(ul, imageWidth);
// 设置小圆点的选中状态
ol.children[lastIndex].className = '';
ol.children[currentIndex].className = 'current';
}
实现自动播放轮播图
直接使用定时器调用 nextImage 即可.
// 6. 实现自动轮播
function autoPlay() {
// 初始情况下, 自动播放
function play() {
carousel.autoPlayTimer = setInterval(function () {
// 直接调用 nextImage 即可切换下一个图片
nextImage();
// 也可以使用 click 方法触发点击事件
// rightArrow.click();
}, 2000);
}
play();
// 鼠标移入, 则取消定时器
carousel.addEventListener('mouseenter', function () {
clearInterval(carousel.autoPlayTimer);
});
// 鼠标移出, 则注册定时器
carousel.addEventListener('mouseleave', play);
}
autoPlay();
节流阀
目的: 解决按钮点击速度过快的 bug.
此时如果快速点击两次右箭头或者左箭头, 会发现, 当一个动画播放过程中, 再次点击按钮, 就会发现图片没有切换, 但是下方的小圆点切换了.
原因不难理解, 主要是取决于 animateX 的函数实现.
观察这个代码
我们的 animateX 函数当上一个动画还没运行完毕时, 下一个动画是不生效的. 所以第二次点击只是触发了圆点的切换, 没有触发图片播放.
解决思路: 点击按钮时临时禁用点击事件. 等动画播放完毕, 再重新注册点击事件.
修改 nextImage 和 prevImage
function nextImage() { // [解决快速点击箭头的bug] // 先临时取消鼠标点击事件理 rightArrow.removeEventListener('click', nextImage); var lastIndex = currentIndex++; if (currentIndex >= imageCount) { currentIndex = 0; } // 移动图片 animateX(ul, -imageWidth, function () { // [实现无缝滚动] // 当发现当前下标是最后一张图的时候, 直接快速移动 ul 到最右侧 // 先播放完动画, 再移动位置. 不放到回调函数内部不能保证和动画的先后顺序 if (currentIndex == 0) { ul.style.left = 0; } // [解决快速点击箭头的bug] // 动画播放完, 再重新设置鼠标点击事件 rightArrow.addEventListener('click', nextImage); }); // 设置小圆点的选中状态 ol.children[lastIndex].className = ''; ol.children[currentIndex].className = 'current';}function prevImage() { // [解决快速点击箭头的bug] // 先临时取消鼠标点击事件理 leftArrow.removeEventListener('click', prevImage); var lastIndex = currentIndex--; if (currentIndex < 0) { currentIndex = imageCount - 1; // [实现无缝滚动] // 当发现当前下标是第一张图的时候, 直接快速移动 ul 到最左侧 // 先移动位置, 后播放动画 ul.style.left = -(imageCount * imageWidth) + 'px'; } // 移动图片 animateX(ul, imageWidth, function () { // [解决快速点击箭头的bug] // 先临时取消鼠标点击事件理 leftArrow.addEventListener('click', prevImage); }); // 设置小圆点的选中状态 ol.children[lastIndex].className = ''; ol.children[currentIndex].className = 'current';}