Shadow: bản chất, tạo bóng đổ trong ứng dụng di động

Tác giả: IT Sectr Đã đăng: 2026-02-28 Thời gian đọc: 9 phút

Shadow (bóng đổ) — hiệu ứng trực quan trong giao diện di động tạo ra ảo giác về chiều sâu và hệ thống phân cấp giữa các phần tử. Bóng đổ cho thấy phần tử nào ở cao hơn trong mặt phẳng z và do đó, quan trọng hơn hoặc có tính tương tác cao hơn. Theo Material Design Guidelines (Google, 2026), bóng đổ hệ thống sử dụng năm độ cao (elevation) chính: 0 dp (phẳng), 1–4 dp (nút), 6–12 dp (thẻ), 16–24 dp (hộp thoại), 24+ dp (cửa sổ phương thức). Tìm hiểu thêm về chiều cao phần tử trong bài viết về Elevation.

Những điểm chính

  • Bóng đổ (shadow) — hiệu ứng trực quan tạo chiều sâu bằng cách làm tối khu vực bên dưới phần tử với làm mờ các cạnh.
  • Trên iOS, bóng đổ được đặt thông qua các thuộc tính CALayer: shadowColor, shadowOffset, shadowRadius, shadowOpacity.
  • Trên Android, bóng đổ được quản lý thông qua elevation (Material Design) hoặc kết xuất tùy chỉnh qua OutlineProvider.
  • Jetpack Compose hỗ trợ bóng đổ thông qua Modifier.shadow() và elevation trong Card/Surface.
  • Trong Flutter, bóng đổ được triển khai thông qua BoxShadow trong BoxDecoration hoặc qua PhysicalModel.

Bóng đổ trong UI là gì?

Bóng đổ (shadow) trong giao diện người dùng là hiệu ứng trực quan mô phỏng ánh sáng và chiều sâu. Bóng đổ được tạo bằng cách vẽ một bản sao tối và mờ của phần tử, dịch chuyển so với bản gốc. Trong thiết kế di động, bóng đổ thực hiện ba chức năng: hiển thị hệ thống phân cấp (phần tử càng cao, bóng càng dài), báo hiệu tính tương tác (nút có bóng trông có thể nhấn được), tách nội dung khỏi nền (thẻ có bóng được đọc như các khối riêng biệt).

Trong Material Design của Google, bóng đổ gắn liền với khái niệm elevation. Mỗi phần tử chiếm một độ cao nhất định trong tọa độ z và bóng đổ là hình chiếu của ánh sáng từ trên xuống. Human Interface Guidelines của Apple không quy định hệ thống độ cao nghiêm ngặt, nhưng khuyến nghị sử dụng bóng đổ để tạo hệ thống phân cấp trực quan và chiều sâu trong giao diện.

Theo một nghiên cứu của Nielsen Norman Group (2024), bóng đổ được đặt đúng cách giúp giảm thời gian tìm phần tử tương tác xuống 24%. Bóng đổ cũng giúp người dùng khiếm thị phân biệt ranh giới của các phần tử. Tuy nhiên, việc sử dụng quá nhiều bóng đổ (3+ cấp độ trên một màn hình) làm tăng tải nhận thức và làm giảm khả năng nhận biết.

Bóng đổ trên iOS: CALayer và SwiftUI

Trên iOS, bóng đổ được triển khai thông qua các thuộc tính CALayer: shadowColor đặt màu bóng, shadowOffset — độ dịch chuyển trong CGSize (x, y), shadowRadius — bán kính làm mờ, shadowOpacity — độ mờ đục từ 0 đến 1. Để có hiệu suất tốt, cũng nên đặt shadowPath — một UIBezierPath mô tả hình dạng bóng, để Core Animation không tự động tính toán.

swift
// UIKit: cấu hình bóng đổ qua CALayer
import UIKit

let cardView = UIView()
cardView.backgroundColor = .systemBackground
cardView.layer.cornerRadius = 12

// Cấu hình bóng đổ
cardView.layer.shadowColor = UIColor.black.cgColor
cardView.layer.shadowOpacity = 0.15
cardView.layer.shadowOffset = CGSize(width: 0, height: 4)
cardView.layer.shadowRadius = 8

"> Tối ưu hóa: shadowPath rõ ràng
cardView.layer.shadowPath = UIBezierPath(
    roundedRect: cardView.bounds,
    cornerRadius: 12
).cgPath

Trong SwiftUI, bóng đổ được đặt thông qua bộ sửa đổi .shadow() với các tham số color, radius, x, y. SwiftUI tự động áp dụng shadowPath và quản lý hiệu suất kết xuất. Đối với bóng đổ nâng cao (bóng bên trong, nhiều bóng), hãy sử dụng overlay() với LinearGradient.

swift
// SwiftUI: bóng đổ qua bộ sửa đổi shadow
import SwiftUI

VStack {
    Text("Thẻ có bóng đổ")
        .padding()
        .background(Color.white)
        .cornerRadius(16)
        .shadow(
            color: .black.opacity(0.15),
            radius: 10,
            x: 0,
            y: 4
        )
}

Hiệu suất bóng đổ trên iOS: nếu không đặt shadowPath, Core Animation sẽ tính toán bóng đổ dựa trên hình dạng của lớp mỗi khung hình, có thể gây giật lag trong quá trình hoạt ảnh. Đối với phần tử tĩnh, luôn đặt shadowPath. Đối với phần tử hoạt ảnh, hãy sử dụng shouldRasterize = true trên CALayer để lưu bóng đổ vào bộ nhớ đệm trong một lớp riêng. Trong SwiftUI, việc rasterization được quản lý thông qua .drawingGroup().

Bóng đổ trên Android: elevation và bóng đổ tùy chỉnh

Trên Android, bóng đổ trong Material Design được triển khai thông qua elevation — thuộc tính View đặt chiều cao của phần tử trong tọa độ z. Hệ thống tự động kết xuất bóng đổ dựa trên elevation và outlineProvider (đường viền phần tử). elevation càng cao, bóng đổ càng lớn và mờ. Đối với View không kế thừa Material Design (ví dụ: AppCompat), bóng đổ chỉ hoạt động khi tăng tốc phần cứng được bật.

xml
<!-- XML: elevation qua thuộc tính -->
<androidx.cardview.widget.CardView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:cardCornerRadius="12dp"
    app:cardElevation="6dp">
    <!-- Nội dung thẻ -->
</androidx.cardview.widget.CardView>

Đối với bóng đổ tùy chỉnh trong Hệ thống View Android, hãy sử dụng OutlineProvider. ViewOutlineProvider cho phép đặt hình dạng tùy ý thông qua outline.setRoundRect() hoặc outline.setPath(). Nếu không có OutlineProvider, bóng đổ được kết xuất dựa trên hình dạng hình chữ nhật của View, không tính đến độ bo tròn góc.

kotlin
// Kotlin: OutlineProvider tùy chỉnh cho bóng đổ
import android.graphics.Outline
import android.view.View
import android.view.ViewOutlineProvider

val myView: View = findViewById(R.id.myView)
myView.outlineProvider = object : ViewOutlineProvider() {
    override fun getOutline(view: View, outline: Outline) {
        outline.setRoundRect(
            0, 0, view.width, view.height,
            12f  // bán kính góc
        )
    }
}
myView.elevation = 8f  // chiều cao bóng đổ tính bằng px
myView.translationZ = 4f  // chiều cao bổ sung khi nhấn

Trong Jetpack Compose, bóng đổ được quản lý thông qua Modifier.shadow() và elevation trong các thành phần Card và Surface. Modifier.shadow() chấp nhận elevation và shape. Không giống như Hệ thống View, Compose kết xuất bóng đổ trong đường dẫn kết xuất riêng của nó, cung cấp hành vi dễ dự đoán hơn trên tất cả các phiên bản Android.

kotlin
// Jetpack Compose: bóng đổ qua Modifier.shadow
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

Card(
    modifier = Modifier.shadow(
        elevation = 6.dp,
        shape = RoundedCornerShape(12.dp),
        clip = false
    ),
    colors = CardDefaults.cardColors(containerColor = Color.White)
) {
    Text("Card với bóng đổ tùy chỉnh")
}

Khuyến nghị sử dụng bóng đổ

Sử dụng bóng đổ đúng cách cải thiện nhận thức về giao diện, trong khi sử dụng sai cách tạo ra nhiễu thị giác và làm giảm hiệu suất. Material Design định nghĩa năm cấp độ elevation: 0 dp (không bóng), 1–4 dp (nút, chip), 6–12 dp (thẻ, menu), 16–24 dp (hộp thoại, trang dưới), 24+ dp (cửa sổ phương thức, FAB).

Cấp độElevationPhần tửĐặc điểm bóng đổ
00 dpVăn bản, biểu tượng, nềnKhông bóng, lớp phẳng
11–4 dpNút, trường nhậpBóng nhẹ, dịch chuyển nhỏ
26–12 dpThẻ, SnackbarBóng vừa phải, làm mờ rõ rệt
316–24 dpHộp thoại, menuBóng sâu, làm mờ lớn
424+ dpCửa sổ phương thứcBóng tối đa, phần tử lơ lửng

Thực hành tốt nhất: (1) không sử dụng bóng đổ cho các phần tử không nên thu hút sự chú ý (nền, dải phân cách), (2) chiều cao bóng đổ phải tương ứng với tầm quan trọng của phần tử — càng quan trọng càng cao, (3) khi nhấn nút, giảm elevation kèm hoạt ảnh để phản hồi xúc giác, (4) tránh bóng đổ trên nền tối — sử dụng overlay (làm sáng/tối) thay vì bóng. Trong Material You (Android 12+), bóng đổ thích ứng với chủ đề: trên nền tối, elevation được thay thế bằng một lớp màu (surface tint).

Bóng đổ trong Flutter: BoxShadow và PhysicalModel

Trong Flutter, bóng đổ được triển khai thông qua BoxShadow bên trong BoxDecoration. BoxShadow chấp nhận color, offset, blurRadius và spreadRadius. Flutter hỗ trợ nhiều bóng đổ: truyền một danh sách BoxShadow cho phép tạo các hiệu ứng phức tạp — ví dụ: bóng bên ngoài + điểm nhấn bên trong. PhysicalModel là một giải pháp thay thế mô phỏng bóng đổ vật lý dựa trên hình dạng (BeveledRectangleBorder, CircleBorder).

dart
// Flutter: bóng đổ qua BoxDecoration + BoxShadow
import 'package:flutter/material.dart';

Container(
    decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(12.0),
        boxShadow: [
            BoxShadow(
                color: Colors.black.withOpacity(0.15),
                offset: Offset(0, 4),
                blurRadius: 8.0,
                spreadRadius: 0,
            ),
        ],
    ),
    child: Text('Vùng chứa có bóng đổ'),
)

// PhysicalModel cho bóng vật lý
PhysicalModel(
    color: Colors.white,
    elevation: 6.0,
    shadowColor: Colors.black.withOpacity(0.2),
    borderRadius: BorderRadius.circular(12.0),
    child: Text('Mô hình vật lý có bóng đổ'),
)

Hiệu suất: BoxShadow với nhiều bóng đổ có thể gây vẽ lại mỗi khi thay đổi. Đối với phần tử tĩnh, hãy sử dụng PhysicalModel — nó lưu bóng đổ vào bộ nhớ đệm trong một lớp riêng (RepaintBoundary). Trong Flutter 3.16+, elevation Material 3 với bóng đổ thích ứng được hỗ trợ, thay đổi màu sắc và độ mờ tùy theo chủ đề (sáng/tối).

Câu hỏi thường gặp

Elevation khác với shadow như thế nào?

Elevation là chiều cao số học của phần tử trong tọa độ z, trong khi shadow (bóng đổ) là hiệu ứng trực quan được tạo ra bởi chiều cao này. Trong Android, elevation là thuộc tính View tự động tạo bóng đổ. Trong thiết kế, elevation và shadow thường được sử dụng thay thế cho nhau, nhưng về mặt kỹ thuật, elevation là nguyên nhân và shadow là kết quả.

Tại sao bóng đổ không hiển thị trên Android?

Kiểm tra: (1) View có elevation lớn hơn 0, (2) outlineProvider đã được đặt (nếu View không phải từ thư viện Material), (3) tăng tốc phần cứng được bật (android:hardwareAccelerated="true" trong tệp kê khai). CardView tự động quản lý outlineProvider. Đối với View tùy chỉnh, hãy gọi view.outlineProvider = ViewOutlineProvider.BACKGROUND.

Làm thế nào để tạo bóng bên trong (inner shadow) trên iOS?

CALayer không hỗ trợ bóng bên trong trực tiếp. Sử dụng kết hợp: CAGradientLayer với màu đen và độ trong suốt bên trong mặt nạ, hoặc phủ một UIImageView với bóng bên trong raster. Trong SwiftUI, bóng bên trong được triển khai thông qua overlay với LinearGradient và blendMode(.multiply).

Làm thế nào để tạo hoạt ảnh bóng đổ khi nhấn nút?

Trong Android, sử dụng translationZ cho hoạt ảnh: ở trạng thái nghỉ elevation = 2 dp, khi nhấn translationZ = 4 dp (tổng chiều cao 6 dp). Trên iOS, tạo hoạt ảnh shadowOffset và shadowRadius đồng thời với biến đổi nút. Trong SwiftUI, sử dụng withAnimation với thay đổi elevation qua Modifier.shadow(). Trong Flutter, sử dụng AnimatedContainer với thay đổi BoxShadow.

Có thể sử dụng bao nhiêu bóng đổ trên một màn hình?

Material Design khuyến nghị không quá ba cấp độ elevation trên một màn hình. Nếu có nhiều phần tử có bóng đổ hơn, hãy nhóm chúng với cùng độ cao. Số lượng bóng đổ quá nhiều tạo ra nhiễu thị giác và làm giảm hiệu suất kết xuất trên các thiết bị tầm trung và bình dân.

Tóm tắt

  • Bóng đổ (shadow) là hiệu ứng chiều sâu trực quan dựa trên elevation của phần tử trong tọa độ z.
  • Trên iOS, bóng đổ được cấu hình qua CALayer: shadowColor, shadowOffset, shadowRadius, shadowOpacity.
  • Trên Android, bóng đổ được quản lý qua elevation và OutlineProvider cho hình dạng tùy chỉnh.
  • Material Design định nghĩa năm cấp độ elevation: 0, 1–4, 6–12, 16–24 và 24+ dp.
  • Jetpack Compose sử dụng Modifier.shadow() và elevation trong Card/Surface để quản lý bóng đổ.
  • Flutter triển khai bóng đổ qua BoxShadow (BoxDecoration) và PhysicalModel (bóng vật lý).
  • Tối ưu hóa: đặt shadowPath (iOS), outlineProvider (Android), sử dụng PhysicalModel (Flutter).

Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay

IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.

Thảo luận dự án

Đọc thêm