构建轮播滑动效果

轮播滑动效果是为轮播界面增添视觉趣味的好方法。当用户滚动浏览幻灯片时,每张幻灯片在进入、居中和退出滚动端口时都可以播放动画。例如,幻灯片可以淡入淡出、旋转或缩放。这能打造动态且富有吸引力的用户体验。与简单的进入/退出动画不同,此效果使用单个连续动画来控制幻灯片在整个滚动窗口中的显示。

如何实现

下面介绍了如何创建轮播幻灯片效果:

  1. 创建滚动条:此元素将用作轮播幻灯片的容器。在此示例中,它使用 overflow-x: scroll 来允许水平滚动。

    <ul class="scroller">
      <li class="entry">1</li>
      <li class="entry">2</li>
      <li class="entry">3</li>
      …
    </ul>
    
    .scroller {
      overflow-x: scroll;
    }
    
  2. 定义动画:创建一个 CSS 动画,用于定义幻灯片在滚动视口中移动时的不同状态。您可以为动画的任何部分定义关键帧。例如,您可以通过添加 50% 关键帧,为幻灯片位于滚动条中心时定义一个状态。在此示例中,scale 属性使幻灯片在靠近中心时变大,在远离中心时变小。

    @keyframes animate {
      0% {
        scale: 0.5;
      }
      50% {
        scale: 1;
      }
      100% {
        scale: 0.5;
      }
    }
    
  3. 应用动画和 view-timeline:将动画附加到轮播界面幻灯片,并将其关联到 view-timeline,以便在元素在容器中滚动时跟踪该元素。

    .scroller > * {
      animation: animate auto linear both;
      animation-timeline: view(inline);
    }
    

    默认情况下,view() 会跟踪 block 轴上的元素。如果您需要在 inline 轴上跟踪它,可以使用 view(inline)。

示例代码

此代码使用匿名 view-timeline 在滚动时为水平滚动器的轮播项添加动画效果:

@keyframes animate {
  0% {
    scale: 0.5;
  }

  50% {
    scale: 1;
  }

  100% {
    scale: 0.5;
  }
}

.scroller > * {
  /* Applies the animation using an `auto` duration */
  animation: animate auto linear both;
  /* Sets the animation timeline to use an anonymous view progress timeline, tracking the element's progress through the scroller on the inline axis */
  animation-timeline: view(inline);
}

以下代码使用命名的 view-timeline,在滚动时为水平滚动器的轮播项添加动画效果:

@keyframes animate {
  0% {
    scale: 0.5;
  }

  50% {
    scale: 1;
  }

  100% {
    scale: 0.5;
  }
}

/* This creates a named view-timeline on each carousel item. The timeline is used to drive the animation that is applied on the same element. */
.scroller > * {
  /* Applies the animation using an `auto` duration */
  animation: animate auto linear both;
  /* Defines a named view progress timeline, tracking the element's progress through the scroller on the inline axis */
  view-timeline: --item inline;
  /* Sets the animation timeline to use the named view progress timeline defined above */
  animation-timeline: --item;
}

最佳做法

使用滚动驱动动画时,请务必遵循一些最佳实践,以确保提供流畅且无障碍的体验:

  • 务必添加功能检测:并非所有浏览器都支持滚动驱动的动画。使用 @supports ((animation-timeline: view()) and (animation-range: entry)) 检查是否支持,并为不支持的浏览器提供后备方案。
    • 此处必须包含 (animation-range: entry) 检查,以滤除仅提供部分支持的浏览器。
    • 请勿将 scroll-timeline-polyfill 软件包用于回退策略,因为它功能不完整,并且存在许多已知问题。
    • 如果动画仅被视为装饰性元素,请选择渐进增强,不要提供后备。
  • 务必尊重用户偏好:有些用户更喜欢在网页上看到较少的动画。使用 prefers-reduced-motion 媒体查询为这些用户停用或减少动画。
  • 务必尝试仅为高性能 CSS 属性添加动画效果:为了获得最流畅的动画效果,请坚持为可由浏览器的合成器线程处理的属性(例如 transform 和 opacity)添加动画效果。为其他属性(例如 width 或 height)添加动画效果可能会导致性能问题。
  • 务必使用正确的声明顺序:使用 animation 简写属性时,请在它之后声明 animation-timeline 和 animation-range,以防止简写属性重置时间轴。

如果多个 DOM 元素需要根据同一时间轴添加动画效果,或者您需要为定义了 view-timeline 的元素的子元素添加动画效果,则最好使用命名的 view-timeline。如果您要添加动画效果的元素也是定义 view-timeline 的元素,则可以使用 view() 使用匿名视图时间轴。

使用 view() 函数创建滚动驱动的动画时:

  • 可选:明确指定要跟踪的轴。如果未定位到默认的 block 轴(例如在水平滚动条中),请使用 view(block) 或 view(inline) 明确指定要跟踪的轴。

使用 view-timeline 属性创建滚动驱动的动画时:

  • 务必使用 CSS <dashed-ident> 作为名称(例如 view-timeline: --my-custom-name)
  • 可选:明确指定要跟踪的轴。如果目标不是默认的 block 轴(例如在水平滚动条中),请使用 view-timeline-axis 明确指定要跟踪的轴。
  • 务必确保查找范围有效:如果声明 view-timeline 的元素不是动画元素的平面树祖先,请在共享祖先上使用 timeline-scope 来提升 view-timeline 名称的可见性。

浏览器支持和后备策略

因此,通常需要采用后备策略。

对于不支持滚动驱动动画的浏览器,您可以使用后备方案来重现视觉效果。后备方案通常使用滚动监听器(用于 ScrollTimeline 效果)或 IntersectionObserver API(用于 ViewTimeline 效果)构建。

在内置支持滚动驱动动画的浏览器中,请始终使用原生 CSS 实现,因为这些实现的性能更高。

请注意,并非所有效果都可以通过后备方法重现。

具体而言,对于此用例,以下脚本会为不支持滚动驱动动画的浏览器应用回退。它使用 Web Animations API (Element.animate()) 为轮播界面中的每个项创建一个暂停的动画。然后,它会监听滚动条上的 scroll 事件,并根据列表项在滚动条中的滚动进度更新每个动画的 currentTime。

// Fallback for browsers that don't support scroll-driven animations
if (!CSS.supports('(animation-timeline: view()) and (animation-range: entry)')) {
  const scroller = document.querySelector('.scroller');
  const entries = document.querySelectorAll('.entry');

  // Create a map to store animations
  const animations = new Map();

  entries.forEach(entry => {
    const animation = entry.animate(
      {
        scale: ['0.5', '1', '0.5']
      },
      {
        duration: 1, // We'll control the time ourselves
        fill: 'both'
      }
    );
    animation.pause();
    animations.set(entry, animation);
  });

  // Update animations on scroll
  const tick = () => {
    const scrollerRect = scroller.getBoundingClientRect();

    entries.forEach(entry => {
      const animation = animations.get(entry);
      if (!animation) return;

      const entryRect = entry.getBoundingClientRect();
      const progress = (entryRect.left + entryRect.width / 2 - scrollerRect.left) / scrollerRect.width;

      animation.currentTime = progress;
    });
  };
    
  scroller.addEventListener('scroll', tick);
  tick();
}