Nền tảng ứng dụng

Nâng tầm ứng dụng bằng các tính năng đồ hoạ mới nhất trong Jetpack Compose và Android

Loa

Giới thiệu

Giới thiệu về bài thuyết trình

Bạn đã sẵn sàng nâng tầm giao diện người dùng Android từ "hoạt động được" lên "một tác phẩm nghệ thuật" chưa?

Hãy cùng chúng tôi tìm hiểu về các tính năng đồ hoạ mới nhất trong Jetpack Compose. Chúng tôi sẽ hướng dẫn bạn cách tạo hiệu ứng hình ảnh mới, từ hiệu ứng chuyển màu lưới đẹp mắt đến hiệu ứng làm mờ tăng dần, cũng như cách sử dụng các tính năng đồ hoạ mới và mạnh mẽ trong Android. Chúng tôi cũng sẽ hướng dẫn bạn cách cải thiện hiệu suất của các tính năng đồ hoạ mà không làm giảm thời lượng pin hoặc tốc độ khung hình.

Tóm tắt

  • Compose 1.12 ra mắt dải chuyển màu dạng lưới để pha trộn nhiều màu sắc trên lưới đỉnh 2D. Trên Android 14 trở lên, bạn có thể sử dụng API drawMesh gốc để tạo hiệu ứng hình ảnh dựa trên hình học tuỳ chỉnh.
  • Compose hỗ trợ gam màu rộng để mang lại hiệu ứng chuyển màu mượt mà hơn trên phần cứng được hỗ trợ. Chỉ bật chế độ gam màu rộng trên hoạt động hoặc cửa sổ khi hiển thị nội dung đa phương tiện.
  • Compose 1.13 ra mắt các API PolygonShape và MorphPolygonShape gốc cho hình học tuỳ chỉnh. MorphPolygonShape đọc tiến trình trong giai đoạn phân giải đường viền, cập nhật hình dạng theo từng khung hình mà không kích hoạt quá trình kết hợp lại.
  • Compose 1.13 bổ sung hiệu ứng làm mờ tăng dần, còn cấp độ API 37.2 ra mắt RenderNode.setBackdropRenderEffect để lấy mẫu nội dung nền. Nhà phát triển có thể kết hợp các hiệu ứng phông nền để tạo hiệu ứng kính mờ hoặc hiệu ứng phóng đại.
  • Tối ưu hoá hiệu suất đồ hoạ bằng cách tránh các thao tác không cần thiết trên các khung hình. Chỉ giải mã các điểm ảnh cần thiết ngoài luồng chính và gói các hiệu ứng ảnh động trong graphicsLayer.

Tài nguyên

Gemini Notebook

Chủ đề

  • jetpack-compose
  • android-graphics
  • mesh-gradients
  • bộ đổ bóng
  • làm mờ
  • color-gamut
  • hiệu suất

Bản chép lời

Giới thiệu về các tính năng đồ hoạ trong Jetpack Compose

  • Chào mọi người, chào mừng các bạn đến với phiên của chúng tôi, Nâng tầm ứng dụng bằng các tính năng đồ hoạ mới nhất trong Compose và Android.

  • Tôi là Rebecca Franks, và cùng tôi lên sân khấu hôm nay là--

  • Tôi là Aayush Chaudhary.

  • Chào bạn, tôi là Rahul Ray.

  • Chúng tôi nhận thấy một xu hướng mới nổi trong cách các bạn vượt qua ranh giới của những gì có thể làm được trên Android và Compose, sử dụng AI để thực sự biến những ý tưởng tuyệt vời của mình thành hiện thực.

  • Chúng tôi cũng rất thích những ứng dụng mang lại cảm giác cao cấp, tự nhiên và thú vị.

  • Chúng tôi biết rằng những ứng dụng mang lại trải nghiệm tốt và luôn được người dùng đánh giá cao thường là những ứng dụng tận dụng các thay đổi mới nhất về API ở cấp hệ thống, có tính liên tục giữa các nền tảng với các phần tử dùng chung và hiệu ứng làm mờ, thích ứng linh hoạt trên nhiều kiểu dáng, có hiệu suất cao, không khiến người dùng phải phỏng đoán và khai thác tối đa tiềm năng của thiết bị.

  • Trong buổi trò chuyện hôm nay, chúng tôi sẽ giới thiệu một số tính năng mới nhất giúp bạn tận dụng Android, từ việc làm hài lòng người dùng bằng dải chuyển màu lưới, hỗ trợ gam màu rộng, chuyển đổi thú vị bằng hình đa giác, tính năng làm mờ mới và cuối cùng, chúng tôi sẽ đề cập đến cách bạn có thể nghĩ về hiệu suất khi thêm các tính năng đồ hoạ mới vào ứng dụng của mình.

  • Nhưng trong suốt buổi trò chuyện hôm nay, chúng ta sẽ đề cập đến một số API mới ở các cấp độ khác nhau của ngăn xếp.

  • Mặc dù bộ công cụ giao diện người dùng khung hiển thị cũ hiện đang ở chế độ bảo trì, nhưng chúng tôi vẫn tiếp tục đầu tư vào các công cụ kết xuất phần cứng đồ hoạ Android cơ bản hỗ trợ Jetpack Compose.

  • Bạn có thể coi đồ hoạ trong Android có 3 lớp: API Compose ở lớp đầu tiên, đây là nơi chứa tất cả API đồ hoạ giao diện người dùng mới, cấp cao, chẳng hạn như trình vẽ dải chuyển màu lưới, hình đa giác và API làm mờ tăng dần.

  • Sau đó, chúng ta có lớp trung gian, nơi DrawScope của Compose, các nút đối tượng sửa đổi vẽ và AGSL cho phép bạn tạo hiệu ứng tuỳ chỉnh.

  • Nhưng cuối cùng, chúng ta có nền tảng Android và Compose luôn có thể truy cập vào Canvas gốc và sử dụng các dữ liệu gốc kết xuất hệ điều hành hoàn toàn mới ngay trong đối tượng sửa đổi Compose.

  • Hôm nay, chúng tôi sẽ giới thiệu một số ví dụ về cách thực hiện việc này.

  • Hãy cùng tìm hiểu nhé.

Chuyển màu lưới và kết xuất lưới tuỳ chỉnh

  • Trước tiên, hãy cùng tìm hiểu về dải chuyển màu dạng lưới, một API mới được giới thiệu trong Compose 1.12.

  • Trước đây, chúng ta đã có dải chuyển màu tuyến tính, dải chuyển màu quét và dải chuyển màu toả tròn. Các dải chuyển màu này rất phù hợp cho các hiệu ứng chuyển đổi đơn giản, nhưng nếu muốn có các hiệu ứng pha trộn nhiều màu sắc phong phú như nền môi trường xung quanh hoặc hiệu ứng ánh sáng rực rỡ, bạn cần có hệ thống lưới 2D, nơi các vị trí và màu sắc có thể pha trộn trong không gian.

  • Bây giờ, để tạo một dải chuyển màu dạng lưới như thế này, trước tiên, hãy chỉ định số hàng và số cột của các mảng mà bạn muốn.

    val gradientPainter = remember {
    MeshGradientPainter(rows = 3, columns = 3) {
    }
    }
  • Sau đó, bên trong trailing lambda (lambda theo sau), hãy gọi setVertex và chỉ định một độ lệch chuẩn hoá từ 0F đến 1F, cùng với một màu cho mỗi giao điểm lưới.

    val gradientPainter = remember {
    MeshGradientPainter(rows = 3, columns = 3) {
       setVertex(0, 0, Offset(0.0f, 0.0f), indigo)
    
    }
    }
  • Sau đó, để tạo ảnh động cho các đỉnh đó, hãy thêm độ lệch ảnh động cho 8 đỉnh.

    val gradientPainter = remember {
    MeshGradientPainter(rows = 3, columns = 3) {
       setVertex(0, 0, Offset(0.0f, 0.0f), indigo)
         // ....
       setVertex(1, 1, Offset(0.2f, 0.4f) + Offset(animatedOffset, animatedOffset), coral)
       // ....
       setVertex(3, 0, Offset(0.0f, 1.0f), sunshine)
       // ....
    }
    }
    
    Box(
       modifier = modifier.padding(32.dp)
           .safeContentPadding()
           .aspectRatio(16 / 9f)
           .fillMaxWidth()
           .paint(gradientPainter)
    )
  • Sau khi xác định trình vẽ, bạn chỉ cần gọi trình vẽ đó bằng bất kỳ composable nào thông qua modifier vẽ.

  • Giờ đây, trong ứng dụng JetChat, chúng tôi đã thêm một tính năng để thích tin nhắn khi bạn nhấn đúp vào tin nhắn đó.

  • Để tạo lưới này, chúng ta thiết lập một lưới 2 x 3 và đặt các điểm dừng màu trên thành phần.

    MeshGradientPainter(
       rows = 2,
       columns = 3,
       hasBicubicColor = true,
    ) {
       // Row 0 (top edge, y = 0.0f)
       setVertex(0, 0, Offset(0.0000f, 0.0000f), Color(0xFFFF6B6B))
       setVertex(0, 1, Offset(0.3300f, 0.0000f), Color(0xFFFFD6D6))
       setVertex(0, 2, Offset(0.6700f, 0.0000f), Color(0xFFE8C3FF))
       setVertex(0, 3, Offset(1.0000f, 0.0000f), Color(0xFFE396FF))
    
       // ... remaining vertices for Rows 0, 1, and 2
    }
  • Lưu ý rằng chúng ta đặt hasBicubicColor thành true ở đây.

  • Theo mặc định, màu sắc nội suy tuyến tính trên các hình tam giác, đôi khi có thể để lại các mảng màu rõ ràng.

  • Khi bật chế độ này, chương trình đổ bóng sẽ chuyển sang nội suy hai khối mượt mà để các màu có thể hoà vào nhau một cách liền mạch.

  • Bây giờ, nếu bạn muốn tạo hiệu ứng phát sáng xung quanh như thế này phía sau hộp nhập thì sao?

  • Để tạo hiệu ứng như thế này bằng dải chuyển màu dạng lưới, bạn cần có hai yếu tố.

  • Đầu tiên là chuyển động mượt mà, tự nhiên cho các đỉnh bên trong và cách làm mềm các cạnh bên ngoài để không tạo ra hình chữ nhật cứng xung quanh thẻ.

  • Vì vậy, chúng ta bắt đầu bằng cách thiết lập một rememberInfiniteTransition với hai góc pha chạy trong thời lượng hơi khác nhau để chuyển động có cảm giác tự nhiên hơn là máy móc.

    val transition = rememberInfiniteTransition(label = "userInputGlowMesh")
    
    val phase1 by transition.animateFloat(
       initialValue = 0f,
       targetValue = (2 * PI).toFloat(),
       animationSpec = infiniteRepeatable(
           animation = tween(durationMillis = 2800, easing = LinearEasing),
           repeatMode = RepeatMode.Restart,
       ),
       label = "glowPhase1",
    )
    
    val phase2 by transition.animateFloat(
       initialValue = 0f,
       targetValue = (2 * PI).toFloat(),
       animationSpec = infiniteRepeatable(
           animation = tween(durationMillis = 4200, easing = LinearEasing),
           repeatMode = RepeatMode.Restart,
       ),
       label = "glowPhase2",
    )
  • Tại đây, chúng ta tạo ảnh động cho các đỉnh bên trong và ghim các vòng ngoài, vòng ngoài của các đỉnh có cạnh có alpha bằng 0.

  • Vì ranh giới bên ngoài cố định và trong suốt, nên ánh sáng sẽ mờ dần một cách tự nhiên.

  • Và cuối cùng, chúng ta sẽ cung cấp các độ lệch ảnh động đó cho MeshGradientPainter.

    MeshGradientPainter(
       rows = 6,
       columns = 5,
       hasBicubicColor = true,
    ) {
       for (r in 0..6) {
           for (c in 0..5) {
                   val offset = if (r in 1..5 && c in 1..4) {
                   val ampX = when (r) {
                       1, 2 -> 0.048f
                       3 -> 0.042f
                       4 -> 0.028f
                       else -> 0.036f
                   }
                   val ampY = when (r) {
                       1, 2 -> 0.028f
                       3 -> 0.020f
                       4 -> 0.022f
                       else -> 0.016f
                   }
                   val d = orbit(c, r, ampX, ampY)
                   Offset(
                       x = (baseU + d.x).coerceIn(0.04f, 0.96f),
                       y = (baseV + d.y).coerceIn(0.05f, 0.95f),
                   )
               } else {
                   Offset(baseU, baseV)
               }
               setVertex(r, c, offset, gridColors[r][c])
           }
       }
    }
  • Chuyển màu lưới mà bạn vừa thấy được vẽ bằng API drawMesh trên Android 14 trở lên.

  • Hãy cùng tìm hiểu kỹ hơn về API này bằng cách tạo một hiệu ứng từ đầu.

  • Đối với màn hình chào mừng của JetChat, chúng ta muốn vẽ một đường dẫn trông giống như một dải ruy băng.

  • Hãy lưu ý hai điều sau: chiều rộng nét vẽ không cố định và màu sắc chạy dọc theo từng nét vẽ của đường dẫn.

  • Hãy cùng phân tích và tìm hiểu những gì chúng ta thực sự cần để tạo hiệu ứng này.

  • Đối với mỗi pixel, chúng ta cần quyết định xem có vẽ pixel đó hay không và sử dụng màu nào.

  • Để tìm ra điều đó, chúng ta cần hai khoảng cách.

  • Giá trị đầu tiên là khoảng cách từ một pixel đến tâm của nét vẽ.

  • Khoảng cách thứ hai là khoảng cách của một pixel dọc theo đường dẫn, từ đầu đến cuối.

  • Điều này cho chúng ta biết nên chọn màu nào từ dải chuyển màu và việc kết hợp một chút giá trị đầu tiên sẽ làm cho màu cong theo chiều rộng.

  • Vậy chúng ta có thể vẽ một dải chuyển màu dọc theo một đường dẫn chỉ bằng đường dẫn và màu vẽ không?

  • Vậy nên, chúng tôi có thể làm được.

  • Điều này đòi hỏi phải làm phẳng một đường dẫn thành danh sách các đường thẳng ngắn, đồng thời cũng cung cấp khoảng cách dọc theo đường dẫn.

    val lines = remember {
            path.asAndroidPath().flatten(0.5f)
        }
    //... inside draw scope
    lines.forEach { line ->
    //...
        drawLine(
            brush = Brush.linearGradient(...),
            start = Offset(line.start.x,
    line.start.y),
            end = Offset(line.end.x, line.end.y),
            strokeWidth = 30f,
        )
    //...
    }
  • Sau đó, chúng ta vẽ từng đường bằng dải chuyển màu riêng.

  • Cách này có hiệu quả, nhưng mỗi dòng cần một hàm gọi vẽ riêng và một đối tượng vẽ, đồng thời toàn bộ đường dẫn vẫn có một chiều rộng duy nhất.

  • Lựa chọn còn lại là chương trình đổ bóng trong thời gian chạy, nhưng lựa chọn này chỉ lấy vị trí của pixel trong giới hạn vùng chứa.

  • Nó không cho chúng ta biết những gì cần thiết để vẽ dải ruy băng này.

  • Và chúng ta cũng không thể dễ dàng tính toán từ vị trí của pixel.

  • Draw mesh cung cấp cho chúng ta chương trình đổ bóng đỉnh cho việc đó.

  • Vậy lưới là gì?

  • Lưới được tạo thành từ các hình tam giác có dữ liệu ở mỗi góc và mỗi góc được gọi là một đỉnh.

  • Mỗi đỉnh mang một bản ghi dữ liệu nhỏ.

  • Thông thường, bản ghi đó giữ một vị trí, nhưng không nhất thiết phải như vậy.

  • Trình đổ bóng đỉnh đọc bản ghi đó và tính toán vị trí từ bản ghi đó.

  • Đây là đường dẫn của một hình tam giác: Chương trình đổ bóng đỉnh chạy cho mỗi góc, tính toán vị trí của các góc và mọi giá trị mà chúng ta muốn truyền cho mỗi pixel.

  • Sau đó, trình tạo điểm ảnh sẽ tìm mọi điểm ảnh được tam giác đó bao phủ và nội suy các giá trị từ ba góc đó cho mỗi điểm ảnh.

  • Cuối cùng, chương trình đổ bóng mảnh sẽ chạy cho các pixel đó, sử dụng các giá trị nội suy đó và tính toán màu của pixel.

  • Hãy cùng xem cách viết chương trình đổ bóng đỉnh và chương trình đổ bóng mảnh bằng một ví dụ đơn giản hơn, một đường thẳng có xung di chuyển dọc theo đường thẳng đó.

    LineShaders.VERTEX
    
    uniform float2 uStart;
    uniform float2 uEnd;
    uniform float uHalfWidth;
    uniform float uTime;
    
    Varyings main(const Attributes a) {
        Varyings v;
        float2 dir = uEnd - uStart;
        float2 normal = normalize(float2(-dir.y, dir.x));
        float along = a.corner.x * length(dir);
        float pulse = (a.corner.x - uTime) / 0.18;
        float radius = uHalfWidth * (1.0 + 0.6 * exp(-pulse * pulse));
        float across = a.corner.y * (radius + 1.0);
        v.position = uStart + dir * a.corner.x + normal * across;
        v.stroke = float3(along, across, radius);
        return v;
    }
  • Trình đổ bóng đỉnh lấy một thuộc tính làm dữ liệu đầu vào và trả về một biến.

  • Thuộc tính là dữ liệu cho một góc.

  • Sau đây là hai con số: tiến trình dọc theo đường thẳng và phía của đường thẳng.

  • Sau đó, các biến thiên được nội suy và chuyển đến chương trình đổ bóng mảnh.

  • Chúng ta tính toán khoảng cách pixel từ điểm bắt đầu và đối với chiều rộng nét vẽ, chúng ta sử dụng thời gian để tạo xung trên bán kính.

  • Sau đó, chúng ta nhân bán kính đó với cạnh góc, từ đó có được khoảng cách dấu trên nét vẽ.

  • Khi GPU nội suy giữa các dấu đó, các đường trung tâm sẽ trở thành 0.

  • Cuối cùng, chúng ta tính toán vị trí của các góc, lưu trữ tất cả giá trị này vào một biến thể nét vẽ và truyền các giá trị đó đến chương trình đổ bóng mảnh.

  • Không giống như chương trình đổ bóng thời gian chạy, chương trình đổ bóng mảnh lưới xuất ra màu thông qua một tham số và trả về một vị trí để lấy mẫu màu nếu có.

    LineShaders.FRAGMENT
    
    uniform float uLength;
    layout(color) uniform float4 uStartColor;
    layout(color) uniform float4 uEndColor;
    
    float2 main(const Varyings v, out float4 color) {
        float distance = abs(v.stroke.y);
        float coverage = 1.0 - smoothstep(
        v.stroke.z - 0.5, v.stroke.z + 0.5, distance);
        float side = distance / v.stroke.z;
        float4 ink = mix(uStartColor, uEndColor,
            clamp(v.stroke.x / uLength + 0.4 * side * side, 0.0, 1.0));
        color = ink * coverage;
        return v.position;
    }
  • Lấy giá trị tuyệt đối của toạ độ ngang sẽ cho chúng ta khoảng cách pixel theo pixel từ đường trung tâm.

  • Chúng ta so sánh khoảng cách đó với bán kính để có được phạm vi bao phủ. Đối với màu sắc, chúng ta kết hợp dọc theo đường dẫn của đường thẳng và uốn cong dải chuyển màu một chút về phía cạnh.

  • Nhân mức độ bao phủ đó với mức độ bao phủ, đó là màu cuối cùng của chúng ta.

  • Về phía Compose, chúng ta liên kết chương trình đổ bóng và bố cục dữ liệu với đối tượng MeshSpecification.

    val spec = remember {
        MeshSpecification.make(
            arrayOf(Attribute(TYPE_FLOAT2, 0, "corner")),
            vertexStride = 8,
            arrayOf(Varying(TYPE_FLOAT3, "stroke")),
            LineShaders.VERTEX, LineShaders.FRAGMENT)
    }
  • Trước tiên, chúng ta khai báo thuộc tính góc dưới dạng hai số thực bắt đầu từ byte 0.

  • Vì hai số thực dấu phẩy động chiếm 8 byte, nên mỗi sải góc trong vùng đệm là 8.

  • Tiếp theo, chúng ta khai báo các biến thiên nét vẽ dưới dạng ba số thực và truyền vào chương trình đổ bóng đỉnh và mảnh mà chúng ta vừa viết.

  • Chúng ta tạo vùng đệm đỉnh.

    fun lineCorners(segments: Int): ByteBuffer {
        val buffer = ByteBuffer.allocateDirect((segments + 1) * 2 * 8)
            .order(ByteOrder.nativeOrder())
        for (i in 0..segments) {
            val t = i / segments.toFloat()
            buffer.putFloat(t).putFloat(-1f) // one side
            buffer.putFloat(t).putFloat(1f)  // the other side
        }
        return buffer.rewind() as ByteBuffer
    }
  • Tại mỗi bước dọc theo đường, chúng ta hoàn thành tiến trình, tính toán tiến trình từ 0 đến 1 và ghi hai góc vào vùng đệm.

  • Cuối cùng, chúng ta kết hợp thông số kỹ thuật và vùng đệm vào một đối tượng lưới.

    val mesh = Mesh(
        spec,
        mode = Mesh.TRIANGLE_STRIP,
        vertexBuffer = corners,
        vertexCount = (segments + 1) * 2,
        bounds = RectF(0f, 0f, size.width, size.height)
    )
  • Canvas của Compose chưa hiển thị drawMesh, vì vậy, chúng ta sẽ chuyển xuống nativeCanvas và vẽ lưới.

    Box(Modifier.fillMaxSize().drawWithCache {
       val mesh = Mesh(...)
       onDrawBehind {
           mesh.setFloatUniform("uTime", time.value)
           drawIntoCanvas {
              it.nativeCanvas.drawMesh(mesh,android.graphics.BlendMode.DST, paint)
           }
       }
    })
  • Vì kiểm soát hình học và pixel, nên bạn có thể vẽ bất kỳ hiệu ứng hình ảnh nào dựa trên hình học bằng cách sử dụng lưới vẽ.

  • Tìm hiểu thêm về API này trong tài liệu.

Hỗ trợ gam màu rộng

  • Trong Compose 1.12, chúng tôi đã ra mắt tính năng hỗ trợ gam màu rộng.

  • Vậy điều này thực sự có nghĩa như thế nào?

  • Bạn có biết rằng màn hình có thể tái tạo dải màu khác với không gian màu sắc sRGB truyền thống không?

  • Ví dụ: nếu nhìn vào hình ảnh này, bạn sẽ thấy hình tam giác màu trắng là dải màu sRGB tiêu chuẩn mà hầu hết các màn hình đều có thể hiển thị.

  • Tuy nhiên, với các màn hình mới hơn, một số màn hình thực sự hỗ trợ dải màu rộng hơn, như được mô tả bằng hình tam giác màu vàng, một số màn hình có độ bão hoà và rực rỡ hơn.

  • Nhưng khi biểu thị một màu, chúng ta thực sự biểu thị màu đó bằng nội dung mô tả của từng đỉnh của tam giác đó.

  • Khi xem xét phép so sánh dải chuyển màu này, bạn cần phân biệt giữa gam màu hiển thị và phép nội suy dải chuyển màu.

  • Trên màn hình sRGB tiêu chuẩn hoặc có thể là máy chiếu này, màn hình không thể hiển thị màu sắc bên ngoài gam màu sRGB này.

  • Vì vậy, việc minh hoạ cho bạn ngay trên sân khấu lúc này thực sự rất khó.

  • Vì vậy, bạn phải tin tôi rằng màu sắc trông đẹp hơn trên Android khi có hỗ trợ gam màu rộng.

  • Trong ví dụ này, những gì bạn thực sự thấy ở đây là hiệu ứng nội suy các điểm dừng màu trong không gian màu sắc rộng hơn.

  • Việc pha trộn màu sắc trong không gian màu sắc rộng hoặc tuyến tính sẽ thay đổi đường dẫn toán học thực tế của dải chuyển màu này, ngăn chặn hiện tượng xám giữa và dải màu gắt có thể xảy ra khi bạn thực hiện mà không có gam màu rộng.

  • Với Compose 1.12, bạn sẽ được hưởng lợi từ việc pha trộn dải chuyển màu mượt mà hơn trên mọi màn hình, cũng như hỗ trợ dải màu rộng thực sự khi chạy trên phần cứng được hỗ trợ.

  • Để bật tính năng hỗ trợ chính xác trên thiết bị, bạn nên đảm bảo hoạt động hoặc cửa sổ của mình được đặt để bật chế độ gam màu rộng.

  • Bạn có thể thực hiện việc này trong AndroidManifest.

    <!-- AndroidManifest.xml -->
    <activity
    android:name=".NavActivity"
    android:colorMode="wideColorGamut"
    android:exported="true">
    </activity>
  • Hoặc theo phương thức lập trình trong thành phần kết hợp.

    // Dynamic toggle in code
    DisposableEffect(window) {
       window?.colorMode = ActivityInfo.COLOR_MODE_WIDE_COLOR_GAMUT
       onDispose {
           window?.colorMode = ActivityInfo.COLOR_MODE_DEFAULT
       }
    }
  • Nếu không đặt colorMode, cửa sổ sẽ vẫn ở chế độ sRGB 8 bit tiêu chuẩn và mọi màu gam màu rộng do GPU kết xuất sẽ bị cắt bớt.

  • Vì vậy, bạn không nên sử dụng màu gam màu rộng nếu màn hình thực sự không hỗ trợ.

  • Bạn chỉ nên bật gam màu rộng cho các hoạt động hoặc đường dẫn mã thực sự hiển thị nội dung nghe nhìn, ảnh hoặc đồ hoạ đa dạng để tiết kiệm pin và hiệu suất.

  • Điều này có nghĩa là bạn có thể tạo màu trong các không gian màu sắc khác nhau trong Compose và các màu này hiện được tuân thủ.

    // 1. Display P3 Color (Wider gamut: greens & reds are more saturated) colorMode=wideColorGamut
    val VividP3Red = Color(
       red = 1.0f,
       green = 0.05f,
       blue = 0.05f,
       alpha = 1.0f,
       colorSpace = ColorSpaces.DisplayP3
    )
    // 2. Extended sRGB (Enables HDR values > 1.0f) use colorMode=hdr
    val HdrNeonCyan = Color(
       red = 0.0f,
       green = 1.8f,  // Peak brightness > 1.0 in extended gamut
       blue = 2.0f,
       alpha = 1.0f,
       colorSpace = ColorSpaces.ExtendedSrgb
    )
    // 3. BT.2020 HLG (Broadcast HDR standard, supported API 34+) - use colorMode=hdr
    val HdrSunsetOrange = Color(
       red = 1.0f,
       green = 0.45f,
       blue = 0.1f,
       alpha = 1.0f,
       colorSpace = ColorSpaces.Bt2020Hlg
    )
  • Bạn có thể truyền vào loại colorSpace mà bạn muốn tạo màu và nếu bạn không truyền vào bất kỳ colorSpace nào, thì sRGB tiêu chuẩn sẽ được sử dụng.

  • Cụ thể đối với Bitmap, bạn có thể kiểm tra nguồn của Bitmap bằng cách sử dụng getColorSpace và các thư viện tải hình ảnh phổ biến sẽ tự động xử lý không gian màu sắc Bitmap chính xác cho bạn.

  • Bạn nên sử dụng Bitmap.Config.HARDWARE để giữ nguyên thông tin màu sắc, vì cấu hình như 565 sẽ thực sự làm mất độ chính xác và có dải màu hiển thị cho người dùng.

  • Để tìm hiểu thêm về màu sắc, bạn có thể quay lại năm 2017 để xem Romain Guy nói về màu sắc trong Compose hoặc về màu sắc trong Android.

Hình dạng đa giác và biến đổi

  • Trong Compose 1.13, chúng tôi sẽ ra mắt tính năng hỗ trợ gốc cho hình đa giác.

  • Điều này sẽ giúp việc tạo hình dạng tuỳ chỉnh và các phần tử trên giao diện người dùng trông tự nhiên trở nên dễ dàng hơn nhiều.

  • Trước đây, nếu muốn vẽ một hình học phức tạp như thế này, bạn phải sử dụng RoundedPolygon để mô tả một hình học sẽ xuất ra một đường dẫn, sau đó liên kết đường dẫn đó với một API hình dạng của Compose.

    fun cookiePolygon(): RoundedPolygon {
        val vertices = FloatArray(32)
        for (i in 0 until 16) {
            val radius = if (i % 2 == 0) 1f else 0.84f
    // …
        }
        return RoundedPolygon(
            vertices,
            rounding = CornerRounding(0.12f)
        )
    }
  • Ví dụ: trước tiên, bạn xác định tỷ lệ bằng RoundedPolygon và chúng tôi đặt Trong đoạn mã này, chúng tôi đặt các điểm bên trong ở mức 84% so với tâm so với các điểm bên ngoài.

  • Bạn cũng có thể áp dụng góc bo tròn tại đây, nhưng thao tác này chỉ mô tả các đường cong toán học.

  • Để thực sự sử dụng đa giác này cho việc cắt hoặc nền trong Compose, bạn cần viết một trình bao bọc Hình dạng tuỳ chỉnh như sau.

    class RoundedPolygonShape(
     polygon: RoundedPolygonShape
    ) : Shape {
        // …
        override fun createOutline(size: Size, layoutDirection: LayoutDirection,
        density: Density,
        ): Outline {
            path = polygon.toPath().asComposePath()
            // …
            matrix.scale(size.width / maxDimension, size.height / maxDimension)
            matrix.translate(-bounds.left, -bounds.top)
            // …
            return Outline.Generic(path)
        }
    }
  • Và bên trong đoạn mã createOutline này, bạn có thể thấy rằng bạn phải tạo đường dẫn, đồng thời tự điều chỉnh tỷ lệ và dịch chuyển chúng theo cách thủ công.

  • Giờ đây, với API PolygonShape mới, bạn không cần trình bao bọc Shape tuỳ chỉnh đó nữa.

    private val AvatarShape = PolygonShape {
        val vertices = List(16) { i ->
            val radius = size.minDimension / 2f * if (i % 2 == 0) 1f else 0.86f
            val angle = i * PI.toFloat() / 8f
            size.center + Offset(cos(angle), sin(angle)) * radius
        }
        polygon(vertices, rounding = CornerRounding(radius = 8.dp))
    }
    
        // …
    Modifier.clip(AvatarShape)
  • Vì đây là trình tạo Hình dạng chuyên dụng của Compose, nên trình tạo này sẽ nhận trực tiếp mật độ và kích thước ngay từ đầu.

  • Tức là bạn có thể tính toán các đỉnh một cách linh động và chỉ định góc bo tròn trực tiếp bằng dp hoặc thậm chí là theo tỷ lệ phần trăm.

  • Sau khi xác định, bạn chỉ cần truyền hình dạng đó đến bất kỳ API nào nhận Hình dạng.

  • Chúng tôi cũng giới thiệu API MorphPolygonShape, cho phép bạn chuyển đổi liền mạch giữa các hình đa giác khác nhau.

    val recordShape = remember {
        MorphPolygonShape(
            start = PolygonShape.circle(),
            end = PolygonShape.star(
                numPoints = 9,
                innerRadiusRatio = 0.8f,
                outerRounding = CornerRounding(percent = 50),
            ),
        ) { shapeProgress.value }
    }
  • Trong ví dụ này, chúng ta có một nút ghi hình biến đổi từ hình tròn thành hình ngôi sao khi bạn tương tác với nút đó.

  • Bạn chỉ cần cung cấp hình dạng bắt đầu, hình dạng kết thúc và giá trị tiến trình.

  • API MorphPolygonShape đọc giá trị tiến trình trong giai đoạn phân giải đường viền, tức là không phải trong quá trình kết hợp lại, không phải trong quá trình kết hợp.

  • Điều này có nghĩa là khi hình dạng của bạn biến đổi giữa từng khung hình, hình dạng đó chỉ đơn giản là phân giải thành một đường viền mới mà không bao giờ kích hoạt quá trình kết hợp lại.

Hiệu ứng làm mờ và kết xuất phông nền

  • Tiếp theo, hãy nói về tính năng làm mờ.

  • Trong vài bản phát hành vừa qua, chúng tôi đang cố gắng giúp việc triển khai hiệu ứng làm mờ trở nên dễ dàng và biểu cảm hơn.

  • Bất cứ khi nào chúng ta nói về hiệu ứng làm mờ, thì đây là đối tượng sửa đổi thường dùng mà chúng ta thấy.

    Modifier
       .blur(
           radiusX = 10.dp,
           radiusY = 10.dp,
           edgeTreatment = BlurredEdgeTreatment(
               shape = RoundedCornerShape(8.dp),
           ),
       )
  • Compose đã có đối tượng sửa đổi này kể từ Android 12.

  • Hiệu ứng này hoạt động hiệu quả khi bạn muốn có hiệu ứng làm mờ đồng nhất, trong đó bán kính làm mờ chính xác được áp dụng trong toàn bộ thành phần kết hợp.

  • Tuy nhiên, các thiết kế giao diện người dùng hiện đại thường yêu cầu chuyển đổi mượt mà từ vùng sắc nét sang vùng mờ thay vì cắt đột ngột.

  • Và điều đó đưa chúng ta đến với tính năng Làm mờ tăng tiến, hiện là một API chính thức bắt đầu từ Compose 1.13.

  • Thay vì một bán kính không đổi duy nhất trên toàn bộ composable, giờ đây, bạn có thể thay đổi cường độ làm mờ trên toàn bộ không gian.

  • Dưới đây là ví dụ về màn hình hồ sơ người dùng trong ứng dụng JetChat.

  • Đối tượng sửa đổi làm mờ mới sẽ lấy thông số bán kính làm mờ.

    Modifier.blur {
    blurRadiusSpec = BlurRadiusSpec.verticalGradient(
    listOf(
    BlurStop(0.5f, 0.dp),
    BlurStop(1.0f, 32.dp),
    )
    )
    edgeTreatment = BlurredEdgeTreatment.Unbounded
    }
  • Ở đây, chúng ta sử dụng dải chuyển màu dọc với hai điểm dừng làm mờ, giữ cho nửa trên của ảnh hoàn toàn sắc nét ở mức 0 dp và tăng dần lên mức làm mờ 32 dp ở cạnh dưới.

  • Lưu ý: chúng ta cũng đặt edgeTreatment là không giới hạn, về cơ bản là yêu cầu làm mờ để lấy mẫu pixel trong suốt bên ngoài giới hạn của composable.

  • Ngoài hiệu ứng mờ dần theo chiều dọc, thông số kỹ thuật về bán kính làm mờ này còn cung cấp cho bạn một số cách tích hợp sẵn để làm mờ dần.

  • Ví dụ: ở bên trái, bạn có thể thấy dải chuyển màu ngang, trong đó bán kính bắt đầu là 0 dp và bán kính kết thúc được điều khiển bằng giá trị thanh trượt.

    BlurRadiusSpec.horizontalGradient(
    startRadius = 0.dp,
    endRadius = sliderValue.dp,
    )
  • Còn ở bên phải, chúng ta có một dải chuyển màu toả tròn, trong đó ở giữa, bán kính làm mờ là 0 dp và khi bạn di chuyển về phía fallOffRadius (trong trường hợp này là 300 dp), độ mờ sẽ tăng dần lên đến 24 dp.

    BlurRadiusSpec.radialGradient(
    center = center,
    fallOffRadius = 300.dp,
    startRadius = 0.dp,
    endRadius = 24.dp,
    )
  • Và nếu tất cả các dải chuyển màu dọc, dải chuyển màu tròn và dải chuyển màu ngang này vẫn chưa đủ, thì bạn luôn có thể tạo hiệu ứng làm mờ tăng dần bằng cách sử dụng chương trình đổ bóng tuỳ chỉnh.

    val customShader = remember {
       RuntimeShader(
           """
           uniform float2 size;
           uniform float time;
    
           half4 main(float2 coords) {
              // Diagonal wave mask
              float normalizedX = coords.x / size.x;
              float normalizedY = coords.y / size.y;
              float wave = sin((normalizedX + normalizedY) * 6.0 + time) * 0.5 + 0.5;
              return half4(0.0, 0.0, 0.0, wave);
           }
           """.trimIndent()
       )
    }
    
    val customBlurRadius = remember(customShader) {
       BlurRadiusSpec.shader(maxRadius = 24.dp) { customShader }
    }
    
    Modifier.blur {
           val w = size.width.toPx()
           val h = size.height.toPx()
           customShader.setFloatUniform("size", w, h)
           customShader.setFloatUniform("time", animatedTime)
           blurRadiusSpec = customBlurRadius
       },
  • Tại đây, chúng ta viết một chương trình đổ bóng mảnh nhỏ để tính toán sóng sin đường chéo động và xuất cường độ đó vào kênh alpha.

  • Sau đó, thông số kỹ thuật của chương trình đổ bóng sẽ liên kết mặt nạ từ 0 đến 1 đó trực tiếp với mặt nạ 24 dp maxRadius.

  • Bây giờ, trước khi nói về hiệu ứng làm mờ nền, hãy cùng xem sự khác biệt giữa hai hiệu ứng này.

  • Bây giờ, ở bên trái, chúng ta có một thanh ứng dụng trên cùng, nơi chúng ta áp dụng hiệu ứng làm mờ lớp.

  • Tức là làm mờ chính thành phần kết hợp.

  • Như bạn có thể thấy, tiêu đề và biểu tượng hành động bị mờ, nhưng thành phần trò chuyện phía sau không bị mờ chút nào.

  • Ở bên phải, chúng ta có tính năng làm mờ nền.

  • Tại đây, chỉ những tin nhắn trò chuyện cuộn phía sau thanh trên cùng mới được lấy mẫu và làm mờ.

  • Tuy nhiên, văn bản và biểu tượng hoàn toàn sắc nét và dễ đọc.

  • Kể từ Android 37.2, bạn có thể làm mờ nền bằng một API mới có tên là RenderNode.setBackdropRenderEffect.

  • Nhưng trước khi tìm hiểu sâu về cách cải thiện hiệu ứng làm mờ, hãy cùng tìm hiểu cách Android hoạt động để đảm bảo hiệu ứng làm mờ hoạt động hiệu quả.

  • Android sử dụng RenderNodes, là các vùng chứa danh sách hiển thị được tăng tốc phần cứng, có nghĩa là có thể thu gọn và lưu trữ một loạt lệnh vẽ khác nhau thông qua lệnh bắt đầu ghi và kết thúc ghi.

  • RenderNode lưu giữ thông tin như bản dịch 3D, chế độ xoay, alpha, tỷ lệ và giới hạn vị trí.

  • Và Compose thực sự sử dụng RenderNode trong phần nâng cao.

  • Vì vậy, để sử dụng hiệu ứng kết xuất phông nền trong Compose, chúng ta có thể tạo một Modifier.Node tuỳ chỉnh để có thể sử dụng lại đối tượng sửa đổi này.

    fun Modifier.backdropBlur(spec: BlurRadiusSpec, shape: Shape, tint: Color): Modifier =
       this then BackdropBlurElement(spec, shape, tint)
    
    private class BackdropBlurNode(...) : Modifier.Node(), DrawModifierNode {
       private var renderNode: RenderNode? = null
    
       override fun ContentDrawScope.draw() {
           val node = getOrCreateNode()
           if (isDirty || sizeChanged) {
               node.setPosition(0, 0, widthPx, heightPx)
               node.setBackdropRenderEffect(spec.createRenderEffect(this))
           }
           drawIntoCanvas { it.nativeCanvas.drawRenderNode(node) }
           drawContent()
       }
    }
  • Chúng ta giữ lại tệp đối chiếu với RenderNode và RenderNode được định cấu hình bằng các hiệu ứng mà chúng ta muốn trong một hàm khác.

  • Ví dụ: chúng ta truyền vào hiệu ứng làm mờ ở đây.

  • Tiếp theo, chúng ta gọi drawIntoCanvas, gọi drawRenderNode trên canvas gốc.

  • Cuối cùng, chúng ta gọi drawContent để vẽ nội dung thực tế không bị làm mờ ở trên cùng.

  • Để sử dụng đối tượng sửa đổi vừa tạo, chúng ta gọi Modifier.backdropBlur và truyền vào thông số bán kính làm mờ.

    JetchatAppBar(
        modifier = modifier
            .backdropBlur(
                tint = MaterialTheme.colorScheme
                    .surface.copy(alpha = 0.5f),
                shape = RectangleShape,
                spec = BlurRadiusSpec.uniform(5.dp)
            )
    )
  • Xin lưu ý rằng chúng ta không cần đặt bất kỳ nội dung nào vào lớp cơ bản ở đây bằng phương pháp này.

  • RenderNode.setBackdropRenderEffect tự động lấy mẫu nội dung nằm bên dưới bề mặt của thanh ứng dụng JetChat.

  • Và kết quả là hiệu ứng làm mờ phông nền tuyệt đẹp này.

  • Chúng ta cũng có thể đặt thông số bán kính làm mờ tăng dần thay vì chỉ đặt thông số tăng dần và để làm mờ nội dung theo chiều dọc.

    JetchatAppBar(
        modifier = modifier
            .backdropBlur(
                tint = MaterialTheme.colorScheme
                    .surface.copy(alpha = 0.5f),
                shape = RectangleShape,
                spec = BlurRadiusSpec.verticalGradient(
                    listOf(
                        BlurStop(0.5f, 32.dp),
                        BlurStop(1f, 0.dp),
                    ),
                )
            )
    )
  • Với dải chuyển màu dọc, nội dung bên dưới sẽ được làm mờ bằng các bán kính khác nhau ở các phần khác nhau của vùng chứa.

  • Chúng tôi đang nỗ lực xây dựng một công cụ sửa đổi làm mờ phông nền tích hợp sẵn trong Compose.

  • Và đây chỉ là một ví dụ về công nghệ cơ bản trên Android có Compose.

  • Ví dụ mà tôi vừa cho bạn xem cũng chỉ hoạt động trên cấp độ API 37.2, nhưng chúng tôi cũng đang nỗ lực điều chỉnh cho phiên bản cũ cho Compose, vì vậy hãy chú ý theo dõi.

  • Một ví dụ thú vị khác về hiệu ứng phông nền RenderNode là chúng ta cũng có thể liên kết các hiệu ứng này với nhau để tạo thành giao diện người dùng thực sự đẹp mắt và thực hiện nhiều hiệu ứng khác nhau trên đó.

  • Vì vậy, để tạo chuỗi hiệu ứng, chúng ta sẽ tạo hai hiệu ứng này.

    val blur = RenderEffect.createBlurEffect(24f, 24f, Shader.TileMode.CLAMP)
    
    // Boost saturation by 30% and apply blur together
    val matrix = ColorMatrix().apply { setSaturation(1.3f) }
    val vibrancy = RenderEffect.createColorFilterEffect(ColorMatrixColorFilter(matrix))
    val vibrantBlur = RenderEffect.createChainEffect(vibrancy, blur)
  • Trong ví dụ này, chúng ta tạo hiệu ứng làm mờ và độ bão hoà 30%.

  • Chúng ta gọi createChainEffect với hiệu ứng làm mờ được áp dụng trước, sau đó là độ bão hoà.

  • Đây là một ví dụ trong JetChat.

    val agslShader = RuntimeShader(CHROMATIC_ABERRATION)
    val noiseEffect = RenderEffect.createRuntimeShaderEffect(agslShader, "backdropInput")
    
    val finalEffect = RenderEffect.createChainEffect(noiseEffect, vibrantBlur)
    renderNode?.setBackdropRenderEffect(finalEffect)
  • Nút chuyển đến cuối trang. Chúng ta có thể làm cho nút này trông giống như một chiếc kính lúp nhỏ bằng cách kết hợp một vài hiệu ứng khác nhau để làm cho các cạnh của thành phần kết hợp này tròn ra như kính lúp.

  • Tuy nhiên, bạn không chỉ có thể áp dụng hiệu ứng phóng đại mà còn có thể áp dụng bất kỳ loại chương trình đổ bóng nào được liên kết với nhau để áp dụng cho phông nền.

  • Sau đây là một số ví dụ thú vị mà chúng tôi đã triển khai về những việc có thể làm khi kết hợp các tính năng này với nhau.

  • Giờ đây, bạn có thể sử dụng hiệu ứng kiểu kính mờ truyền thống.

  • Nhưng chúng ta cũng có thể tạo các hiệu ứng thú vị khác, chẳng hạn như kính vạn hoa.

  • Hoặc nếu muốn biến linh vật Android này thành một loại phần tử trên giao diện người dùng biến đổi, giúp biến đổi nội dung bên dưới, chúng ta cũng có thể áp dụng các hiệu ứng kết xuất phông nền này để tạo cảm giác như chúng ta đang nhìn qua chú robot nhỏ này.

  • Và chúng ta cũng có thể làm điều này bằng Hiệu ứng kết xuất phông nền.

  • Vì vậy, không chỉ hiệu ứng làm mờ phông nền và kính mờ được mở khoá bằng API mới này, mà giờ đây, giới hạn duy nhất là trí tưởng tượng của bạn.

  • Một API mới khác mà chúng tôi đã giới thiệu vào nền tảng là SurfaceView.setBlurRegions.

  • Hiện tại, bạn có thể làm mờ các khung hiển thị kết cấu và mọi composable bằng cách sử dụng Hiệu ứng phông nền Modifier.blur và RenderNode, nhưng việc làm mờ các phần của SurfaceView là một việc khó khăn và không thuộc cây RenderNode của khung hiển thị ứng dụng, vì thực tế việc này sẽ tạo ra một lỗ hổng trên cửa sổ.

  • Vì vậy, Compose không thể thực sự lấy mẫu vùng đệm sao lưu của SurfaceView, khiến việc này không thể thực hiện được cho đến API 37.2.

  • Vì vậy, setBlurRegions của SurfaceView cho phép ứng dụng khai báo trực tiếp các vùng làm mờ.

  • Ví dụ: trên trình phát video này trong JetChat, chúng tôi đã thêm hiệu ứng làm mờ SurfaceView khi các nút điều khiển hiện và ẩn.

  • Để sử dụng API mới này, bạn có thể truy cập vào setBlurRegions trên một phương thức SurfaceView.

    val region = RoundedRectBlurRegion().apply {
       bounds = RectF(left, top, right, bottom)
       setCornerRadii(spec.cornerRadiusPx.coerceAtLeast(0f))
       alpha = spec.alpha.coerceIn(0f, 1f)
       blurRadius = spec.blurRadiusPx.coerceAtLeast(0f)
    }
    
    surfaceView.setBlurRegions(listOf(region))
    
    surfaceView.setBlurRegions(emptyList()) // Clear
  • Thao tác này sẽ lấy danh sách các vùng ranh giới, bán kính làm mờ, alpha và danh sách bán kính góc trên đó.

  • Khi truyền các ranh giới này, chúng phải tương ứng với SurfaceView trên màn hình.

  • Vì vậy, bạn nên theo dõi toạ độ của SurfaceView và vùng mà bạn muốn làm mờ.

RuntimeColorFilter

  • Một tính năng mới khác trên Android 16 trở lên là RuntimeColorFilter.

  • Trước đây, với hình ảnh, bạn chỉ có thể đặt bộ lọc màu ma trận tĩnh và tạo các hiệu ứng như độ bão hoà.

  • Tuy nhiên, với Android 16 trở lên, giờ đây bạn có thể truyền vào một chương trình đổ bóng lấy màu đầu vào, pixel đầu vào cho nội dung cụ thể đó và tạo ra một màu mới.

  • Ví dụ: nếu muốn áp dụng bộ lọc cyberpunk cho ảnh, chúng ta có thể xác định một chương trình đổ bóng nhỏ được dùng làm dữ liệu đầu vào cho RuntimeColorFilter.

    @Language("AGSL")
    private const val CYBERPUNK_SHADER = """
        vec4 main(half4 in_color) {
            float lum = dot(in_color.rgb, vec3(0.299, 0.587, 0.114));
            vec3 shadow = vec3(0.06, 0.04, 0.25);
            vec3 mid = vec3(0.92, 0.12, 0.58);
            vec3 high = vec3(0.18, 0.94, 0.98);
            vec3 col = lum < 0.5 ? mix(shadow, mid, lum * 2.0) : mix(mid, high, (lum - 0.5) * 2.0);
            return vec4(mix(in_color.rgb, col, 0.7), in_color.a);
        }
    """
  • Chương trình đổ bóng này tạo ra một chủ đề cyberpunk, nhưng bạn có thể tạo bất kỳ hiệu ứng nào khác mà bạn muốn.

  • Vì vậy, để sử dụng chương trình đổ bóng, chúng ta sẽ áp dụng RuntimeColorFilter và chuyển đổi thành bộ lọc màu của Compose.

    AsyncImage(
        model = imageUri,
        contentDescription = filter.displayName,
        colorFilter = RuntimeColorFilter(CYBERPUNK_SHADER)
                       .asComposeColorFilter(),
        contentScale = ContentScale.Crop,
        modifier = Modifier.fillMaxSize(),
    )
  • Như bạn có thể thấy trong ví dụ này, giờ đây, chúng ta có thể tạo nhiều hiệu ứng động hơn trước mà không cần tìm hiểu sâu hơn về OpenGL, chế độ thiết lập SurfaceView hoặc bất kỳ nội dung nào tương tự.

  • Và bạn cũng có thể áp dụng cách này cho bất kỳ thành phần kết hợp nào, không chỉ hình ảnh.

Tối ưu hoá hiệu suất đồ hoạ

  • Cảm ơn bạn.

  • Chúng ta đã tạo được một số hiệu ứng rất đẹp, nhưng hãy đảm bảo ứng dụng vẫn hoạt động nhanh và mượt mà.

  • Khi chúng ta di chuyển qua cuộc trò chuyện, tất cả hiệu ứng sẽ chạy cùng nhau.

  • Hãy bắt đầu bằng cách đo lường thao tác cuộn này.

    @Test
    fun scrollFling() = measureScroll(swipeSteps = 5)
    
    private fun measureScroll(swipeSteps: Int) = rule.measureRepeated(
        packageName = TARGET_PACKAGE,
        metrics = defaultComposeScrollingMetrics(),
        compilationMode = CompilationMode.Full(),
        iterations = 5,
        setupBlock = {
            killProcess()
        }
    //...
  • Chúng tôi sử dụng một điểm chuẩn vĩ mô để khởi chạy ứng dụng trong một quy trình mới và chạy thao tác cuộn qua cuộc trò chuyện.

  • Sau đó, chúng tôi truy vấn dấu vết để đếm số khung hình bị lỡ thời hạn.

    select count(*) as late_frames
    from actual_frame_timeline_slice
    where upid = (select upid from process where name = 'com.example.compose.jetchat.perf')
      and jank_type like '%App Deadline Missed%'
  • Như vậy, chúng ta có 6 khung hình bị trễ.

  • Hãy mở một trong các khung hình bị trễ.

  • Vì vậy, ngay trước khung hình này, Compose sẽ tìm nạp trước tin nhắn tiếp theo trước khi cuộn lên vào khung hiển thị.

  • Tính năng tìm nạp trước được cho là phù hợp với khoảng trống ngắn giữa các khung hình, nhưng việc chuẩn bị thông báo này cũng giải mã hình ảnh của thông báo ngay trên luồng chính, chạy trong thời gian dài mà không mang lại kết quả.

  • Vì luồng chính bị kẹt khi giải mã hình ảnh này, nên không có khung hình mới nào có thể bắt đầu.

  • Sau đó, khi thông báo đó thực sự cuộn vào khung hiển thị, RenderThread phải phân bổ bộ nhớ và tải tất cả các pixel đó lên GPU, vì vậy, cùng một hình ảnh chính xác sẽ trì hoãn khung hình thứ hai.

  • Bây giờ, chúng ta đã biết thời gian được dùng vào đâu, nhưng liệu chúng ta có cần tất cả công việc này để vẽ khung hình này không?

  • Và làm sao chúng ta biết được?

  • Dấu vết sẽ cho chúng ta biết.

  • Bất cứ khi nào một bitmap lớn được vẽ vào một vùng nhỏ hơn nhiều trên màn hình, Android sẽ ghi lại một điểm đánh dấu ngay vào dấu vết.

  • Công cụ này cho thấy hai số tính bằng pixel: kích thước bitmap đầy đủ và diện tích thực tế mà bitmap bao phủ trên màn hình.

  • Chia giá trị đó cho mật độ màn hình và bạn sẽ nhận được giá trị đó theo DP.

  • Sau đó, chúng ta tìm kiếm mã bằng giá trị DP đó để tìm hình ảnh.

  • Đây là những gì chúng tôi tìm thấy.

  • Hình ảnh được tải bằng painterResource, không biết vùng chứa lớn đến mức nào, vì vậy, hình ảnh sẽ giải mã toàn bộ hình ảnh ở độ phân giải đầy đủ.

    Image(
        painter = painterResource(it),
        contentScale = ContentScale.Fit,
        modifier = Modifier.size(160.dp),
        contentDescription = stringResource(id = R.string.attached_image),
    )
  • Vì vậy, chúng ta sẽ thay thế bằng AsyncImage từ Coil.

    AsyncImage(
        model = it,
        contentScale = ContentScale.Fit,
        modifier = Modifier.size(160.dp),
        contentDescription = stringResource(id = R.string.attached_image),
    )
  • Trước tiên, thư viện này đo lường vùng chứa để chỉ giải mã pixel cần thiết và thực hiện công việc đó trên một luồng khác.

  • Khi chạy lại cùng một điểm chuẩn, quá trình giải mã hình ảnh trên luồng chính giảm xuống 0 và khung hình trễ giảm xuống 2.

  • Chúng ta sẽ không gỡ lỗi hai khung hình đó ở đây, nhưng điểm mấu chốt rất đơn giản: cố gắng không tính toán pixel sẽ không bao giờ đến được khung hình.

  • Vì vậy, việc giải mã hình ảnh đủ lớn để khiến khung hình bị trễ, nhưng không phải chi phí nào cũng xuất hiện dưới dạng khung hình bị rớt và vẫn có những chi phí nhỏ hơn mà chúng ta có thể tránh được khi viết mã.

  • Hãy cùng xem hai trong số đó.

  • Bất cứ khi nào sử dụng RuntimeShader, chúng ta chỉ nên tạo một lần vì mặc dù chỉ biên dịch và được lưu vào bộ nhớ đệm, nhưng nó vẫn phân tích cú pháp chuỗi nguồn mỗi khi chúng ta tạo một chuỗi.

  • Và bất cứ khi nào một hiệu ứng tạo ảnh động cho mọi khung hình, chúng ta có thể gói hiệu ứng đó trong một graphicsLayer để chỉ phần tử ảnh động được ghi lại, thay vì vẽ lại giao diện người dùng xung quanh phần tử đó.

  • Nút Chuyển đến cuối sẽ xuất hiện bất cứ khi nào chúng ta cuộn.

  • Thao tác này sử dụng hai RuntimeShader để thể hiện hiệu ứng đó và mỗi khi nút xuất hiện, chúng ta sẽ tạo lại cả hai chương trình đổ bóng này từ chuỗi nguồn của chúng.

    val lensShader = RuntimeShader(MAGNIFIER_LENS_SHADER).apply {
        setFloatUniform("size", size.width, size.height)
        setFloatUniform("zoom", zoom)
    // …
    val lensEffect = RenderEffect.createRuntimeShaderEffect(lensShader, "content")
    // …
    val glassShader = RuntimeShader(GLASS_FINISH_SHADER).apply {
  • Việc phân tích cú pháp hai chương trình đổ bóng đó và tạo hiệu ứng sẽ mất hàng trăm micro giây trên luồng chính mỗi khi nút xuất hiện.

  • Thay vào đó, chúng ta tạo chương trình đổ bóng một lần cho toàn bộ quá trình hoặc có thể ghi nhớ chương trình đổ bóng đó, sau đó cập nhật đồng nhất.

    val lensShader = MagnifierShaders.lens.apply {
        setFloatUniform("size", size.width, size.height)
        setFloatUniform("zoom", zoom)
    // …
    private object MagnifierShaders {
        val lens: RuntimeShader by lazy(LazyThreadSafetyMode.NONE) { RuntimeShader(MAGNIFIER_LENS_SHADER) }
        val glassFinish: RuntimeShader by lazy(LazyThreadSafetyMode.NONE) { RuntimeShader(GLASS_FINISH_SHADER) }
  • Nhờ thay đổi đó, việc tạo hiệu ứng nhanh hơn gần 40 lần và chi phí khi nút xuất hiện giảm 20 lần.

  • Trong hiệu ứng phát sáng chuyển màu dạng lưới mà chúng ta đã thấy trước đó, hiệu ứng phát sáng sẽ tạo ảnh động cho mọi khung hình bên trong drawBehind.

    Box(
        modifier = Modifier
            .matchParentSize()
            .drawBehind {
                // Behind the card, scaled past its bounds.
                translate(top = -58.dp.toPx()) {
                    scale(scaleX = 1.38f, scaleY = 2.85f) {
                        with(glowMeshPainter) {
    draw(size, alpha = glowAlpha)
    }
  • Vì vậy, việc đọc trạng thái trong giai đoạn vẽ sẽ giữ nguyên thành phần và bố cục, nhưng danh sách hiển thị không thể cập nhật một lệnh vẽ tại chỗ.

  • Vì vậy, Compose sẽ đi lên cây để tìm nút gần nhất có lớp đồ hoạ, ở đây là đường viền màu đỏ, tức là toàn bộ màn hình, và ghi lại toàn bộ lớp đó trong mỗi khung hình.

  • Bằng cách đặt graphicsLayer ngay trên hộp, quá trình truyền tải sẽ dừng ở đường viền màu xanh lục, vì vậy, chỉ có hộp phát sáng được ghi lại và lát cắt vẽ luồng chính giảm 2/10 mili giây.

    Box(
        modifier = Modifier
            .matchParentSize()
                      .graphicsLayer()
            .drawBehind {
                // Behind the card, scaled past its bounds.
                translate(top = -58.dp.toPx()) {
                    scale(scaleX = 1.38f, scaleY = 2.85f) {
                        with(glowMeshPainter) {
    draw(size, alpha = glowAlpha)
    }
  • Với Perfetto, việc phân tích lỗi trong dấu vết có thể gặp khó khăn, vì vậy, bạn có thể sử dụng kỹ năng phân tích dấu vết để thu thập một số thông tin chi tiết.

  • Nếu bạn muốn được hỗ trợ thêm trong việc viết mã Compose hiệu suất cao, hãy nhớ cài đặt Android CLI và sử dụng kỹ năng hiệu suất của Compose để bắt đầu.

    android skills add perfetto-trace-analysis
    android skills add jetpack-compose-performance

Tóm tắt và tài nguyên

  • Nếu bạn muốn xem các ví dụ mà chúng tôi đã minh hoạ hôm nay, hãy xem ứng dụng JetChat tại đường liên kết này.

Mã

Đoạn mã

Đoạn mã Kotlin ở phút 02:47 trong video "Chuyển màu lưới và kết xuất lưới tuỳ chỉnh"
val gradientPainter = remember {
MeshGradientPainter(rows = 3, columns = 3) {
}
}
Đoạn mã Kotlin ở phút 02:55 trong video "Chuyển màu lưới và kết xuất lưới tuỳ chỉnh"
val gradientPainter = remember {
MeshGradientPainter(rows = 3, columns = 3) {
   setVertex(0, 0, Offset(0.0f, 0.0f), indigo)

}
}
Đoạn mã Kotlin ở phút 03:05 trong video "Mesh gradients and custom mesh rendering" (Chuyển màu lưới và kết xuất lưới tuỳ chỉnh)
val gradientPainter = remember {
MeshGradientPainter(rows = 3, columns = 3) {
   setVertex(0, 0, Offset(0.0f, 0.0f), indigo)
     // ....
   setVertex(1, 1, Offset(0.2f, 0.4f) + Offset(animatedOffset, animatedOffset), coral)
   // ....
   setVertex(3, 0, Offset(0.0f, 1.0f), sunshine)
   // ....
}
}

Box(
   modifier = modifier.padding(32.dp)
       .safeContentPadding()
       .aspectRatio(16 / 9f)
       .fillMaxWidth()
       .paint(gradientPainter)
)
Đoạn mã Kotlin ở phút 03:24 trong video "Chuyển màu lưới và kết xuất lưới tuỳ chỉnh"
MeshGradientPainter(
   rows = 2,
   columns = 3,
   hasBicubicColor = true,
) {
   // Row 0 (top edge, y = 0.0f)
   setVertex(0, 0, Offset(0.0000f, 0.0000f), Color(0xFFFF6B6B))
   setVertex(0, 1, Offset(0.3300f, 0.0000f), Color(0xFFFFD6D6))
   setVertex(0, 2, Offset(0.6700f, 0.0000f), Color(0xFFE8C3FF))
   setVertex(0, 3, Offset(1.0000f, 0.0000f), Color(0xFFE396FF))

   // ... remaining vertices for Rows 0, 1, and 2
}
Đoạn mã Kotlin ở phút 04:09 trong video "Chuyển màu lưới và kết xuất lưới tuỳ chỉnh"
val transition = rememberInfiniteTransition(label = "userInputGlowMesh")

val phase1 by transition.animateFloat(
   initialValue = 0f,
   targetValue = (2 * PI).toFloat(),
   animationSpec = infiniteRepeatable(
       animation = tween(durationMillis = 2800, easing = LinearEasing),
       repeatMode = RepeatMode.Restart,
   ),
   label = "glowPhase1",
)

val phase2 by transition.animateFloat(
   initialValue = 0f,
   targetValue = (2 * PI).toFloat(),
   animationSpec = infiniteRepeatable(
       animation = tween(durationMillis = 4200, easing = LinearEasing),
       repeatMode = RepeatMode.Restart,
   ),
   label = "glowPhase2",
)
Đoạn mã Kotlin ở phút 04:33 trong video "Chuyển màu lưới và kết xuất lưới tuỳ chỉnh"
MeshGradientPainter(
   rows = 6,
   columns = 5,
   hasBicubicColor = true,
) {
   for (r in 0..6) {
       for (c in 0..5) {
               val offset = if (r in 1..5 && c in 1..4) {
               val ampX = when (r) {
                   1, 2 -> 0.048f
                   3 -> 0.042f
                   4 -> 0.028f
                   else -> 0.036f
               }
               val ampY = when (r) {
                   1, 2 -> 0.028f
                   3 -> 0.020f
                   4 -> 0.022f
                   else -> 0.016f
               }
               val d = orbit(c, r, ampX, ampY)
               Offset(
                   x = (baseU + d.x).coerceIn(0.04f, 0.96f),
                   y = (baseV + d.y).coerceIn(0.05f, 0.95f),
               )
           } else {
               Offset(baseU, baseV)
           }
           setVertex(r, c, offset, gridColors[r][c])
       }
   }
}
Đoạn mã Kotlin ở phút 05:40 trong video "Chuyển màu lưới và kết xuất lưới tuỳ chỉnh"
val lines = remember {
        path.asAndroidPath().flatten(0.5f)
    }
//... inside draw scope
lines.forEach { line ->
//...
    drawLine(
        brush = Brush.linearGradient(...),
        start = Offset(line.start.x,
line.start.y),
        end = Offset(line.end.x, line.end.y),
        strokeWidth = 30f,
    )
//...
}
Đoạn văn bản ở phút 07:03 trong video "Kết xuất lưới chuyển màu và lưới tuỳ chỉnh"
LineShaders.VERTEX

uniform float2 uStart;
uniform float2 uEnd;
uniform float uHalfWidth;
uniform float uTime;

Varyings main(const Attributes a) {
    Varyings v;
    float2 dir = uEnd - uStart;
    float2 normal = normalize(float2(-dir.y, dir.x));
    float along = a.corner.x * length(dir);
    float pulse = (a.corner.x - uTime) / 0.18;
    float radius = uHalfWidth * (1.0 + 0.6 * exp(-pulse * pulse));
    float across = a.corner.y * (radius + 1.0);
    v.position = uStart + dir * a.corner.x + normal * across;
    v.stroke = float3(along, across, radius);
    return v;
}
Đoạn văn bản ở phút 08:01 trong video "Kết xuất lưới tuỳ chỉnh và dải chuyển màu lưới"
LineShaders.FRAGMENT

uniform float uLength;
layout(color) uniform float4 uStartColor;
layout(color) uniform float4 uEndColor;

float2 main(const Varyings v, out float4 color) {
    float distance = abs(v.stroke.y);
    float coverage = 1.0 - smoothstep(
    v.stroke.z - 0.5, v.stroke.z + 0.5, distance);
    float side = distance / v.stroke.z;
    float4 ink = mix(uStartColor, uEndColor,
        clamp(v.stroke.x / uLength + 0.4 * side * side, 0.0, 1.0));
    color = ink * coverage;
    return v.position;
}
Đoạn mã Kotlin ở phút 08:37 trong video "Mesh gradients and custom mesh rendering" (Chuyển màu lưới và kết xuất lưới tuỳ chỉnh)
val spec = remember {
    MeshSpecification.make(
        arrayOf(Attribute(TYPE_FLOAT2, 0, "corner")),
        vertexStride = 8,
        arrayOf(Varying(TYPE_FLOAT3, "stroke")),
        LineShaders.VERTEX, LineShaders.FRAGMENT)
}
Đoạn mã Kotlin ở phút 09:06 trong video "Chuyển màu lưới và kết xuất lưới tuỳ chỉnh"
fun lineCorners(segments: Int): ByteBuffer {
    val buffer = ByteBuffer.allocateDirect((segments + 1) * 2 * 8)
        .order(ByteOrder.nativeOrder())
    for (i in 0..segments) {
        val t = i / segments.toFloat()
        buffer.putFloat(t).putFloat(-1f) // one side
        buffer.putFloat(t).putFloat(1f)  // the other side
    }
    return buffer.rewind() as ByteBuffer
}
Đoạn mã Kotlin ở phút 09:18 trong video "Chuyển màu lưới và kết xuất lưới tuỳ chỉnh"
val mesh = Mesh(
    spec,
    mode = Mesh.TRIANGLE_STRIP,
    vertexBuffer = corners,
    vertexCount = (segments + 1) * 2,
    bounds = RectF(0f, 0f, size.width, size.height)
)
Đoạn mã Kotlin ở phút 09:24 trong video "Chuyển màu lưới và kết xuất lưới tuỳ chỉnh"
Box(Modifier.fillMaxSize().drawWithCache {
   val mesh = Mesh(...)
   onDrawBehind {
       mesh.setFloatUniform("uTime", time.value)
       drawIntoCanvas {
          it.nativeCanvas.drawMesh(mesh,android.graphics.BlendMode.DST, paint)
       }
   }
})
Đoạn mã XML ở phút 11:22 trong phần "Hỗ trợ gam màu rộng"
<!-- AndroidManifest.xml -->
<activity
android:name=".NavActivity"
android:colorMode="wideColorGamut"
android:exported="true">
</activity>
Đoạn mã Kotlin ở phút 11:24 trong video "Hỗ trợ gam màu rộng"
// Dynamic toggle in code
DisposableEffect(window) {
   window?.colorMode = ActivityInfo.COLOR_MODE_WIDE_COLOR_GAMUT
   onDispose {
       window?.colorMode = ActivityInfo.COLOR_MODE_DEFAULT
   }
}
Đoạn mã Kotlin ở phút 11:49 trong video "Hỗ trợ gam màu rộng"
// 1. Display P3 Color (Wider gamut: greens & reds are more saturated) colorMode=wideColorGamut
val VividP3Red = Color(
   red = 1.0f,
   green = 0.05f,
   blue = 0.05f,
   alpha = 1.0f,
   colorSpace = ColorSpaces.DisplayP3
)
// 2. Extended sRGB (Enables HDR values > 1.0f) use colorMode=hdr
val HdrNeonCyan = Color(
   red = 0.0f,
   green = 1.8f,  // Peak brightness > 1.0 in extended gamut
   blue = 2.0f,
   alpha = 1.0f,
   colorSpace = ColorSpaces.ExtendedSrgb
)
// 3. BT.2020 HLG (Broadcast HDR standard, supported API 34+) - use colorMode=hdr
val HdrSunsetOrange = Color(
   red = 1.0f,
   green = 0.45f,
   blue = 0.1f,
   alpha = 1.0f,
   colorSpace = ColorSpaces.Bt2020Hlg
)
Đoạn mã Kotlin ở phút 12:45 trong video "Hình đa giác và biến đổi hình dạng"
fun cookiePolygon(): RoundedPolygon {
    val vertices = FloatArray(32)
    for (i in 0 until 16) {
        val radius = if (i % 2 == 0) 1f else 0.84f
// …
    }
    return RoundedPolygon(
        vertices,
        rounding = CornerRounding(0.12f)
    )
}
Đoạn mã Kotlin ở phút 13:15 trong video "Hình đa giác và biến đổi hình dạng"
class RoundedPolygonShape(
 polygon: RoundedPolygonShape
) : Shape {
    // …
    override fun createOutline(size: Size, layoutDirection: LayoutDirection,
    density: Density,
    ): Outline {
        path = polygon.toPath().asComposePath()
        // …
        matrix.scale(size.width / maxDimension, size.height / maxDimension)
        matrix.translate(-bounds.left, -bounds.top)
        // …
        return Outline.Generic(path)
    }
}
Đoạn mã Kotlin ở phút 13:30 trong video "Polygon shapes and morphing" (Hình dạng đa giác và biến đổi hình dạng)
private val AvatarShape = PolygonShape {
    val vertices = List(16) { i ->
        val radius = size.minDimension / 2f * if (i % 2 == 0) 1f else 0.86f
        val angle = i * PI.toFloat() / 8f
        size.center + Offset(cos(angle), sin(angle)) * radius
    }
    polygon(vertices, rounding = CornerRounding(radius = 8.dp))
}

    // …
Modifier.clip(AvatarShape)
Đoạn mã Kotlin ở phút 13:53 trong video "Hình đa giác và biến đổi hình dạng"
val recordShape = remember {
    MorphPolygonShape(
        start = PolygonShape.circle(),
        end = PolygonShape.star(
            numPoints = 9,
            innerRadiusRatio = 0.8f,
            outerRounding = CornerRounding(percent = 50),
        ),
    ) { shapeProgress.value }
}
Đoạn mã Kotlin ở phút 14:37 trong video "Blurring and backdrop render effects" (Hiệu ứng làm mờ và kết xuất phông nền)
Modifier
   .blur(
       radiusX = 10.dp,
       radiusY = 10.dp,
       edgeTreatment = BlurredEdgeTreatment(
           shape = RoundedCornerShape(8.dp),
       ),
   )
Đoạn mã Kotlin ở phút 15:15 trong video "Hiệu ứng làm mờ và kết xuất phông nền"
Modifier.blur {
blurRadiusSpec = BlurRadiusSpec.verticalGradient(
listOf(
BlurStop(0.5f, 0.dp),
BlurStop(1.0f, 32.dp),
)
)
edgeTreatment = BlurredEdgeTreatment.Unbounded
}
Đoạn mã Kotlin ở phút 15:42 trong video "Hiệu ứng làm mờ và kết xuất phông nền"
BlurRadiusSpec.horizontalGradient(
startRadius = 0.dp,
endRadius = sliderValue.dp,
)
Đoạn mã Kotlin ở phút 15:53 trong video "Hiệu ứng làm mờ và kết xuất phông nền"
BlurRadiusSpec.radialGradient(
center = center,
fallOffRadius = 300.dp,
startRadius = 0.dp,
endRadius = 24.dp,
)
Đoạn mã Kotlin ở phút 16:07 trong video "Hiệu ứng làm mờ và kết xuất phông nền"
val customShader = remember {
   RuntimeShader(
       """
       uniform float2 size;
       uniform float time;

       half4 main(float2 coords) {
          // Diagonal wave mask
          float normalizedX = coords.x / size.x;
          float normalizedY = coords.y / size.y;
          float wave = sin((normalizedX + normalizedY) * 6.0 + time) * 0.5 + 0.5;
          return half4(0.0, 0.0, 0.0, wave);
       }
       """.trimIndent()
   )
}

val customBlurRadius = remember(customShader) {
   BlurRadiusSpec.shader(maxRadius = 24.dp) { customShader }
}

Modifier.blur {
       val w = size.width.toPx()
       val h = size.height.toPx()
       customShader.setFloatUniform("size", w, h)
       customShader.setFloatUniform("time", animatedTime)
       blurRadiusSpec = customBlurRadius
   },
Đoạn mã Kotlin ở phút 17:40 trong video "Hiệu ứng làm mờ và kết xuất phông nền"
fun Modifier.backdropBlur(spec: BlurRadiusSpec, shape: Shape, tint: Color): Modifier =
   this then BackdropBlurElement(spec, shape, tint)

private class BackdropBlurNode(...) : Modifier.Node(), DrawModifierNode {
   private var renderNode: RenderNode? = null

   override fun ContentDrawScope.draw() {
       val node = getOrCreateNode()
       if (isDirty || sizeChanged) {
           node.setPosition(0, 0, widthPx, heightPx)
           node.setBackdropRenderEffect(spec.createRenderEffect(this))
       }
       drawIntoCanvas { it.nativeCanvas.drawRenderNode(node) }
       drawContent()
   }
}
Đoạn mã Kotlin ở phút 18:09 trong video "Hiệu ứng làm mờ và kết xuất phông nền"
JetchatAppBar(
    modifier = modifier
        .backdropBlur(
            tint = MaterialTheme.colorScheme
                .surface.copy(alpha = 0.5f),
            shape = RectangleShape,
            spec = BlurRadiusSpec.uniform(5.dp)
        )
)
Đoạn mã Kotlin ở phút 18:30 trong video "Hiệu ứng làm mờ và kết xuất phông nền"
JetchatAppBar(
    modifier = modifier
        .backdropBlur(
            tint = MaterialTheme.colorScheme
                .surface.copy(alpha = 0.5f),
            shape = RectangleShape,
            spec = BlurRadiusSpec.verticalGradient(
                listOf(
                    BlurStop(0.5f, 32.dp),
                    BlurStop(1f, 0.dp),
                ),
            )
        )
)
Đoạn mã Kotlin ở phút 19:10 trong video "Blurring and backdrop render effects" (Hiệu ứng làm mờ và kết xuất phông nền)
val blur = RenderEffect.createBlurEffect(24f, 24f, Shader.TileMode.CLAMP)

// Boost saturation by 30% and apply blur together
val matrix = ColorMatrix().apply { setSaturation(1.3f) }
val vibrancy = RenderEffect.createColorFilterEffect(ColorMatrixColorFilter(matrix))
val vibrantBlur = RenderEffect.createChainEffect(vibrancy, blur)
Đoạn mã Kotlin ở phút 19:23 trong video "Hiệu ứng làm mờ và kết xuất phông nền"
val agslShader = RuntimeShader(CHROMATIC_ABERRATION)
val noiseEffect = RenderEffect.createRuntimeShaderEffect(agslShader, "backdropInput")

val finalEffect = RenderEffect.createChainEffect(noiseEffect, vibrantBlur)
renderNode?.setBackdropRenderEffect(finalEffect)
Đoạn mã Kotlin ở phút 20:53 trong video "Hiệu ứng làm mờ và kết xuất phông nền"
val region = RoundedRectBlurRegion().apply {
   bounds = RectF(left, top, right, bottom)
   setCornerRadii(spec.cornerRadiusPx.coerceAtLeast(0f))
   alpha = spec.alpha.coerceIn(0f, 1f)
   blurRadius = spec.blurRadiusPx.coerceAtLeast(0f)
}

surfaceView.setBlurRegions(listOf(region))

surfaceView.setBlurRegions(emptyList()) // Clear
Đoạn mã Kotlin ở phút 21:33 trong "RuntimeColorFilter"
@Language("AGSL")
private const val CYBERPUNK_SHADER = """
    vec4 main(half4 in_color) {
        float lum = dot(in_color.rgb, vec3(0.299, 0.587, 0.114));
        vec3 shadow = vec3(0.06, 0.04, 0.25);
        vec3 mid = vec3(0.92, 0.12, 0.58);
        vec3 high = vec3(0.18, 0.94, 0.98);
        vec3 col = lum < 0.5 ? mix(shadow, mid, lum * 2.0) : mix(mid, high, (lum - 0.5) * 2.0);
        return vec4(mix(in_color.rgb, col, 0.7), in_color.a);
    }
"""
Đoạn mã Kotlin ở phút 21:48 trong phần "RuntimeColorFilter"
AsyncImage(
    model = imageUri,
    contentDescription = filter.displayName,
    colorFilter = RuntimeColorFilter(CYBERPUNK_SHADER)
                   .asComposeColorFilter(),
    contentScale = ContentScale.Crop,
    modifier = Modifier.fillMaxSize(),
)
Đoạn mã Kotlin ở phút 22:21 trong video "Tối ưu hoá hiệu suất đồ hoạ"
@Test
fun scrollFling() = measureScroll(swipeSteps = 5)

private fun measureScroll(swipeSteps: Int) = rule.measureRepeated(
    packageName = TARGET_PACKAGE,
    metrics = defaultComposeScrollingMetrics(),
    compilationMode = CompilationMode.Full(),
    iterations = 5,
    setupBlock = {
        killProcess()
    }
//...
Đoạn mã SQL ở phút 22:32 trong video "Tối ưu hoá hiệu suất đồ hoạ"
select count(*) as late_frames
from actual_frame_timeline_slice
where upid = (select upid from process where name = 'com.example.compose.jetchat.perf')
  and jank_type like '%App Deadline Missed%'
Đoạn mã Kotlin ở phút 23:54 trong video "Tối ưu hoá hiệu suất đồ hoạ"
Image(
    painter = painterResource(it),
    contentScale = ContentScale.Fit,
    modifier = Modifier.size(160.dp),
    contentDescription = stringResource(id = R.string.attached_image),
)
Đoạn mã Kotlin ở phút 24:02 trong video "Tối ưu hoá hiệu suất đồ hoạ"
AsyncImage(
    model = it,
    contentScale = ContentScale.Fit,
    modifier = Modifier.size(160.dp),
    contentDescription = stringResource(id = R.string.attached_image),
)
Đoạn mã Kotlin ở phút 25:14 trong video "Tối ưu hoá hiệu suất đồ hoạ"
val lensShader = RuntimeShader(MAGNIFIER_LENS_SHADER).apply {
    setFloatUniform("size", size.width, size.height)
    setFloatUniform("zoom", zoom)
// …
val lensEffect = RenderEffect.createRuntimeShaderEffect(lensShader, "content")
// …
val glassShader = RuntimeShader(GLASS_FINISH_SHADER).apply {
Đoạn mã Kotlin ở phút 25:32 trong video "Tối ưu hoá hiệu suất đồ hoạ"
val lensShader = MagnifierShaders.lens.apply {
    setFloatUniform("size", size.width, size.height)
    setFloatUniform("zoom", zoom)
// …
private object MagnifierShaders {
    val lens: RuntimeShader by lazy(LazyThreadSafetyMode.NONE) { RuntimeShader(MAGNIFIER_LENS_SHADER) }
    val glassFinish: RuntimeShader by lazy(LazyThreadSafetyMode.NONE) { RuntimeShader(GLASS_FINISH_SHADER) }
Đoạn mã Kotlin ở phút 25:49 trong video "Tối ưu hoá hiệu suất đồ hoạ"
Box(
    modifier = Modifier
        .matchParentSize()
        .drawBehind {
            // Behind the card, scaled past its bounds.
            translate(top = -58.dp.toPx()) {
                scale(scaleX = 1.38f, scaleY = 2.85f) {
                    with(glowMeshPainter) {
draw(size, alpha = glowAlpha)
}
Đoạn mã Kotlin ở phút 26:15 trong video "Tối ưu hoá hiệu suất đồ hoạ"
Box(
    modifier = Modifier
        .matchParentSize()
                  .graphicsLayer()
        .drawBehind {
            // Behind the card, scaled past its bounds.
            translate(top = -58.dp.toPx()) {
                scale(scaleX = 1.38f, scaleY = 2.85f) {
                    with(glowMeshPainter) {
draw(size, alpha = glowAlpha)
}
Đoạn mã Shell ở phút 26:37 trong video "Tối ưu hoá hiệu suất đồ hoạ"
android skills add perfetto-trace-analysis
android skills add jetpack-compose-performance