Hero 过渡

通过共享元素的连续性保持上下文的过渡

Hero 过渡

Hero 过渡是两个屏幕之间共享元素自然移动和变形的连续性过渡(Continuity Transition)。元素从一个屏幕"旅行"到另一个屏幕,为用户提供强大的视觉连接和上下文。

演示

Loading demo...

UX 原则

何时使用?

Hero 过渡用于连续性和上下文保持重要的场景。

内容关系适用性

콘텐츠 관계적합성설명
无关内容没有共享元素时不适用
兄弟关系⚠️在画廊等特定情况下有效
层级关系最适合列表→详情等同一对象的不同视图

主要用例

  • 画廊 → 全屏:图片放大并切换到详细视图
  • 产品列表 → 详情:产品卡片扩展为详情页面
  • 个人资料 → 编辑:保持个人资料照片并切换到编辑模式
  • 媒体播放器:从迷你播放器扩展到完整播放器

为什么这样工作?

  1. 视觉连续性:跟踪元素移动以保持方向感
  2. 表达空间关系:从小到大,从摘要到详细
  3. 最小化认知负担:自然变形代替突然变化
  4. 兴趣和愉悦:动态动效增加用户参与度

动效设计

Hero 元素的旅程:
1. 起点 → 记录位置、大小、形状
2. 变形中 → 平滑插值动画
3. 终点 → 定位到新位置和大小

其他元素:
- 离开页面:淡出
- 进入页面:Hero 元素定位后淡入

基本用法

1. 过渡设置

import { Ssgoi } from '@ssgoi/react';
import { hero } from '@ssgoi/react/view-transitions';

const config = {
  transitions: [
    {
      from: '/gallery',
      to: '/gallery/*',
      transition: hero(),
      symmetric: true
    }
  ]
};

export default function App() {
  return (
    <Ssgoi config={config}>
      {/* 应用内容 */}
    </Ssgoi>
  );
}

2. 元素标记

为共享元素添加 data-hero-key 属性:

// 列表页面
function ProductList() {
  return (
    <div className="grid">
      {products.map(product => (
        <Link key={product.id} to={`/product/${product.id}`}>
          <img 
            data-hero-key={`product-${product.id}`}
            src={product.image}
            alt={product.name}
          />
          <h3>{product.name}</h3>
        </Link>
      ))}
    </div>
  );
}

// 详情页面
function ProductDetail({ product }) {
  return (
    <div>
      <img 
        data-hero-key={`product-${product.id}`}
        src={product.image}
        alt={product.name}
        className="w-full"
      />
      <h1>{product.name}</h1>
      <p>{product.description}</p>
    </div>
  );
}

实践示例

1. 图片画廊

从缩略图扩展到全屏:

// 画廊网格
function Gallery() {
  return (
    <div className="grid grid-cols-3 gap-4">
      {images.map(img => (
        <Link to={`/photo/${img.id}`} key={img.id}>
          <div 
            data-hero-key={`photo-${img.id}`}
            className="aspect-square overflow-hidden"
          >
            <img src={img.thumb} className="object-cover w-full h-full" />
          </div>
        </Link>
      ))}
    </div>
  );
}

// 全屏视图
function PhotoView({ id }) {
  return (
    <div 
      data-hero-key={`photo-${id}`}
      className="fixed inset-0 bg-black"
    >
      <img src={image.full} className="w-full h-full object-contain" />
    </div>
  );
}

2. 卡片扩展

从小卡片到全屏详情:

// 卡片列表
function CardGrid() {
  return (
    <div className="grid gap-6">
      {items.map(item => (
        <article 
          key={item.id}
          data-hero-key={`card-${item.id}`}
          onClick={() => navigate(`/item/${item.id}`)}
          className="bg-white rounded-lg shadow-lg p-4 cursor-pointer"
        >
          <h3>{item.title}</h3>
          <p>{item.summary}</p>
        </article>
      ))}
    </div>
  );
}

// 扩展详情
function ItemDetail({ item }) {
  return (
    <article 
      data-hero-key={`card-${item.id}`}
      className="min-h-screen bg-white p-8"
    >
      <h1 className="text-4xl">{item.title}</h1>
      <p className="text-lg">{item.fullContent}</p>
    </article>
  );
}

3. 多个 Hero 元素

多个元素同时过渡:

// 个人资料卡片
function ProfileCard({ user }) {
  return (
    <div onClick={() => navigate(`/profile/${user.id}`)}>
      <img 
        data-hero-key={`avatar-${user.id}`}
        src={user.avatar}
        className="w-16 h-16 rounded-full"
      />
      <h3 data-hero-key={`name-${user.id}`}>
        {user.name}
      </h3>
    </div>
  );
}

// 个人资料详情
function ProfileDetail({ user }) {
  return (
    <div>
      <img 
        data-hero-key={`avatar-${user.id}`}
        src={user.avatar}
        className="w-32 h-32 rounded-full"
      />
      <h1 data-hero-key={`name-${user.id}`}>
        {user.name}
      </h1>
    </div>
  );
}

注意事项

Hero 键规则

  • 每个页面中 data-hero-key 必须唯一
  • 键是字符串,可以动态生成
  • 只有具有相同键的元素才会连接

性能考虑

  • Hero 元素在 DOM 中被克隆并动画化
  • 过多的 Hero 元素会影响性能
  • 预加载图片以防止闪烁

无障碍性

  • 减少动画设置时立即过渡
  • 焦点管理支持键盘导航
  • 屏幕阅读器将其识别为普通页面过渡

最佳实践

✅ 应该

  • 用于视觉上重要的元素
  • 应用于同一对象的不同表示
  • 保持自然的大小/位置变化
  • 用于引导用户视线

❌ 不应该

  • 不要同时应用于太多元素
  • 不要连接无关元素
  • 避免极端的大小变化(超过 10 倍)
  • 对文本内容谨慎使用