跳到主要內容

Hero 動畫

您可能已經看過許多次英雄動畫。例如,螢幕會顯示代表待售商品的縮圖清單。選擇一個商品會將它飛到一個新的畫面,其中包含更多詳細資料和「購買」按鈕。將圖片從一個畫面飛到另一個畫面在 Flutter 中稱為英雄動畫,儘管相同的動作有時會稱為共享元素轉場

您可能想觀看這段一分鐘的影片,介紹 Hero Widget


Hero | 每週 Flutter Widget

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

您可以使用 Hero Widget 在 Flutter 中建立此動畫。當英雄從來源路由動畫到目的地路由時,目的地路由 (不包含英雄) 會淡入畫面。通常,英雄是使用者介面的小部分,例如兩個路由共有的影像。從使用者的角度來看,英雄會在路由之間「飛行」。本指南說明如何建立下列英雄動畫

標準英雄動畫

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

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


Flutter 中的標準英雄動畫

放射狀英雄動畫

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

以下影片 (以慢速錄製) 顯示了放射狀英雄動畫的範例。在開始時,會在路由底部顯示一排三個圓形影像。點擊任何圓形影像會將該影像飛到新的路由,該路由會以方形顯示它。點擊方形影像會將英雄飛回原始路由,並以圓形顯示。


Flutter 中的放射狀英雄動畫

在移動到 標準放射狀 英雄動畫的特定章節之前,請閱讀 英雄動畫的基本結構,了解如何建構英雄動畫程式碼,以及 幕後花絮,了解 Flutter 如何執行英雄動畫。

英雄動畫的基本結構

#

英雄動畫是使用兩個 Hero Widget 實作的:一個描述來源路由中的 Widget,另一個描述目的地路由中的 Widget。從使用者的角度來看,英雄似乎是共享的,只有程式設計師需要了解此實作細節。英雄動畫程式碼具有下列結構

  1. 定義一個起始的 Hero Widget,稱為來源英雄。英雄指定其圖形表示 (通常是影像) 和識別標籤,並且在來源路由定義的目前顯示 Widget 樹狀結構中。
  2. 定義一個結束的 Hero Widget,稱為目的地英雄。此英雄也會指定其圖形表示,以及與來源英雄相同的標籤。必須使用相同的標籤建立兩個英雄 Widget,通常是表示基礎資料的物件。為了獲得最佳效果,英雄應具有幾乎相同的 Widget 樹狀結構。
  3. 建立包含目的地英雄的路由。目的地路由會定義動畫結束時存在的 Widget 樹狀結構。
  4. 藉由將目的地路由推送到 Navigator 的堆疊上來觸發動畫。Navigator 的推送和彈出操作會為來源和目的地路由中每對具有相符標籤的英雄觸發英雄動畫。

Flutter 會計算補間動畫,使 Hero 的邊界從起點動畫到終點 (插補大小和位置),並在覆蓋層中執行動畫。

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

幕後花絮

#

以下說明 Flutter 如何執行從一個路由到另一個路由的轉換。

Before the transition the source hero appears in the source route

在轉換之前,來源英雄會在來源路由的 Widget 樹狀結構中等待。目的地路由尚不存在,且覆蓋層是空的。


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 會將英雄 Widget 從覆蓋層移到目的地路由。覆蓋層現在是空的。

  • 目的地英雄會以其在目的地路由中的最終位置顯示。

  • 來源英雄會還原到其路由。


彈出路由會執行相同的過程,使英雄動畫回到其在來源路由中的大小和位置。

重要類別

#

本指南中的範例使用下列類別來實作英雄動畫

Hero
Hero
InkWell
指定點擊英雄時會發生的動作。InkWellonTap() 方法會建立新的路由並將其推送到 Navigator 的堆疊。
Navigator
Navigator 會管理路由堆疊。將路由推送到 Navigator 的堆疊或從中彈出路由會觸發動畫。
Route
指定螢幕或頁面。大多數應用程式 (超出最基本應用程式) 都具有多個路由。

標準英雄動畫

#

發生了什麼事?

#

使用 Flutter 的英雄 Widget 可以輕鬆實作將影像從一個路由飛到另一個路由的功能。當使用 MaterialPageRoute 指定新的路由時,影像會沿著彎曲的路徑飛行,如 Material Design 動態規格中所述。

建立新的 Flutter 範例並使用來自 hero_animation 的檔案進行更新。

若要執行此範例

  • 點擊首頁路由的照片,將影像飛到新的路由,其中會以不同的位置和比例顯示相同的照片。
  • 點擊影像或使用裝置的返回上一個路由手勢,返回上一個路由。
  • 您可以使用 timeDilation 屬性進一步減慢轉換速度。

PhotoHero 類別

#

自訂的 PhotoHero 類別會維護英雄,以及其大小、影像和點擊時的行為。PhotoHero 會建構下列 Widget 樹狀結構

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 包裹著圖片,使得在來源和目標的 hero 上添加點擊手勢變得非常簡單。
  • 使用透明顏色定義 Material 小部件,可以使圖片在飛向目的地時從背景中「彈出」。
  • SizedBox 指定了動畫開始和結束時 hero 的大小。
  • 將圖片的 fit 屬性設定為 BoxFit.contain,可確保圖片在過渡期間盡可能放大,同時不改變其長寬比。

HeroAnimation 類別

#

HeroAnimation 類別會建立來源和目標的 PhotoHero,並設定過渡效果。

以下是程式碼

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();
                      },
                    ),
                  ),
                );
              }
            ));
          },
        ),
      ),
    );
  }
}

重要資訊

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

放射狀英雄動畫

#

當 hero 從圓形轉換為矩形時,將其從一個路由飛到另一個路由是一個很棒的效果,您可以使用 Hero 小部件來實作。為此,程式碼會將兩個剪輯形狀的交集動畫化:一個圓形和一個方形。在整個動畫過程中,圓形剪輯(以及圖片)會從 minRadius 縮放到 maxRadius,而方形剪輯則保持恆定大小。同時,圖片會從來源路由中的位置飛到目標路由中的位置。如需此轉換的視覺範例,請參閱 Material 動態規格中的徑向轉換

這個動畫可能看起來很複雜(而且確實如此),但您可以自訂提供的範例以滿足您的需求。繁重的工作已為您完成。

發生了什麼事?

#

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

Radial transformation from beginning to end

藍色漸層(代表圖片)表示剪輯形狀的相交位置。在過渡開始時,交集的結果是圓形剪輯 (ClipOval)。在轉換期間,ClipOval 會從 minRadius 縮放到 maxRadius,而 ClipRect 則保持恆定大小。在過渡結束時,圓形和矩形剪輯的交集會產生一個與 hero 小部件大小相同的矩形。換句話說,在過渡結束時,圖片不再被剪裁。

建立新的 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.withOpacity(0.25),
      child: InkWell(
        onTap: onTap,
        child: Image.asset(
          photo,
          fit: BoxFit.contain,
        ),
      ),
    );
  }
}

重要資訊

  • InkWell 會捕捉點擊手勢。呼叫函式會將 onTap() 函式傳遞給 Photo 的建構函式。
  • 在飛行過程中,InkWell 會在其第一個 Material 祖先上繪製其水波紋效果。
  • Material 小部件具有稍微不透明的顏色,因此圖片的透明部分會以顏色呈現。這確保了即使對於具有透明度的圖片,也很容易看到圓形到方形的轉換。
  • Photo 類別在其小部件樹狀結構中不包含 Hero。為了使動畫生效,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 clipRectSize;
  final Widget child;

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

重要資訊

  • hero 會包裹 RadialExpansion 小部件。

  • 當 hero 飛行時,其大小會改變,並且由於它會限制其子項的大小,因此 RadialExpansion 小部件也會改變大小以匹配。

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

  • 範例使用 MaterialRectCenterArcTween 定義補間插值。hero 動畫的預設飛行路徑會使用 hero 的角落來插值補間。此方法會影響徑向轉換期間 hero 的長寬比,因此新的飛行路徑會使用 MaterialRectCenterArcTween 來使用每個 hero 的中心點插值補間。

    以下是程式碼

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

    hero 的飛行路徑仍然遵循弧形,但圖片的長寬比保持恆定。