跳到主內容

英雄動畫

如何將一個元件動畫化,使其在兩個螢幕之間飛行。

你可能已經多次見過英雄動畫。例如,一個螢幕顯示一個縮圖列表,代表待售商品。選擇一個商品會將其飛行到新螢幕,其中包含更多詳細資訊和一個“購買”按鈕。在 Flutter 中,將影像從一個螢幕飛行到另一個螢幕被稱為英雄動畫,儘管相同的動作有時也稱為共享元素過渡

你可能想觀看這段介紹 Hero 元件的一分鐘影片

在新的標籤頁上在 YouTube 上觀看:“Hero | Flutter widget of the week”(英雄 | Flutter 每週小部件)

本指南演示瞭如何構建標準的英雄動畫,以及在飛行過程中將影像從圓形形狀轉換為方形形狀的英雄動畫。

你可以使用 Hero 小部件在 Flutter 中建立此動畫。當英雄從源路由動畫到目標路由時,目標路由(減去英雄)會淡入檢視。通常,英雄是 UI 的一小部分,例如影像,兩個路由都有。從使用者的角度來看,英雄“飛行”在路由之間。本指南展示瞭如何建立以下英雄動畫

標準的英雄動畫

一個標準英雄動畫會將英雄從一個路由飛行到一個新路由,通常降落在不同的位置並具有不同的尺寸。

以下影片(以慢速錄製)顯示了一個典型的示例。點選路由中心的可翻轉片飛到新的藍色路由的左上角,尺寸較小。點選藍色路由中的可翻轉片(或使用裝置的返回上一路由手勢)會將可翻轉片飛回原始路由。

在新的標籤頁上在 YouTube 上觀看:“Flutter 中的標準英雄動畫”

徑向英雄動畫

徑向英雄動畫中,當英雄在路由之間飛行時,其形狀似乎會從圓形變為矩形。

以下影片(以慢速錄製)顯示了一個徑向英雄動畫的示例。在開始時,一行三個圓形影像出現在路由底部。點選任何圓形影像都會將其飛行到新路由,該路由以方形顯示它。點選方形影像會將英雄飛回原始路由,以圓形顯示。

在新的標籤頁上在 YouTube 上觀看:“Flutter 中的徑向英雄動畫”

在轉向特定於 標準徑向 英雄動畫的部分之前,請閱讀 英雄動畫的基本結構,以瞭解如何構建英雄動畫程式碼,並閱讀 幕後原理,以瞭解 Flutter 如何執行英雄動畫。

英雄動畫的基本結構

#

英雄動畫使用兩個 Hero 小部件實現:一個描述源路由中的小部件,另一個描述目標路由中的小部件。從使用者的角度來看,英雄似乎是共享的,只有程式設計師需要理解這個實現細節。英雄動畫程式碼具有以下結構

  1. 定義一個起始 Hero 小部件,稱為源英雄。英雄指定其圖形表示(通常是影像)和標識標籤,並且位於由源路由定義的當前顯示的部件樹中。
  2. 定義一個結束 Hero 小部件,稱為目標英雄。此英雄還指定其圖形表示以及與源英雄相同的標籤。確保兩個英雄小部件都使用相同的標籤建立,通常是代表底層資料的物件。為了獲得最佳結果,英雄應該具有幾乎相同的部件樹。
  3. 建立一個包含目標英雄的路由。目標路由定義了動畫結束時存在的部件樹。
  4. 透過將目標路由推入 Navigator 的堆疊來觸發動畫。Navigator 的推送和彈出操作會為源路由和目標路由中具有匹配標籤的每個英雄對觸發英雄動畫。

Flutter 計算從起始點到終點的英雄邊界的插值器(插值大小和位置),並在疊加層中執行動畫。

下一節將更詳細地描述 Flutter 的過程。

幕後原理

#

以下描述了 Flutter 如何從一個路由過渡到另一個路由。

Before the transition the source hero appears in the source route

在過渡之前,源英雄等待在源路由的部件樹中。目標路由尚不存在,並且疊加層為空。


The transition begins

將路由推送到 Navigator 會觸發動畫。在 t=0.0 時,Flutter 執行以下操作

  • 使用 Material 運動規範中描述的曲線運動計算目標英雄的路徑,超出螢幕。Flutter 現在知道英雄的最終位置。

  • 將目標英雄放置在疊加層中,與英雄相同的位置和大小。將英雄新增到疊加層會更改其 Z 順序,使其出現在所有路由之上。

  • 將源英雄移出螢幕。


The hero flies in the overlay to its final position and size

當英雄飛行時,其矩形邊界使用 Tween<Rect> 進行動畫處理,該插值器在 Hero 的 createRectTween 屬性中指定。預設情況下,Flutter 使用 MaterialRectArcTween 的例項,該例項沿曲線路徑動畫化矩形的相對角。


When the transition is complete, the hero is moved from the overlay to the destination route

當飛行完成時

  • Flutter 將英雄小部件從疊加層移動到目標路由。疊加層現在為空。

  • 目標英雄出現在目標路由中的最終位置。

  • 源英雄恢復到其路由。


彈出路由執行相同的過程,將英雄動畫化回源路由中的大小和位置。

必要的類

#

本指南中的示例使用以下類來實現英雄動畫

英雄

從源路由到目標路由飛行的部件。為源路由和目標路由定義一個 Hero,併為每個 Hero 分配相同的標籤。Flutter 會動畫化具有匹配標籤的英雄對。

InkWell

指定點選英雄時發生的情況。InkWellonTap() 方法構建新路由並將其推送到 Navigator 的堆疊。

Navigator

Navigator 管理路由堆疊。將路由推入或從 Navigator 的堆疊中彈出會觸發動畫。

Route

指定螢幕或頁面。大多數應用程式,除了最基本的應用程式之外,都有多個路由。

標準的英雄動畫

#

發生了什麼?

#

使用 Flutter 的英雄小部件將影像從一個路由飛行到另一個路由很容易實現。當使用 MaterialPageRoute 指定新路由時,影像會沿 Material Design 運動規範 描述的曲線路徑飛行。

建立一個新的 Flutter 應用程式 並使用 hero_animation 中的檔案對其進行更新。

要執行示例

  • 點選主路由中的照片,將影像飛行到顯示相同照片的新路由,該照片位於不同的位置和比例。
  • 點選影像或使用裝置的返回手勢返回上一路線。
  • 您可以使用 timeDilation 屬性進一步減慢過渡速度。

PhotoHero 類

#

自定義 PhotoHero 類維護英雄、其大小、影像和點選行為。PhotoHero 構建以下小部件樹

PhotoHero class widget tree

這是程式碼

dart
class PhotoHero extends StatelessWidget {
  const PhotoHero({
    super.key,
    required this.photo,
    this.onTap,
    required this.width,
  });

  final String photo;
  final VoidCallback? onTap;
  final double width;

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: width,
      child: Hero(
        tag: photo,
        child: Material(
          color: Colors.transparent,
          child: InkWell(
            onTap: onTap,
            child: Image.asset(
              photo,
              fit: BoxFit.contain,
            ),
          ),
        ),
      ),
    );
  }
}

關鍵資訊

  • HeroAnimation 作為應用的 home 屬性提供時,起始路線由 MaterialApp 隱式推送。
  • InkWell 包裝影像,使為源英雄和目標英雄新增點選手勢變得微不足道。
  • 使用透明顏色定義 Material 小部件,可以使影像在飛向目標位置時“彈出”背景。
  • SizedBox 指定動畫開始和結束時英雄的大小。
  • 將 Image 的 fit 屬性設定為 BoxFit.contain,可確保影像在過渡期間儘可能大,而不會改變其縱橫比。

HeroAnimation 類

#

HeroAnimation 類建立源和目標 PhotoHeroes,並設定過渡。

這是程式碼

dart
class HeroAnimation extends StatelessWidget {
  const HeroAnimation({super.key});

  Widget build(BuildContext context) {
    timeDilation = 5.0; // 1.0 means normal animation speed.

    return Scaffold(
      appBar: AppBar(
        title: const Text('Basic Hero Animation'),
      ),
      body: Center(
        child: PhotoHero(
          photo: 'images/flippers-alpha.png',
          width: 300.0,
          onTap: () {
            Navigator.of(context).push(MaterialPageRoute<void>(
              builder: (context) {
                return Scaffold(
                  appBar: AppBar(
                    title: const Text('Flippers Page'),
                  ),
                  body: Container(
                    // Set background to blue to emphasize that it's a new route.
                    color: Colors.lightBlueAccent,
                    padding: const EdgeInsets.all(16),
                    alignment: Alignment.topLeft,
                    child: PhotoHero(
                      photo: 'images/flippers-alpha.png',
                      width: 100.0,
                      onTap: () {
                        Navigator.of(context).pop();
                      },
                    ),
                  ),
                );
              }
            ));
          },
        ),
      ),
    );
  }
}

關鍵資訊

  • 當用戶點選包含源英雄的 InkWell 時,程式碼使用 MaterialPageRoute 建立目標路線。將目標路線推送到 Navigator 的堆疊會觸發動畫。
  • ContainerPhotoHero 定位在目標路線的左上角,位於 AppBar 下方。
  • 目標 PhotoHeroonTap() 方法會彈出 Navigator 的堆疊,從而觸發將 Hero 飛回原始路線的動畫。
  • 使用 timeDilation 屬性在除錯時減慢過渡速度。

徑向英雄動畫

#

將英雄從一個路線飛到另一個路線,同時將其從圓形形狀轉換為矩形形狀,是一種流暢的效果,您可以使用 Hero 小部件來實現。為此,程式碼會動畫化兩個剪輯形狀的交集:一個圓形和一個正方形。在整個動畫過程中,圓形剪輯(以及影像)從 minRadius 縮放到 maxRadius,而方形剪輯保持恆定大小。與此同時,影像從源路線中的位置飛到目標路線中的位置。有關此過渡的視覺示例,請參閱 徑向變換 在 Material motion 規範中。

此動畫可能看起來很複雜(而且確實如此),但您可以 根據需要自定義提供的示例。 大部分繁重的工作已經為您完成。

發生了什麼?

#

下圖顯示了動畫開始 (t = 0.0) 和結束 (t = 1.0) 時剪輯的影像。

Radial transformation from beginning to end

藍色漸變(代表影像)指示剪輯形狀相交的位置。在過渡開始時,交集的結果是圓形剪輯 (ClipOval)。在轉換過程中,ClipOvalminRadius 縮放到 maxRadius,而 ClipRect 保持恆定大小。在過渡結束時,圓形和矩形剪輯的交集產生一個與英雄小部件大小相同的矩形。換句話說,在過渡結束時,影像不再被剪輯。

建立一個新的 Flutter 應用 並使用 radial_hero_animation GitHub 目錄中的檔案進行更新。

要執行示例

  • 點選三個圓形縮圖中的一個,以將影像動畫化到更大的正方形,該正方形位於遮擋原始路線的新路線的中間。
  • 點選影像或使用裝置的返回手勢返回上一路線。
  • 您可以使用 timeDilation 屬性進一步減慢過渡速度。

Photo 類

#

Photo 類構建包含影像的小部件樹

dart
class Photo extends StatelessWidget {
  const Photo({super.key, required this.photo, this.color, this.onTap});

  final String photo;
  final Color? color;
  final VoidCallback onTap;

  Widget build(BuildContext context) {
    return Material(
      // Slightly opaque color appears where the image has transparency.
      color: Theme.of(context).primaryColor.withValues(alpha: 0.25),
      child: InkWell(
        onTap: onTap,
        child: Image.asset(
          photo,
          fit: BoxFit.contain,
        ),
      ),
    );
  }
}

關鍵資訊

  • InkWell 捕獲點選手勢。呼叫函式將 onTap() 函式傳遞給 Photo 的建構函式。
  • 飛行過程中,InkWell 會在其第一個 Material 祖先上繪製其飛濺效果。
  • Material 小部件具有略微不透明的顏色,因此影像的透明部分將以顏色渲染。這可確保即使對於具有透明度的影像,也能輕鬆看到圓形到正方形的過渡。
  • Photo 類不包含其小部件樹中的 Hero。為了使動畫正常工作,英雄必須包裝 RadialExpansion 小部件。

RadialExpansion 類

#

RadialExpansion 小部件是演示的核心,它構建在過渡期間剪輯影像的小部件樹。剪輯形狀是圓形剪輯(在飛行過程中增長)與矩形剪輯(在整個過程中保持恆定大小)的交集的結果。

為此,它構建以下小部件樹

RadialExpansion widget tree

這是程式碼

dart
class RadialExpansion extends StatelessWidget {
  const RadialExpansion({
    super.key,
    required this.maxRadius,
    this.child,
  }) : clipRectSize = 2.0 * (maxRadius / math.sqrt2);

  final double maxRadius;
  final double clipRectSize;
  final Widget? child;

  @override
  Widget build(BuildContext context) {
    return ClipOval(
      child: Center(
        child: SizedBox(
          width: clipRectSize,
          height: clipRectSize,
          child: ClipRect(
            child: child, // Photo
          ),
        ),
      ),
    );
  }
}

關鍵資訊

  • 英雄包裝 RadialExpansion 小部件。

  • 當英雄飛行時,其大小會發生變化,並且由於它約束其子小部件的大小,RadialExpansion 小部件的大小會發生變化以匹配。

  • RadialExpansion 動畫由兩個重疊的剪輯建立。

  • 該示例使用 MaterialRectCenterArcTween 定義了 tween 插值。英雄動畫的預設飛行路徑使用英雄的角來插值 tween。這種方法會影響徑向變換期間英雄的縱橫比,因此新的飛行路徑使用 MaterialRectCenterArcTween 來使用每個英雄的中心點來插值 tween。

    這是程式碼

    dart
    static RectTween _createRectTween(Rect? begin, Rect? end) {
      return MaterialRectCenterArcTween(begin: begin, end: end);
    }
    

    英雄的飛行路徑仍然遵循弧線,但影像的縱橫比保持恆定。