Tạo ảnh động cho các phần tử trong Jetpack Compose

Sử dụng bộ sưu tập để sắp xếp ngăn nắp các trang Lưu và phân loại nội dung dựa trên lựa chọn ưu tiên của bạn.

1. Giới thiệu

5bb2e531a22c7de0.png

Lần cập nhật gần đây nhất: ngày 27 tháng 05 năm 2022

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 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

Bạn cần có

2. Thiết lập

Tải mã lớp học lập trình xuống. Bạn có thể sao chép kho lưu trữ như bên dưới:

$ git clone git@github.com:googlecodelabs/android-compose-codelabs.git

Ngoài ra, bạn có thể tải tệp zip xuống.

Nhập dự án AnimationCodelab trong Android Studio.

Nhập lớp học lập trình ảnh động vào 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 hoàn tất 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.

Hiển thị điểm bắt đầu đã chọn trong Android Studio

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ụ VIỆC CẦN LÀM trong Android Studio và chuyển đến từng nhận xét VIỆC CẦN LÀM cho chương đó.

Danh sách VIỆC CẦN LÀM trong Android Studio

3. Tạo ảnh động cho một thay đổi về giá trị đơn giản

Hãy bắt đầu với 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 và 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 thực sự 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.

Đã chọn thẻ Trang chủ

Đã chọn thẻ Công việc

Nhấp vào VIỆC CẦN LÀM 1 trong cửa sổ công cụ VIỆC CẦN LÀM và xem cách này được triển khai. Tệp này nằm trong thành phần kết hợp Home.

val backgroundColor = if (tabPage == TabPage.Home) Purple100 else Green300

Ở đây, tabPage là một Int được hỗ trợ bởi một đối tượng State. Tuỳ thuộc vào giá trị, màu nền sẽ chuyển đổi giữa màu tím và màu xanh lục. Chúng ta muốn tạo ảnh động cho các thay đổi về giá trị này.

Để tạo ảnh động thay đổi giá trị đơn giản 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, là animateColorAsState trong trường hợp này. Giá trị trả về là đối tượng State<T>, vì vậy chúng ta có thể sử dụng thuộc tính được ủy quyền cục bộ với khai báo by để coi giá trị này là một biến thông thường.

val backgroundColor by animateColorAsState(if (tabPage == TabPage.Home) Purple100 else Green300)

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.

Ảnh động thay đổi màu giữa các thẻ

4. Ảnh động hiển thị

Nếu cuộn nội dung của ứng dụng, bạn sẽ nhận thấy nút hành động nổi mở rộng và thu nhỏ tùy thuộc vào hướng cuộn.

Đã mở rộng nút Chỉnh sửa hành động nổi

Chỉnh sửa nút hành động nổi nhỏ

Tìm VIỆC CẦN LÀM 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)
    )
}

Việc tạo ảnh động cho thay đổi chế độ hiển thị này chỉ đơn giản là thay thế if bằng một 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 đồng thời xem FAB sẽ mở rộng và thu nhỏ như thế nào.

Ảnh động nút chỉnh sửa thao tác nổi

AnimatedVisibility chạy ảnh động mỗi khi giá trị Boolean được chỉ định thay đổi. Theo mặc định, AnimatedVisibility hiển thị phần tử bằng cách làm mờ và mở rộng phần tử đó, ngoài ra còn ẩn phần tử đó bằng cách làm mờ và thu nhỏ. Hành vi này phù hợp với ví dụ này về FAB. Tuy nhiên, chúng ta cũng có thể tùy chỉnh hành vi này.

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 trên cùng.

Thông báo chi tiết về việc tính năng chỉnh sửa không được hỗ trợ

Tìm VIỆC CẦN LÀM 2-2 và xem mã trong thành phần kết hợp EditMessage.

AnimatedVisibility(
    visible = shown
) {
    Surface(
        modifier = Modifier.fillMaxWidth(),
        color = MaterialTheme.colors.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ố enterexit 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 EnterTransitionslideOutVertically 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ể nhận 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 slideInVerticallyslideOutVertically sử dụng một nửa chiều cao của mục.

Trượt dọc theo chiều cắt ngắn

Đố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 cho mục bằng cách đặt 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 sẽ trả về giá trị âm vì phần trên cùng của màn hình có giá trị là 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, do đó 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.colors.secondary,
        elevation = 4.dp
    ) {
        Text(
            text = stringResource(R.string.edit_message),
            modifier = Modifier.padding(16.dp)
        )
    }
}

Khi chạy lại ứng dụng, có thể thấy ảnh động phù hợp hơn với những gì chúng ta mong đợi:

Ảnh động với hiệu ứng độ lệch hoạt động

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 EnterTransitionExitTransition. 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.colors.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 hiện đã trượt vào và ra khỏi đầu màn hình với các hàm và khoảng thời gian khác nhau:

Ảnh động minh họa thông báo chỉnh sửa trượt từ trên xuố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. Hãy thử nhấp vào một trong số các thẻ đó, thẻ 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 hiển thị hoặc ẩn.

Danh sách chủ đề được thu gọn

Đã mở rộng danh sách chủ đề

Xem mã cho VIỆC CẦN LÀM 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 sự thay đổi về kích thước bằng cách thêm từ khóa xác định 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 nhỏ bằng ảnh động.

Đã mở rộng và thu gọn ảnh động cho danh sách chủ đề

Bạn cũng có thể tuỳ chỉnh animateContentSize bằng một animationSpec tuỳ chỉnh. Chúng tôi 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ề Tuỳ chỉnh ảnh động để biết thêm thông tin chi tiết.

6. Tạo ảnh động nhiều giá trị

Giờ thì khi chúng ta đã quen với một số API ảnh động cơ bản, hãy xem API Transition cho phép chúng ta tạo các ảnh động phức tạp hơn. Việc sử dụng API Transition cho phép chúng ta theo dõi khi tất cả ảnh động trên Transition hoàn tất, điều này không thể xảy ra khi sử dụng từng API animate*AsState mà chúng ta đã thấy trước đó. API Transition cũng cho phép chúng ta xác định các transitionSpec khác nhau 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 sẽ tùy chỉnh chỉ báo thẻ. Đây là hình chữ nhật hiển thị trên thẻ hiện được chọn.

Đã chọn thẻ Trang chủ

Đã chọn thẻ Công việc

Tìm VIỆC CẦN LÀM 4 trong thành phần kết hợp HomeTabIndicator và 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) Purple700 else Green800

Ở đâ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 tím và màu xanh lục.

Để tạo ảnh động cho nhiều giá trị cùng 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 animateDpanimateColor. 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 chúng là gì, do đó có thể gói các giá trị đó như bên dưới. Vui lòng lưu ý chúng ta có thể sử dụng nội dung khai báo by và tạo lại thuộc tính được ủy quyền cục bộ tại đây, vì 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) Purple700 else Green800
}

Chạy ứng dụng ngay và bạn có thể thấy là việc chuyển đổi thẻ hiện thú vị hơn rất nhiều. Việc nhấp vào thẻ này sẽ thay đổi giá trị của trạng thái tabPage, tất cả giá trị ảnh động liên kết với transition sẽ bắt đầu tạo ảnh động thành giá trị được chỉ định cho trạng thái mục tiêu.

Ảnh động giữa thẻ trang chủ và thẻ công việc

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ụ như 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) Purple700 else Green800
}

Chạy lại ứng dụng và thử chuyển đổi các thẻ.

Hiệu ứng co giãn tuỳ chỉnh khi chuyển đổi thẻ

Android Studio hỗ trợ việc kiểm tra hoạt động chuyển đổi trong Xem trước ảnh động. Để 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 Chế độ xem trước có thể kết hợp (biểu tượng 9c05a5608a23b407.png). Bạn nên bật tính năng này trong chế độ cài đặt thử nghiệm theo hướng dẫn tại đây, nếu không tìm thấy biểu tượng này. 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*.

Tìm ảnh động trong Android Studio

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.

Hình ảnh tĩnh của thẻ thông tin giữ chỗ chưa được tạo ảnh động.

Tìm VIỆC CẦN LÀM 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 các giá trị dựa trên thay đổi về trạng thái, thì InfiniteTransition sẽ tạo ảnh động cho các 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à 0ftargetValue 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
    )
)

Mặc định repeatModeRepeatMode.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à tweenspring) cho phép thay đổi giá trị đang diễn ra ở các mili giây khác nhau. 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ụ như ở 500 mili giây của ảnh động, chúng ta muốn giá trị alpha là 0,7f. Điều này sẽ thay đổi tiến trình ảnh động: nó sẽ 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, 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.

Lặp lại nội dung giữ chỗ động

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 VIỆC CẦN LÀM 6-1 trong công cụ 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 tọa độ 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 của họ. Mục mà họ đang chạm cần di chuyển bằng ngón tay, vì vậy 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 được mô tả trong tài liệu về Cử chỉ soạn. Bằng cách sử dụng công cụ 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ỏ đến và 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 nhấn.

Animatable là API ảnh động cấp thấp nhất mà chúng tôi thấy từ trước đến nay. Phiên bản này có một số tính năng hữu ích trong các trường hợp cử chỉ, chẳng hạn như khả năng điều chỉnh ngay lập tức giá trị mới từ một cử chỉ, và dừng bất kỳ ảnh động nào đang diễn ra 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 và 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.

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) {
            // ...

VIỆC CẦN LÀM 6-2 là nơi chúng tôi 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 VIỆC CẦN LÀM 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ì awaitPointerEventScopehorizontalDrag là các phạm vi coroutine bị hạn chế. Điều này có nghĩa 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, so its not passed to other event handlers
    change.consumePositionChange()
}

VIỆC CẦN LÀM 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 VIỆC CẦN LÀM 6-5, chúng tôi sắp bắt đầu tạo ảnh động. Nhưng trước đó, chúng ta muốn đặt các giới hạn giá trị trên và dưới thànhAnimatable, để nó dừng ngay khi đạt đến giới hạn (-size.widthsize.width vì chúng tôi không muốn offsetX có thể mở rộng qua hai giá trị này). Công cụ sửa đổi pointerInput cho phép chúng ta 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 của chúng ta.

offsetX.updateBounds(
    lowerBound = -size.width.toFloat(),
    upperBound = size.width.toFloat()
)

VIỆC CẦN LÀM 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 tôi so sánh vị trí sắp đặt của cử chỉ hất đã được tính toán trước đó và 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 là tốc độ của cử chỉ hất không đủ. 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 VIỆC CẦN LÀM 6-7. Chúng ta đã thiết lập tất cả ảnh động và cử chỉ, vì vậy đừng quên áp dụng độ lệch cho phần tử. Điều này giúp di chuyển phần tử trên màn hình về giá trị do cử chỉ hoặc ảnh động của chúng ta 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.

Vuốt ảnh động bằng cử chỉ để loại bỏ các mục

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ất
  • updateTransition để tạo ảnh động mang nhiều giá trị
  • infiniteTransition để tạo ảnh động cho các giá trị vô thời hạn
  • Animatable để 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: