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 choáng ngợp khi biết nên 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ề tập hợp đầy đủ các tuỳ chọn API dành cho bạn, vui lòng đọc toàn bộ tài liệu về Ảnh động của 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 bạn có thể tạo ảnh động cho các thuộc tính phổ biến của một thành phần kết hợp.

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

Thành phần kết hợp màu xanh lục xuất hiện và tự ẩ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. Phần tử 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 mình.

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 hoạt động khi thành phần đó 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 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 alpha theo thời gian bằ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 nằm trong thành phần và tiếp tục chiếm không gian đượ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, cuối cùng AnimatedVisibility sẽ xoá mục khỏi thành phần kết hợp.

Tạo ảnh động alpha của thành phần kết hợp
Hình 2. Tạo ảnh động 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 sẽ 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
}

Tuỳ chọn này hiệu quả hơn so với việc sử dụng Modifier.background(). Modifier.background() có thể được chấp nhận đối với chế độ cài đặt màu cho một lần chụp, nhưng khi tạo hiệu ứng ảnh động cho một màu theo thời gian, điều này có thể dẫn đến nhiều lần kết hợp lại hơn so với 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 phần ảnh động.

Tạo ảnh động 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 thay đổi kích thước một cách suôn sẻ.
Hình 4. Thành phần kết hợp tạo ảnh động mượt mà giữa kích thước nhỏ và kích thước 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() cho ả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 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ể dùng Modifier.animateContentSize() để chuyển đổi suôn sẻ 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 cùng với SizeTransform để mô tả cách diễn ra các thay đổi về 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 mượt mà xuống và sang phải
Hình 5. Thành phần kết hợp di chuyển theo một mức bù trừ

Để 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 các thành phần kết hợp không được vẽ trên hoặc 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í cho phần tử mẹ, sau đó ảnh hưởng đến các phần tử 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ứ hai tạo ảnh động cho vị trí X,Y của nó, hộp thứ ba cũng phản ứng bằng cách tự di chuyển theo số 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 thu nhỏ và lớn hơn khi nhấp vào, với khoảng đệm là ảnh động
Hình 7. Thành phần kết hợp với hiệu ứ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 cho độ cao của một thành phần kết hợp

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

Để 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{ }. Để thay đổi độ 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ì 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 thành phần kết hợp Card và đặt thuộc tính độ cao thành các giá trị riêng cho mỗi trạng thái.

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

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

Khi tạo ảnh động theo tỷ lệ, dịch hoặc xoay văn bản, hãy đặt tham số textMotion trên TextStyle thành TextMotion.Animated. Điều này đảm bảo quá trình 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 chuyển 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 màu văn bản động

Các từ
Hình 10. Ví dụ minh hoạ màu văn bản ảnh độ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

Màn hình xanh lục nói
Hình 11. Sử dụng AnimatedContent để tạo hiệu ứng chuyển động cho các thay đổi giữa các thành phần kết hợp (bị 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 tạo 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()
        }
    }
}

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

Tạo ảnh động trong khi điều hướng đế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 có tên Trang đích và một thành phần màu xanh dương có nội dung Chi tiết, tạo ảnh động bằng cách trượt thành phần kết hợp chi tiết qua thành phần kết hợp đí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 được sử dụng cho mọi đí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 vào và thoát á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.

Lặp lại ảnh động

Nền xanh lục biến thành nền xanh dương, vô tận bằng cách tạo hiệu ứng động giữa hai màu sắc.
Hình 13. Màu nền tạo ảnh động giữa hai giá trị, vô 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 thức qua lại.

Sử dụng finiteRepeatable để lặp lại một số lần đã đặt.

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

LaunchedEffect chạy khi một thành phần kết hợp nhập vào thành phần. Nó sẽ bắt đầu ảnh động khi khởi chạy một thành phần kết hợp, bạn có thể sử dụng ảnh động này để thúc đẩy sự thay đổi trạng thái của ả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ự

4 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, tạo ảnh động cho nhau.
Hình 14. Sơ đồ cho biết cách tiến trình của một ảnh động tuần tự, từng ảnh động.

Sử dụng các API coroutine Animatable để thực hiện các ảnh động tuần tự hoặc đồng thời. Việc gọi lần lượt animateTo trên Animatable sẽ khiến mỗi ảnh động phải đợi các ảnh động trước đó kết thúc trước khi tiếp tục . Lý do là vì đâ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

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

Sử dụng các API coroutine (Animatable#animateTo() hoặc animate) hoặc API Transition để tạo các ảnh động đồng thời. Nếu bạn sử dụng nhiều hàm khởi chạy trong ngữ cảnh coroutine, thì các hàm này sẽ 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 cùng lúc. Ví dụ bên dưới tạo ảnh động cho 2 thuộc tính được kiểm soát bởi 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 của ảnh động: di chuyển hoặc thay đổi nhanh các pixel trên màn hình, theo từng khung hình để tạo ảo giác về 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à 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 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ẽ có hiệu suất cao hơn so với khi chạy ảnh động trong giai đoạn bố cục vì tổng thể sẽ mất ít công sức hơ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 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 Hoãn đọ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 có lò xo cho hầu hết ảnh động. Lò xo hay ảnh động dựa trên vật lý, tạo cảm giác tự nhiên hơn. Những tham số này cũng có thể gây 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 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 minh hoạ ở trên đều có khả năng đặt animationSpec để tuỳ chỉnh cách chạy ảnh động, cho dù bạn muốn thực thi trong một thời lượng nhất định hay tăng độ nảy.

Sau đây là phần tóm tắt 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 để đạt được giao diện ảnh động khác nhau.
  • tween (viết tắt của pending): Ả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 nhất đị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 vĩnh viễn.
  • snap: Ngay lập tức chụp giá trị cuối cùng mà không cần bất kỳ ảnh động nào.
Viết văn bản thay thế của bạn tại đây
Hình 16. Chưa đặt thông số kỹ thuật so với thông số của Custom Spring

Đọ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 nội dung sau: