Apps foundation

ยกระดับแอปด้วยฟีเจอร์กราฟิกล่าสุดใน Jetpack Compose และ Android

ลำโพง

เกี่ยวกับ

เกี่ยวกับการเสวนา

พร้อมที่จะยกระดับ UI ของ Android จาก "ใช้งานได้" เป็น "สวยงามราวกับงานศิลปะ" แล้วหรือยัง

เข้าร่วมกับเราเพื่อดูความสามารถด้านกราฟิกล่าสุดใน Jetpack Compose เราจะแสดงวิธีสร้างเอฟเฟกต์ภาพใหม่ๆ ตั้งแต่การไล่ระดับสีแบบเมชที่สวยงามไปจนถึงการเบลอแบบโปรเกรสซีฟ และการใช้ฟีเจอร์กราฟิกใหม่ที่มีประสิทธิภาพใน Android นอกจากนี้ เรายังจะแสดงวิธีปรับปรุงประสิทธิภาพของฟีเจอร์กราฟิกโดยไม่ส่งผลต่ออายุการใช้งานแบตเตอรี่หรืออัตราเฟรม

สรุป

  • Compose 1.12 เปิดตัวการไล่ระดับสีแบบเมชสำหรับการผสมสีที่หลากหลายในตารางกริด 2 มิติของจุดยอด ใน Android 14 ขึ้นไป คุณสามารถใช้ drawMesh API แบบเนทีฟเพื่อสร้างเอฟเฟกต์ภาพที่อิงตามเรขาคณิตที่กำหนดเองได้
  • Compose รองรับขอบเขตสีกว้างเพื่อให้การผสมการไล่ระดับสีราบรื่นยิ่งขึ้นในฮาร์ดแวร์ที่รองรับ เปิดใช้โหมดขอบเขตสีกว้างในกิจกรรมหรือหน้าต่างเฉพาะเมื่อแสดงริชมีเดียเท่านั้น
  • Compose 1.13 เปิดตัว PolygonShape และ MorphPolygonShape API แบบเนทีฟสำหรับเรขาคณิตที่กำหนดเอง MorphPolygonShape จะอ่านความคืบหน้าในระหว่างขั้นตอนการแก้ไขโครงร่าง โดยอัปเดตรูปร่างทีละเฟรมโดยไม่ทริกเกอร์การจัดองค์ประกอบใหม่
  • Compose 1.13 เพิ่มการเบลอแบบโปรเกรสซีฟ ส่วนระดับ API 37.2 เปิดตัว RenderNode.setBackdropRenderEffect สำหรับการสุ่มตัวอย่างเนื้อหาพื้นหลัง นักพัฒนาแอปสามารถเชื่อมโยงเอฟเฟกต์ฉากหลังเข้าด้วยกันเพื่อสร้างเอฟเฟกต์กระจกฝ้าหรือเอฟเฟกต์แว่นขยายได้
  • เพิ่มประสิทธิภาพกราฟิกโดยหลีกเลี่ยงการทำงานที่ไม่จำเป็นในเฟรมต่างๆ ถอดรหัสเฉพาะพิกเซลรูปภาพที่จำเป็นออกจากเทรดหลัก และรวมเอฟเฟกต์ภาพเคลื่อนไหวไว้ใน graphicsLayer

แหล่งข้อมูล

Gemini Notebook

หัวข้อ

  • jetpack-compose
  • android-graphics
  • mesh-gradients
  • เฉดสี
  • การเบลอ
  • color-gamut
  • ประสิทธิภาพ

ข้อความถอดเสียง

ข้อมูลเบื้องต้นเกี่ยวกับฟีเจอร์กราฟิกใน Jetpack Compose

  • สวัสดีทุกคน และยินดีต้อนรับสู่เซสชัน "ยกระดับแอปด้วยฟีเจอร์กราฟิกล่าสุดใน Compose และ Android"

  • ฉันชื่อ Rebecca Franks และผู้ที่จะมาร่วมพูดคุยกับฉันบนเวทีในวันนี้คือ

  • ฉันชื่อ Aayush Chaudhary

  • สวัสดี ฉันชื่อ Rahul Ray

  • เราเห็นเทรนด์ใหม่ๆ ที่คุณกำลังก้าวข้ามขีดจำกัดของสิ่งที่เป็นไปได้ใน Android และ Compose โดยใช้ AI เพื่อทำให้ไอเดียอันน่าทึ่งของคุณเป็นจริง

  • และเราชอบเห็นแอปที่ให้ความรู้สึกพรีเมียม เป็นแบบเนทีฟ และน่าพึงพอใจ

  • เราทราบดีว่าแอปที่ให้ความรู้สึกดีและได้รับคะแนนที่ยอดเยี่ยมจากผู้ใช้อย่างสม่ำเสมอคือแอปที่โดยทั่วไปแล้วใช้ประโยชน์จากการเปลี่ยนแปลง API ระดับระบบล่าสุด มีความต่อเนื่องระหว่างแพลตฟอร์มต่างๆ ด้วยองค์ประกอบที่ใช้ร่วมกันและการเบลอ ปรับเปลี่ยนได้อย่างน่าพึงพอใจในรูปแบบต่างๆ และมีประสิทธิภาพ ไม่ทำให้ผู้ใช้ต้องคาดเดา และขยายขอบเขตสิ่งที่เป็นไปได้ด้วยอุปกรณ์ของคุณ

  • ในการพูดคุยวันนี้ เราจะแสดงฟีเจอร์ล่าสุดบางส่วนที่ช่วยให้คุณใช้ประโยชน์จาก Android ได้ ตั้งแต่การสร้างความพึงพอใจให้ผู้ใช้ด้วยการไล่ระดับสีแบบเมช การเปิดใช้การรองรับขอบเขตสีกว้าง การเปลี่ยนภาพที่น่าพึงพอใจด้วยรูปทรงหลายเหลี่ยม สิ่งใหม่ๆ ในการเบลอ และสุดท้าย เราจะพูดถึงวิธีคิดเกี่ยวกับประสิทธิภาพเมื่อเพิ่มฟีเจอร์กราฟิกใหม่ๆ ลงในแอป

  • แต่ตลอดการพูดคุยในวันนี้ เราจะพูดถึง API ใหม่บางรายการในระดับต่างๆ ของสแต็ก

  • แม้ว่าชุดเครื่องมือ UI ของมุมมองเดิมจะอยู่ในโหมดการบำรุงรักษาแล้ว แต่เรายังคงลงทุนในเครื่องมือแสดงผลฮาร์ดแวร์กราฟิก Android พื้นฐานที่ขับเคลื่อน Jetpack Compose

  • คุณอาจมองกราฟิกใน Android เป็น 3 เลเยอร์ที่แตกต่างกัน ได้แก่ Compose API ในเลเยอร์แรก ซึ่งเป็นที่ที่ API กราฟิก UI ใหม่ระดับสูงทั้งหมดอยู่ เช่น Mesh Gradient Painter, รูปทรงหลายเหลี่ยม และ Progressive Blur API

  • จากนั้นเรามีเลเยอร์ระดับกลาง ซึ่ง DrawScope ของ Compose, โหนดตัวแก้ไขการวาด และ AGSL ช่วยให้คุณสร้างเอฟเฟกต์ที่กำหนดเองได้

  • แต่สุดท้ายแล้ว เรามีแพลตฟอร์ม Android และ Compose สามารถเข้าถึง Canvas แบบเนทีฟและใช้พื้นฐานการแสดงผลระบบปฏิบัติการใหม่ล่าสุดได้เสมอภายในตัวแก้ไข Compose

  • และเราจะแสดงตัวอย่างการดำเนินการนี้ให้คุณดูในวันนี้

  • มาเริ่มกันเลย

การไล่ระดับสีแบบเมชและการแสดงผลเมชที่กำหนดเอง

  • ก่อนอื่น เรามาดูการไล่ระดับสีแบบเมช ซึ่งเป็น API ใหม่ที่เปิดตัวใน Compose 1.12 กัน

  • ก่อนหน้านี้เรามีการไล่ระดับสีแบบเส้นตรง แบบกวาด และแบบรัศมี ซึ่งเหมาะสำหรับการเปลี่ยนสีแบบง่ายๆ แต่หากต้องการการผสมสีที่หลากหลาย เช่น พื้นหลังแบบแอมเบียนท์หรือเอฟเฟกต์แสงฟลัดไลท์ คุณต้องใช้ตารางกริด 2 มิติของระบบที่ตำแหน่งและสีสามารถผสมกันได้ทั่วทั้งพื้นที่

  • หากต้องการสร้างการไล่ระดับสีแบบเมชเช่นนี้ ก่อนอื่นให้ระบุจำนวนแถวและคอลัมน์ของแพตช์ที่ต้องการ

    val gradientPainter = remember {
    MeshGradientPainter(rows = 3, columns = 3) {
    }
    }
  • จากนั้นภายใน Lambda ต่อท้าย ให้เรียก setVertex และกำหนดออฟเซ็ตที่ปรับมาตรฐานแล้วซึ่งมีค่าตั้งแต่ 0F ถึง 1F พร้อมกับสีสำหรับจุดตัดของตารางกริดแต่ละจุด

    val gradientPainter = remember {
    MeshGradientPainter(rows = 3, columns = 3) {
       setVertex(0, 0, Offset(0.0f, 0.0f), indigo)
    
    }
    }
  • จากนั้นหากต้องการสร้างภาพเคลื่อนไหวให้จุดยอดเหล่านั้น ให้เพิ่มออฟเซ็ตแบบเคลื่อนไหวสำหรับจุดยอด 8 จุด

    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)
    )
  • เมื่อกำหนด Painter แล้ว ให้เรียกใช้ด้วย Composable ใดก็ได้ผ่านตัวแก้ไขการวาด

  • ตอนนี้เราได้เพิ่มฟีเจอร์ในแอป JetChat เพื่อกดชอบข้อความเมื่อแตะสองครั้ง

  • ในการสร้าง เราจะตั้งค่าตารางกริดขนาด 2 คูณ 3 และวางจุดสีต่างๆ ไว้ทั่วคอมโพเนนต์

    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
    }
  • โปรดทราบว่าเราตั้งค่า hasBicubicColor เป็น "จริง" ที่นี่

  • โดยค่าเริ่มต้น สีจะสอดแทรกเป็นเส้นตรงข้ามสามเหลี่ยม ซึ่งบางครั้งอาจทำให้มองเห็นเป็นหย่อมๆ

  • การเปิดตัวเลือกนี้จะเปลี่ยนเฉดสีเป็นการประมาณค่าในช่วงแบบไบคิวบิกที่ราบรื่น เพื่อให้สีต่างๆ ผสมผสานกันได้อย่างราบรื่น

  • ทีนี้ถ้าคุณต้องการสร้างแสงเรืองรอบๆ แบบเคลื่อนไหวเหมือนในตัวอย่างนี้ไว้ด้านหลังช่องป้อนข้อมูลล่ะ

  • หากต้องการสร้างเอฟเฟกต์แบบนี้ด้วยการไล่ระดับสีแบบเมช คุณต้องมี 2 สิ่ง

  • อย่างแรกคือการเคลื่อนไหวแบบออร์แกนิกที่ราบรื่นสำหรับจุดยอดภายใน และวิธีทำให้ขอบด้านนอกดูนุ่มนวลขึ้นเพื่อไม่ให้เกิดสี่เหลี่ยมผืนผ้าแข็งๆ รอบการ์ด

  • เราจึงเริ่มด้วยการตั้งค่า rememberInfiniteTransition ที่มีมุมเฟส 2 มุมซึ่งทำงานในระยะเวลาที่ต่างกันเล็กน้อย เพื่อให้การเคลื่อนไหวดูเป็นธรรมชาติมากกว่ากลไก

    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",
    )
  • ในที่นี้ เราจะทำให้จุดยอดภายในเคลื่อนไหวและปักหมุดวงแหวนรอบนอก ซึ่งเป็นวงแหวนรอบนอกของจุดยอดที่มีขอบซึ่งมีค่าอัลฟาเป็น 0

  • เนื่องจากขอบเขตด้านนอกคงที่และโปร่งใส แสงจึงค่อยๆ จางลงอย่างเป็นธรรมชาติ

  • และสุดท้าย เราจะป้อนออฟเซ็ตแบบเคลื่อนไหวเหล่านั้นลงใน 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])
           }
       }
    }
  • การไล่ระดับสีแบบเมชที่คุณเพิ่งเห็นนั้นวาดขึ้นโดยใช้ drawMesh API ใน Android 14 ขึ้นไป

  • มาดูรายละเอียดเพิ่มเติมเกี่ยวกับ API นี้ด้วยการสร้างเอฟเฟกต์ตั้งแต่ต้นกัน

  • สำหรับหน้าจอต้อนรับของ JetChat เราต้องการวาดเส้นทางที่ดูเหมือนริบบิ้น

  • โปรดสังเกต 2 สิ่งที่นี่ ได้แก่ ความกว้างของเส้นไม่คงที่และสีจะวิ่งไปตามแต่ละเส้นของเส้นทาง

  • มาดูรายละเอียดและทำความเข้าใจสิ่งที่เราต้องใช้ในการสร้างเอฟเฟกต์นี้กัน

  • สำหรับแต่ละพิกเซล เราต้องตัดสินใจว่าจะวาดหรือไม่ และจะใช้สีใด

  • ในการหาคำตอบ เราต้องใช้ระยะทาง 2 ค่า

  • ค่าแรกคือระยะห่างของพิกเซลจากจุดศูนย์กลางของเส้นขีด

  • ระยะทางที่ 2 คือระยะทางตามเส้นทางของพิกเซลจากจุดเริ่มต้นไปยังจุดสิ้นสุด

  • ซึ่งจะบอกเราว่าจะเลือกสีใดจากการไล่ระดับสี และการผสมค่าแรกเล็กน้อยจะทำให้สีโค้งตามความกว้าง

  • เราจะวาดการไล่ระดับสีตามเส้นทางโดยใช้เพียงเส้นทางและสีได้ไหม

  • เราทำได้

  • ซึ่งต้องทำให้เส้นทางแบนราบเป็นรายการเส้นตรงสั้นๆ ซึ่งจะให้ระยะทางตามเส้นทางด้วย

    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,
        )
    //...
    }
  • จากนั้นเราจะวาดแต่ละเส้นด้วยการไล่ระดับสีของตัวเอง

  • วิธีนี้ใช้ได้ แต่แต่ละเส้นต้องใช้คำสั่งประมวลผลและออบเจ็กต์การวาดแยกกัน และทั้งเส้นทางยังคงมีความกว้างเดียว

  • อีกตัวเลือกหนึ่งคือ Shader รันไทม์ แต่จะได้รับเฉพาะตำแหน่งของพิกเซลภายในขอบเขตคอนเทนเนอร์เท่านั้น

  • แต่ไม่ได้บอกว่าเราต้องใช้อะไรบ้างในการวาดริบบิ้นนี้

  • และเราไม่สามารถคำนวณจากตำแหน่งของพิกเซลได้ง่ายๆ เช่นกัน

  • Draw mesh จะให้เวอร์เท็กซ์เชเดอร์สำหรับสิ่งนั้น

  • แล้ว Mesh คืออะไร

  • Mesh ประกอบด้วยสามเหลี่ยมที่มีข้อมูลอยู่ที่แต่ละมุม และแต่ละมุมเรียกว่าจุดยอด

  • จุดยอดแต่ละจุดจะมีบันทึกข้อมูลขนาดเล็ก

  • โดยปกติแล้ว ระเบียนนั้นจะเก็บตำแหน่งไว้ แต่ก็ไม่จำเป็นเสมอไป

  • เวอร์เท็กซ์เชเดอร์จะอ่านระเบียนนั้นและคำนวณตำแหน่งจากระเบียนดังกล่าว

  • นี่คือเส้นทางที่สามเหลี่ยมหนึ่งใช้: ตัวปรับแสงเงาเวอร์เท็กซ์จะทำงานสำหรับแต่ละมุม โดยจะคำนวณตำแหน่งของมุม และค่าใดๆ ที่เราต้องการส่งต่อสำหรับแต่ละพิกเซล

  • จากนั้นตัวสร้างราสเตอร์จะค้นหาทุกพิกเซลที่สามเหลี่ยมนั้นครอบคลุม และประมาณค่าจากมุมทั้ง 3 มุมสำหรับแต่ละพิกเซล

  • สุดท้าย Fragment Shader จะทำงานสำหรับพิกเซลเหล่านั้น โดยใช้ค่าที่ประมาณในช่วงและคำนวณสีของพิกเซลที่คำนวณ

  • มาดูวิธีเขียนเวอร์เท็กซ์และแฟรกเมนต์เชเดอร์ด้วยตัวอย่างที่ง่ายขึ้นกัน ซึ่งก็คือเส้นตรงที่มีพัลส์เคลื่อนที่ไปตามเส้น

    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;
    }
  • เวอร์เท็กซ์เชเดอร์จะรับแอตทริบิวต์เป็นอินพุตและแสดงผลลัพธ์เป็นตัวแปร

  • แอตทริบิวต์คือข้อมูลสำหรับมุมเดียว

  • ซึ่งมีตัวเลขเพียง 2 ตัว ได้แก่ ความคืบหน้าตามเส้น และด้านของเส้นที่อยู่

  • จากนั้นระบบจะประมาณค่า Varying และส่งไปยัง Fragment Shader

  • เราคำนวณระยะทางพิกเซลจากจุดเริ่มต้น และสำหรับความกว้างของเส้นขีด เราใช้เวลาในการสร้างพัลส์บนรัศมี

  • จากนั้นเราจะคูณรัศมีนั้นด้วยด้านมุม ซึ่งจะทำให้เราได้ระยะห่างของเครื่องหมายตลอดเส้นขีด

  • เมื่อ GPU ทำการประมาณค่าระหว่างเครื่องหมายเหล่านั้น เส้นกึ่งกลางจะกลายเป็นศูนย์

  • สุดท้าย เราจะคำนวณตำแหน่งของมุม และจัดเก็บค่าทั้งหมดนี้ไว้ในตัวแปรของเส้นขอบ แล้วส่งไปยัง Fragment Shader

  • Mesh Fragment Shader จะแสดงผลสีผ่านพารามิเตอร์ และแสดงผลตำแหน่งสำหรับการสุ่มตัวอย่างสีหากมีการตั้งค่าไว้ ซึ่งแตกต่างจากรันไทม์ Shader

    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;
    }
  • การใช้ค่าสัมบูรณ์ของพิกัดแนวนอนจะทำให้เราได้ระยะห่างพิกเซลต่อพิกเซลจากเส้นกึ่งกลาง

  • เราเปรียบเทียบระยะทางนั้นกับรัศมีเพื่อให้ได้ความครอบคลุม และสำหรับสี เราจะผสมสีตามเส้นทางของเส้น และโค้งไล่ระดับสีไปทางขอบเล็กน้อย

  • คูณความครอบคลุมนั้นด้วยความครอบคลุมอีกครั้ง นั่นคือสีสุดท้ายของเรา

  • ในฝั่ง Compose เราจะเชื่อมโยงเฉดสีและเลย์เอาต์ข้อมูลเข้ากับออบเจ็กต์ MeshSpecification

    val spec = remember {
        MeshSpecification.make(
            arrayOf(Attribute(TYPE_FLOAT2, 0, "corner")),
            vertexStride = 8,
            arrayOf(Varying(TYPE_FLOAT3, "stroke")),
            LineShaders.VERTEX, LineShaders.FRAGMENT)
    }
  • ก่อนอื่น เราจะประกาศแอตทริบิวต์มุมเป็นค่า Float 2 ค่าที่เริ่มต้นที่ไบต์ 0

  • เนื่องจาก Float 2 ตัวใช้ 8 ไบต์ สไตรด์มุมแต่ละมุมในบัฟเฟอร์จึงเป็น 8

  • ถัดไป เราจะประกาศตัวแปร Stroke Stroke เป็น Float 3 ตัว และส่งผ่าน Vertex และ Fragment Shader ที่เราเพิ่งเขียน

  • เราสร้างบัฟเฟอร์จุดยอด

    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
    }
  • ในแต่ละขั้นตอนตามเส้น เราจะคำนวณความคืบหน้าจาก 0 ถึง 1 และเขียนมุม 2 มุมลงในบัฟเฟอร์

  • สุดท้าย เราจะรวมข้อมูลจำเพาะและบัฟเฟอร์เข้าเป็นออบเจ็กต์ Mesh

    val mesh = Mesh(
        spec,
        mode = Mesh.TRIANGLE_STRIP,
        vertexBuffer = corners,
        vertexCount = (segments + 1) * 2,
        bounds = RectF(0f, 0f, size.width, size.height)
    )
  • Canvas ของ Compose ยังไม่แสดง drawMesh เราจึงลดระดับลงเป็น nativeCanvas และวาด Mesh

    Box(Modifier.fillMaxSize().drawWithCache {
       val mesh = Mesh(...)
       onDrawBehind {
           mesh.setFloatUniform("uTime", time.value)
           drawIntoCanvas {
              it.nativeCanvas.drawMesh(mesh,android.graphics.BlendMode.DST, paint)
           }
       }
    })
  • เนื่องจากคุณควบคุมเรขาคณิตและพิกเซลได้ คุณจึงวาดเอฟเฟกต์ภาพที่ขับเคลื่อนด้วยเรขาคณิตได้โดยใช้เมชการวาด

  • ดูข้อมูลเพิ่มเติมเกี่ยวกับ API นี้ได้จากเอกสารประกอบ

การรองรับขอบเขตสีกว้าง

  • ใน Compose 1.12 เราได้เปิดตัวการรองรับขอบเขตสีกว้าง

  • แล้วข้อมูลนี้มีประโยชน์อย่างไร

  • คุณทราบไหมว่าหน้าจอสามารถแสดงสีได้หลากหลายกว่าขอบเขตสี sRGB แบบเดิม

  • ตัวอย่างเช่น หากดูรูปภาพนี้ คุณจะเห็นว่าสามเหลี่ยมสีขาวคือช่วงสี sRGB มาตรฐานที่หน้าจอส่วนใหญ่แสดงได้

  • อย่างไรก็ตาม หน้าจอใหม่ๆ บางรุ่นรองรับสีที่หลากหลายกว่า ดังที่แสดงด้วยสามเหลี่ยมสีเหลือง ซึ่งบางสีมีความอิ่มตัวและสดใสมากกว่า

  • แต่เมื่อเราแสดงสี เราจะแสดงสีด้วยคำอธิบายของจุดยอดแต่ละจุดของสามเหลี่ยมนั้น

  • เมื่อดูการเปรียบเทียบการไล่ระดับสีนี้ คุณควรแยกความแตกต่างระหว่างขอบเขตสีของจอแสดงผลและการประมาณค่าในช่วงการไล่ระดับสี

  • ในจอแสดงผล sRGB มาตรฐานหรือโปรเจ็กเตอร์นี้ หน้าจอไม่สามารถแสดงผลสีนอกขอบเขต sRGB นี้ได้จริง

  • ดังนั้นจึงเป็นเรื่องยากมากที่จะสาธิตให้คุณดูบนเวทีในตอนนี้

  • ดังนั้น คุณจะต้องเชื่อใจเราว่าสีจะดูดีขึ้นใน Android ที่รองรับขอบเขตสีกว้าง

  • ในตัวอย่างนี้ สิ่งที่คุณเห็นคือผลของการประมาณค่าสีในช่วงขอบเขตสีที่กว้างขึ้น

  • การผสมสีในขอบเขตสีกว้างหรือเชิงเส้นจะเปลี่ยนเส้นทางทางคณิตศาสตร์ที่แท้จริงของการไล่ระดับสีนี้ ซึ่งจะช่วยป้องกันไม่ให้เกิดสีเทาตรงกลางและแถบสีที่ชัดเจนซึ่งอาจเกิดขึ้นได้เมื่อคุณทำโดยไม่มีขอบเขตสีกว้าง

  • เมื่อใช้ Compose 1.12 คุณจะได้รับประโยชน์จากการผสมผสานการไล่ระดับสีที่ราบรื่นยิ่งขึ้นบนจอแสดงผลทุกประเภท รวมถึงการรองรับขอบเขตสีกว้างที่แท้จริงเมื่อใช้งานบนฮาร์ดแวร์ที่รองรับ

  • หากต้องการเปิดใช้การรองรับที่ถูกต้องในอุปกรณ์ คุณควรตรวจสอบว่าได้ตั้งค่ากิจกรรมหรือหน้าต่างให้เปิดใช้โหมดขอบเขตสีกว้างแล้ว

  • ซึ่งทำได้ใน AndroidManifest

    <!-- AndroidManifest.xml -->
    <activity
    android:name=".NavActivity"
    android:colorMode="wideColorGamut"
    android:exported="true">
    </activity>
  • หรือแบบเป็นโปรแกรมใน Composable

    // Dynamic toggle in code
    DisposableEffect(window) {
       window?.colorMode = ActivityInfo.COLOR_MODE_WIDE_COLOR_GAMUT
       onDispose {
           window?.colorMode = ActivityInfo.COLOR_MODE_DEFAULT
       }
    }
  • หากไม่ได้ตั้งค่า colorMode หน้าต่างจะยังคงอยู่ใน sRGB แบบ 8 บิตมาตรฐาน และสีขอบเขตกว้างที่แสดงผลโดย GPU จะถูกตัดออก

  • ดังนั้น คุณจึงควรหลีกเลี่ยงการใช้สีที่มีขอบเขตสีกว้างหากจอแสดงผลไม่รองรับ

  • คุณควรเปิดใช้ขอบเขตสีกว้างในกิจกรรมหรือเส้นทางโค้ดที่แสดงสื่อ รูปภาพ หรือกราฟิกที่สมบูรณ์เท่านั้นเพื่อประหยัดแบตเตอรี่และประสิทธิภาพ

  • ซึ่งหมายความว่าคุณสามารถสร้างสีในขอบเขตสีต่างๆ ใน Compose ได้ และตอนนี้ระบบจะใช้สีเหล่านี้

    // 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
    )
  • คุณสามารถส่งผ่านประเภท colorSpace ที่ต้องการสร้างสี และหากไม่ส่งผ่าน colorSpace ระบบจะใช้ sRGB มาตรฐาน

  • สำหรับบิตแมปโดยเฉพาะ คุณสามารถตรวจสอบแหล่งที่มาของบิตแมปได้โดยใช้ getColorSpace และไลบรารีการโหลดรูปภาพยอดนิยมจะจัดการขอบเขตสีบิตแมปที่ถูกต้องให้คุณโดยอัตโนมัติ

  • คุณควรใช้ Bitmap.Config.HARDWARE เพื่อรักษาข้อมูลสี เนื่องจากการกำหนดค่าอย่าง 565 จะทำให้ความแม่นยำลดลงและมีแถบสีที่ผู้ใช้มองเห็นได้

  • หากต้องการดูข้อมูลเพิ่มเติมเกี่ยวกับสี คุณสามารถย้อนเวลากลับไปในปี 2017 เพื่อดู Romain Guy พูดถึงเรื่องสีทั้งหมดใน Compose หรือเกี่ยวกับสีใน Android

รูปร่างหลายเหลี่ยมและการเปลี่ยนรูปร่าง

  • ใน Compose 1.13 เราขอแนะนำการรองรับรูปทรงหลายเหลี่ยมแบบเนทีฟ

  • ซึ่งจะช่วยให้การสร้างรูปร่างที่กำหนดเองและองค์ประกอบ UI ที่ดูเป็นธรรมชาติง่ายขึ้นมาก

  • ก่อนหน้านี้ หากต้องการวาดเรขาคณิตที่ซับซ้อนเช่นนี้ คุณต้องใช้ RoundedPolygon เพื่ออธิบายเรขาคณิตที่จะแสดงผลเป็นเส้นทาง แล้วแมปเส้นทางนั้นกับ API รูปร่างของ 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)
        )
    }
  • เช่น ขั้นแรก คุณกำหนดสัดส่วนโดยใช้ RoundedPolygon และเราตั้งค่าในข้อมูลโค้ดนี้ เราตั้งค่าจุดด้านในให้อยู่ที่ 84% จากจุดศูนย์กลางเมื่อเทียบกับจุดด้านนอก

  • คุณยังใช้การลบมุมได้ที่นี่ แต่จะอธิบายเฉพาะเส้นโค้งทางคณิตศาสตร์เท่านั้น

  • หากต้องการใช้รูปหลายเหลี่ยมนี้สำหรับการคลิปหรือพื้นหลังใน Compose คุณจะต้องเขียน Wrapper รูปร่างที่กำหนดเองดังนี้

    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)
        }
    }
  • และภายในข้อมูลโค้ด createOutline นี้ คุณจะเห็นว่าคุณต้องสร้างเส้นทางและปรับขนาดและแปลด้วยตนเอง

  • แต่ตอนนี้เมื่อมี PolygonShape API ใหม่ คุณไม่จำเป็นต้องใช้ Wrapper รูปร่างที่กำหนดเองนั้นอีกต่อไป

    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)
  • เนื่องจากเป็นเครื่องมือสร้าง Compose Shape โดยเฉพาะ จึงรับความหนาแน่นและขนาดได้โดยตรงตั้งแต่แกะกล่อง

  • ซึ่งหมายความว่าคุณสามารถคำนวณจุดยอดแบบไดนามิกและระบุการปัดเศษมุมเป็น dp หรือแม้แต่เป็นเปอร์เซ็นต์ได้โดยตรง

  • เมื่อกำหนดแล้ว คุณก็เพียงแค่ส่งไปยัง API ใดก็ได้ที่ใช้ Shape

  • นอกจากนี้ เรายังเปิดตัว MorphPolygonShape API ซึ่งช่วยให้คุณเปลี่ยนระหว่างรูปทรงหลายเหลี่ยมต่างๆ ได้อย่างราบรื่น

    val recordShape = remember {
        MorphPolygonShape(
            start = PolygonShape.circle(),
            end = PolygonShape.star(
                numPoints = 9,
                innerRadiusRatio = 0.8f,
                outerRounding = CornerRounding(percent = 50),
            ),
        ) { shapeProgress.value }
    }
  • ในตัวอย่างนี้ เรามีปุ่มบันทึกที่เปลี่ยนจากวงกลมเป็นดาวเมื่อคุณโต้ตอบด้วย

  • คุณเพียงแค่ระบุรูปร่างเริ่มต้น รูปร่างสิ้นสุด และค่าความคืบหน้า

  • API MorphPolygonShape จะอ่านค่าความคืบหน้าในระหว่างขั้นตอนการแก้ไขโครงร่าง ซึ่งหมายความว่าจะไม่อ่านในระหว่างการจัดองค์ประกอบใหม่หรือการจัดองค์ประกอบ

  • ซึ่งหมายความว่าเมื่อรูปร่างเปลี่ยนไปในแต่ละเฟรม รูปร่างจะเปลี่ยนเป็นโครงร่างใหม่โดยไม่ทริกเกอร์การจัดองค์ประกอบใหม่

เอฟเฟกต์การเรนเดอร์การเบลอและฉากหลัง

  • ต่อไปเรามาพูดถึงการเบลอกัน

  • ในช่วง 2-3 รุ่นที่ผ่านมา เราพยายามทำให้การใช้การเบลอทำได้ง่ายขึ้นและสื่อความหมายได้มากขึ้น

  • เมื่อใดก็ตามที่เราพูดถึงการเบลอ นี่คือตัวแก้ไขที่ใช้กันโดยทั่วไปที่เราเห็น

    Modifier
       .blur(
           radiusX = 10.dp,
           radiusY = 10.dp,
           edgeTreatment = BlurredEdgeTreatment(
               shape = RoundedCornerShape(8.dp),
           ),
       )
  • Compose มีตัวแก้ไขนี้ตั้งแต่ Android 12

  • ซึ่งเหมาะอย่างยิ่งเมื่อคุณต้องการการเบลอที่สม่ำเสมอ โดยใช้รัศมีการเบลอเดียวกันตลอดทั้ง Composable

  • แต่การออกแบบ UI สมัยใหม่มักต้องการการเปลี่ยนผ่านที่ราบรื่นจากบริเวณที่คมชัดไปยังบริเวณที่เบลอ แทนที่จะเป็นการตัดขอบที่ชัดเจน

  • และนั่นก็นำเราไปสู่ Progressive Blur ซึ่งตอนนี้เป็น API อย่างเป็นทางการแล้ว โดยเริ่มตั้งแต่ Compose 1.13

  • ตอนนี้คุณสามารถปรับความเข้มของภาพเบลอในพื้นที่ต่างๆ ได้ แทนที่จะใช้รัศมีคงที่เดียวทั่วทั้ง Composable

  • นี่คือตัวอย่างจากแอป JetChat จากหน้าจอโปรไฟล์ผู้ใช้

  • ตัวแก้ไขการเบลอใหม่จะใช้ข้อกำหนดรัศมีการเบลอ

    Modifier.blur {
    blurRadiusSpec = BlurRadiusSpec.verticalGradient(
    listOf(
    BlurStop(0.5f, 0.dp),
    BlurStop(1.0f, 32.dp),
    )
    )
    edgeTreatment = BlurredEdgeTreatment.Unbounded
    }
  • ในที่นี้เราใช้การไล่ระดับสีแนวตั้งที่มีจุดเบลอ 2 จุด โดยให้ครึ่งบนของรูปภาพคมชัดอย่างสมบูรณ์ที่ 0 dp และเพิ่มความเบลอเป็น 32 dp ที่ขอบด้านล่าง

  • โปรดทราบว่าเรายังตั้งค่า edgeTreatment เป็นแบบไม่จำกัดด้วย ซึ่งโดยพื้นฐานแล้วจะบอกให้เบลอเพื่อสุ่มตัวอย่างพิกเซลโปร่งใสนอกขอบเขตที่ประกอบได้ด้วย

  • นอกเหนือจากการเฟดออกในแนวตั้งแล้ว ข้อกำหนดรัศมีการเบลอนี้ยังมีวิธีต่างๆ ในตัวเพื่อสร้างการเบลอแบบต่อเนื่อง

  • ตัวอย่างเช่น ทางด้านซ้าย คุณจะเห็นการไล่ระดับสีในแนวนอน โดยรัศมีเริ่มต้นคือ 0 dp และรัศมีสิ้นสุดจะขึ้นอยู่กับค่าแถบเลื่อน

    BlurRadiusSpec.horizontalGradient(
    startRadius = 0.dp,
    endRadius = sliderValue.dp,
    )
  • และทางด้านขวา เรามีการไล่ระดับสีแบบรัศมี โดยที่ตรงกลาง รัศมีการเบลอคือ 0 dp และเมื่อคุณเลื่อนไปยัง fallOffRadius ซึ่งในกรณีนี้คือ 300 dp การเบลอจะค่อยๆ เพิ่มขึ้นอย่างราบรื่นจนถึง 24 dp

    BlurRadiusSpec.radialGradient(
    center = center,
    fallOffRadius = 300.dp,
    startRadius = 0.dp,
    endRadius = 24.dp,
    )
  • และหากการไล่ระดับสีแนวตั้ง รัศมี และแนวนอนทั้งหมดนี้ยังไม่เพียงพอ คุณก็สามารถใช้ Shaders ที่กำหนดเองเพื่อสร้างการเบลอแบบโปรเกรสซีฟได้เสมอ

    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
       },
  • ในที่นี้ เราจะเขียน Fragment Shader ขนาดเล็กที่คำนวณคลื่นไซน์แนวทแยงแบบเคลื่อนไหว และแสดงผลความเข้มนั้นลงในช่องอัลฟ่า

  • จากนั้นข้อกำหนดของเฉดสีจะแมปมาสก์ 0 ถึง 1 นั้นกับมาสก์ 24 dp โดยตรง maxRadius

  • ก่อนที่เราจะพูดถึงการเบลอพื้นหลัง มาดูความแตกต่างระหว่างทั้ง 2 แบบกันก่อน

  • ทางด้านซ้าย เรามีแถบแอปด้านบนที่ใช้การเบลอเลเยอร์

  • กล่าวคือจะเบลอ Composable เอง

  • อย่างที่คุณเห็น ชื่อและไอคอนการดำเนินการเบลอ แต่องค์ประกอบแชทที่อยู่ด้านหลังไม่เบลอเลย

  • ทางด้านขวามือ เรามีการเบลอพื้นหลัง

  • ในกรณีนี้ ระบบจะสุ่มตัวอย่างและเบลอเฉพาะข้อความแชทที่เลื่อนอยู่ด้านหลังแถบด้านบนเท่านั้น

  • อย่างไรก็ตาม ข้อความและไอคอนมีความคมชัดและอ่านได้ง่าย

  • ตั้งแต่ Android 37.2 เป็นต้นไป คุณสามารถเบลอพื้นหลังได้โดยใช้ API ใหม่ที่ชื่อว่า RenderNode.setBackdropRenderEffect

  • แต่ก่อนที่จะเจาะลึกถึงวิธีปรับปรุงการเบลอ เรามาทำความเข้าใจวิธีการทำงานของ Android เบื้องหลังกันก่อนเพื่อให้มั่นใจว่าการเบลอจะมีประสิทธิภาพ

  • Android ใช้ RenderNode เบื้องหลัง ซึ่งเป็นคอนเทนเนอร์รายการแสดงผลที่เร่งด้วยฮาร์ดแวร์ ซึ่งหมายความว่าสามารถบีบอัดและจัดเก็บคำสั่งการวาดต่างๆ จำนวนมากผ่านการเริ่มบันทึกและสิ้นสุดการบันทึก

  • RenderNode จะเก็บข้อมูลต่างๆ เช่น การแปล 3 มิติ การหมุน อัลฟา ขนาด และขอบเขตตำแหน่ง

  • และ Compose ใช้ RenderNode เป็นส่วนประกอบพื้นฐานในการทำงาน

  • ดังนั้น หากต้องการใช้เอฟเฟกต์การแสดงผลฉากหลังใน Compose เราสามารถสร้าง Modifier.Node ที่กำหนดเองเพื่อให้ใช้ตัวแก้ไขนี้ซ้ำได้

    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()
       }
    }
  • เราเก็บข้อมูลอ้างอิงไปยัง RenderNode และ RenderNode ได้รับการกำหนดค่าด้วยเอฟเฟกต์ที่เราต้องการในฟังก์ชันอื่น

  • ตัวอย่างเช่น เราส่งเอฟเฟกต์เบลอเข้าไปที่นี่

  • จากนั้นเราจะเรียกใช้ drawIntoCanvas โดยเรียกใช้ drawRenderNode ใน Canvas แบบเนทีฟ

  • และสุดท้าย เราจะเรียก drawContent เพื่อวาดเนื้อหาจริงที่ไม่เบลอไว้ด้านบนด้วย

  • หากต้องการใช้ตัวแก้ไขที่เราเพิ่งสร้างขึ้น เราจะเรียก Modifier.backdropBlur และส่งข้อมูลจำเพาะของรัศมีการเบลอ

    JetchatAppBar(
        modifier = modifier
            .backdropBlur(
                tint = MaterialTheme.colorScheme
                    .surface.copy(alpha = 0.5f),
                shape = RectangleShape,
                spec = BlurRadiusSpec.uniform(5.dp)
            )
    )
  • โปรดทราบว่าเราไม่จำเป็นต้องวางสิ่งใดๆ บนเลเยอร์ที่อยู่ด้านล่างด้วยวิธีนี้

  • RenderNode.setBackdropRenderEffect กำลังสุ่มตัวอย่างเนื้อหาที่อยู่ใต้แถบแอป JetChat โดยอัตโนมัติ

  • และผลลัพธ์ที่ได้คือการเบลอฉากหลังที่สวยงามนี้

  • นอกจากนี้ เรายังกำหนดข้อกำหนดรัศมีการเบลอแบบต่อเนื่องแทนที่จะเป็นแบบต่อเนื่องเพียงอย่างเดียวได้ด้วย และเพื่อให้ได้การเบลอแนวตั้งในเนื้อหาด้วย

    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),
                    ),
                )
            )
    )
  • ซึ่งการไล่ระดับสีแนวตั้งจะแสดงเนื้อหาด้านล่างที่เบลอด้วยรัศมีที่แตกต่างกันในสัดส่วนต่างๆ ของคอนเทนเนอร์

  • เรากำลังพัฒนาตัวแก้ไขการเบลอฉากหลังในตัวใน Compose

  • และนี่เป็นเพียงตัวอย่างหนึ่งของเทคโนโลยีพื้นฐานใน Android ที่ใช้ Compose

  • ตัวอย่างที่แสดงให้คุณเห็นนี้จะทำงานได้ในระดับ API 37.2 เท่านั้น แต่เรากำลังพัฒนา Backport สำหรับ Compose ด้วย โปรดติดตาม

  • ตัวอย่างสนุกๆ อีกตัวอย่างหนึ่งของเอฟเฟกต์ฉากหลัง RenderNode คือเราสามารถเชื่อมโยงเอฟเฟกต์เข้าด้วยกันเพื่อสร้าง UI ที่ดูดีมากๆ และใช้เอฟเฟกต์ต่างๆ หลายรายการซ้อนกันได้

  • ดังนั้นหากต้องการเชื่อมโยงเอฟเฟกต์ เราจะสร้างเอฟเฟกต์ 2 รายการนี้

    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)
  • ในตัวอย่างนี้ เราสร้างเอฟเฟกต์เบลอและความอิ่มตัว 30%

  • เราเรียกใช้ createChainEffect โดยใช้เอฟเฟกต์เบลอก่อน แล้วจึงใช้ความอิ่มตัวของสีในภายหลัง

  • นี่คือตัวอย่างใน JetChat

    val agslShader = RuntimeShader(CHROMATIC_ABERRATION)
    val noiseEffect = RenderEffect.createRuntimeShaderEffect(agslShader, "backdropInput")
    
    val finalEffect = RenderEffect.createChainEffect(noiseEffect, vibrantBlur)
    renderNode?.setBackdropRenderEffect(finalEffect)
  • ปุ่มข้ามไปยังด้านล่าง ซึ่งเราสามารถทำให้ดูเหมือนแว่นขยายขนาดเล็กได้โดยการรวมเอฟเฟกต์ต่างๆ เข้าด้วยกันเพื่อทำให้ขอบของ Composable นี้โค้งมนเหมือนแว่นขยาย

  • แต่คุณไม่เพียงใช้เอฟเฟกต์การขยายได้เท่านั้น แต่ยังใช้เฉดสีประเภทใดก็ได้ที่เชื่อมโยงเข้าด้วยกันเพื่อใช้กับฉากหลังได้ด้วย

  • ต่อไปนี้คือตัวอย่างสนุกๆ ที่เราได้นำมาใช้เพื่อแสดงให้เห็นถึงสิ่งที่เป็นไปได้เมื่อนำเทคโนโลยีเหล่านี้มารวมกัน

  • ตอนนี้คุณสามารถใช้เอฟเฟกต์แบบกระจกฝ้าแบบดั้งเดิมได้แล้ว

  • แต่เรายังทำเอฟเฟกต์สนุกๆ อื่นๆ ได้ด้วย เช่น กล้องสลับลาย

  • หรือหากเราต้องการเปลี่ยนมาสคอต Android นี้ให้เป็นองค์ประกอบ UI ที่เปลี่ยนรูปแบบได้ซึ่งจะเปลี่ยนเนื้อหาที่อยู่ด้านล่าง เราก็สามารถใช้เอฟเฟกต์การแสดงผลฉากหลังเหล่านี้เพื่อให้ดูเหมือนว่าเรากำลังมองผ่านหุ่นยนต์ตัวน้อยนี้ได้ด้วย

  • และเรายังทำแบบนี้ได้ด้วยเอฟเฟกต์การแสดงผลฉากหลัง

  • ดังนั้น API ใหม่นี้จึงไม่เพียงแต่ปลดล็อกการเบลอฉากหลังและกระจกฝ้าเท่านั้น แต่ตอนนี้ขีดจำกัดเพียงอย่างเดียวคือจินตนาการของคุณ

  • API ใหม่อีกรายการที่เราได้เปิดตัวในแพลตฟอร์มคือ SurfaceView.setBlurRegions

  • ปัจจุบันคุณสามารถเบลอมุมมองพื้นผิวและ Composable ใดๆ โดยใช้เอฟเฟกต์ฉากหลัง Modifier.blur และ RenderNode แต่การเบลอบางส่วนของ SurfaceView นั้นทำได้ยาก และไม่ได้เป็นส่วนหนึ่งของต้นไม้ RenderNode ของมุมมองแอป เนื่องจากเป็นการเจาะรูผ่านหน้าต่างแทน

  • ดังนั้น Compose จึงไม่สามารถสุ่มตัวอย่างบัฟเฟอร์สำรองของ SurfaceView ได้ ซึ่งทำให้ก่อนหน้านี้ไม่สามารถทำได้จนกว่าจะมี API 37.2

  • ดังนั้น setBlurRegions ของ SurfaceView จึงช่วยให้แอปประกาศขอบเขตการเบลอได้โดยตรง

  • ตัวอย่างเช่น ในวิดีโอเพลเยอร์นี้ใน JetChat เราได้เพิ่มการเบลอ SurfaceView เมื่อตัวควบคุมแสดงและซ่อน

  • หากต้องการใช้ API ใหม่นี้ คุณสามารถเข้าถึง setBlurRegions ในเมธอด 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
  • ซึ่งจะใช้รายการพื้นที่ของขอบเขต รัศมีการเบลอ อัลฟ่า และรายการรัศมีมุมในนั้น

  • เมื่อส่งขอบเขตเหล่านี้ ขอบเขตต้องสัมพันธ์กับ SurfaceView บนหน้าจอ

  • ดังนั้น คุณจะต้องติดตามพิกัดของ SurfaceView และพื้นที่ที่ต้องการเบลอ

RuntimeColorFilter

  • อีกหนึ่งสิ่งใหม่จาก Android 16 ขึ้นไปคือ RuntimeColorFilter

  • ก่อนหน้านี้ คุณทำได้เพียงตั้งค่าฟิลเตอร์สีเมทริกซ์แบบคงที่และใช้เอฟเฟกต์ต่างๆ เช่น ความอิ่มตัวของสีกับรูปภาพ

  • อย่างไรก็ตาม ตั้งแต่ Android 16 ขึ้นไป คุณสามารถส่ง Shader ที่ใช้สีอินพุต ซึ่งเป็นพิกเซลอินพุตสำหรับเนื้อหานั้นๆ และสร้างสีใหม่ได้

  • ตัวอย่างเช่น หากต้องการใช้ฟิลเตอร์ไซเบอร์พังก์กับรูปภาพ เราสามารถกำหนดเฉดสีขนาดเล็กที่ใช้เป็นอินพุตใน 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);
        }
    """
  • เฉดสีนี้จะสร้างธีมไซเบอร์พังก์ แต่คุณสามารถสร้างเอฟเฟกต์อื่นๆ ที่ต้องการได้

  • ดังนั้น หากต้องการใช้ตัวปรับเฉดสี เราจะใช้ RuntimeColorFilter และแปลงเป็นฟิลเตอร์สี Compose

    AsyncImage(
        model = imageUri,
        contentDescription = filter.displayName,
        colorFilter = RuntimeColorFilter(CYBERPUNK_SHADER)
                       .asComposeColorFilter(),
        contentScale = ContentScale.Crop,
        modifier = Modifier.fillMaxSize(),
    )
  • ดังที่เห็นในตัวอย่างนี้ ตอนนี้เราสามารถสร้างเอฟเฟกต์แบบไดนามิกได้มากกว่าเดิมโดยไม่ต้องเจาะลึกเรื่อง OpenGL, การตั้งค่า SurfaceView หรืออะไรทำนองนั้น

  • และยังใช้กับ Composable ใดก็ได้ ไม่ใช่แค่รูปภาพเท่านั้น

การเพิ่มประสิทธิภาพกราฟิก

  • ขอขอบคุณ

  • เราได้สร้างเอฟเฟกต์ที่สวยงามมาก แต่ก็ต้องตรวจสอบว่าแอปยังคงทำงานได้อย่างรวดเร็วและราบรื่น

  • เมื่อเราเลื่อนดูการสนทนา เอฟเฟกต์ทั้งหมดจะทำงานร่วมกัน

  • มาเริ่มด้วยการวัดการเลื่อนนี้กัน

    @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()
        }
    //...
  • เราใช้การเปรียบเทียบมาโครที่เปิดแอปในกระบวนการใหม่และเลื่อนดูการสนทนา

  • จากนั้นเราจะค้นหาการติดตามเพื่อนับเฟรมที่พลาดกำหนดเวลา

    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%'
  • ซึ่งทำให้เรามีเฟรมที่ล่าช้า 6 เฟรม

  • มาเปิดเฟรมที่ล่าช้าเฟรมหนึ่งกัน

  • ดังนั้นก่อนเฟรมนี้ Compose จะดึงข้อมูลข้อความถัดไปล่วงหน้าก่อนที่จะเลื่อนขึ้นมาในมุมมอง

  • การดึงข้อมูลล่วงหน้าควรจะพอดีกับช่องว่างสั้นๆ ระหว่างเฟรม แต่การเตรียมข้อความนี้ยังถอดรหัสรูปภาพลงในเทรดหลักโดยตรง ซึ่งทำงานเป็นเวลานานโดยไม่หยุดพัก

  • เนื่องจากเทรดหลักติดขัดในการถอดรหัสรูปภาพนี้ จึงไม่สามารถเริ่มเฟรมใหม่ได้

  • จากนั้นเมื่อข้อความนั้นเลื่อนเข้ามาในมุมมองจริงๆ RenderThread จะต้องจัดสรรหน่วยความจำและอัปโหลดพิกเซลทั้งหมดเหล่านั้นไปยัง GPU ดังนั้นรูปภาพเดียวกันจึงทำให้เฟรมที่ 2 ล่าช้า

  • ตอนนี้เราเห็นแล้วว่าเวลาของเราหายไปไหน แต่เราจำเป็นต้องทำงานทั้งหมดนี้เพื่อวาดเฟรมนี้ไหม

  • และเราจะรู้ได้อย่างไร

  • การติดตามจะบอกเรา

  • เมื่อใดก็ตามที่มีการวาดบิตแมปขนาดใหญ่ลงในพื้นที่ขนาดเล็กกว่ามากบนหน้าจอ Android จะบันทึกเครื่องหมายลงในการติดตามโดยตรง

  • โดยจะแสดงตัวเลข 2 ตัวเป็นพิกเซล ได้แก่ ขนาดบิตแมปแบบเต็ม และพื้นที่ที่ครอบคลุมบนหน้าจอจริงๆ

  • หารด้วยความหนาแน่นของหน้าจอ แล้วคุณจะได้ค่าเป็น DP

  • จากนั้นเราจะค้นหาโค้ดด้วยค่า DP นั้นเพื่อค้นหารูปภาพดังกล่าว

  • นี่คือสิ่งที่เราพบ

  • ระบบโหลดรูปภาพด้วย painterResource ซึ่งไม่ทราบขนาดของคอนเทนเนอร์ จึงถอดรหัสรูปภาพทั้งหมดที่ความละเอียดเต็ม

    Image(
        painter = painterResource(it),
        contentScale = ContentScale.Fit,
        modifier = Modifier.size(160.dp),
        contentDescription = stringResource(id = R.string.attached_image),
    )
  • เราจึงแทนที่ด้วย AsyncImage จากขดลวด

    AsyncImage(
        model = it,
        contentScale = ContentScale.Fit,
        modifier = Modifier.size(160.dp),
        contentDescription = stringResource(id = R.string.attached_image),
    )
  • โดยจะวัดคอนเทนเนอร์ก่อน เพื่อให้ถอดรหัสเฉพาะพิกเซลที่ต้องการ และดำเนินการดังกล่าวในชุดข้อความอื่น

  • เมื่อเรียกใช้การเปรียบเทียบเดียวกันอีกครั้ง การถอดรหัสรูปภาพในเทรดหลักลดลงเหลือ 0 และเฟรมที่ล่าช้าลดลงเหลือ 2

  • เราจะไม่แก้ไขข้อบกพร่องของทั้ง 2 เฟรมนี้ที่นี่ แต่ประเด็นสำคัญนั้นง่ายมาก นั่นคือพยายามอย่าคำนวณพิกเซลที่จะไม่ไปถึงเฟรม

  • ดังนั้น การถอดรหัสรูปภาพจึงมีขนาดใหญ่พอที่จะทำให้เฟรมล่าช้า แต่ไม่ใช่ว่าต้นทุนทุกอย่างจะแสดงเป็นเฟรมที่ถูกข้าม และยังมีต้นทุนที่น้อยกว่าซึ่งเราสามารถหลีกเลี่ยงได้ขณะเขียนโค้ด

  • มาดู 2 ตัวอย่างกัน

  • เมื่อใดก็ตามที่เราใช้ RuntimeShader เราควรสร้างเพียงครั้งเดียว เพราะแม้ว่าจะคอมไพล์และแคชเท่านั้น แต่ก็ยังคงแยกวิเคราะห์สตริงต้นทางทุกครั้งที่เราสร้าง

  • และเมื่อใดก็ตามที่เอฟเฟกต์ทำให้ทุกเฟรมเคลื่อนไหว เราสามารถรวมเอฟเฟกต์นั้นไว้ใน graphicsLayer เพื่อให้ระบบบันทึกเฉพาะองค์ประกอบที่เคลื่อนไหวอีกครั้ง แทนที่จะวาด UI รอบๆ องค์ประกอบนั้นใหม่

  • สำหรับปุ่มข้ามไปด้านล่าง ปุ่มนี้จะปรากฏขึ้นทุกครั้งที่เราเลื่อน

  • ซึ่งใช้ RuntimeShader 2 ตัวเพื่อแสดงเอฟเฟกต์นั้น และทุกครั้งที่ปุ่มปรากฏขึ้น เราจะสร้างทั้ง 2 ตัวขึ้นมาใหม่ โดยสร้างเฉดสีทั้ง 2 ตัวนี้จากสตริงต้นทาง

    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 {
  • การแยกวิเคราะห์เฉดสีทั้ง 2 รายการและการสร้างเอฟเฟกต์จะใช้เวลาหลายร้อยไมโครวินาทีในเทรดหลักทุกครั้งที่ปุ่มปรากฏขึ้น

  • แต่เราจะสร้างเฉดสีเพียงครั้งเดียวสำหรับทั้งกระบวนการ หรือเราจะจดจำเฉดสีนั้นไว้ แล้วอัปเดตตัวแปร Uniform ของเฉดสีนั้น

    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) }
  • การเปลี่ยนแปลงดังกล่าวทำให้การสร้างเอฟเฟกต์เร็วขึ้นเกือบ 40 เท่า และต้นทุนเมื่อปุ่มปรากฏลดลง 20 เท่า

  • ในแสงเรืองรองแบบไล่ระดับสีที่เห็นก่อนหน้านี้ แสงเรืองรองจะเคลื่อนไหวทุกเฟรมภายใน 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)
    }
  • ดังนั้นการอ่านสถานะในเฟสการวาดจะคงองค์ประกอบและเลย์เอาต์ไว้ แต่รายการที่แสดงจะอัปเดตคำสั่งการวาดรายการเดียวในตำแหน่งเดิมไม่ได้

  • ดังนั้น Compose จึงเดินขึ้นไปตามโครงสร้างเพื่อค้นหาโหนดที่ใกล้ที่สุดที่มีเลเยอร์กราฟิก ซึ่งในที่นี้คือเส้นขอบสีแดงซึ่งเป็นทั้งหน้าจอ และบันทึกทั้งเลเยอร์นั้นซ้ำทุกเฟรม

  • การใส่ graphicsLayer ไว้บนกล่องโดยตรงจะทำให้การเดินทางหยุดที่เส้นขอบสีเขียว ดังนั้นระบบจะบันทึกเฉพาะกล่องเรืองแสงซ้ำ และส่วนการวาดเทรดหลักจะลดลง 2 ใน 10 ของมิลลิวินาที

    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)
    }
  • Perfetto อาจวิเคราะห์สิ่งที่ผิดพลาดในการติดตามได้ยาก คุณจึงใช้ทักษะการวิเคราะห์การติดตามเพื่อช่วยรวบรวมข้อมูลเชิงลึกได้

  • และหากต้องการความช่วยเหลือเพิ่มเติมในการเขียนโค้ด Compose ที่มีประสิทธิภาพ โปรดติดตั้ง Android CLI และใช้ทักษะด้านประสิทธิภาพของ Compose เพื่อเริ่มต้นใช้งาน

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

สรุปและแหล่งข้อมูล

  • หากต้องการดูตัวอย่างที่เราสาธิตในวันนี้ ให้ดูที่แอป JetChat ซึ่งมีให้ดาวน์โหลดที่ลิงก์นี้

รหัส

ข้อมูลโค้ด

ข้อมูลโค้ด Kotlin ที่ 02:47 ใน "Mesh gradients and custom mesh rendering"
val gradientPainter = remember {
MeshGradientPainter(rows = 3, columns = 3) {
}
}
ข้อมูลโค้ด Kotlin ที่ 02:55 ใน "การไล่ระดับสีแบบเมชและการแสดงผลเมชที่กำหนดเอง"
val gradientPainter = remember {
MeshGradientPainter(rows = 3, columns = 3) {
   setVertex(0, 0, Offset(0.0f, 0.0f), indigo)

}
}
ข้อมูลโค้ด Kotlin ที่ 03:05 ใน "การไล่ระดับสีแบบเมชและการแสดงผลเมชที่กำหนดเอง"
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)
)
ข้อมูลโค้ด Kotlin ที่ 03:24 ใน "การไล่ระดับสีแบบเมชและการแสดงผลเมชที่กำหนดเอง"
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
}
ข้อมูลโค้ด Kotlin ที่นาที 04:09 ใน "การไล่ระดับสีแบบเมชและการแสดงผลเมชที่กำหนดเอง"
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",
)
ข้อมูลโค้ด Kotlin ที่ 04:33 ใน "การไล่ระดับสีแบบเมชและการแสดงผลเมชที่กำหนดเอง"
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])
       }
   }
}
ข้อมูลโค้ด Kotlin ที่ 05:40 ใน "Mesh gradients and custom mesh rendering"
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,
    )
//...
}
ตัวอย่างข้อความที่ 07:03 ใน "Mesh gradients and custom mesh rendering"
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;
}
ตัวอย่างข้อความที่ 08:01 ใน "Mesh gradients and custom mesh rendering"
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;
}
ข้อมูลโค้ด Kotlin ที่นาที 08:37 ใน "Mesh gradients and custom mesh rendering"
val spec = remember {
    MeshSpecification.make(
        arrayOf(Attribute(TYPE_FLOAT2, 0, "corner")),
        vertexStride = 8,
        arrayOf(Varying(TYPE_FLOAT3, "stroke")),
        LineShaders.VERTEX, LineShaders.FRAGMENT)
}
ข้อมูลโค้ด Kotlin ที่ 09:06 ใน "Mesh gradients and custom mesh rendering"
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
}
ข้อมูลโค้ด Kotlin ที่ 09:18 ใน "การไล่ระดับสีแบบเมชและการแสดงผลเมชที่กำหนดเอง"
val mesh = Mesh(
    spec,
    mode = Mesh.TRIANGLE_STRIP,
    vertexBuffer = corners,
    vertexCount = (segments + 1) * 2,
    bounds = RectF(0f, 0f, size.width, size.height)
)
ข้อมูลโค้ด Kotlin ที่ 09:24 ใน "การไล่ระดับสีแบบเมชและการแสดงผลเมชที่กำหนดเอง"
Box(Modifier.fillMaxSize().drawWithCache {
   val mesh = Mesh(...)
   onDrawBehind {
       mesh.setFloatUniform("uTime", time.value)
       drawIntoCanvas {
          it.nativeCanvas.drawMesh(mesh,android.graphics.BlendMode.DST, paint)
       }
   }
})
ข้อมูลโค้ด XML ที่ 11:22 ใน "การรองรับช่วงสีกว้าง"
<!-- AndroidManifest.xml -->
<activity
android:name=".NavActivity"
android:colorMode="wideColorGamut"
android:exported="true">
</activity>
ข้อมูลโค้ด Kotlin ที่ 11:24 ใน "การรองรับขอบเขตสีกว้าง"
// Dynamic toggle in code
DisposableEffect(window) {
   window?.colorMode = ActivityInfo.COLOR_MODE_WIDE_COLOR_GAMUT
   onDispose {
       window?.colorMode = ActivityInfo.COLOR_MODE_DEFAULT
   }
}
ข้อมูลโค้ด Kotlin ที่ 11:49 ใน "การรองรับขอบเขตสีกว้าง"
// 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
)
ข้อมูลโค้ด Kotlin ที่นาที 12:45 ใน "รูปร่างหลายเหลี่ยมและการเปลี่ยนรูปร่าง"
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)
    )
}
ข้อมูลโค้ด Kotlin ที่นาที 13:15 ใน "รูปร่างหลายเหลี่ยมและการเปลี่ยนรูปร่าง"
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)
    }
}
ข้อมูลโค้ด Kotlin ที่นาที 13:30 ใน "รูปร่างหลายเหลี่ยมและการเปลี่ยนรูปร่าง"
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)
ข้อมูลโค้ด Kotlin ที่นาที 13:53 ใน "รูปร่างหลายเหลี่ยมและการเปลี่ยนรูปร่าง"
val recordShape = remember {
    MorphPolygonShape(
        start = PolygonShape.circle(),
        end = PolygonShape.star(
            numPoints = 9,
            innerRadiusRatio = 0.8f,
            outerRounding = CornerRounding(percent = 50),
        ),
    ) { shapeProgress.value }
}
ข้อมูลโค้ด Kotlin ที่นาที 14:37 ใน "เอฟเฟกต์การแสดงผลการเบลอและฉากหลัง"
Modifier
   .blur(
       radiusX = 10.dp,
       radiusY = 10.dp,
       edgeTreatment = BlurredEdgeTreatment(
           shape = RoundedCornerShape(8.dp),
       ),
   )
ข้อมูลโค้ด Kotlin ที่นาที 15:15 ใน "เอฟเฟกต์การเบลอและการแสดงผลฉากหลัง"
Modifier.blur {
blurRadiusSpec = BlurRadiusSpec.verticalGradient(
listOf(
BlurStop(0.5f, 0.dp),
BlurStop(1.0f, 32.dp),
)
)
edgeTreatment = BlurredEdgeTreatment.Unbounded
}
ข้อมูลโค้ด Kotlin ที่นาที 15:42 ใน "เอฟเฟกต์การเบลอและการแสดงผลฉากหลัง"
BlurRadiusSpec.horizontalGradient(
startRadius = 0.dp,
endRadius = sliderValue.dp,
)
ข้อมูลโค้ด Kotlin ที่ 15:53 ใน "เอฟเฟกต์การเบลอและการแสดงผลฉากหลัง"
BlurRadiusSpec.radialGradient(
center = center,
fallOffRadius = 300.dp,
startRadius = 0.dp,
endRadius = 24.dp,
)
ข้อมูลโค้ด Kotlin ที่ 16:07 ใน "เอฟเฟกต์การเบลอและการแสดงผลฉากหลัง"
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
   },
ข้อมูลโค้ด Kotlin ที่ 17:40 ใน "เอฟเฟกต์การเบลอและการแสดงผลฉากหลัง"
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()
   }
}
ข้อมูลโค้ด Kotlin ที่ 18:09 ใน "เอฟเฟกต์การเบลอและการแสดงผลฉากหลัง"
JetchatAppBar(
    modifier = modifier
        .backdropBlur(
            tint = MaterialTheme.colorScheme
                .surface.copy(alpha = 0.5f),
            shape = RectangleShape,
            spec = BlurRadiusSpec.uniform(5.dp)
        )
)
ข้อมูลโค้ด Kotlin ที่ 18:30 ใน "เอฟเฟกต์การแสดงผลการเบลอและฉากหลัง"
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),
                ),
            )
        )
)
ข้อมูลโค้ด Kotlin ที่นาที 19:10 ใน "เอฟเฟกต์การเบลอและการแสดงผลฉากหลัง"
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)
ข้อมูลโค้ด Kotlin ที่นาที 19:23 ใน "เอฟเฟกต์การเบลอและการแสดงผลฉากหลัง"
val agslShader = RuntimeShader(CHROMATIC_ABERRATION)
val noiseEffect = RenderEffect.createRuntimeShaderEffect(agslShader, "backdropInput")

val finalEffect = RenderEffect.createChainEffect(noiseEffect, vibrantBlur)
renderNode?.setBackdropRenderEffect(finalEffect)
ข้อมูลโค้ด Kotlin ที่นาที 20:53 ใน "เอฟเฟกต์การแสดงผลการเบลอและฉากหลัง"
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
ข้อมูลโค้ด Kotlin ที่นาที 21:33 ใน "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);
    }
"""
ข้อมูลโค้ด Kotlin ที่ 21:48 ใน "RuntimeColorFilter"
AsyncImage(
    model = imageUri,
    contentDescription = filter.displayName,
    colorFilter = RuntimeColorFilter(CYBERPUNK_SHADER)
                   .asComposeColorFilter(),
    contentScale = ContentScale.Crop,
    modifier = Modifier.fillMaxSize(),
)
ข้อมูลโค้ด Kotlin ที่เวลา 22:21 ใน "การเพิ่มประสิทธิภาพกราฟิก"
@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()
    }
//...
ข้อมูลโค้ด SQL ที่ 22:32 ใน "การเพิ่มประสิทธิภาพกราฟิก"
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%'
ข้อมูลโค้ด Kotlin ที่ 23:54 ใน "การเพิ่มประสิทธิภาพกราฟิก"
Image(
    painter = painterResource(it),
    contentScale = ContentScale.Fit,
    modifier = Modifier.size(160.dp),
    contentDescription = stringResource(id = R.string.attached_image),
)
ข้อมูลโค้ด Kotlin ที่นาที 24:02 ใน "การเพิ่มประสิทธิภาพกราฟิก"
AsyncImage(
    model = it,
    contentScale = ContentScale.Fit,
    modifier = Modifier.size(160.dp),
    contentDescription = stringResource(id = R.string.attached_image),
)
ข้อมูลโค้ด Kotlin ที่ 25:14 ใน "การเพิ่มประสิทธิภาพกราฟิก"
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 {
ข้อมูลโค้ด Kotlin ที่นาที 25:32 ใน "การเพิ่มประสิทธิภาพกราฟิก"
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) }
ข้อมูลโค้ด Kotlin ที่นาที 25:49 ใน "การเพิ่มประสิทธิภาพกราฟิก"
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)
}
ข้อมูลโค้ด Kotlin ที่ 26:15 ใน "การเพิ่มประสิทธิภาพกราฟิก"
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)
}
Shell snippet at 26:37 in "Graphics performance optimization"
android skills add perfetto-trace-analysis
android skills add jetpack-compose-performance