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ể sẽ bị choáng ngợp khi phải chọn cơ chế nào. Dưới đâ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ề bộ đầy đủ các tuỳ chọn API khác nhau mà bạn có thể sử dụng, hãy đọc tài liệu đầy đủ về Ảnh động trong Compose.

Tạo ảnh động cho các thuộc tính có khả năng kết hợp phổ biến

Compose cung cấp các API thuận tiện 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 trình bày cách bạn có thể tạo ảnh động phổ biến các thuộc tính của một thành phần kết hợp.

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 cho thấy và ẩn chính 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ó trẻ em bên trong AnimatedVisibility có thể sử dụng Modifier.animateEnterExit() để nhập riêng hoặc thoát khỏi quá trình chuyển đổi.

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ố nhập và thoát của AnimatedVisibility cho phép bạn định cấu hình cách một thành phần kết hợp sẽ hoạt động khi nó xuất hiện rồi biến mất. Hãy đọc tài liệu đầy đủ để biết thêm thông tin.

Một cách khác để tạo ảnh động cho chế độ hiển thị của thành phần kết hợp 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 một cảnh báo rằng thành phần kết hợp vẫn được giữ lại trong cấu trúc và tiếp tục chiếm không gian đã 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, cuối cùng AnimatedVisibility sẽ xoá mục từ cấu trúc.

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 alpha của thành phần kết hợp

Tạo ảnh động 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 sẽ chuyển dần vào nhau.
Hình 3. Ảnh động 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
}

Tuỳ chọn này hiệu quả hơn so với việc sử dụng Modifier.background(). Modifier.background() được chấp nhận cho 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 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 sự thay đổi kích thước một cách mượt mà.
Hình 4. Thành phần kết hợp tạo ảnh động mượt mà từ kích thước nhỏ đến lớn

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

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

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 cùng 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 tạo ảnh động trơn tru xuống và sang phải
Hình 5. Di chuyển thành phần kết hợp theo độ dời

Để tạo ảnh động cho vị trí của một thành phần kết hợp, 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 bị vẽ lên trên hoặc xuống dưới các các thành phần kết hợp 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 sẽ truyền các thay đổi về kích thước và vị trí cho 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 với 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 với hộp thứ 2 tạo ảnh động cho vị trí X,Y của nó, hộp thứ ba 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 thành phần kết hợp

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

Để tạo ảnh động cho khoảng đệm của một thành phần kết hợp, 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 nâng cao cho thành phần kết hợp

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

Để tạo ảnh động cho độ cao của một thành phần kết hợp, 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 đổ bóng, hãy sử dụng Đối tượng sửa đổi Modifier.graphicsLayer{ } là tuỳ chọn có hiệu suất cao 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 thành phần kết hợp Card và đặt thuộc tính độ cao thành các giá trị khác nhau cho mỗi trạng thái.

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

Thành phần kết hợp văn bản nói
Hình 9. Ảnh động văn bản mượt mà giữa hai kích thước

Khi tạo ảnh động cho tỷ lệ, bản dịch hoặc xoay văn bản, hãy đặt giá trị textMotion từ TextStyle đến TextMotion.Animated. Thao tác này đảm bảo mượt mà hơn hiệu ứng chuyển tiếp giữa các hoạt ảnh 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 ảnh động cho màu văn bản

Từ
Hình 10. Ví dụ minh hoạ màu văn bản động

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

Dòng chữ trên phông xanh
Hình 11. Sử dụng AnimatedContent để tạo ảnh động cho các thay đổi giữa các thành phần kết hợp (chậm lại)

Sử dụng AnimatedContent để tạo ảnh động giữa các thành phần kết hợp nếu bạn chỉ muốn hiệu ứng làm mờ tiêu 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()
        }
    }
}

AnimatedContent có thể được tuỳ chỉnh để hiển thị nhiều kiểu nhập và nhập chuyển đổi thoát. Để 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 này về AnimatedContent.

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

2 thành phần kết hợp, một thành phần màu xanh lục biểu thị trang đích và một thành phần màu xanh dương thể hiện "Detail" (Chi tiết), tạo hiệu ứng động bằng cách trượt thành phần kết hợp chi tiết đó lên trên thành phần kết hợp trang đí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 hiệu ứng chuyển đổi dạng ảnh động 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 enterTransitionexitTransition 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 loại chuyển đổi nhập và thoát khác nhau áp dụng các hiệu ứng khác nhau đối với nội dung đến và đi, hãy xem tài liệu khác để biết thêm thông tin.

Lặp lại ảnh động

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

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

Sử dụng finiteRepeatable để 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 khởi chạy một thành phần kết hợp

LaunchedEffect chạy khi một thành phần kết hợp đi vào thành phần. Phương thức này bắt đầu một ảnh động khi khởi chạy một thành phần kết hợp. Bạn có thể sử dụng phương thức này để điều khiển 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 các vòng tròn, lần lượt tạo ảnh động cho từng vòng tròn.
Hình 14. Sơ đồ cho biết cách một ảnh động tuần tự tiến triển, 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 chờ ảnh động trước đó hoàn tất trước khi tiếp tục. Lý do là hàm này là 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

Mỗi vòng tròn có ba vòng tròn có mũi tên màu xanh lục tạo hiệu ứng ảnh động cùng lúc.
Hình 15. Sơ đồ cho biết tiến trình của các ảnh động đồng thời.

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

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 để dùng cùng một trạng thái cho việc lái xe nhiều ảnh động thuộc tính khác nhau cùng một lúc. Ví dụ bên dưới về ảnh động hai thuộc tính được kiểm soát bằng sự thay đổi trạng thái, rectborderWidth:

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 ảnh động là gì: di chuyển hoặc thay đổi các pixel trên màn hình một cách nhanh chóng, từng khung hình để tạo ảo giác về chuyển động.

Hãy cân nhắc các giai đoạn của Compose: thành phần, bố cục và vẽ. Nếu ảnh động thay đổi giai đoạn bố cục, thì tất cả các thành phần kết hợp bị ảnh hưởng sẽ phải bố trí lại và vẽ lại. Nếu ảnh động của bạn xảy ra trong giai đoạn vẽ, thì theo mặc định, ảnh động sẽ hoạt động hiệu quả 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 công việc hơn để thực hiện.

Để đảm bảo ứng dụng của bạn hoạt động ít 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 bỏ qua việc kết hợp lại và thực hiện ảnh động 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 bản vẽ pha. Để biết thêm thông tin về vấn đề này, hãy xem phần lượt đọc trì hoãn trong tài liệu về hiệu suất.

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

Theo mặc định, Compose sử dụng ảnh động mùa xuân cho hầu hết ảnh động. Suối, hoặc hoạt ảnh dựa trên vật lý, tạo cảm giác tự nhiên hơn. Các hoạt động này cũng có thể bị gián đoạn vì tính đến vận tốc hiện tại của đối tượng, thay vì một khoảng thời gian cố định. Nếu bạn muốn ghi đè giá trị mặc định, tất cả các API ảnh động được trình bày ở trên có thể đặt animationSpec để tuỳ chỉnh cách chạy ảnh động, dù bạn muốn quảng cáo thực thi trong một khoảng thời gian nhất định hay sôi nổi hơn.

Sau đây là nội dung tóm tắt về các tuỳ 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ó được ảnh động khác giao diện.
  • 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: Thông số kỹ thuật để chỉ định các giá trị tại một số điểm chính trong ảnh động.
  • repeatable: Thông số kỹ thuật dựa trên thời lượng chạy một số lần nhất định, do RepeatMode chỉ định.
  • infiniteRepeatable: Thông số kỹ thuật dựa trên thời lượng chạy mãi mãi.
  • snap: Nhanh chóng chuyển sang giá trị cuối cùng mà không có ảnh động nào.
Viết văn bản thay thế tại đây
Hình 16. Không có thông số kỹ thuật nào được thiết lập và thông số kỹ thuật của Spring tuỳ chỉnh

Hãy đọc tài liệu đầy đủ để biết thêm thông tin về AnimationSpecs.

Tài nguyên khác

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