关于 h5 在 ios safari上动画 transfor 和 opacity 的 step1 函数的冲突问题分析和解决

212 阅读5分钟

iOS Safari 中 transform 和 opacity step(1) 动画冲突问题分析

问题概述

在开发过程中遇到了动画使用 opacity 隐藏 dom 在 ios 机型上失效的问题。后面发现是在 H5 页面中,当同时应用两个动画效果时出现的冲突:

  1. 使用 transform: translate3d 让元素从一个位置移动到另一个位置
  2. 同时使用 opacity 配合 steps(1) 时间函数在动画最后一帧将元素隐藏

特别是在 iOS Safari 浏览器上,steps(1)opacity 设置为 0 的效果失败了,导致元素没有按预期隐藏。 我的机型:Iphone·16 IOS18.4

内核分析

这个问题主要与 WebKit 内核(Safari 使用的浏览器引擎)处理复合动画的方式有关。WebKit 在处理同时应用于同一元素的 transform 和 opacity 动画时,特别是当使用 steps() 这样的非线性时间函数时,可能会出现优化冲突。

Safari 的渲染引擎在处理 CSS 动画时会尝试将某些属性(如 transform)放到 GPU 加速层上,而当同一元素上同时应用了多种动画属性且时间函数不同时,可能导致渲染优先级冲突。

解决方案

1. 分离动画到不同的 DOM 元素(推荐)

最可靠的解决方案是将不同的动画效果应用到不同的嵌套 DOM 元素上:

  • 外层元素处理 opacity 动画
  • 内层元素处理 transform 动画

2. 使用 requestAnimationFrame 手动控制

通过 JavaScript 使用 requestAnimationFrame 手动控制动画,可以更精确地控制每个属性的变化时机。

3. 使用 Web Animation API

Web Animation API 提供了更细粒度的动画控制,可以避免 CSS 动画的一些限制。

4. 替换 steps(1) 函数(实用)

在某些情况下,可以用其他方式实现类似效果,例如使用 setTimeout 在适当时机直接修改 opacity。 或者使用 linear 函数,将 opacity 仅在 100% 时设置为 0,99.9% 的时候都是 1,这样也能实现 step(1)一步到位的效果。

演示 Demo

下面是一个演示这个问题的 HTML 文件,包含了对照组(正确写法)和实验组(问题写法):

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <title>Transform 和 Opacity 动画冲突演示</title>
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
            margin: 0;
            padding: 20px;
            background-color: #f5f5f5;
        }
        
        h1 {
            font-size: 20px;
            margin-bottom: 20px;
        }
        
        h2 {
            font-size: 16px;
            margin: 15px 0 10px;
        }
        
        .container {
            margin-bottom: 30px;
            padding: 15px;
            background-color: #fff;
            border-radius: 8px;
            box-shadow: 0 2px 5px rgba(0,0,0,0.1);
        }
        
        .animation-box {
            position: relative;
            height: 100px;
            border: 1px solid #ddd;
            overflow: hidden;
            margin-bottom: 10px;
        }
        
        /* 对照组 - 正确写法(分离动画) */
        .good-outer {
            position: absolute;
            width: 100px;
            height: 50px;
            animation: fade-out 3s steps(1, end) forwards;
        }
        
        .good-inner {
            width: 100%;
            height: 100%;
            background-color: #4CAF50;
            color: white;
            display: flex;
            align-items: center;
            justify-content: center;
            animation: move-right 3s linear forwards;
        }
        
        /* 实验组 - 问题写法(动画在同一元素) */
        .bad-element {
            position: absolute;
            width: 100px;
            height: 50px;
            background-color: #F44336;
            color: white;
            display: flex;
            align-items: center;
            justify-content: center;
            animation: 
                move-right 3s linear forwards,
                fade-out 3s steps(1, end) forwards;
        }
        
        /* 动画定义 */
        @keyframes move-right {
            0% {
                transform: translate3d(0, 0, 0);
            }
            100% {
                transform: translate3d(200px, 0, 0);
            }
        }
        
        @keyframes fade-out {
            0%, 99% {
                opacity: 1;
            }
            100% {
                opacity: 0;
            }
        }
        
        /* 按钮样式 */
        .button {
            background-color: #2196F3;
            color: white;
            border: none;
            padding: 8px 15px;
            border-radius: 4px;
            font-size: 14px;
            cursor: pointer;
            margin-right: 10px;
        }
        
        .info {
            font-size: 14px;
            color: #666;
            margin: 10px 0;
            line-height: 1.5;
        }
    </style>
</head>
<body>
    <h1>Transform 和 Opacity 动画冲突演示</h1>
    
    <div class="container">
        <h2>对照组 - 正确写法(分离动画到不同元素)</h2>
        <div class="info">
            将 transform 和 opacity 动画分别应用到不同的嵌套元素上,避免冲突。
            动画结束后元素应该完全消失。
        </div>
        <div class="animation-box" id="good-demo">
            <div class="good-outer">
                <div class="good-inner">正确写法</div>
            </div>
        </div>
        <button class="button" onclick="resetGoodDemo()">重新播放</button>
    </div>
    
    <div class="container">
        <h2>实验组 - 问题写法(动画在同一元素)</h2>
        <div class="info">
            将 transform 和 opacity 动画同时应用到同一元素上,
            在 Safari 上可能导致 opacity 的 steps(1) 失效,元素不会消失。
        </div>
        <div class="animation-box" id="bad-demo">
            <div class="bad-element">问题写法</div>
        </div>
        <button class="button" onclick="resetBadDemo()">重新播放</button>
    </div>
    
    <div class="info">
        <p><strong>设备信息:</strong> <span id="device-info"></span></p>
        <p><strong>浏览器信息:</strong> <span id="browser-info"></span></p>
    </div>
    
    <script>
        // 重置演示函数
        function resetGoodDemo() {
            const container = document.getElementById('good-demo');
            container.innerHTML = '';
            
            const outer = document.createElement('div');
            outer.className = 'good-outer';
            
            const inner = document.createElement('div');
            inner.className = 'good-inner';
            inner.textContent = '正确写法';
            
            outer.appendChild(inner);
            container.appendChild(outer);
        }
        
        function resetBadDemo() {
            const container = document.getElementById('bad-demo');
            container.innerHTML = '';
            
            const element = document.createElement('div');
            element.className = 'bad-element';
            element.textContent = '问题写法';
            
            container.appendChild(element);
        }
        
        // 显示设备和浏览器信息
        document.getElementById('device-info').textContent = 
            navigator.platform || '未知设备';
        
        document.getElementById('browser-info').textContent = 
            navigator.userAgent || '未知浏览器';
    </script>
</body>
</html>

问题原因深入分析

  1. 渲染层合成问题

    • Safari 的 WebKit 引擎在处理 CSS 动画时会创建合成层
    • 当同一元素上有多个动画属性时,可能导致层处理冲突
  2. steps() 函数的特殊性

    • steps() 是一个离散的时间函数,而不是连续的
    • 当与连续的 transform 动画混合时,Safari 可能优先处理 transform 动画
  3. 硬件加速冲突

    • transform 属性通常会触发硬件加速
    • 当与非硬件加速的属性混合时,可能导致时序问题

更多解决方案

  1. 使用 animation-fill-mode: forwards 确保最终状态: 确保动画结束后保持最终状态。

  2. 使用 JavaScript 控制

    setTimeout(() => {
      element.style.opacity = 0;
    }, animationDuration);
    
  3. 使用 will-change 属性

    .element {
      will-change: opacity, transform;
    }
    
  4. 使用 CSS 变量动态控制: 通过 JavaScript 设置 CSS 变量来控制动画状态。

  5. **使用 linear 模拟 stpe(1) **:

    @keyframes hide {
      0% {
          opacity: 1
      }
      99.9% {
          opacity: 1
      }
      100% {
          opacity: 0
      }
    }
    

浏览器兼容性考虑

这个问题在不同浏览器上表现不同:

  • Safari (iOS/macOS) 上最明显
  • Chrome 和 Firefox 可能不会出现此问题或表现较轻

建议始终使用分离动画到不同元素的方法,这是最可靠的跨浏览器解决方案。