Hướng dẫn nhanh về Ảnh động trong Compose

Compose có nhiều cơ chế ảnh động tích hợp sẵn và bạn có thể cảm thấy bối rối khi không biết nên chọn cơ chế nào. Sau đây là danh sách các trường hợp sử dụng ảnh động phổ biến. Để biết thêm thông tin chi tiết về toàn bộ các tuỳ chọn API có sẵn, hãy đọc toàn bộ tài liệu về Ảnh động trong Compose.

Tạo ảnh động cho các thuộc tính thành phần kết hợp phổ biến

Compose cung cấp các API tiện lợi cho phép bạn giải quyết nhiều trường hợp sử dụng ảnh động phổ biến. Phần này minh hoạ cách tạo ảnh động cho các thuộc tính phổ biến của một composable.

Tạo ảnh động xuất hiện / biến mất

Thành phần kết hợp màu xanh lục tự hiện và ẩn
Hình 1. Tạo ảnh động cho sự xuất hiện và biến mất của một mục trong Cột

Sử dụng AnimatedVisibility để ẩn hoặc hiện một thành phần kết hợp. Các thành phần con bên trong AnimatedVisibility có thể sử dụng Modifier.animateEnterExit() cho quá trình chuyển đổi vào hoặc thoát của riêng chúng.

var visible by remember {
    mutableStateOf(true)
}
// Animated visibility will eventually remove the item from the composition once the animation has finished.
AnimatedVisibility(visible) {
    // your composable here
    // ...
}

Các tham số enter và exit của AnimatedVisibility cho phép bạn định cấu hình cách một composable hoạt động khi xuất hiện và biến mất. Hãy đọc tài liệu đầy đủ để biết thêm thông tin.

Một lựa chọn khác để tạo ảnh động cho chế độ hiển thị của một composable là tạo ảnh động cho alpha theo thời gian bằng cách sử dụng animateFloatAsState:

var visible by remember {
    mutableStateOf(true)
}
val animatedAlpha by animateFloatAsState(
    targetValue = if (visible) 1.0f else 0f,
    label = "alpha"
)
Box(
    modifier = Modifier
        .size(200.dp)
        .graphicsLayer {
            alpha = animatedAlpha
        }
        .clip(RoundedCornerShape(8.dp))
        .background(colorGreen)
        .align(Alignment.TopCenter)
) {
}

Tuy nhiên, việc thay đổi alpha đi kèm với lưu ý rằng composable vẫn nằm trong composition và tiếp tục chiếm không gian mà nó được bố trí. Điều này có thể khiến trình đọc màn hình và các cơ chế hỗ trợ tiếp cận khác vẫn xem xét mục trên màn hình. Mặt khác, AnimatedVisibility cuối cùng sẽ xoá mục khỏi thành phần kết hợp.

Tạo ảnh động cho alpha của một thành phần kết hợp
Hình 2. Tạo ảnh động cho alpha của một thành phần kết hợp

Tạo ảnh động cho màu nền

Thành phần kết hợp có màu nền thay đổi theo thời gian dưới dạng ảnh động, trong đó các màu mờ dần vào nhau.
Hình 3. Tạo ảnh động cho màu nền của thành phần kết hợp

val animatedColor by animateColorAsState(
    if (animateBackgroundColor) colorGreen else colorBlue,
    label = "color"
)
Column(
    modifier = Modifier.drawBehind {
        drawRect(animatedColor)
    }
) {
    // your composable here
}

Lựa chọn này hiệu quả hơn so với việc sử dụng Modifier.background(). Modifier.background() có thể chấp nhận được đối với chế độ cài đặt màu một lần, nhưng khi tạo ảnh động cho màu theo thời gian, điều này có thể gây ra nhiều lần kết hợp lại hơn mức cần thiết.

Để tạo ảnh động vô hạn cho màu nền, hãy xem phần lặp lại ảnh động.

Tạo ảnh động cho kích thước của một thành phần kết hợp

Thành phần kết hợp màu xanh lục tạo ảnh động cho việc thay đổi kích thước một cách mượt mà.
Hình 4. Composable tạo ảnh động mượt mà giữa kích thước nhỏ và kích thước lớn hơn

Compose cho phép bạn tạo ảnh động cho kích thước của thành phần kết hợp theo một vài cách. Sử dụng animateContentSize() cho ảnh động giữa các thay đổi về kích thước của composable.

Ví dụ: nếu có một hộp chứa văn bản có thể mở rộng từ một đến nhiều dòng, bạn có thể sử dụng Modifier.animateContentSize() để chuyển đổi mượt mà hơn:

var expanded by remember { mutableStateOf(false) }
Box(
    modifier = Modifier
        .background(colorBlue)
        .animateContentSize()
        .height(if (expanded) 400.dp else 200.dp)
        .fillMaxWidth()
        .clickable(
            interactionSource = remember { MutableInteractionSource() },
            indication = null
        ) {
            expanded = !expanded
        }

) {
}

Bạn cũng có thể sử dụng AnimatedContent, với SizeTransform để mô tả cách thay đổi kích thước.

Tạo ảnh động cho vị trí của thành phần kết hợp

Thành phần kết hợp màu xanh lục chuyển động mượt mà xuống dưới và sang phải
Hình 5. Thành phần kết hợp di chuyển theo độ lệch

Để tạo ảnh động cho vị trí của một composable, hãy sử dụng Modifier.offset{ } kết hợp với animateIntOffsetAsState().

var moved by remember { mutableStateOf(false) }
val pxToMove = with(LocalDensity.current) {
    100.dp.toPx().roundToInt()
}
val offset by animateIntOffsetAsState(
    targetValue = if (moved) {
        IntOffset(pxToMove, pxToMove)
    } else {
        IntOffset.Zero
    },
    label = "offset"
)

Box(
    modifier = Modifier
        .offset {
            offset
        }
        .background(colorBlue)
        .size(100.dp)
        .clickable(
            interactionSource = remember { MutableInteractionSource() },
            indication = null
        ) {
            moved = !moved
        }
)

Nếu bạn muốn đảm bảo rằng các thành phần kết hợp không được vẽ đè lên hoặc nằm dưới các thành phần kết hợp khác khi tạo ảnh động cho vị trí hoặc kích thước, hãy sử dụng Modifier.layout{ }. Đối tượng sửa đổi này truyền các thay đổi về kích thước và vị trí đến thành phần mẹ, sau đó ảnh hưởng đến các thành phần con khác.

Ví dụ: nếu bạn đang di chuyển Box trong Column và các thành phần con khác cần di chuyển khi Box di chuyển, hãy thêm thông tin về độ lệch bằng Modifier.layout{ } như sau:

var toggled by remember {
    mutableStateOf(false)
}
val interactionSource = remember {
    MutableInteractionSource()
}
Column(
    modifier = Modifier
        .padding(16.dp)
        .fillMaxSize()
        .clickable(indication = null, interactionSource = interactionSource) {
            toggled = !toggled
        }
) {
    val offsetTarget = if (toggled) {
        IntOffset(150, 150)
    } else {
        IntOffset.Zero
    }
    val offset = animateIntOffsetAsState(
        targetValue = offsetTarget, label = "offset"
    )
    Box(
        modifier = Modifier
            .size(100.dp)
            .background(colorBlue)
    )
    Box(
        modifier = Modifier
            .layout { measurable, constraints ->
                val offsetValue = if (isLookingAhead) offsetTarget else offset.value
                val placeable = measurable.measure(constraints)
                layout(placeable.width + offsetValue.x, placeable.height + offsetValue.y) {
                    placeable.placeRelative(offsetValue)
                }
            }
            .size(100.dp)
            .background(colorGreen)
    )
    Box(
        modifier = Modifier
            .size(100.dp)
            .background(colorBlue)
    )
}

2 hộp, trong đó hộp thứ 2 tạo ảnh động cho vị trí X, Y, hộp thứ 3 phản hồi bằng cách tự di chuyển theo lượng Y.
Hình 6. Tạo ảnh động bằng Modifier.layout{ }

Tạo ảnh động cho khoảng đệm của một composable

Thành phần kết hợp màu xanh lục thu nhỏ và phóng to khi nhấp, với khoảng đệm được tạo ảnh động
Hình 7. Thành phần kết hợp có khoảng đệm tạo ảnh động

Để tạo ảnh động cho khoảng đệm của một composable, hãy sử dụng animateDpAsState kết hợp với Modifier.padding():

var toggled by remember {
    mutableStateOf(false)
}
val animatedPadding by animateDpAsState(
    if (toggled) {
        0.dp
    } else {
        20.dp
    },
    label = "padding"
)
Box(
    modifier = Modifier
        .aspectRatio(1f)
        .fillMaxSize()
        .padding(animatedPadding)
        .background(Color(0xff53D9A1))
        .clickable(
            interactionSource = remember { MutableInteractionSource() },
            indication = null
        ) {
            toggled = !toggled
        }
)

Tạo ảnh động cho độ cao của một thành phần kết hợp

Hình 8. Ảnh động về độ cao của thành phần kết hợp khi nhấp

Để tạo ảnh động cho độ cao của một composable, hãy sử dụng animateDpAsState kết hợp với Modifier.graphicsLayer{ }. Đối với các thay đổi về độ cao một lần, hãy sử dụng Modifier.shadow(). Nếu bạn đang tạo ảnh động cho bóng, thì việc sử dụng đối tượng sửa đổi Modifier.graphicsLayer{ } là lựa chọn hiệu quả hơn.

val mutableInteractionSource = remember {
    MutableInteractionSource()
}
val pressed = mutableInteractionSource.collectIsPressedAsState()
val elevation = animateDpAsState(
    targetValue = if (pressed.value) {
        32.dp
    } else {
        8.dp
    },
    label = "elevation"
)
Box(
    modifier = Modifier
        .size(100.dp)
        .align(Alignment.Center)
        .graphicsLayer {
            this.shadowElevation = elevation.value.toPx()
        }
        .clickable(interactionSource = mutableInteractionSource, indication = null) {
        }
        .background(colorGreen)
) {
}

Ngoài ra, hãy sử dụng composable Card và đặt thuộc tính độ cao thành các giá trị khác nhau cho mỗi state.

Tạo hiệu ứng động cho tỷ lệ, bản dịch hoặc chế độ xoay văn bản

Thành phần kết hợp văn bản có nội dung "Hello" (Xin chào) tạo ảnh động giữa kích thước nhỏ và lớn hơn.
Hình 9. Văn bản tạo ảnh động mượt mà giữa hai kích thước

Khi tạo ảnh động cho tỷ lệ, bản dịch hoặc chế độ xoay văn bản, hãy đặt tham số textMotion trên TextStyle thành TextMotion.Animated. Điều này giúp đảm bảo hiệu ứng chuyển đổi mượt mà hơn giữa các ảnh động văn bản. Sử dụng Modifier.graphicsLayer{ } để dịch, xoay hoặc điều chỉnh tỷ lệ văn bản.

val infiniteTransition = rememberInfiniteTransition(label = "infinite transition")
val scale by infiniteTransition.animateFloat(
    initialValue = 1f,
    targetValue = 8f,
    animationSpec = infiniteRepeatable(tween(1000), RepeatMode.Reverse),
    label = "scale"
)
Box(modifier = Modifier.fillMaxSize()) {
    Text(
        text = "Hello",
        modifier = Modifier
            .graphicsLayer {
                scaleX = scale
                scaleY = scale
                transformOrigin = TransformOrigin.Center
            }
            .align(Alignment.Center),
        // Text composable does not take TextMotion as a parameter.
        // Provide it via style argument but make sure that we are copying from current theme
        style = LocalTextStyle.current.copy(textMotion = TextMotion.Animated)
    )
}

Tạo hiệu ứng động cho màu văn bản

Dòng chữ Hello Compose chuyển đổi màu sắc giữa xanh lục và xanh dương
Hình 10. Ví dụ cho thấy cách tạo ảnh động cho màu văn bản

Để tạo ảnh động cho màu văn bản, hãy sử dụng hàm lambda color trên thành phần kết hợp BasicText:

val infiniteTransition = rememberInfiniteTransition(label = "infinite transition")
val animatedColor by infiniteTransition.animateColor(
    initialValue = Color(0xFF60DDAD),
    targetValue = Color(0xFF4285F4),
    animationSpec = infiniteRepeatable(tween(1000), RepeatMode.Reverse),
    label = "color"
)

BasicText(
    text = "Hello Compose",
    color = {
        animatedColor
    },
    // ...
)

Chuyển đổi giữa các loại nội dung

Màn hình màu xanh lục cho biết Đang tải, màn hình màu xanh dương cho biết Đã tải và màn hình màu trắng cho biết Lỗi, các thành phần kết hợp khác nhau đang được lặp lại, kèm theo một ảnh động đơn giản.
Hình 11. Sử dụng AnimatedContent để tạo ảnh động cho các thay đổi giữa nhiều thành phần kết hợp (đã làm chậm)

Sử dụng AnimatedContent để tạo ảnh động giữa các thành phần kết hợp khác nhau. Nếu bạn chỉ muốn có hiệu ứng mờ dần chuẩn giữa các thành phần kết hợp, hãy sử dụng Crossfade.

var state by remember {
    mutableStateOf(UiState.Loading)
}
AnimatedContent(
    state,
    transitionSpec = {
        fadeIn(
            animationSpec = tween(3000)
        ) togetherWith fadeOut(animationSpec = tween(3000))
    },
    modifier = Modifier.clickable(
        interactionSource = remember { MutableInteractionSource() },
        indication = null
    ) {
        state = when (state) {
            UiState.Loading -> UiState.Loaded
            UiState.Loaded -> UiState.Error
            UiState.Error -> UiState.Loading
        }
    },
    label = "Animated Content"
) { targetState ->
    when (targetState) {
        UiState.Loading -> {
            LoadingScreen()
        }
        UiState.Loaded -> {
            LoadedScreen()
        }
        UiState.Error -> {
            ErrorScreen()
        }
    }
}

Bạn có thể tuỳ chỉnh AnimatedContent để hiển thị nhiều kiểu chuyển đổi vào và ra. Để biết thêm thông tin, hãy đọc tài liệu về AnimatedContent hoặc đọc bài đăng trên blog về AnimatedContent.

Tạo ảnh động khi di chuyển đến các đích đến khác nhau

Hai composable, một màu xanh lục có chữ Đích và một màu xanh dương có chữ Chi tiết, tạo ảnh động bằng cách trượt composable chi tiết lên trên composable đích.
Hình 12. Tạo ảnh động giữa các thành phần kết hợp bằng navigation-compose

Để tạo ảnh động cho hiệu ứng chuyển đổi giữa các thành phần kết hợp khi sử dụng cấu phần phần mềm navigation-compose, hãy chỉ định enterTransition và exitTransition trên một thành phần kết hợp. Bạn cũng có thể đặt ảnh động mặc định để sử dụng cho tất cả đích đến ở cấp cao nhất NavHost:

val navController = rememberNavController()
NavHost(
    navController = navController, startDestination = "landing",
    enterTransition = { EnterTransition.None },
    exitTransition = { ExitTransition.None }
) {
    composable("landing") {
        ScreenLanding(
            // ...
        )
    }
    composable(
        "detail/{photoUrl}",
        arguments = listOf(navArgument("photoUrl") { type = NavType.StringType }),
        enterTransition = {
            fadeIn(
                animationSpec = tween(
                    300, easing = LinearEasing
                )
            ) + slideIntoContainer(
                animationSpec = tween(300, easing = EaseIn),
                towards = AnimatedContentTransitionScope.SlideDirection.Start
            )
        },
        exitTransition = {
            fadeOut(
                animationSpec = tween(
                    300, easing = LinearEasing
                )
            ) + slideOutOfContainer(
                animationSpec = tween(300, easing = EaseOut),
                towards = AnimatedContentTransitionScope.SlideDirection.End
            )
        }
    ) { backStackEntry ->
        ScreenDetails(
            // ...
        )
    }
}

Có nhiều kiểu chuyển đổi vào và ra khác nhau, áp dụng các hiệu ứng khác nhau cho nội dung đến và đi, hãy xem tài liệu để biết thêm thông tin.

Lặp lại ảnh động

Nền màu xanh lục chuyển đổi thành nền màu xanh dương, lặp lại vô hạn bằng cách tạo ảnh động giữa hai màu.
Hình 13. Màu nền tạo ảnh động giữa hai giá trị, vô thời hạn

Sử dụng rememberInfiniteTransition với infiniteRepeatable animationSpec để liên tục lặp lại ảnh động. Thay đổi RepeatModes để chỉ định cách chuyển động qua lại.

Sử dụng repeatable để lặp lại một số lần nhất định.

val infiniteTransition = rememberInfiniteTransition(label = "infinite")
val color by infiniteTransition.animateColor(
    initialValue = Color.Green,
    targetValue = Color.Blue,
    animationSpec = infiniteRepeatable(
        animation = tween(1000, easing = LinearEasing),
        repeatMode = RepeatMode.Reverse
    ),
    label = "color"
)
Column(
    modifier = Modifier.drawBehind {
        drawRect(color)
    }
) {
    // your composable here
}

Bắt đầu ảnh động khi chạy một thành phần kết hợp

LaunchedEffect chạy khi một composable tham gia composition. Hàm này bắt đầu một ảnh động khi khởi chạy một composable. Bạn có thể dùng hàm này để thúc đẩy thay đổi trạng thái ảnh động. Sử dụng Animatable với phương thức animateTo để bắt đầu ảnh động khi khởi chạy:

val alphaAnimation = remember {
    Animatable(0f)
}
LaunchedEffect(Unit) {
    alphaAnimation.animateTo(1f)
}
Box(
    modifier = Modifier.graphicsLayer {
        alpha = alphaAnimation.value
    }
)

Tạo ảnh động tuần tự

Bốn vòng tròn có mũi tên màu xanh lục tạo ảnh động giữa mỗi vòng tròn, lần lượt tạo ảnh động.
Hình 14. Sơ đồ cho biết cách một ảnh động tuần tự diễn ra, từng ảnh một.

Sử dụng API coroutine Animatable để thực hiện ảnh động tuần tự hoặc đồng thời. Việc gọi animateTo trên Animatable lần lượt sẽ khiến mỗi ảnh động phải đợi ảnh động trước đó hoàn tất rồi mới tiếp tục. Điều này là do đây là một hàm tạm ngưng.

val alphaAnimation = remember { Animatable(0f) }
val yAnimation = remember { Animatable(0f) }

LaunchedEffect("animationKey") {
    alphaAnimation.animateTo(1f)
    yAnimation.animateTo(100f)
    yAnimation.animateTo(500f, animationSpec = tween(100))
}

Tạo ảnh động đồng thời

Ba vòng tròn có mũi tên màu xanh lục chuyển động đến từng vòng tròn, tất cả đều chuyển động cùng một lúc.
Hình 15. Sơ đồ cho biết cách các ảnh động đồng thời tiến triển, tất cả cùng một lúc.

Sử dụng API coroutine (Animatable#animateTo() hoặc animate) hoặc API Transition để tạo ảnh động đồng thời. Nếu bạn sử dụng nhiều hàm khởi chạy trong một ngữ cảnh coroutine, thì hàm này sẽ khởi chạy các ảnh động cùng một lúc:

val alphaAnimation = remember { Animatable(0f) }
val yAnimation = remember { Animatable(0f) }

LaunchedEffect("animationKey") {
    launch {
        alphaAnimation.animateTo(1f)
    }
    launch {
        yAnimation.animateTo(100f)
    }
}

Bạn có thể dùng API updateTransition để sử dụng cùng một trạng thái nhằm điều khiển nhiều ảnh động thuộc tính khác nhau cùng một lúc. Ví dụ sau đây tạo ảnh động cho hai thuộc tính được kiểm soát bằng một thay đổi trạng thái, rect và borderWidth:

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

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

Tối ưu hoá hiệu suất ảnh động

Ảnh động trong Compose có thể gây ra vấn đề về hiệu suất. Điều này là do bản chất của ảnh động: di chuyển hoặc thay đổi nhanh chóng các pixel trên màn hình, từng khung hình để tạo ảo giác chuyển động.

Hãy xem xét các giai đoạn khác nhau của Compose: thành phần, bố cục và bản vẽ. Nếu ảnh động của bạn thay đổi giai đoạn bố cục, thì tất cả thành phần kết hợp chịu ảnh hưởng đều phải bố trí lại và vẽ lại. Nếu ảnh động của bạn xuất hiện trong giai đoạn vẽ, thì theo mặc định, ảnh động đó sẽ có hiệu suất cao hơn so với khi bạn chạy ảnh động trong giai đoạn bố cục, vì ảnh động đó sẽ có ít việc phải làm hơn.

Để đảm bảo ứng dụng của bạn thực hiện ít thao tác nhất có thể trong khi tạo ảnh động, hãy chọn phiên bản lambda của Modifier nếu có thể. Thao tác này sẽ bỏ qua quá trình kết hợp lại và thực hiện ảnh động bên ngoài giai đoạn kết hợp, nếu không, hãy sử dụng Modifier.graphicsLayer{ } vì đối tượng sửa đổi này luôn chạy trong giai đoạn vẽ. Để biết thêm thông tin về vấn đề này, hãy xem phần trì hoãn việc đọc trong tài liệu về hiệu suất.

Thay đổi thời gian ảnh động

Theo mặc định, Compose sử dụng ảnh động spring cho hầu hết các ảnh động. Lực lò xo, hay ảnh động dựa trên vật lý, mang lại cảm giác tự nhiên hơn. Các ảnh động này cũng có thể bị gián đoạn vì chúng tính đến vận tốc hiện tại của đối tượng, thay vì thời gian cố định. Nếu bạn muốn ghi đè chế độ mặc định, tất cả các API ảnh động được minh hoạ trước đó đều có khả năng đặt animationSpec để tuỳ chỉnh cách chạy ảnh động, cho dù bạn muốn ảnh động thực thi trong một khoảng thời gian nhất định hay có độ nảy cao hơn.

Sau đây là thông tin tóm tắt về các lựa chọn animationSpec:

  • spring: Ảnh động dựa trên vật lý, mặc định cho tất cả ảnh động. Bạn có thể thay đổi độ cứng hoặc dampingRatio để có giao diện và cảm nhận khác cho ảnh động.
  • tween (viết tắt của between): Ảnh động dựa trên thời lượng, tạo ảnh động giữa hai giá trị bằng hàm Easing.
  • keyframes: Quy cách để chỉ định giá trị tại một số điểm chính trong ảnh động.
  • repeatable: Quy cách dựa trên thời lượng chạy một số lần nhất định, được chỉ định bởi RepeatMode.
  • infiniteRepeatable: Thông số dựa trên thời lượng chạy vĩnh viễn.
  • snap: Chuyển ngay đến giá trị cuối mà không có ảnh động.
Hai ảnh động minh hoạ trường hợp không đặt thông số kỹ thuật so với trường hợp đặt thông số kỹ thuật tuỳ chỉnh cho Spring.
Hình 16. Không có bộ thông số kỹ thuật so với bộ thông số kỹ thuật Spring tuỳ chỉnh

Hãy đọc toàn bộ tài liệu để biết thêm thông tin về animationSpecs.

Tài nguyên khác

Để xem thêm ví dụ về ảnh động thú vị trong Compose, hãy xem các nội dung sau: