Ảnh động

Jetpack Compose cung cấp các API mạnh mẽ và có thể mở rộng giúp việc triển khai nhiều nội dung ảnh động trở nên dễ dàng hơn trong giao diện người dùng của ứng dụng. Tài liệu này mô tả cách sử dụng các API này và cho biết cần dùng API nào theo từng trường hợp ảnh động của bạn.

Tổng quan

Ảnh động là yếu tố cần thiết trong ứng dụng dành cho thiết bị di động hiện đại để mang lại trải nghiệm người dùng suôn sẻ và dễ hiểu. Nhiều API Ảnh động có sẵn trong Jetpack Compose dưới dạng hàm có khả năng kết hợp giống như bố cục và các thành phần khác trên giao diện người dùng. Các API này được hỗ trợ bởi các API cấp thấp hơn được xây dựng bằng hàm tạm ngưng coroutine trong Kotlin. Hướng dẫn này bắt đầu với các API cấp cao hữu ích trong nhiều trường hợp thực tế, rồi chuyển sang giải thích về các API cấp thấp nhằm giúp bạn tăng khả năng kiểm soát và tuỳ chỉnh.

Sơ đồ dưới đây giúp bạn quyết định nên sử dụng API nào để triển khai ảnh động.

Sơ đồ quy trình mô tả cây quyết định để chọn API ảnh động thích hợp

  • Nếu bạn đang tạo ảnh động thay đổi nội dung trong bố cục:
  • Nếu ảnh động dựa trên trạng thái:
    • Nếu ảnh động diễn ra trong suốt quá trình hợp thành:
  • Nếu bạn muốn kiểm soát tốt thời gian chạy ảnh động:
    • Hãy sử dụng Animation, chẳng hạn như TargetBasedAnimation hoặc DecayAnimation.
  • Nếu ảnh động là nguồn đáng tin cậy duy nhất
  • Nếu không, hãy sử dụng AnimationState hoặc animate.

API ảnh động cấp cao

Compose cung cấp các API ảnh động cấp cao cho một số mẫu ảnh động phổ biến được sử dụng trong nhiều ứng dụng. Các API này được điều chỉnh để phù hợp với các phương pháp hay nhất về Chuyển động Material Design.

AnimatedVisibility

Thành phần kết hợp AnimatedVisibility tạo ảnh động làm xuất hiện nội dung của ảnh động và làm nội dung đó biến mất.

var editable by remember { mutableStateOf(true) }
AnimatedVisibility(visible = editable) {
    Text(text = "Edit")
}

Theo mặc định, nội dung xuất hiện theo kiểu rõ dần và mở rộng, và biến mất theo kiểu mờ dần và thu nhỏ. Quá trình chuyển đổi có thể được tuỳ chỉnh bởi hàm cụ thể EnterTransitionExitTransition.

var visible by remember { mutableStateOf(true) }
val density = LocalDensity.current
AnimatedVisibility(
    visible = visible,
    enter = slideInVertically {
        // Slide in from 40 dp from the top.
        with(density) { -40.dp.roundToPx() }
    } + expandVertically(
        // Expand from the top.
        expandFrom = Alignment.Top
    ) + fadeIn(
        // Fade in with the initial alpha of 0.3f.
        initialAlpha = 0.3f
    ),
    exit = slideOutVertically() + shrinkVertically() + fadeOut()
) {
    Text("Hello", Modifier.fillMaxWidth().height(200.dp))
}

Như có thể thấy trong ví dụ trên, bạn có thể kết hợp nhiều đối tượng EnterTransition hoặc ExitTransition với toán tử + và mỗi đối tượng sẽ chấp nhận các tham số tuỳ chọn để tuỳ chỉnh hoạt động của đối tượng đó. Hãy xem phần tham khảo để biết thêm thông tin.

Ví dụ về EnterTransitionExitTransition

EnterTransition ExitTransition
fadeIn
ảnh động với hiệu ứng rõ dần
fadeOut
ảnh động với hiệu ứng mờ dần
slideIn
ảnh động với hiệu ứng trượt vào
slideOut
ảnh động với hiệu ứng trượt ra
slideInHorizontally
ảnh động với hiệu ứng trượt vào theo chiều ngang
slideOutHorizontally
ảnh động với hiệu ứng trượt ra theo chiều ngang
slideInVertically
ảnh động với hiệu ứng trượt vào theo chiều dọc
slideOutVertically
ảnh động với hiệu ứng trượt ra theo chiều dọc
scaleIn
ảnh động với hiệu ứng thu nhỏ
scaleOut
ảnh động với hiệu ứng phóng to
expandIn
ảnh động với hiệu ứng mở rộng
shrinkOut
ảnh động với hiệu ứng thu nhỏ
expandHorizontally
mở rộng ảnh động theo chiều ngang
shrinkHorizontally
thu nhỏ ảnh động theo chiều ngang
expandVertically
mở rộng ảnh động theo chiều dọc
shrinkVertically
thu nhỏ ảnh động theo chiều dọc

AnimatedVisibility cũng cung cấp một biến thể nhận MutableTransitionState. Biến thể này cho phép bạn kích hoạt một ảnh động ngay khi bạn thêm AnimatedVisibility vào cây sáng tác. Việc này cũng hữu ích khi quan sát trạng thái ảnh động.

// Create a MutableTransitionState<Boolean> for the AnimatedVisibility.
val state = remember {
    MutableTransitionState(false).apply {
        // Start the animation immediately.
        targetState = true
    }
}
Column {
    AnimatedVisibility(visibleState = state) {
        Text(text = "Hello, world!")
    }

    // Use the MutableTransitionState to know the current animation state
    // of the AnimatedVisibility.
    Text(
        text = when {
            state.isIdle && state.currentState -> "Visible"
            !state.isIdle && state.currentState -> "Disappearing"
            state.isIdle && !state.currentState -> "Invisible"
            else -> "Appearing"
        }
    )
}

Nhập và thoát hình ảnh động cho bố cục con

Nội dung trong AnimatedVisibility (bố cục con trực tiếp hoặc gián tiếp) có thể sử dụng công cụ sửa đổi animateEnterExit để xác định chế độ ảnh động khác nhau cho từng thành phần. Hiệu ứng hình ảnh cho mỗi bố cục con là sự kết hợp của các ảnh động được chỉ định trong thành phần kết hợp AnimatedVisibility và nhập và thoát ảnh động của riêng bố cục con.

AnimatedVisibility(
    visible = visible,
    enter = fadeIn(),
    exit = fadeOut()
) {
    // Fade in/out the background and the foreground.
    Box(Modifier.fillMaxSize().background(Color.DarkGray)) {
        Box(
            Modifier
                .align(Alignment.Center)
                .animateEnterExit(
                    // Slide in/out the inner box.
                    enter = slideInVertically(),
                    exit = slideOutVertically()
                )
                .sizeIn(minWidth = 256.dp, minHeight = 64.dp)
                .background(Color.Red)
        ) {
            // Content of the notification…
        }
    }
}

Trong một số trường hợp, bạn có thể không áp dụng AnimatedVisibility cho ảnh động nào để bố cục con có thể có ảnh động của riêng mình bằng animateEnterExit. Để đạt được mục đích này, hãy chỉ định EnterTransition.NoneExitTransition.None tại thành phần kết hợp AnimatedVisibility.

Thêm ảnh động tuỳ chỉnh

Nếu bạn muốn thêm các hiệu ứng ảnh động tuỳ chỉnh ngoài các ảnh động nhập và thoát được tích hợp sẵn, hãy truy cập phiên bản Transition dưới đây thông qua thuộc tính transition bên trong hàm lambda nội dung dành cho AnimatedVisibility. Mọi trạng thái ảnh động được thêm vào phiên bản Chuyển đổi sẽ chạy đồng thời với các ảnh động nhập và thoát của AnimatedVisibility. AnimatedVisibility đợi cho đến khi tất cả các ảnh động trong Transition hoàn tất trước khi xoá nội dung. Đối với các ảnh động thoát được tạo độc lập với Transition (chẳng hạn như sử dụng animate*AsState), AnimatedVisibility sẽ không thể tính đến các ảnh động đó và vì thế, có thể xoá nội dung thành phần kết hợp trước khi hoàn tất.

AnimatedVisibility(
    visible = visible,
    enter = fadeIn(),
    exit = fadeOut()
) { // this: AnimatedVisibilityScope
    // Use AnimatedVisibilityScope#transition to add a custom animation
    // to the AnimatedVisibility.
    val background by transition.animateColor { state ->
        if (state == EnterExitState.Visible) Color.Blue else Color.Gray
    }
    Box(modifier = Modifier.size(128.dp).background(background))
}

Hãy xem updateTransition để biết thông tin chi tiết về Transition.

animate*AsState

Các hàm animate*AsState là API ảnh động đơn giản nhất trong Compose để tạo ảnh động cho một giá trị duy nhất. Bạn chỉ cung cấp giá trị cuối cùng (hoặc giá trị mục tiêu) và API sẽ bắt đầu tạo ảnh động từ giá trị hiện tại đến giá trị đã chỉ định.

Dưới đây là ví dụ về cách tạo ảnh động alpha bằng API này. Chỉ cần gói giá trị mục tiêu trong animateFloatAsState, giá trị alpha giờ là giá trị ảnh động giữa các giá trị đã cung cấp (1f hoặc 0.5f trong trường hợp này).

val alpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f)
Box(
    Modifier.fillMaxSize()
        .graphicsLayer(alpha = alpha)
        .background(Color.Red)
)

Lưu ý rằng bạn không cần tạo một phiên bản của bất kỳ lớp ảnh động nào, hoặc xử lý gián đoạn. Trong trường hợp này, một đối tượng ảnh động (cụ thể là một thực thể Animatable) sẽ được tạo và ghi nhớ tại nơi hàm được gọi, với giá trị mục tiêu đầu tiên chính là giá trị ban đầu. Kể từ đó, bất cứ khi nào bạn cung cấp cho thành phần kết hợp này một giá trị mục tiêu khác, hệ thống sẽ tự động bắt đầu một ảnh động theo giá trị đó. Nếu đã có ảnh động trong giai đoạn hiển thị, thì ảnh động sẽ bắt đầu từ giá trị hiện tại (và vận tốc) và tạo ảnh động hướng tới giá trị mục tiêu. Trong quá trình ảnh động, thành phần kết hợp này sẽ được ghép lại và trả về một giá trị ảnh động được cập nhật cho mọi khung hình.

Ngay lập tức, Compose cung cấp các hàm animate*AsState cho Float, Color, Dp, Size, Offset, Rect, Int , IntOffsetIntSize. Bạn có thể dễ dàng thêm tính năng hỗ trợ cho các loại dữ liệu khác bằng cách cung cấp TwoWayConverter cho animateValueAsState nhận một loại chung.

Bạn có thể sử dụng AnimationSpec để tuỳ chỉnh thông số kỹ thuật của ảnh động. Hãy xem AnimationSpec để biết thêm thông tin.

AnimatedContent (thử nghiệm)

Thành phần kết hợp AnimatedContent tạo hoạt ảnh cho nội dung của nó khi thay đổi theo trạng thái mục tiêu.

Row {
    var count by remember { mutableStateOf(0) }
    Button(onClick = { count++ }) {
        Text("Add")
    }
    AnimatedContent(targetState = count) { targetCount ->
        // Make sure to use `targetCount`, not `count`.
        Text(text = "Count: $targetCount")
    }
}

Lưu ý phải luôn sử dụng tham số hàm lambda và phản ánh tham số đó vào nội dung. API sử dụng giá trị này làm chìa khoá để xác định nội dung hiện đang hiển thị.

Theo mặc định, nội dung ban đầu mờ dần và sau đó nội dung mục tiêu rõ dần (hoạt động này được gọi là mờ dần qua). Bạn có thể tuỳ chỉnh hành vi ảnh động này bằng cách chỉ định đối tượng ContentTransform cho tham số transitionSpec. Bạn có thể tạo ContentTransform bằng cách kết hợp EnterTransition với ExitTransition bằng cách sử dụng hàm infix with. Bạn có thể áp dụng SizeTransform cho ContentTransform bằng cách gắn nó với hàm infix using

AnimatedContent(
    targetState = count,
    transitionSpec = {
        // Compare the incoming number with the previous number.
        if (targetState > initialState) {
            // If the target number is larger, it slides up and fades in
            // while the initial (smaller) number slides up and fades out.
            slideInVertically { height -> height } + fadeIn() with
                slideOutVertically { height -> -height } + fadeOut()
        } else {
            // If the target number is smaller, it slides down and fades in
            // while the initial number slides down and fades out.
            slideInVertically { height -> -height } + fadeIn() with
                slideOutVertically { height -> height } + fadeOut()
        }.using(
            // Disable clipping since the faded slide-in/out should
            // be displayed out of bounds.
            SizeTransform(clip = false)
        )
    }
) { targetCount ->
    Text(text = "$targetCount")
}

EnterTransition xác định cách hiển thị của nội dung mục tiêu, còn ExitTransition xác định cách làm biến mất nội dung ban đầu. Ngoài tất cả các hàm EnterTransitionExitTransition có sẵn cho AnimatedVisibility, AnimatedContent cung cấp slideIntoContainerslideOutOfContainer. Đây là các lựa chọn thay thế thuận tiện cho slideInHorizontally/VerticallyslideOutHorizontally/Vertically để tính khoảng cách trên trang trình bày dựa trên kích thước của nội dung ban đầu và nội dung mục tiêu của AnimatedContent.

SizeTransform xác định cách kích thước sẽ tạo ảnh động giữa nội dung ban đầu và nội dung mục tiêu. Bạn có quyền truy cập vào cả kích thước ban đầu và kích thước mục tiêu khi tạo ảnh động. SizeTransform cũng kiểm soát việc có nên cắt nội dung thành kích thước thành phần trong các hình ảnh động hay không.

var expanded by remember { mutableStateOf(false) }
Surface(
    color = MaterialTheme.colors.primary,
    onClick = { expanded = !expanded }
) {
    AnimatedContent(
        targetState = expanded,
        transitionSpec = {
            fadeIn(animationSpec = tween(150, 150)) with
                fadeOut(animationSpec = tween(150)) using
                SizeTransform { initialSize, targetSize ->
                    if (targetState) {
                        keyframes {
                            // Expand horizontally first.
                            IntSize(targetSize.width, initialSize.height) at 150
                            durationMillis = 300
                        }
                    } else {
                        keyframes {
                            // Shrink vertically first.
                            IntSize(initialSize.width, targetSize.height) at 150
                            durationMillis = 300
                        }
                    }
                }
        }
    ) { targetExpanded ->
        if (targetExpanded) {
            Expanded()
        } else {
            ContentIcon()
        }
    }
}

Nhập/thoát ảnh động cho trẻ em

Cũng giống như AnimatedVisibility, công cụ sửa đổi animateEnterExit có sẵn bên trong nội dung hàm lambda của AnimatedContent. Sử dụng thuộc tính này để áp dụng EnterAnimationExitAnimation cho từng bố cục con trực tiếp hoặc gián tiếp.

Thêm ảnh động tuỳ chỉnh

Giống như AnimatedVisibility, trường transition có sẵn bên trong hàm lambda nội dung của AnimatedContent. Sử dụng trường này để tạo hiệu ứng ảnh động tuỳ chỉnh chạy đồng thời với quá trình chuyển đổi AnimatedContent. Hãy xem updateTransition để biết thông tin chi tiết.

animateContentSize

Công cụ sửa đổi animateContentSize sẽ tạo ảnh động cho việc thay đổi về kích thước.

var message by remember { mutableStateOf("Hello") }
Box(
    modifier = Modifier.background(Color.Blue).animateContentSize()
) {
    Text(text = message)
}

Chuyển đổi

Crossfade tạo ảnh động giữa hai bố cục bằng một ảnh động chuyển đổi. Bằng cách chuyển đổi giá trị được truyền vào tham số current, nội dung được chuyển bằng một ảnh động chuyển đổi.

var currentPage by remember { mutableStateOf("A") }
Crossfade(targetState = currentPage) { screen ->
    when (screen) {
        "A" -> Text("Page A")
        "B" -> Text("Page B")
    }
}

updateTransition

Transition quản lý một hoặc nhiều ảnh động dưới dạng thành phần con và chạy đồng thời giữa nhiều trạng thái.

Các trạng thái có thể thuộc bất kỳ loại dữ liệu nào. Trong nhiều trường hợp, bạn có thể sử dụng loại enum tuỳ chỉnh để đảm bảo loại an toàn, như trong ví dụ này:

enum class BoxState {
    Collapsed,
    Expanded
}

updateTransition tạo và ghi nhớ một phiên bản của Transition và cập nhật trạng thái của phiên bản đó.

var currentState by remember { mutableStateOf(BoxState.Collapsed) }
val transition = updateTransition(currentState)

Bạn có thể sử dụng một trong các hàm mở rộng animate* để xác định ảnh động bố cục con trong hoạt động chuyển đổi này. Chỉ định các giá trị mục tiêu cho mỗi trạng thái. Các hàm animate* này trả về một giá trị ảnh động. Mọi khung hình đều cập nhật trong suốt chế độ ảnh động khi trạng thái chuyển đổi được cập nhật với updateTransition.

val rect by transition.animateRect { state ->
    when (state) {
        BoxState.Collapsed -> Rect(0f, 0f, 100f, 100f)
        BoxState.Expanded -> Rect(100f, 100f, 300f, 300f)
    }
}
val borderWidth by transition.animateDp { state ->
    when (state) {
        BoxState.Collapsed -> 1.dp
        BoxState.Expanded -> 0.dp
    }
}

Bạn có thể chuyển tham số transitionSpec để chỉ định một AnimationSpec khác cho mỗi kiểu kết hợp của các thay đổi trạng thái chuyển đổi. Hãy xem AnimationSpec để biết thêm thông tin.

val color by transition.animateColor(
    transitionSpec = {
        when {
            BoxState.Expanded isTransitioningTo BoxState.Collapsed ->
                spring(stiffness = 50f)
            else ->
                tween(durationMillis = 500)
        }
    }
) { state ->
    when (state) {
        BoxState.Collapsed -> MaterialTheme.colors.primary
        BoxState.Expanded -> MaterialTheme.colors.background
    }
}

Khi hoạt động chuyển đổi đạt đến trạng thái mục tiêu, Transition.currentState sẽ giống với Transition.targetState. Bạn có thể sử dụng hoạt động này như một tín hiệu cho biết hoạt động chuyển đổi đã hoàn tất hay chưa.

Đôi khi, chúng ta muốn có một trạng thái ban đầu khác với trạng thái mục tiêu đầu tiên. Chúng ta có thể sử dụng updateTransition cùng với MutableTransitionState để đạt được điều này. Ví dụ: chúng ta được quyền khởi động ảnh động ngay khi mã nhập vào phương thức hợp thành.

// Start in collapsed state and immediately animate to expanded
var currentState = remember { MutableTransitionState(BoxState.Collapsed) }
currentState.targetState = BoxState.Expanded
val transition = updateTransition(currentState)
// ...

Đối với quá trình chuyển đổi phức tạp hơn liên quan đến nhiều hàm có khả năng kết hợp, bạn có thể sử dụng createChildTransition để tạo nội dung chuyển đổi con. Kỹ thuật này dùng để phân biệt các mối lo ngại giữa nhiều thành phần phụ trong một thành phần kết hợp phức tạp. Quá trình chuyển đổi thành phần mẹ sẽ nhận ra được tất cả các giá trị ảnh động trong các chuyển đổi bố cục con.

enum class DialerState { DialerMinimized, NumberPad }

@Composable
fun DialerButton(isVisibleTransition: Transition<Boolean>) {
    // `isVisibleTransition` spares the need for the content to know
    // about other DialerStates. Instead, the content can focus on
    // animating the state change between visible and not visible.
}

@Composable
fun NumberPad(isVisibleTransition: Transition<Boolean>) {
    // `isVisibleTransition` spares the need for the content to know
    // about other DialerStates. Instead, the content can focus on
    // animating the state change between visible and not visible.
}

@Composable
fun Dialer(dialerState: DialerState) {
    val transition = updateTransition(dialerState)
    Box {
        // Creates separate child transitions of Boolean type for NumberPad
        // and DialerButton for any content animation between visible and
        // not visible
        NumberPad(
            transition.createChildTransition {
                it == DialerState.NumberPad
            }
        )
        DialerButton(
            transition.createChildTransition {
                it == DialerState.DialerMinimized
            }
        )
    }
}

Sử dụng hiệu ứng chuyển đổi với AnimatedVisibility và AnimatedContent

AnimatedVisibilityAnimatedContent có sẵn dưới dạng các hàm mở rộng của Transition. targetState cho Transition.AnimatedVisibilityTransition.AnimatedContent có nguồn gốc từ Transition và kích hoạt các nội dung chuyển đổi nhập/thoát cần thiết khi targetState của Transition thay đổi. Các hàm mở rộng này cho phép tất cả các ảnh động enter/exit/sizeTransform mà nếu không nội bộ bên trong AnimatedVisibility/AnimatedContent sẽ được nâng lên thành Transition. Bạn có thể ghi nhận từ bên ngoài sự thay đổi trạng thái của AnimatedVisibility/AnimatedContent ở các hàm mở rộng này, Thay vì thông số visible boolean, phiên bản này của AnimatedVisibility sẽ lấy một hàm lambda chuyển đổi trạng thái mục tiêu của lệnh chuyển đổi thành phần mẹ thành một boolean.

Hãy xem AnimatedVisibilityAnimatedContent để biết thêm chi tiết.

var selected by remember { mutableStateOf(false) }
// Animates changes when `selected` is changed.
val transition = updateTransition(selected)
val borderColor by transition.animateColor { isSelected ->
    if (isSelected) Color.Magenta else Color.White
}
val elevation by transition.animateDp { isSelected ->
    if (isSelected) 10.dp else 2.dp
}
Surface(
    onClick = { selected = !selected },
    shape = RoundedCornerShape(8.dp),
    border = BorderStroke(2.dp, borderColor),
    elevation = elevation
) {
    Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
        Text(text = "Hello, world!")
        // AnimatedVisibility as a part of the transition.
        transition.AnimatedVisibility(
            visible = { targetSelected -> targetSelected },
            enter = expandVertically(),
            exit = shrinkVertically()
        ) {
            Text(text = "It is fine today.")
        }
        // AnimatedContent as a part of the transition.
        transition.AnimatedContent { targetState ->
            if (targetState) {
                Text(text = "Selected")
            } else {
                Icon(imageVector = Icons.Default.Phone, contentDescription = "Phone")
            }
        }
    }
}

Đóng gói lượt chuyển đổi và đặt lượt chuyển đổi đó ở chế độ tái sử dụng được

Đối với các trường hợp sử dụng đơn giản, việc xác định ảnh động chuyển đổi trong cùng một thành phần kết hợp như giao diện người dùng là tuỳ chọn hoàn toàn hợp lệ. Tuy nhiên, khi thao tác trên một thành phần phức tạp có một số giá trị được tạo ảnh động, bạn có thể muốn tách riêng việc triển khai ảnh động với giao diện người dùng có thể kết hợp.

Bạn có thể thực hiện việc này bằng cách tạo một lớp chứa tất cả các giá trị ảnh động và hàm "update" để trả về một thực thể của lớp đó. Việc triển khai chuyển đổi có thể được trích xuất vào hàm riêng mới. Mẫu này rất hữu ích khi cần phải tập trung vào logic ảnh động hoặc làm cho các ảnh động phức tạp có thể sử dụng lại.

enum class BoxState { Collapsed, Expanded }

@Composable
fun AnimatingBox(boxState: BoxState) {
    val transitionData = updateTransitionData(boxState)
    // UI tree
    Box(
        modifier = Modifier
            .background(transitionData.color)
            .size(transitionData.size)
    )
}

// Holds the animation values.
private class TransitionData(
    color: State<Color>,
    size: State<Dp>
) {
    val color by color
    val size by size
}

// Create a Transition and return its animation values.
@Composable
private fun updateTransitionData(boxState: BoxState): TransitionData {
    val transition = updateTransition(boxState)
    val color = transition.animateColor { state ->
        when (state) {
            BoxState.Collapsed -> Color.Gray
            BoxState.Expanded -> Color.Red
        }
    }
    val size = transition.animateDp { state ->
        when (state) {
            BoxState.Collapsed -> 64.dp
            BoxState.Expanded -> 128.dp
        }
    }
    return remember(transition) { TransitionData(color, size) }
}

rememberInfiniteTransition

InfiniteTransition lưu giữ một hoặc nhiều ảnh động bố cục con như Transition, nhưng các ảnh động bắt đầu chạy ngay khi chúng ta nhập phương thức hợp thành và không dừng lại trừ phi bạn xoá chúng. Bạn có thể tạo một phiên bản của InfiniteTransition với rememberInfiniteTransition. Ảnh động bố cục con có thể được thêm vào với animateColor, animatedFloat hoặc animatedValue. Ngoài ra, bạn còn cần chỉ định infiniteRepeatable làm thông số kỹ thuật của ảnh động.

val infiniteTransition = rememberInfiniteTransition()
val color by infiniteTransition.animateColor(
    initialValue = Color.Red,
    targetValue = Color.Green,
    animationSpec = infiniteRepeatable(
        animation = tween(1000, easing = LinearEasing),
        repeatMode = RepeatMode.Reverse
    )
)

Box(Modifier.fillMaxSize().background(color))

API ảnh động cấp thấp

Tất cả các API ảnh động cấp cao đã đề cập trong phần trước đều được xây dựng dựa trên nền tảng của các API ảnh động cấp thấp.

Các hàm animate*AsState là các API đơn giản nhất để hiển thị thay đổi giá trị tức thì dưới dạng giá trị ảnh động. Hàm này được Animatable hỗ trợ vì đây là một API dựa vào coroutine để tạo ảnh động cho một giá trị duy nhất. updateTransition tạo một đối tượng chuyển đổi có thể quản lý nhiều giá trị ảnh động và chạy các giá trị đó dựa trên sự thay đổi về trạng thái. rememberInfiniteTransition tương tự như vậy, nhưng hàm này tạo một sự chuyển đổi vô hạn có thể quản lý nhiều ảnh động tiếp tục chạy vô thời hạn. Tất cả các API này đều là thành phần kết hợp ngoại trừ Animatable, nghĩa là các ảnh động này có thể được tạo bên ngoài phương thức hợp thành.

Tất cả các API này đều dựa trên API Animation nền tảng hơn. Mặc dù hầu hết các ứng dụng sẽ không tương tác trực tiếp với Animation, nhưng một số khả năng tuỳ chỉnh dành cho Animation lại có sẵn thông qua các API cấp cao hơn. Hãy xem Tuỳ chỉnh ảnh động để biết thêm thông tin về AnimationVectorAnimationSpec.

Sơ đồ chỉ ra mối quan hệ giữa nhiều API ảnh động cấp thấp

Animatable

Animatable là trình lưu giữ giá trị có thể tạo ảnh động cho giá trị khi được thay đổi thông qua animateTo. Đây là API đang sao lưu nội dung triển khai animate*AsState. Giao diện này đảm bảo tiếp tục duy trì tính nhất quán và loại trừ nhau, nghĩa là việc thay đổi giá trị luôn diễn ra liên tục và mọi ảnh động đang diễn ra sẽ bị huỷ.

Nhiều tính năng của Animatable, bao gồm cả animateTo, được cung cấp dưới dạng hàm tạm ngưng. Nghĩa là các tính năng này cần được chứa trong một phạm vi coroutine thích hợp. Ví dụ: bạn có thể sử dụng thành phần kết hợp LaunchedEffect để tạo một phạm vi chỉ trong khoảng thời gian của khoá-giá trị được chỉ định.

// Start out gray and animate to green/red based on `ok`
val color = remember { Animatable(Color.Gray) }
LaunchedEffect(ok) {
    color.animateTo(if (ok) Color.Green else Color.Red)
}
Box(Modifier.fillMaxSize().background(color.value))

Trong ví dụ trên, chúng ta tạo và ghi nhớ một thực thể của Animatable với giá trị ban đầu là Color.Gray. Tuỳ thuộc vào giá trị của cờ boolean ok, màu sắc kết hợp ảnh động cho một trong hai hàm Color.Green hoặc Color.Red. Bất kỳ thay đổi nào tiếp theo đối với giá trị boolean sẽ bắt đầu chuyển ảnh động thành màu khác. Nếu có một ảnh động đang hoạt động khi giá trị bị thay đổi, thì ảnh động sẽ bị huỷ và ảnh động mới sẽ bắt đầu từ giá trị hiện tại của ảnh chụp nhanh với vận tốc hiện tại.

Đây là triển khai ảnh động sao lưu API animate*AsState đã đề cập trong phần trước. So với animate*AsState, việc sử dụng Animatable trực tiếp giúp kiểm soát chặt chẽ hơn trên một vài khía cạnh. Trước tiên, Animatable có thể có giá trị ban đầu khác với giá trị mục tiêu đầu tiên. Ví dụ: ví dụ về mã ở trên cho thấy một hộp màu xám ở đầu tiên, ngay lập tức bắt đầu tạo ảnh động sang màu xanh lục hoặc đỏ. Thứ hai, Animatable cung cấp nhiều thao tác trên giá trị nội dung, cụ thể là snapToanimateDecay. snapTo đặt giá trị hiện tại thành giá trị mục tiêu ngay. Điều này hữu ích khi ảnh động không phải là nguồn đáng tin cậy duy nhất và phải được đồng bộ hoá với các trạng thái khác, chẳng hạn như các sự kiện chạm. animateDecay bắt đầu một ảnh động chậm lại từ tốc độ đã cho. Điều này rất hữu ích trong việc triển khai hành vi vuốt nhanh. Hãy xem Cử chỉ và ảnh động để biết thêm thông tin.

Ngay từ đầu, Animatable hỗ trợ FloatColor, nhưng bạn có thể dùng bất kỳ loại dữ liệu nào bằng cách cung cấp một TwoWayConverter. Hãy xem AnimationVector để biết thêm thông tin.

Bạn có thể sử dụng AnimationSpec để tuỳ chỉnh thông số kỹ thuật của ảnh động. Hãy xem AnimationSpec để biết thêm thông tin.

Ảnh động

Animation là API Ảnh động cấp thấp nhất hiện có. Nhiều ảnh động mà chúng ta thấy cho đến nay đã xây dựng trên trang Ảnh động. Có hai loại phụ Animation: TargetBasedAnimationDecayAnimation.

Bạn chỉ nên dùng Animation để kiểm soát thời lượng của ảnh động theo cách thủ công. Animation không có trạng thái và không có bất kỳ khái niệm nào về vòng đời. Nó đóng vai trò như một công cụ tính toán ảnh động mà các API cấp cao hơn sử dụng.

TargetBasedAnimation

Các API khác dùng được trong hầu hết các trường hợp, nhưng việc sử dụng trực tiếp TargetBasedAnimation cho phép bạn tự kiểm soát thời lượng phát ảnh động. Trong ví dụ bên dưới, thời gian phát của TargetAnimation được kiểm soát theo cách thủ công dựa trên khung thời gian do withFrameNanos cung cấp.

val anim = remember {
    TargetBasedAnimation(
        animationSpec = tween(200),
        typeConverter = Float.VectorConverter,
        initialValue = 200f,
        targetValue = 1000f
    )
}
var playTime by remember { mutableStateOf(0L) }

LaunchedEffect(anim) {
    val startTime = withFrameNanos { it }

    do {
        playTime = withFrameNanos { it } - startTime
        val animationValue = anim.getValueFromNanos(playTime)
    } while (someCustomCondition())
}

DecayAnimation

Không giống như TargetBasedAnimation, DecayAnimation không yêu cầu cung cấp targetValue. Thay vào đó, phương thức này tính toán targetValue dựa trên các điều kiện bắt đầu do initialVelocityinitialValue cung cấp, cũng như DecayAnimationSpec được cung cấp.

Ảnh động phân rã thường được sử dụng sau cử chỉ hất để làm chậm các phần tử xuống điểm dừng. Tốc độ hoạt ảnh bắt đầu ở giá trị do initialVelocityVector đặt và sẽ chậm lại theo thời gian.

Tuỳ chỉnh ảnh động

Nhiều API ảnh động thường chấp nhận các tham số để tuỳ chỉnh hoạt động của chúng.

AnimationSpec

Hầu hết các API ảnh động đều cho phép nhà phát triển tuỳ chỉnh thông số kỹ thuật của ảnh động bằng tham số AnimationSpec tuỳ chọn.

val alpha: Float by animateFloatAsState(
    targetValue = if (enabled) 1f else 0.5f,
    // Configure the animation duration and easing.
    animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)
)

Có nhiều loại AnimationSpec khác nhau để tạo các loại ảnh động khác nhau.

spring

spring tạo một ảnh động dựa trên kiến thức vật lý giữa giá trị bắt đầu và giá trị kết thúc. Nội dung này có 2 thông số: dampingRatiostiffness.

dampingRatio xác định khả năng bật của spring. Giá trị mặc định là Spring.DampingRatioNoBouncy.

Đồ hoạ ảnh động hiển thị hoạt động của các tỷ lệ giảm chấn

stiffness xác định tốc độ chuyển động của spring hướng về giá trị cuối cùng. Giá trị mặc định là Spring.StiffnessMedium.

val value by animateFloatAsState(
    targetValue = 1f,
    animationSpec = spring(
        dampingRatio = Spring.DampingRatioHighBouncy,
        stiffness = Spring.StiffnessMedium
    )
)

spring có thể xử lý sự cố gián đoạn một cách mượt mà hơn các mã AnimationSpec dựa trên thời lượng vì các mã này đảm bảo tính liên tục của tốc độ khi giá trị mục tiêu thay đổi giữa các nội dung ảnh động. spring được sử dụng như thông số ảnh động mặc định của nhiều API ảnh động, chẳng hạn như animate*AsStateupdateTransition.

tiền thiếu niên

tween tạo ảnh động giữa giá trị bắt đầu và giá trị kết thúc qua việc sử dụng một easing curve durationMillis được chỉ định. Hãy xem Easing để biết thêm thông tin. Bạn cũng có thể chỉ định delayMillis để trì hoãn việc bắt đầu ảnh động.

val value by animateFloatAsState(
    targetValue = 1f,
    animationSpec = tween(
        durationMillis = 300,
        delayMillis = 50,
        easing = LinearOutSlowInEasing
    )
)

keyframes

keyframes tạo ảnh động dựa trên các giá trị ảnh chụp nhanh được chỉ định tại các dấu thời gian trong suốt thời gian tạo ảnh động. Tại một thời điểm bất kỳ, giá trị ảnh động sẽ được tự ý thêm vào giữa 2 giá trị khung hình chính. Đối với mỗi khung hình chính, bạn có thể chỉ định Easing để xác định đường cong nội suy.

Có thể tuỳ chọn chỉ định các giá trị ở 0 giây và tại khoảng thời gian. Nếu bạn không chỉ định các giá trị này, thì giá trị mặc định lần lượt là giá trị bắt đầu và kết thúc của ảnh động.

val value by animateFloatAsState(
    targetValue = 1f,
    animationSpec = keyframes {
        durationMillis = 375
        0.0f at 0 with LinearOutSlowInEasing // for 0-15 ms
        0.2f at 15 with FastOutLinearInEasing // for 15-75 ms
        0.4f at 75 // ms
        0.4f at 225 // ms
    }
)

repeatable

repeatable chạy một ảnh động dựa trên thời lượng (chẳng hạn như tween hoặc keyframes) lặp lại cho đến khi ảnh động đạt đến số vòng lặp được chỉ định. Bạn có thể chuyển thông số repeatMode để chỉ định xem ảnh động nên lặp lại bằng cách bắt đầu từ đầu (RepeatMode.Restart) hay từ cuối (RepeatMode.Reverse).

val value by animateFloatAsState(
    targetValue = 1f,
    animationSpec = repeatable(
        iterations = 3,
        animation = tween(durationMillis = 300),
        repeatMode = RepeatMode.Reverse
    )
)

infiniteRepeatable

infiniteRepeatable giống như repeatable nhưng số lượng vòng lặp lại là vô hạn.

val value by animateFloatAsState(
    targetValue = 1f,
    animationSpec = infiniteRepeatable(
        animation = tween(durationMillis = 300),
        repeatMode = RepeatMode.Reverse
    )
)

Trong các thử nghiệm sử dụng ComposeTestRule, các ảnh động sử dụng infiniteRepeatable sẽ không chạy. Thành phần sẽ được hiển thị bằng việc sử dụng giá trị ban đầu của từng giá trị ảnh động.

snap

snap là một AnimationSpec đặc biệt sẽ ngay lập tức chuyển giá trị đó sang giá trị kết thúc. Bạn có thể chỉ định delayMillis để hoãn việc khởi động ảnh động.

val value by animateFloatAsState(
    targetValue = 1f,
    animationSpec = snap(delayMillis = 50)
)

Hàm Easing

Các thao tác AnimationSpec dựa trên thời lượng (chẳng hạn như tween hoặc keyframes) sử dụng Easing để điều chỉnh một phân đoạn của ảnh động. Các hoạt động này cho phép giá trị ảnh động tăng tốc và chậm lại, thay vì di chuyển với tốc độ không đổi. Một phân đoạn là một giá trị nằm trong khoảng từ 0 (bắt đầu) đến 1.0 (kết thúc) cho biết điểm hiện tại trong ảnh động.

Easing thực chất là một hàm nhận giá trị phân số từ 0 đến 1.0 và trả về một số thực có độ chính xác đơn. Giá trị được trả về có thể nằm ngoài ranh giới đại diện cho tình trạng quá mức hoặc không đạt mức. Hàm Easing tuỳ chỉnh có thể được tạo giống như mã bên dưới.

val CustomEasing = Easing { fraction -> fraction * fraction }

@Composable
fun EasingUsage() {
    val value by animateFloatAsState(
        targetValue = 1f,
        animationSpec = tween(
            durationMillis = 300,
            easing = CustomEasing
        )
    )
    // ...
}

Compose cung cấp nhiều hàm Easing được tích hợp sẵn cho hầu hết các trường hợp sử dụng. Vui lòng xem bài viết Speed (Tốc độ) – Material Design để biết thêm thông tin về mục đích sử dụng hàm Easing theo tình huống của bạn.

  • FastOutSlowInEasing
  • LinearOutSlowInEasing
  • FastOutLinearEasing
  • LinearEasing
  • CubicBezierEasing
  • Xem thêm

AnimationVector

Hầu hết các API ảnh động trong Compose đều hỗ trợ Float, Color, Dp và các loại dữ liệu cơ bản khác dưới dạng giá trị ảnh động ngay từ đầu, nhưng đôi khi bạn cần tạo ảnh động cho các loại dữ liệu khác bao gồm cả các dữ liệu tuỳ chỉnh. Trong suốt thời gian tạo ảnh động, mọi giá trị ảnh động đều được biểu thị dưới dạng AnimationVector. Giá trị được chuyển đổi thành AnimationVector và ngược lại bằng TwoWayConverter tương ứng để hệ thống ảnh động chính có thể xử lý thống nhất. Ví dụ: Int được biểu thị dưới dạng AnimationVector1D và có một giá trị số thực duy nhất. TwoWayConverter cho Int có dạng như:

val IntToVector: TwoWayConverter<Int, AnimationVector1D> =
    TwoWayConverter({ AnimationVector1D(it.toFloat()) }, { it.value.toInt() })

Color cơ bản là một tập hợp gồm 4 giá trị: đỏ, xanh lá cây, xanh lam và alpha, vì vậy, Color được chuyển đổi thành AnimationVector4D chứa 4 giá trị số thực chính xác. Bằng cách này mọi loại dữ liệu sử dụng trong ảnh động sẽ được chuyển đổi thành AnimationVector1D, AnimationVector2D, AnimationVector3D hoặc AnimationVector4D tuỳ thuộc vào kích thước. Điều này cho phép các thành phần khác nhau của đối tượng được tạo ảnh động độc lập, mỗi thành phần có theo dõi vận tốc riêng. Có thể truy cập các bộ chuyển đổi tích hợp cho kiểu dữ liệu cơ bản bằng cách sử dụng Color.VectorConverter, Dp.VectorConverter, v.v..

Khi muốn hỗ trợ thêm cho một loại dữ liệu mới dưới dạng giá trị ảnh động, bạn có thể tạo TwoWayConverter riêng của mình và cung cấp dữ liệu đó cho API. Ví dụ: bạn có thể sử dụng animateValueAsState để tạo ảnh động cho loại dữ liệu tuỳ chỉnh của mình như sau:

data class MySize(val width: Dp, val height: Dp)

@Composable
fun MyAnimation(targetSize: MySize) {
    val animSize: MySize by animateValueAsState<MySize, AnimationVector2D>(
        targetSize,
        TwoWayConverter(
            convertToVector = { size: MySize ->
                // Extract a float value from each of the `Dp` fields.
                AnimationVector2D(size.width.value, size.height.value)
            },
            convertFromVector = { vector: AnimationVector2D ->
                MySize(vector.v1.dp, vector.v2.dp)
            }
        )
    )
}

Tài nguyên vectơ động (thử nghiệm)

Để dùng tài nguyên AnimatedVectorDrawable, hãy tải tệp có thể vẽ lên bằng animatedVectorResource và truyền vào boolean để chuyển đổi giữa trạng thái bắt đầu và kết thúc của đối tượng có thể vẽ.

@Composable
fun AnimatedVectorDrawable() {
    val image = AnimatedImageVector.animatedVectorResource(R.drawable.ic_hourglass_animated)
    var atEnd by remember { mutableStateOf(false) }
    Image(
        painter = rememberAnimatedVectorPainter(image, atEnd),
        contentDescription = "Timer",
        modifier = Modifier.clickable {
            atEnd = !atEnd
        },
        contentScale = ContentScale.Crop
    )
}

Để biết thêm thông tin chi tiết về định dạng của tệp có thể vẽ được, vui lòng xem nội dung Tạo ảnh động cho đồ hoạ có thể vẽ được.

Ảnh động cho mục danh sách

Nếu bạn đang tìm cách tạo ảnh động cho việc sắp xếp lại thứ tự mục bên trong lưới hoặc danh sách Lazy, hãy xem tài liệu ảnh động của mục bố cục lazy.

Cử chỉ và ảnh động (nâng cao)

Có một số điều mà chúng ta phải cân nhắc khi thao tác với cả với các sự kiện chạm và ảnh động, so với khi chúng ta chỉ thao tác với các ảnh động. Trước hết, chúng ta có thể cần làm gián đoạn ảnh động đang diễn ra khi các sự kiện chạm bắt đầu do tương tác của người dùng sẽ có mức độ ưu tiên cao nhất.

Trong ví dụ bên dưới, chúng tôi sử dụng Animatable để thể hiện cho vị trí chênh lệch của thành phần vòng kết nối. Các sự kiện nhấn được xử lý bằng công cụ sửa đổi pointerInput. Khi phát hiện một sự kiện nhấn mới, chúng ta gọi lệnh animateTo để tạo ảnh động cho giá trị chênh lệch vào vị trí nhấn. Một sự kiện chạm cũng có thể xảy ra trong quá trình tạo ảnh động và trong trường hợp đó, animateTo làm gián đoạn ảnh động đang diễn ra và chạy ảnh động đó đến vị trí mục tiêu mới trong khi vẫn duy trì tốc độ của ảnh động bị gián đoạn.

@Composable
fun Gesture() {
    val offset = remember { Animatable(Offset(0f, 0f), Offset.VectorConverter) }
    Box(
        modifier = Modifier
            .fillMaxSize()
            .pointerInput(Unit) {
                coroutineScope {
                    while (true) {
                        // Detect a tap event and obtain its position.
                        val position = awaitPointerEventScope {
                            awaitFirstDown().position
                        }
                        launch {
                            // Animate to the tap position.
                            offset.animateTo(position)
                        }
                    }
                }
            }
    ) {
        Circle(modifier = Modifier.offset { offset.value.toIntOffset() })
    }
}

private fun Offset.toIntOffset() = IntOffset(x.roundToInt(), y.roundToInt())

Một mẫu thường gặp khác là chúng ta cần đồng bộ hoá các giá trị ảnh động với các giá trị hình thành từ các sự kiện chạm, chẳng hạn như kéo. Trong ví dụ bên dưới, chúng ta thấy tính năng "vuốt để loại bỏ" được triển khai dưới dạng Modifier (thay vì sử dụng thành phần kết hợp SwipeToDismiss). Độ chênh lệch chiều ngang của thành phần được biểu thị dưới dạng Animatable. API này có một đặc điểm hữu ích trong ảnh động cử chỉ. Sự kiện nhấn cũng như nội dung ảnh động có thể thay đổi giá trị của API. Khi nhận được một sự kiện nhấn, chúng ta sẽ dừng Animatable bằng phương thức stop để có thể chặn mọi ảnh động đang phát.

Trong một sự kiện kéo, chúng ta sử dụng snapTo để cập nhật giá trị Animatable bằng giá trị được tính từ các sự kiện nhấn. Để vuốt nhanh, công cụ Compose cung cấp VelocityTracker để ghi lại các sự kiện kéo và tính tốc độ. Vận tốc có thể được cung cấp trực tiếp cho animateDecay để tạo ảnh động vuốt nhanh. Khi muốn trượt giá trị độ dời trở về vị trí ban đầu, chúng ta chỉ định giá trị chênh lệch mục tiêu của 0f bằng phương thức animateTo.

fun Modifier.swipeToDismiss(
    onDismissed: () -> Unit
): Modifier = composed {
    val offsetX = remember { Animatable(0f) }
    pointerInput(Unit) {
        // Used to calculate fling decay.
        val decay = splineBasedDecay<Float>(this)
        // Use suspend functions for touch events and the Animatable.
        coroutineScope {
            while (true) {
                // Detect a touch down event.
                val pointerId = awaitPointerEventScope { awaitFirstDown().id }
                val velocityTracker = VelocityTracker()
                // Stop any ongoing animation.
                offsetX.stop()
                awaitPointerEventScope {
                    horizontalDrag(pointerId) { change ->
                        // Update the animation value with touch events.
                        launch {
                            offsetX.snapTo(
                                offsetX.value + change.positionChange().x
                            )
                        }
                        velocityTracker.addPosition(
                            change.uptimeMillis,
                            change.position
                        )
                    }
                }
                // No longer receiving touch events. Prepare the animation.
                val velocity = velocityTracker.calculateVelocity().x
                val targetOffsetX = decay.calculateTargetValue(
                    offsetX.value,
                    velocity
                )
                // The animation stops when it reaches the bounds.
                offsetX.updateBounds(
                    lowerBound = -size.width.toFloat(),
                    upperBound = size.width.toFloat()
                )
                launch {
                    if (targetOffsetX.absoluteValue <= size.width) {
                        // Not enough velocity; Slide back.
                        offsetX.animateTo(
                            targetValue = 0f,
                            initialVelocity = velocity
                        )
                    } else {
                        // The element was swiped away.
                        offsetX.animateDecay(velocity, decay)
                        onDismissed()
                    }
                }
            }
        }
    }
        .offset { IntOffset(offsetX.value.roundToInt(), 0) }
}

Thử nghiệm

Compose cung cấp ComposeTestRule cho phép bạn viết các phép kiểm thử cho ảnh động theo cách thức xác định với toàn quyền kiểm soát trên đồng hồ kiểm thử. Tính năng này cho phép bạn xác minh các giá trị ảnh động trung gian. Ngoài ra, một thử nghiệm ảnh động có thể chạy nhanh hơn thời lượng thực tế của ảnh động.

ComposeTestRule cho thấy đồng hồ thử nghiệm là mainClock. Bạn có thể thiết lập thuộc tính autoAdvance thành false để kiểm soát đồng hồ trong mã thử nghiệm. Sau khi bắt đầu ảnh động mà bạn muốn thử nghiệm, đồng hồ có thể được di chuyển về phía trước bằng advanceTimeBy.

Cần lưu ý là advanceTimeBy không di chuyển đồng hồ một cách chính xác theo thời lượng đã chỉ định. Thay vào đó, hệ thống sẽ làm tròn lên thời lượng gần nhất bằng hệ số của thời lượng khung hình.

@get:Rule
val rule = createComposeRule()

@Test
fun testAnimationWithClock() {
    // Pause animations
    rule.mainClock.autoAdvance = false
    var enabled by mutableStateOf(false)
    rule.setContent {
        val color by animateColorAsState(
            targetValue = if (enabled) Color.Red else Color.Green,
            animationSpec = tween(durationMillis = 250)
        )
        Box(Modifier.size(64.dp).background(color))
    }

    // Initiate the animation.
    enabled = true

    // Let the animation proceed.
    rule.mainClock.advanceTimeBy(50L)

    // Compare the result with the image showing the expected result.
    // `assertAgainGolden` needs to be implemented in your code.
    rule.onRoot().captureToImage().assertAgainstGolden()
}

Hỗ trợ công cụ

Android Studio hỗ trợ việc kiểm tra updateTransitionanimatedVisibility trong phần Xem trước ảnh động. Bạn có thể làm như sau:

  • Xem trước quá trình chuyển đổi theo khung
  • Kiểm tra các giá trị cho tất cả ảnh động trong quá trình chuyển đổi
  • Xem trước quá trình chuyển đổi giữa trạng thái ban đầu và trạng thái mục tiêu
  • Kiểm tra và phối hợp nhiều ảnh động cùng lúc

Khi khởi động tính năng Xem trước ảnh động, bạn sẽ thấy ngăn "Animations". Trong ngăn này, bạn có thể chạy bất kỳ lượt chuyển đổi nào có trong bản xem trước. Quá trình chuyển đổi cũng như mỗi giá trị ảnh động trong quá trình đó đều được gắn nhãn với tên mặc định. Bạn có thể tuỳ chỉnh nhãn bằng cách chỉ định tham số label trong các hàm updateTransitionAnimatedVisibility. Để biết thêm thông tin, hãy xem phần Xem trước ảnh động.

Bảng điều khiển xem trước ảnh động

Tìm hiểu thêm

Để tìm hiểu thêm về ảnh động trong Jetpack Compose, hãy tham khảo thêm các tài nguyên sau đây:

Mẫu

Bài đăng trên blog

Lớp học lập trình

Video