动态生成CSS轮播动画

0 阅读



1. 实现轮播动效,每个展位显示3s,然后0.5s滚动到下一个展位,依次循环播放。
2. 展位个数是服务端下发的任意数量。
本文尝试一步一个脚印给你呈现如何干得漂亮。

阅读原文:动态生成CSS轮播动画

实现轮播动效,每个展位显示3s,然后0.5s滚动到下一个展位,依次循环播放。 展位个数是服务端下发的任意数量。 本文尝试一步一个脚印给你呈现如何干得漂亮。

需求

  1. 实现轮播动效,每个展位显示3s,然后0.5s滚动到下一个展位,依次循环播放。
  2. 展位个数是服务端下发的任意数量。

思路

  1. 实现固定展位个数的静态轮播CSS动画。
  2. 实现任意展位个数的动态轮播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>

本文遵守 CC-BY-NC-4.0 许可协议。

Creative Commons License

欢迎转载,转载需注明出处,且禁止用于商业目的。

上篇上下求索 flexbox 优雅布局
下篇View 测量算法我知道