1. Giới thiệu
Lần cập nhật gần đây nhất: 21/11/2023
Trong lớp học lập trình này, bạn sẽ tìm hiểu cách sử dụng một số API Ảnh động trong Jetpack Compose.
Jetpack Compose là một bộ công cụ giao diện người dùng hiện đại được thiết kế để đơn giản hoá quá trình phát triển giao diện người dùng. Nếu mới sử dụng Jetpack Compose, bạn có thể muốn thử một số lớp học lập trình trước đây.
- Kiến thức cơ bản về Jetpack Compose
- Bố cục trong Jetpack Compose
- Sử dụng trạng thái trong Jetpack Compose
Kiến thức bạn sẽ học được
- Cách sử dụng một số API ảnh động cơ bản
Điều kiện tiên quyết
- Kiến thức cơ bản về Kotlin.
- Kiến thức cơ bản về Compose bao gồm:
- Bố cục đơn giản (Cột, Hàng, Hộp, v.v.)
- Các thành phần trên giao diện người dùng đơn giản (Nút, Văn bản, v.v.)
- Trạng thái và quá trình kết hợp lại
Bạn cần có
2. Thiết lập
Tải mã lớp học lập trình xuống. Sau đây là cách sao chép kho lưu trữ:
$ git clone https://github.com/android/codelab-android-compose.git
Ngoài ra, bạn có thể tải kho lưu trữ xuống dưới dạng tệp ZIP:
Nhập dự án AnimationCodelab
trong Android Studio.
Dự án có nhiều mô-đun trong đó:
start
là trạng thái bắt đầu của lớp học lập trình.finished
là trạng thái cuối cùng của ứng dụng sau khi bạn hoàn thành lớp học lập trình này.
Hãy đảm bảo bạn đã chọn start
trong trình đơn thả xuống cho cấu hình chạy.
Chúng ta sẽ bắt đầu xử lý một số tình huống ảnh động trong chương tiếp theo. Mỗi đoạn mã chúng ta xử lý trong lớp học lập trình này đều được đánh dấu bằng một chú thích // TODO
. Một mẹo nhỏ là mở cửa sổ công cụ TODO trong Android Studio rồi chuyển đến từng nhận xét về TODO cho chương đó.
3. Tạo ảnh động cho thay đổi đơn giản về giá trị
Hãy bắt đầu bằng một trong những API Ảnh động đơn giản nhất trong Compose: API animate*AsState
. Bạn nên sử dụng API này khi tạo ảnh động cho các thay đổi của State
.
Chạy cấu hình start
rồi thử chuyển đổi các thẻ bằng cách nhấp vào nút "Home" (Trang chủ) và "Work" (Công việc) ở trên cùng. Tính năng này không chuyển đổi nội dung thẻ, nhưng bạn có thể thấy màu nền của nội dung thay đổi.
Nhấp vào TODO 1 trong cửa sổ công cụ TODO và xem cách này được triển khai. Nó nằm trong thành phần kết hợp Home
.
val backgroundColor = if (tabPage == TabPage.Home) Seashell else GreenLight
Ở đây, tabPage
là một TabPage
được đối tượng State
hỗ trợ. Màu nền sẽ chuyển đổi giữa màu hồng đào và màu xanh lục tuỳ thuộc vào giá trị. Chúng ta muốn tạo ảnh động cho sự thay đổi về giá trị này.
Để tạo ảnh động cho một thay đổi đơn giản về giá trị như thế này, chúng ta có thể sử dụng các API animate*AsState
. Bạn có thể tạo giá trị ảnh động bằng cách gói giá trị thay đổi với biến thể tương ứng của thành phần kết hợp animate*AsState
, trong trường hợp này là animateColorAsState
. Giá trị trả về là đối tượng State<T>
nên chúng ta có thể sử dụng thuộc tính uỷ quyền cục bộ cùng phần khai báo by
để xử lý giá trị này như một biến thông thường.
val backgroundColor by animateColorAsState(
targetValue = if (tabPage == TabPage.Home) Seashell else GreenLight,
label = "background color")
Chạy lại ứng dụng rồi thử chuyển sang các thẻ khác. Ảnh thay đổi màu giờ sẽ là ảnh động.
4. Ảnh động hiển thị
Nếu cuộn nội dung của ứng dụng thì bạn sẽ thấy nút hành động nổi mở rộng và thu nhỏ tuỳ theo hướng cuộn.
Tìm TODO 2-1 và kiểm tra cách hoạt động của nút này Tệp này nằm trong thành phần kết hợp HomeFloatingActionButton
. Dòng chữ "EDIT" (Chỉnh sửa) sẽ xuất hiện hoặc bị ẩn bằng câu lệnh if
.
if (extended) {
Text(
text = stringResource(R.string.edit),
modifier = Modifier
.padding(start = 8.dp, top = 3.dp)
)
}
Để tạo ảnh động cho thay đổi trong chế độ hiển thị này, bạn chỉ cần thay thế if
bằng thành phần kết hợp AnimatedVisibility
.
AnimatedVisibility(extended) {
Text(
text = stringResource(R.string.edit),
modifier = Modifier
.padding(start = 8.dp, top = 3.dp)
)
}
Chạy ứng dụng ngay và đồng thời xem FAB sẽ mở rộng và thu nhỏ như thế nào.
AnimatedVisibility
chạy ảnh động mỗi khi giá trị Boolean
được chỉ định thay đổi. Theo mặc định, AnimatedVisibility
cho thấy phần tử bằng cách làm mờ và mở rộng, ngoài ra còn ẩn phần tử đó bằng cách làm mờ và thu nhỏ. Hành vi trên phù hợp với ví dụ về FAB này. Tuy vậy, chúng ta cũng có thể tuỳ chỉnh hành vi đó.
Thử nhấp vào nút hành động nổi và bạn sẽ thấy thông báo có nội dung "Tính năng chỉnh sửa không được hỗ trợ". Mã này cũng sử dụng AnimatedVisibility
để tạo hiệu ứng ảnh động cho sự xuất hiện và biến mất của nó. Tiếp theo, bạn sẽ tuỳ chỉnh hành vi này để nội dung thông báo trượt vào từ trên cùng và trượt ra lên trên cùng.
Tìm TODO 2-2 và xem mã trong thành phần kết hợp EditMessage
.
AnimatedVisibility(
visible = shown
) {
Surface(
modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colorScheme.secondary,
elevation = 4.dp
) {
Text(
text = stringResource(R.string.edit_message),
modifier = Modifier.padding(16.dp)
)
}
}
Để tuỳ chỉnh ảnh động, hãy thêm tham số enter
và exit
vào thành phần kết hợp AnimatedVisibility
.
Tham số enter
phải là một thực thể của EnterTransition
. Trong ví dụ này, chúng ta có thể sử dụng hàm slideInVertically
để tạo một EnterTransition
và slideOutVertically
cho quá trình chuyển đổi thoát. Thay đổi mã như bên dưới:
AnimatedVisibility(
visible = shown,
enter = slideInVertically(),
exit = slideOutVertically()
)
Chạy lại ứng dụng, nhấp vào nút chỉnh sửa, bạn có thể thấy ảnh động trông đẹp hơn nhưng không chính xác. Đó là do hành vi mặc định của slideInVertically
và slideOutVertically
sử dụng một nửa chiều cao của mục.
Đối với chuyển đổi enter (vào): chúng ta có thể điều chỉnh hành vi mặc định để sử dụng toàn bộ chiều cao của mục nhằm tạo ảnh động đúng cách bằng cách thiết lập tham số initialOffsetY
. initialOffsetY
phải là hàm lambda trả về vị trí ban đầu.
Hàm lambda nhận một đối số, chiều cao của phần tử. Để đảm bảo mục đó trượt vào từ đầu màn hình, chúng ta trả về giá trị âm vì điểm đầu màn hình có giá trị bằng 0. Chúng ta muốn ảnh động bắt đầu từ -height
đến 0
(vị trí nghỉ cuối cùng) để ảnh động trượt vào từ trên xuống và tạo hiệu ứng động.
Khi sử dụng slideInVertically
, độ lệch mục tiêu sau khi trượt vào luôn là 0
(pixel). Bạn có thể chỉ định initialOffsetY
dưới dạng một giá trị tuyệt đối, hoặc tỷ lệ phần trăm chiều cao đầy đủ của phần tử thông qua hàm lambda.
Tương tự, slideOutVertically
giả định độ lệch ban đầu là 0 nên bạn chỉ cần chỉ định targetOffsetY
.
AnimatedVisibility(
visible = shown,
enter = slideInVertically(
// Enters by sliding down from offset -fullHeight to 0.
initialOffsetY = { fullHeight -> -fullHeight }
),
exit = slideOutVertically(
// Exits by sliding up from offset 0 to -fullHeight.
targetOffsetY = { fullHeight -> -fullHeight }
)
) {
Surface(
modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colorScheme.secondary,
elevation = 4.dp
) {
Text(
text = stringResource(R.string.edit_message),
modifier = Modifier.padding(16.dp)
)
}
}
Khi chạy lại ứng dụng, chúng ta có thể thấy ảnh động phù hợp hơn những gì mà chúng ta mong đợi:
Chúng ta có thể tuỳ chỉnh ảnh động nhiều hơn bằng tham số animationSpec
. animationSpec
là một tham số phổ biến cho nhiều API Ảnh động, bao gồm EnterTransition
và ExitTransition
. Chúng ta có thể truyền một trong các loại AnimationSpec
để chỉ định cách giá trị ảnh động sẽ thay đổi theo thời gian. Trong ví dụ này, hãy sử dụng AnimationSpec
dựa trên thời lượng đơn giản. Bạn có thể tạo hàm này bằng hàm tween
. Thời lượng là 150 mili giây và tốc độ là LinearOutSlowInEasing
. Đối với ảnh động thoát, hãy sử dụng cùng một hàm tween
cho tham số animationSpec
, nhưng có thời lượng là 250 mili giây và tốc độ là FastOutLinearInEasing
.
Mã kết quả sẽ có dạng như bên dưới:
AnimatedVisibility(
visible = shown,
enter = slideInVertically(
// Enters by sliding down from offset -fullHeight to 0.
initialOffsetY = { fullHeight -> -fullHeight },
animationSpec = tween(durationMillis = 150, easing = LinearOutSlowInEasing)
),
exit = slideOutVertically(
// Exits by sliding up from offset 0 to -fullHeight.
targetOffsetY = { fullHeight -> -fullHeight },
animationSpec = tween(durationMillis = 250, easing = FastOutLinearInEasing)
)
) {
Surface(
modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colorScheme.secondary,
elevation = 4.dp
) {
Text(
text = stringResource(R.string.edit_message),
modifier = Modifier.padding(16.dp)
)
}
}
Chạy ứng dụng rồi nhấp lại vào nút hành động nổi. Bạn có thể thấy thông báo đang trượt vào và ra khỏi đầu màn hình theo nhiều hàm easing và thời lượng:
5. Ảnh động thay đổi kích thước nội dung
Ứng dụng này hiển thị một số chủ đề trong nội dung. Thử nhấp vào một trong các thẻ, thẻ này sẽ mở ra và hiển thị văn bản nội dung cho chủ đề. Thẻ chứa văn bản sẽ mở rộng và thu nhỏ khi nội dung được ẩn hoặc hiển thị.
Xem mã cho TODO 3 trong thành phần kết hợp TopicRow
.
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
// ... the title and the body
}
Thành phần kết hợp Column
này sẽ thay đổi kích thước khi nội dung của nó thay đổi. Chúng ta có thể tạo ảnh động cho thay đổi về kích thước bằng cách thêm đối tượng sửa đổi animateContentSize
.
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.animateContentSize()
) {
// ... the title and the body
}
Chạy ứng dụng rồi nhấp vào một trong các chủ đề. Bạn có thể thấy chủ đề mở rộng và thu gọn cùng với ảnh động.
Bạn cũng có thể tuỳ chỉnh animateContentSize
bằng một animationSpec
tuỳ chỉnh. Chúng ta có thể cung cấp các lựa chọn để thay đổi loại ảnh động từ spring (ảnh động dựa trên lực lò xo) sang tween (quá trình tạo hình ảnh đi giữa các khung hình chính), v.v. Vui lòng xem tài liệu về Ảnh động tuỳ chỉnh để biết thêm thông tin chi tiết.
6. Tạo ảnh động nhiều giá trị
Giờ thì chúng ta đã quen với một số API ảnh động cơ bản, hãy tìm hiểu cách API Transition
hỗ trợ chúng ta tạo ảnh động phức tạp hơn. API Transition
giúp chúng ta theo dõi cho đến khi tất cả ảnh động trên Transition
hoàn tất. Như đã thấy ở phần trước, chúng ta chưa làm được việc này khi sử dụng từng API animate*AsState
. API Transition
cũng giúp chúng ta xác định từng transitionSpec
khi chuyển đổi giữa các trạng thái. Hãy xem chúng ta có thể sử dụng bằng cách nào:
Đối với ví dụ này, chúng ta tuỳ chỉnh chỉ báo thẻ. Đây là hình chữ nhật hiển thị trên thẻ hiện được chọn.
Tìm TODO 4 trong thành phần kết hợp HomeTabIndicator
rồi xem cách triển khai chỉ báo thẻ.
val indicatorLeft = tabPositions[tabPage.ordinal].left
val indicatorRight = tabPositions[tabPage.ordinal].right
val color = if (tabPage == TabPage.Home) PaleDogwood else Green
Ở đây, indicatorLeft
là vị trí theo chiều ngang cạnh bên trái của chỉ báo trong hàng thẻ. indicatorRight
là vị trí theo chiều ngang cạnh bên phải của chỉ báo. Màu sắc cũng thay đổi giữa màu hồng đào và màu xanh lục.
Để tạo ảnh động cho nhiều giá trị này cùng một lúc, chúng ta có thể sử dụng Transition
. Bạn có thể tạo Transition
bằng hàm updateTransition
. Truyền chỉ mục của thẻ hiện được chọn làm tham số targetState
.
Bạn có thể khai báo cho từng giá trị ảnh động bằng các hàm mở rộng animate*
của Transition
. Trong ví dụ này, chúng ta sẽ sử dụng animateDp
và animateColor
. Các khối này lấy một khối lambda, và chúng ta có thể chỉ định giá trị mục tiêu cho mỗi trạng thái. Chúng ta đã biết giá trị mục tiêu của trạng thái trên là gì, do đó có thể gói những giá trị này như bên dưới. Đừng quên rằng chúng ta có thể sử dụng nội dung khai báo by
và tạo lại thuộc tính uỷ quyền cục bộ tại đây do các hàm animate*
trả về đối tượng State
.
val transition = updateTransition(tabPage, label = "Tab indicator")
val indicatorLeft by transition.animateDp(label = "Indicator left") { page ->
tabPositions[page.ordinal].left
}
val indicatorRight by transition.animateDp(label = "Indicator right") { page ->
tabPositions[page.ordinal].right
}
val color by transition.animateColor(label = "Border color") { page ->
if (page == TabPage.Home) PaleDogwood else Green
}
Hãy chạy ứng dụng ngay để thấy rằng việc chuyển đổi thẻ giờ đã thú vị hơn rất nhiều. Khi nhấp vào thẻ, giá trị trạng thái tabPage
sẽ thay đổi và tất cả giá trị ảnh động liên kết với transition
cũng bắt đầu tạo ảnh động thành giá trị được chỉ định cho trạng thái mục tiêu.
Ngoài ra, chúng ta có thể chỉ định tham số transitionSpec
để tuỳ chỉnh hoạt động của ảnh động. Ví dụ: chúng ta có thể đạt được hiệu ứng đàn hồi cho chỉ báo bằng cách làm cho cạnh gần đích đến di chuyển nhanh hơn các cạnh khác. Chúng ta có thể sử dụng hàm infix isTransitioningTo
trong lambda transitionSpec
để xác định hướng thay đổi trạng thái.
val transition = updateTransition(
tabPage,
label = "Tab indicator"
)
val indicatorLeft by transition.animateDp(
transitionSpec = {
if (TabPage.Home isTransitioningTo TabPage.Work) {
// Indicator moves to the right.
// The left edge moves slower than the right edge.
spring(stiffness = Spring.StiffnessVeryLow)
} else {
// Indicator moves to the left.
// The left edge moves faster than the right edge.
spring(stiffness = Spring.StiffnessMedium)
}
},
label = "Indicator left"
) { page ->
tabPositions[page.ordinal].left
}
val indicatorRight by transition.animateDp(
transitionSpec = {
if (TabPage.Home isTransitioningTo TabPage.Work) {
// Indicator moves to the right
// The right edge moves faster than the left edge.
spring(stiffness = Spring.StiffnessMedium)
} else {
// Indicator moves to the left.
// The right edge moves slower than the left edge.
spring(stiffness = Spring.StiffnessVeryLow)
}
},
label = "Indicator right"
) { page ->
tabPositions[page.ordinal].right
}
val color by transition.animateColor(
label = "Border color"
) { page ->
if (page == TabPage.Home) PaleDogwood else Green
}
Chạy lại ứng dụng và thử chuyển đổi các thẻ.
Android Studio hỗ trợ hoạt động kiểm tra hiệu ứng Chuyển tiếp trong tính năng Xem trước của Compose. Để sử dụng tính năng Xem trước ảnh động, hãy bắt đầu chế độ tương tác bằng cách nhấp vào biểu tượng "Bắt đầu xem trước ảnh động" ở góc trên cùng bên phải của một thành phần kết hợp trong bản xem trước (biểu tượng ). Hãy thử nhấp vào biểu tượng của thành phần kết hợp PreviewHomeTabBar
. Thao tác này sẽ mở một ngăn "Animations" mới.
Bạn có thể chạy ảnh động bằng cách nhấp vào nút biểu tượng "Phát". Bạn cũng có thể kéo thanh tua để xem từng khung ảnh động. Để xem thông tin mô tả rõ hơn về các giá trị ảnh động, bạn có thể chỉ định tham số label
trong updateTransition
cũng như các phương thức animate*
.
7. Hoạt ảnh lặp lại
Hãy thử nhấp vào nút làm mới bên cạnh nhiệt độ hiện tại. Ứng dụng bắt đầu tải thông tin thời tiết mới nhất (giả vờ). Trước khi quá trình tải hoàn tất, bạn sẽ thấy chỉ báo đang tải là một vòng tròn màu xám và một thanh. Hãy tạo ảnh động cho giá trị alpha của chỉ báo này để làm rõ hơn quy trình đang diễn ra.
Tìm TODO 5 trong thành phần kết hợp LoadingRow
.
val alpha = 1f
Chúng tôi muốn làm cho giá trị này tạo hiệu ứng động từ 0f đến 1f nhiều lần. Chúng ta có thể sử dụng InfiniteTransition
cho mục đích này. API này tương tự như API Transition
trong phần trước. Cả hai đều tạo ảnh động cho nhiều giá trị, nhưng trong khi Transition
tạo ảnh động cho giá trị dựa trên thay đổi về trạng thái, InfiniteTransition
sẽ tạo ảnh động cho giá trị vô thời hạn.
Để tạo InfiniteTransition
, hãy sử dụng hàm rememberInfiniteTransition
. Sau đó, bạn có thể khai báo từng thay đổi giá trị ảnh động bằng một trong các hàm mở rộng animate*
của InfiniteTransition
. Trong trường hợp này, chúng ta sẽ tạo ảnh động giá trị alpha, vì vậy hãy sử dụng animatedFloat
. Tham số initialValue
phải là 0f
và targetValue
1f
. Chúng ta cũng có thể chỉ định AnimationSpec
cho ảnh động này, nhưng API này chỉ sử dụng InfiniteRepeatableSpec
. Hãy dùng hàm infiniteRepeatable
để tạo một hàm. AnimationSpec
này gói mọi AnimationSpec
dựa trên thời lượng và khiến nó lặp lại được. Ví dụ như mã kết quả sẽ có dạng như dưới đây.
val infiniteTransition = rememberInfiniteTransition()
val alpha by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = keyframes {
durationMillis = 1000
0.7f at 500
},
repeatMode = RepeatMode.Reverse
),
label = "alpha"
)
Mặc định repeatMode
là RepeatMode.Restart
. Quá trình chuyển đổi này từ initialValue
sang targetValue
và bắt đầu lại tại initialValue
. Bằng cách đặt repeatMode
thành RepeatMode.Reverse
, ảnh động sẽ chuyển từ initialValue
sang targetValue
, sau đó từ targetValue
sang initialValue
. Ảnh động tiến từ 0 đến 1 rồi 1 đến 0.
Ảnh động keyFrames
là một loại animationSpec
khác (một số ảnh động khác là tween
và spring
) cho phép thay đổi giá trị đang diễn ra ở từng mili giây. Ban đầu, chúng tôi đặt durationMillis
thành 1000 mili giây. Sau đó, chúng ta có thể xác định các khung hình chính trong ảnh động, ví dụ: ở 500 mili giây của ảnh động, chúng ta muốn giá trị alpha là 0,7f. Quá trình trên thay đổi tiến trình ảnh động, giúp tiến trình này tiến triển nhanh từ 0 đến 0,7 trong vòng 500 mili giây của ảnh động, và từ 0,7 đến 1,0 từ 500 mili giây đến 1000 mili giây của ảnh động, sau đó chậm lại ở cuối.
Nếu muốn có nhiều khung hình chính, chúng ta có thể xác định nhiều keyFrames
như bên dưới:
animation = keyframes {
durationMillis = 1000
0.7f at 500
0.9f at 800
}
Chạy ứng dụng rồi thử nhấp vào nút làm mới. Bây giờ, bạn có thể xem ảnh động chỉ báo tải.
8. Ảnh động cử chỉ
Trong phần cuối cùng này, chúng ta sẽ tìm hiểu về cách chạy ảnh động dựa trên phương thức nhập bằng cách chạm. Chúng ta sẽ xây dựng một công cụ sửa đổi swipeToDismiss
từ đầu.
Tìm TODO 6-1 trong đối tượng sửa đổi swipeToDismiss
. Ở đây, chúng ta đang cố gắng tạo một công cụ sửa đổi có thể vuốt thành phần khi chạm. Khi phần tử xoay sang cạnh màn hình, chúng ta gọi lệnh gọi lại onDismissed
để có thể xoá phần tử.
Để xây dựng một công cụ sửa đổi swipeToDismiss
, có một số khái niệm chính mà chúng ta cần hiểu rõ. Trước tiên, người dùng đặt ngón tay trên màn hình tạo ra một sự kiện chạm với toạ độ x và y. Sau đó, họ sẽ di chuyển ngón tay sang phải – di chuyển x và y dựa trên chuyển động. Mục mà họ đang chạm cần di chuyển bằng ngón tay, vậy nên chúng ta sẽ cập nhật vị trí của mục này dựa trên vị trí và tốc độ của sự kiện chạm.
Chúng ta có thể sử dụng một số khái niệm nêu trong tài liệu về Cử chỉ trong Compose. Bằng cách sử dụng đối tượng sửa đổi pointerInput
, chúng ta có thể truy cập ở cấp thấp vào các sự kiện chạm con trỏ sắp đến cũng như theo dõi tốc độ mà người dùng kéo bằng cách sử dụng cùng một con trỏ. Nếu người dùng thả ra trước khi vượt quá ranh giới bị loại bỏ, thì mục sẽ khôi phục trở lại vị trí.
Trong trường hợp này, bạn cần xem xét một vài điều khác biệt. Trước tiên, bất kỳ ảnh động nào đang diễn ra cũng đều có thể bị chặn bằng sự kiện chạm. Tiếp đến, giá trị ảnh động có thể không phải là nguồn đáng tin cậy duy nhất. Nói cách khác, chúng ta có thể cần đồng bộ hoá giá trị ảnh động với các giá trị đến từ sự kiện chạm.
Animatable
là API ảnh động cấp thấp nhất mà chúng tôi thấy từ trước đến nay. API này có nhiều tính năng hữu ích cho nhiều trường hợp cử chỉ (chẳng hạn như khả năng điều chỉnh giá trị mới ngay lập tức sau một cử chỉ) và dừng mọi ảnh động đang chạy khi một sự kiện chạm mới được kích hoạt. Hãy tạo một thực thể của Animatable
rồi dùng thực thể này để thể hiện độ dời theo chiều ngang của phần tử có thể vuốt. Lưu ý nhập Animatable
từ androidx.compose.animation.core.Animatable
, không phải từ androidx.compose.animation.Animatable
.
val offsetX = remember { Animatable(0f) } // Add this line
// used to receive user touch events
pointerInput {
// Used to calculate a settling position of a fling animation.
val decay = splineBasedDecay<Float>(this)
// Wrap in a coroutine scope to use suspend functions for touch events and animation.
coroutineScope {
while (true) {
// ...
TODO 6-2 là nơi chúng ta vừa nhận được một sự kiện chạm. Chúng ta nên chặn ảnh động nếu nó hiện đang chạy. Bạn có thể thực hiện việc này bằng cách gọi stop
trên Animatable
. Lưu ý cuộc gọi sẽ bị bỏ qua nếu ảnh động không chạy. VelocityTracker
sẽ được dùng để tính tốc độ di chuyển của người dùng từ trái sang phải. awaitPointerEventScope
là một hàm tạm ngưng có thể chờ các sự kiện do người dùng nhập, đồng thời phản hồi các sự kiện đó.
// Wait for a touch down event. Track the pointerId based on the touch
val pointerId = awaitPointerEventScope { awaitFirstDown().id }
offsetX.stop() // Add this line to cancel any on-going animations
// Prepare for drag events and record velocity of a fling gesture
val velocityTracker = VelocityTracker()
// Wait for drag events.
awaitPointerEventScope {
Tại TODO 6-3, chúng tôi liên tục nhận được các sự kiện kéo. Chúng ta phải đồng bộ hoá vị trí của sự kiện nhấn vào giá trị ảnh động. Chúng ta có thể sử dụng snapTo
trên Animatable
. snapTo
phải được gọi bên trong một khối launch
khác, vì awaitPointerEventScope
và horizontalDrag
là các phạm vi coroutine bị hạn chế. Tức là chúng chỉ có thể suspend
cho awaitPointerEvents
, snapTo
không phải là một sự kiện con trỏ.
horizontalDrag(pointerId) { change ->
// Add these 4 lines
// Get the drag amount change to offset the item with
val horizontalDragOffset = offsetX.value + change.positionChange().x
// Need to call this in a launch block in order to run it separately outside of the awaitPointerEventScope
launch {
// Instantly set the Animable to the dragOffset to ensure its moving
// as the user's finger moves
offsetX.snapTo(horizontalDragOffset)
}
// Record the velocity of the drag.
velocityTracker.addPosition(change.uptimeMillis, change.position)
// Consume the gesture event, not passed to external
if (change.positionChange() != Offset.Zero) change.consume()
}
TODO 6-4 là nơi phần tử vừa được giải phóng và tung ra. Chúng ta cần tính toán vị trí cuối cùng mà cử chỉ hất được bố trí, để có thể quyết định xem chúng ta nên trượt phần tử này về vị trí ban đầu hay trượt phần tử đó ra xa và gọi lệnh gọi lại. Chúng ta sẽ sử dụng đối tượng decay
đã tạo trước đó để tính toán targetOffsetX
:
// Dragging finished. Calculate the velocity of the fling.
val velocity = velocityTracker.calculateVelocity().x
// Add this line to calculate where it would end up with
// the current velocity and position
val targetOffsetX = decay.calculateTargetValue(offsetX.value, velocity)
Trong TODO 6-5, chúng tôi sắp bắt đầu tạo ảnh động. Nhưng trước đó, chúng ta sẽ thiết lập giới hạn giá trị trên và dưới thành Animatable
để ảnh động dừng ngay khi đạt giới hạn (-size.width
và size.width
vì chúng ta không muốn offsetX
có thể mở rộng và vượt quá hai giá trị này). Đối tượng sửa đổi pointerInput
cho phép truy cập vào kích thước của phần tử theo thuộc tính size
. Vì vậy, hãy sử dụng giá trị đó để nhận các giới hạn mà chúng ta vừa thiết lập.
offsetX.updateBounds(
lowerBound = -size.width.toFloat(),
upperBound = size.width.toFloat()
)
TODO 6-6 là nơi cuối cùng chúng ta có thể bắt đầu ảnh động. Trước tiên, chúng ta so sánh vị trí sắp đặt của cử chỉ hất đã được tính toán trước đó cũng như kích thước của phần tử. Nếu vị trí sắp đặt dưới kích thước, điều đó có nghĩa tốc độ của cử chỉ hất chưa đủ. Chúng ta có thể sử dụng animateTo
để tạo ảnh động trở lại giá trị thành 0f. Nếu không, chúng ta sẽ sử dụng animateDecay
để bắt đầu ảnh động vuốt nhanh. Khi ảnh động hoàn tất (nhiều khả năng là trong các giới hạn đã đặt trước đó), chúng ta có thể gọi lệnh gọi lại.
launch {
if (targetOffsetX.absoluteValue <= size.width) {
// Not enough velocity; Slide back.
offsetX.animateTo(targetValue = 0f, initialVelocity = velocity)
} else {
// Enough velocity to slide away the element to the edge.
offsetX.animateDecay(velocity, decay)
// The element was swiped away.
onDismissed()
}
}
Cuối cùng, hãy xem mục TODO 6-7. Chúng ta đã thiết lập tất cả ảnh động và cử chỉ, vậy nên đừng quên áp dụng độ bù trừ cho phần tử. Thao tác này sẽ giúp di chuyển phần tử trên màn hình về giá trị do cử chỉ hoặc ảnh động tạo ra:
.offset { IntOffset(offsetX.value.roundToInt(), 0) }
Kết quả của phần này sẽ là mã dành cho bạn như bên dưới:
private fun Modifier.swipeToDismiss(
onDismissed: () -> Unit
): Modifier = composed {
// This Animatable stores the horizontal offset for the element.
val offsetX = remember { Animatable(0f) }
pointerInput(Unit) {
// Used to calculate a settling position of a fling animation.
val decay = splineBasedDecay<Float>(this)
// Wrap in a coroutine scope to use suspend functions for touch events and animation.
coroutineScope {
while (true) {
// Wait for a touch down event.
val pointerId = awaitPointerEventScope { awaitFirstDown().id }
// Interrupt any ongoing animation.
offsetX.stop()
// Prepare for drag events and record velocity of a fling.
val velocityTracker = VelocityTracker()
// Wait for drag events.
awaitPointerEventScope {
horizontalDrag(pointerId) { change ->
// Record the position after offset
val horizontalDragOffset = offsetX.value + change.positionChange().x
launch {
// Overwrite the Animatable value while the element is dragged.
offsetX.snapTo(horizontalDragOffset)
}
// Record the velocity of the drag.
velocityTracker.addPosition(change.uptimeMillis, change.position)
// Consume the gesture event, not passed to external
change.consumePositionChange()
}
}
// Dragging finished. Calculate the velocity of the fling.
val velocity = velocityTracker.calculateVelocity().x
// Calculate where the element eventually settles after the fling animation.
val targetOffsetX = decay.calculateTargetValue(offsetX.value, velocity)
// The animation should end as soon as it reaches these bounds.
offsetX.updateBounds(
lowerBound = -size.width.toFloat(),
upperBound = size.width.toFloat()
)
launch {
if (targetOffsetX.absoluteValue <= size.width) {
// Not enough velocity; Slide back to the default position.
offsetX.animateTo(targetValue = 0f, initialVelocity = velocity)
} else {
// Enough velocity to slide away the element to the edge.
offsetX.animateDecay(velocity, decay)
// The element was swiped away.
onDismissed()
}
}
}
}
}
// Apply the horizontal offset to the element.
.offset { IntOffset(offsetX.value.roundToInt(), 0) }
}
Chạy ứng dụng rồi thử vuốt một trong các mục tác vụ. Bạn có thể thấy phần tử trượt trở lại vị trí mặc định, hoặc trượt đi và bị xoá tuỳ thuộc vào tốc độ hất. Bạn cũng có thể tìm thấy phần tử đó trong khi tạo ảnh động.
9. Xin chúc mừng!
Xin chúc mừng! Bạn đã tìm hiểu về các API Ảnh động cơ bản.
Trong lớp học lập trình này, chúng ta đã tìm hiểu cách sử dụng:
API ảnh động cấp cao
animatedContentSize
AnimatedVisibility
API ảnh động cấp thấp hơn:
animate*AsState
để tạo ảnh động cho một giá trị duy nhấtupdateTransition
để tạo ảnh động mang nhiều giá trịinfiniteTransition
để tạo ảnh động cho các giá trị vô thời hạnAnimatable
để tạo ảnh động tuỳ chỉnh bằng các cử chỉ chạm
Tiếp theo là gì?
Hãy tham khảo các lớp học lập trình khác trên Lộ trình học tập về Compose.
Để tìm hiểu thêm, vui lòng tham khảo nội dung Ảnh động của Compose và các tài liệu tham khảo sau: