动态生成CSS轮播动画
1. 实现轮播动效,每个展位显示3s,然后0.5s滚动到下一个展位,依次循环播放。
2. 展位个数是服务端下发的任意数量。
本文尝试一步一个脚印给你呈现如何干得漂亮。
阅读原文:动态生成CSS轮播动画
实现轮播动效,每个展位显示3s,然后0.5s滚动到下一个展位,依次循环播放。 展位个数是服务端下发的任意数量。 本文尝试一步一个脚印给你呈现如何干得漂亮。
需求
- 实现轮播动效,每个展位显示3s,然后0.5s滚动到下一个展位,依次循环播放。
- 展位个数是服务端下发的任意数量。
思路
- 实现固定展位个数的静态轮播CSS动画。
- 实现任意展位个数的动态轮播CSS动画。
动作
固定展位个数轮播静态CSS动画
众所周知,帧动画可以控制不同帧的样式,简单在菜鸟Demo可以看到,详见如下keyframes动画示例:
上面帧是一直在滚动,并不是需求中要求的停下展示一会再滚动。灵机一动,两帧直接不产生位移不就可以做到停下展示了么?直接固定父布局作为视窗,超出隐藏,即 overflow: hidden。详见如下keyframes&停下再滚动动画示例:
上面帧是一个大长条在滚动,和轮播不是一回事。这也难不倒我,如果把视窗固定,让大长条从视窗前滚动,可不就是轮播效果么。详见如下keyframes&停下再滚动&超出不显示动画示例
现在还剩下最后一个问题,最后一个左滑时后面是空白,而需求是轮播,即最后一个后面接着第一个,不断循环。这个问题就考到了轮播的关键了(划重点),有种最简单做法就是展位循环生成100个,最后一次会滚动出空白,不过一般用户不会看到,所以很难发现,勉强能混过去。详见如下keyframes&停下再滚动&超出不显示动画&多组件循环示例:
显然,这种勉强混日子的方案入不了眼,其实这个问题本身很简单,自己在纸上画一下就出来了。
1. 尾部肯定要加上第一个展位,否则最后一个左滑尾部是空白。
2. 这一步也是最关键的一步,其实啥也不用干,因为此刻回复到最初状态,最后一个和第一个都是第一个展位内容,因为内容一样,不会产生视觉跳变,虽然实际上的确变换了。
稍微用下面一张分解图就理解了。
具体详见如下keyframes&停下再滚动&超出不显示动画&加一组件循环示例:
任意展位个数的动态轮播CSS动画
上面的样式是写死在html文档里,不满足需求中要求的服务端下发任意个数,这个思路比较简单,根据服务的下发的展位个数使用代码生成CSS样式就可以了,这么个通用问题肯定有现成方案,Google搜索一下关键字“动态生成keyframes”,比较下找到了一篇动态更改keyframes。
第一步肯定是如何使用代码生成目标CSS样式内容,这个参考静态CSS照葫芦画瓢修改一下参数即可,详见下图代码示意。
第二步将生成的CSS动画样式注入到全局上下文,确保设置了属性能生效。动态更改keyframes在我的项目里面实测有问题,简单说就是通过styleSheet.insertRule插入样式,成功的前提是目标styleSheet本身已有样式,即styleSheet.rules数组非空,上述文章也说到该问题,作者通过在document.styleSheets尾部styleSheet插入的rules来规避,但是这种在尾部styleSheet.rules为空下依旧报“Uncaught DOMException: Failed to execute ‘insertRule’ on ‘CSSStyleSheet’: Cannot access StyleSheet to insertRule”异常。
这个异常从字面上看是没有权限,咋一看是兼容性问题,我在本地Chrome浏览器调试没有遇到,发布就出问题了。Google上有很多人遇到,几个方案试了还是崩溃。其中insertRule()规则无法注入文章虽然没有解决问题,但是带来了欢乐,作者抛出问题后又一个回复是将动态生成写成静态…,作者的回复给我整乐了,“回答很棒,下次不要答了”,👍。
继续搜索下发现了Uncaught DOMException: Failed to execute ‘insertRule’ on ‘CSSStyleSheet’,是通过生成style标签插入DOM的做法,一时半会不太理解,感觉像是另外一种方案来绕过上面问题。
此时和同事永健深入交流了一下,直接在异常处断点,发现document.styleSheets的最后一个styleSheet是非内联CSS(通过css url拉取加载),对应rules为空,此时我才发现,为啥不直接报rules数组为空,非整个不明觉厉的“Cannot access StyleSheet to insertRule”,让我在“没有权限”的兼容性错误方向迅猛的奔跑。进一步看主文档html的DOM树,也的确一一对应,我们项目内联CSS是第一个,手动改成第一个测试通过。
接着永健让我通过创建style标签的方式将生成的CSS样式注入DOM树,这也可以有效避免styleSheet.insertRule改动其他上下文CSS,更加安全可靠,自然也不会有上面的异常。这个是个简单DOM加style标签动作,代码如下图:
最后附上完整动态生成CSS轮播动画示例:
<html>
<head>
<meta charset="utf-8">
<title>动态CSS轮播动画</title>
</head>
<body>
<div style="display: flex; align-items: center; height: 50px; font-size: 20px; padding-top: 50px;">
<input type="checkbox" style="width: 30px; height: 30px;" id="horizontal" checked="true" onchange="refresh()">
横向
<input type="checkbox" style="width: 30px; height: 30px; margin-left: 30px;" id="overflow-hidden" checked="true"
onchange="refresh()">
超出隐藏
<button id="plusButton"
style="display: inline-block; width: 100px; height: 50px; margin-left: 30px; background-color: #FCB526;font-size: 35px;border-radius: 25px;"
onclick="plus()">+1</button>
</div>
<div id="demoDisplay">
<div id="demoContainer"></div>
</div>
<script>
const palette = ['#863D91', '#F29900', '#F2DE5C', '#F7E9D0', '#B893B6'];
const ANIM_NAME = 'carousel-anim';
let count = 0;
plus();
function plus(isHorizontal, isOverflowHidden) {
count++;
appendItem(isHorizontal);
refresh();
}
function refresh() {
const isHorizontal = document.getElementById("horizontal").checked;
const isOverflowHidden = document.getElementById("overflow-hidden").checked;
const cssInfo = appendKeyFrames(count, isHorizontal, isOverflowHidden);
let style = `display: flex; width: 160px; height: 160px; margin-left: 420px; margin-top: 120px; padding: 10px; border: 5px dotted black; border-radius: 25px; flex-direction: ${isHorizontal ? 'row' : 'column'};`;
if (isOverflowHidden) {
style += "overflow: hidden;";
}
document.getElementById("demoDisplay").style = style;
const itemTitleElementList = demoContainer.getElementsByClassName("item");
for (let index = 0; index < itemTitleElementList.length; index++) {
const itemStyle = "width: 150px; height: 150px; background-color: " + getPaletteColor(index % count) + "; text-align: center; line-height: 150px; font-size: 35px; border-radius: 25px; margin-bottom: 30px; margin-right: 30px;" + (isHorizontal ? "flex-shrink: 0;" : "");
itemTitleElementList[index].style = itemStyle;
}
let demoContainerStyle = `display: flex; flex-shrink: 0; border-radius: 25px; padding: 5px; flex-direction: ${isHorizontal ? 'row' : 'column'};`;
if (cssInfo) {
appendStyle(cssInfo.cssStr);
demoContainerStyle += "animation:" + ANIM_NAME + " " + cssInfo.animDuration + "s linear infinite;";
}
if (!isOverflowHidden) {
demoContainerStyle += "border:1px solid gray;";
}
document.getElementById("demoContainer").style = demoContainerStyle;
}
function getPaletteColor(index) {
return palette[index % palette.length];
}
function createItemElement(isHorizontal, bgColor) {
const itemDiv = document.createElement("div");
itemDiv.className = 'item';
const itemTxt = document.createTextNode('title');
itemDiv.appendChild(itemTxt);
return itemDiv;
}
function appendItem(isHorizontal) {
const demoContainer = document.getElementById("demoContainer");
if (count === 1) {
demoContainer.appendChild(createItemElement(isHorizontal));
} else if (count === 2) {
demoContainer.appendChild(createItemElement(isHorizontal));
demoContainer.appendChild(createItemElement(isHorizontal));
} else {
demoContainer.insertBefore(createItemElement(isHorizontal), demoContainer.lastChild);
}
const itemTitleElementList = demoContainer.getElementsByClassName("item")
for (let index = 0; index < itemTitleElementList.length; index++) {
const element = itemTitleElementList[index];
element.textContent = (index % count + 1) + '/' + count;
}
}
function appendKeyFrames(count, isHorizontal, isOverflowHidden) {
if (count < 2) {
return;
}
// 公共动画参数,确保滚动同步
const scrollTime = 0.5;
// 停留展示时间
const displayTime = 2;
// item高或宽
const step = 150;
// item垂直或水平间隔
const blankStep = 30;
// 构造动画样式,并且返回CSS字符和动画时间间隔
return buildKeyFramesAndReturnCSSInfo(isHorizontal, count, step, blankStep, scrollTime, displayTime);
}
function buildKeyFramesAndReturnCSSInfo(isHorizontal, length, step, blankStep, scrollTime, displayTime) {
const translate = isHorizontal ? 'translateX' : 'translateY';
const sumTime = displayTime * length + scrollTime * length;
let sumPercentage = 0;
let sumTop = 0;
let cssStr = `@keyframes ${ANIM_NAME}{0%{transform:${translate}(0px);}`;
for (let index = 0; index < length; index++) {
// 禁止展示动画区段
sumPercentage += displayTime / sumTime;
cssStr += `${(sumPercentage * 100).toFixed(0)}%{transform:${translate}(-${sumTop.toFixed(2)}px);}`;
// 滚动动画区段
sumPercentage += scrollTime / sumTime;
sumTop += step + blankStep;
cssStr += `${(sumPercentage * 100).toFixed(0)}%{transform:${translate}(-${sumTop.toFixed(2)}px);}`;
}
cssStr += '}';
return {
animDuration: sumTime,
cssStr,
};
};
function appendStyle(cssStr) {
// 将CSS样式信息写入dom head节点
const head = document.head || document.getElementsByTagName('head')[0];
const style = document.createElement('style');
style.type = 'text/css';
if (style.styleSheet) {
style.styleSheet.cssText = cssStr;
} else {
style.appendChild(document.createTextNode(cssStr));
}
head.appendChild(style);
}
</script>
</body>
</html>