Điểm chính
Hero Animation là kỹ thuật hoạt họna giao diện trong đó một phần tử dùng chung di chuyển mượt mà từ màn hình nguồn sang màn hình đích, bảo toàn danh tính của nó. Tên gọi phản ánh phép ẩn dụ của một “anh hùng” — phần tử chính trải qua một chuyển tiếp được kịch bản giữa các màn hình, thu hút sự chú ý của người dùng. Không giống như các chuyển tiếp tiêu chuẩn, Hero Animation không phá vỡ kết nối trực quan: người dùng thấy quỹ đạo liên tục của phần tử, cải thiện hiểu biết về hệ phân cấp điều hướng.
Khái niệm Hero Animation được thiết lập trong Material Design như một khuyến nghị cho các màn hình danh sách-chi tiết, thư viện ảnh và thẻ. Thuật toán hoạt họna gồm ba giai đoạn: trên màn hình nguồn, phần tử được làm nổi bật và cố định ở vị trí tuyệt đối; trong quá trình chuyển tiếp, nó thu phóng, di chuyển và thay đổi hình dạng theo vùng chứa đích; trên màn hình cuối cùng, phần tử được nhúng vào bố cục mới. Theo Material Design 3, hoạt họna anh hùng được triển khai đúng cách giảm số lần nhấn nút “quay lại” sai 22%.
Trong Flutter, Hero Animation được triển khai qua widget Hero, nhận tham số bắt buộc tag — một định danh duy nhất. Flutter tự động tìm cặp widget Hero có cùng thẻ trên các tuyến đường nguồn và đích và khởi chạy hoạt họna. Widget Hero không yêu cầu quản lý hoạt họna thủ công — framework tự tính toán quỹ đạo và áp dụng biến đổi qua đối tượng FlightShuttle nội bộ.
FlightShuttle là một “tàu con thoi” vô hình, tại thời điểm chuyển tiếp, tạo một bản sao lớp phủ của widget Hero, đặt nó trong hệ tọa độ tuyệt đối và hoạt họna biến đổi (tỷ lệ, tịnh tiến, xoay) từ vị trí ban đầu đến vị trí cuối cùng. Các đường cong tùy chỉnh có thể được truyền cho FlightShuttle qua thuộc tính flightShuttleBuilder, cho phép điều khiển quỹ đạo bay: bay theo vòng cung, nảy khỏi ranh giới hoặc giảm tốc trước khi đến. Trong Flutter 3.16+, Hero hỗ trợ hoạt họna bảo toàn clipRect và borderRadius, tạo hiệu ứng các thẻ tròn biến thành màn hình hình chữ nhật.
Trong SwiftUI, tương đương của Hero Animation là bộ sửa đổi matchedGeometryEffect, được giới thiệu trong iOS 14. Nó liên kết hai phần tử qua một định danh (id) và một namespace. Không giống như Flutter, nơi Hero tự động chặn chuyển tiếp, SwiftUI yêu cầu khai báo tường minh namespace và áp dụng bộ sửa đổi cho cả hai phần của cảnh — nguồn và đích — trong cùng một hệ phân cấp hoặc qua GeometryReader.
Hoạt họna matchedGeometryEffect hỗ trợ biến đổi vị trí, kích thước, xoay và bán kính góc. Để có sự mượt mà, một Animation được chỉ định trong withAnimation. SwiftUI tự động nội suy các trạng thái trung gian dựa trên đường cong mặc định easeInOut. Trong iOS 17+, hỗ trợ matchedTransitionSource đã được thêm vào cho TabView và NavigationStack — một phân hệ thống tương tự Hero Animation với quỹ đạo được xác định trước cho các chuyển tiếp tiêu chuẩn.
Trong Android, Hero Animation được gọi là Shared Element Transition và được triển khai qua ActivityOptions.makeSceneTransitionAnimation hoặc FragmentTransaction.addSharedElement. Mỗi phần tử dùng chung được đánh dấu bằng thuộc tính transitionName trong XML hoặc qua mã, và Android xử lý hoạt họna giữa các hoạt động. Bắt đầu từ Android 5.0 (API 21), các chuyển tiếp đã trở thành một phần của framework; trước đó, phải viết các hoạt họna tùy chỉnh bằng ValueAnimator.
Trong Jetpack Compose, Shared Element Transition được triển khai qua các bộ sửa đổi Modifier.sharedElement và Modifier.sharedBounds kết hợp với AnimatedContent hoặc SharedTransitionLayout (có sẵn từ Compose BOM 2024.01). Không giống như hệ thống View, Compose vẽ lại các phần tử một cách lười biếng, do đó phần tử dùng chung được lưu vào bộ nhớ đệm trong CompositionLocal. Compose cũng hỗ trợ hoạt họna từ danh sách đến chi tiết với bảo toàn scrollOffset — một khả năng chính cho các chuyển tiếp Hero. Theo Google I/O 2024, Compose Shared Element Transition nhanh hơn 15% so với phiên bản View nhờ quản lý RenderNode trực tiếp.
Một ví dụ cơ bản về Hero Animation trong Flutter: hình thu nhỏ trên màn hình danh sách và hình ảnh toàn màn hình trên màn hình chi tiết. Thẻ image-hero liên kết hai widget và FlightShuttle tự động hoạt họna chuyển tiếp từ thẻ sang toàn màn hình.
import 'package:flutter/material.dart';
class ListScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: [
Hero(
tag: 'image-hero',
child: GestureDetector(
onTap: () {
Navigator.push(context,
MaterialPageRoute(
builder: (_) => DetailScreen(),
),
);
},
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(
'https://picsum.photos/200',
width: 100, height: 100,
fit: BoxFit.cover,
),
),
),
),
],
),
);
}
}
class DetailScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Detail')),
body: Center(
child: Hero(
tag: 'image-hero',
child: Image.network(
'https://picsum.photos/800',
width: 300, height: 400,
fit: BoxFit.cover,
),
),
),
);
}
}
Điểm chính là cùng một thẻ (image-hero) trên cả hai màn hình. Flutter so khớp các widget Hero theo thẻ trong quá trình chuyển tiếp tuyến đường. Nếu các thẻ khác nhau, hoạt họna sẽ không bắt đầu — phần tử sẽ xuất hiện trên màn hình mới mà không có chuyển tiếp. Có thể sử dụng bất kỳ loại nào cho thẻ, nhưng chuỗi là lựa chọn đáng tin cậy nhất.
Một ví dụ về Hero Animation trong SwiftUI với NavigationStack. Bộ sửa đổi matchedGeometryEffect liên kết hình ảnh trong danh sách và màn hình chi tiết qua một namespace, và withAnimation kích hoạt chuyển tiếp mượt mà.
import SwiftUI
struct HeroExampleView: View {
@Namespace private var heroNamespace
@State private var isDetailShown = false
var body: some View {
VStack {
if isDetailShown {
DetailView(namespace: heroNamespace)
.transition(.identity)
} else {
ListView(namespace: heroNamespace)
.transition(.identity)
}
}
.animation(Animation.easeInOut(duration: 0.4),
value: isDetailShown)
}
}
struct ListView: View {
let namespace: Namespace.ID
var body: some View {
Image("thumbnail")
.resizable()
.frame(width: 100, height: 100)
.cornerRadius(12)
.matchedGeometryEffect(id: "hero", in: namespace)
}
}
struct DetailView: View {
let namespace: Namespace.ID
var body: some View {
Image("thumbnail")
.resizable()
.frame(width: 300, height: 400)
.cornerRadius(0)
.matchedGeometryEffect(id: "hero", in: namespace)
}
}
@Namespace tạo một không gian tên cho định danh matchedGeometryEffect. Cả hai phần tử sử dụng cùng một id (“hero”) và một namespace dùng chung. Khi isDetailShown chuyển đổi, SwiftUI hoạt họna sự biến đổi kích thước, vị trí và bán kính góc giữa hai chế độ xem, tạo hiệu ứng anh hùng. Đối với NavigationLink tiêu chuẩn với iOS 16+, hãy sử dụng navigationTransition(.hero).
Một ví dụ về Shared Element Transition trong Jetpack Compose với SharedTransitionLayout. Bộ sửa đổi sharedElement hoạt họna một phần tử dùng chung giữa các màn hình trong quá trình điều hướng.
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun HeroExample() {
SharedTransitionLayout {
var isDetail by remember { mutableStateOf(false) }
AnimatedContent(
targetState = isDetail,
transitionSpec = {
fadeIn() togetherWith fadeOut()
}
) { detail ->
if (detail) {
Box(
modifier = Modifier
.fillMaxSize()
.sharedElement(
state = rememberSharedContentState(key = "hero"),
animatedVisibilityScope = this
)
) {
AsyncImage(
model = "https://picsum.photos/800",
contentDescription = null,
modifier = Modifier.fillMaxSize()
)
}
} else {
Box(
modifier = Modifier
.size(100.dp)
.sharedElement(
state = rememberSharedContentState(key = "hero"),
animatedVisibilityScope = this
)
) {
AsyncImage(
model = "https://picsum.photos/200",
contentDescription = null,
modifier = Modifier.fillMaxSize()
)
}
}
}
}
}
Compose SharedTransitionLayout (Compose BOM 2024.01+) bao bọc hệ phân cấp, và bộ sửa đổi sharedElement với khóa “hero” liên kết các phần tử ban đầu và cuối cùng. AnimatedContent chuyển đổi trạng thái và Compose tự động hoạt họna kích thước, vị trí và hình dạng của hình ảnh. Một lợi thế chính là hỗ trợ hoạt họna bounds và clipBounds, mang lại hiệu ứng làm tròn thẻ trong quá trình chuyển tiếp.
Câu hỏi thường gặp
Có, Hero Animation có sẵn trên cả hai nền tảng. iOS sử dụng matchedGeometryEffect trong SwiftUI hoặc UINavigationController.transitioningDelegate. Android sử dụng ActivityOptions.makeSceneTransitionAnimation hoặc Shared Element Transition trong Jetpack Compose với Modifier.sharedElement. Flutter triển khai Hero Animation qua widget Hero, hoạt động giống hệt trên cả hai nền tảng.
Có, Hero Animation hỗ trợ nhiều phần tử dùng chung. Trong Flutter, chỉ định nhiều widget Hero với các thẻ khác nhau. Trong SwiftUI, sử dụng matchedGeometryEffect với các định danh khác nhau trên mỗi phần tử. Tất cả các hoạt họna chạy song song — mỗi phần tử di chuyển theo quỹ đạo riêng của nó.
Hiệu suất phụ thuộc vào cách triển khai. FlightShuttle của Flutter xử lý hình ảnh qua RenderObject, đạt 60fps. matchedGeometryEffect của SwiftUI được vector hóa. Đối với hình ảnh lớn, hãy sử dụng precacheImage trong Flutter hoặc prefetch trong SwiftUI — điều này loại bỏ độ trễ trong lần chuyển tiếp đầu tiên. Android Compose yêu cầu tải chậm qua AsyncImage.
Chuyển tiếp push tiêu chuẩn trượt toàn bộ màn hình, tạo ảo giác về một chồng thẻ. Hero Animation cố định sự chú ý trực quan vào một phần tử duy nhất — người dùng thấy chuyển động liên tục của nó, cải thiện sự hiểu biết về mối quan hệ giữa các màn hình. Push phù hợp cho điều hướng phân cấp, Hero phù hợp để nhấn mạnh nội dung.
Không, Hero Animation được triển khai bằng các công cụ tích hợp sẵn. Trong Flutter, đó là widget Hero từ gói flutter/material.dart. Trong SwiftUI, đó là bộ sửa đổi matchedGeometryEffect. Trong Android Jetpack Compose, đó là SharedTransitionLayout và Modifier.sharedElement từ Compose tiêu chuẩn. Thư viện bổ sung chỉ cần thiết cho các quỹ đạo không tiêu chuẩn hoặc các biến đổi phức tạp.
Tóm tắt
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm