ดีไซน์ Material 2 ใน Compose

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

Jetpack Compose ใช้แนวคิดเหล่านี้กับ Composable MaterialTheme ดังนี้

MaterialTheme(
    colors = // ...
    typography = // ...
    shapes = // ...
) {
    // app content
}

กำหนดค่าพารามิเตอร์ที่คุณส่งไปยัง MaterialTheme เพื่อกำหนดธีมของแอปพลิเคชัน

ภาพหน้าจอ 2 ภาพที่แตกต่างกัน ภาพแรกใช้การจัดรูปแบบ MaterialTheme เริ่มต้น ส่วนภาพหน้าจอที่ 2 ใช้การจัดรูปแบบที่แก้ไขแล้ว
รูปที่ 1 ภาพหน้าจอแรกแสดงแอปที่ไม่ได้กำหนดค่า `MaterialTheme` จึงใช้การจัดรูปแบบเริ่มต้น ภาพหน้าจอที่ 2 แสดงแอปที่ส่งพารามิเตอร์ไปยัง `MaterialTheme` เพื่อปรับแต่งสไตล์

สี

สีได้รับการจำลองใน Compose ด้วยคลาส Color ซึ่งเป็นคลาสที่เก็บข้อมูล

val Red = Color(0xffff0000)
val Blue = Color(red = 0f, green = 0f, blue = 1f)

แม้ว่าคุณจะจัดระเบียบค่าเหล่านี้ได้ตามต้องการ (เป็นค่าคงที่ระดับบนสุด ภายใน Singleton หรือกำหนดแบบอินไลน์) แต่เราขอแนะนำให้ระบุสีใน ธีมและดึงสีจากที่นั่น วิธีนี้ช่วยให้รองรับธีมมืดและธีมที่ซ้อนกันได้

ตัวอย่างชุดสีของธีม
รูปที่ 2 ระบบสี Material

Compose มีคลาส Colors เพื่อสร้างโมเดลระบบสี Material Colors มีฟังก์ชัน Builder เพื่อสร้างชุดสีอ่อนหรือเข้ม ดังนี้

private val Yellow200 = Color(0xffffeb46)
private val Blue200 = Color(0xff91a4fc)
// ...

private val DarkColors = darkColors(
    primary = Yellow200,
    secondary = Blue200,
    // ...
)
private val LightColors = lightColors(
    primary = Yellow500,
    primaryVariant = Yellow400,
    secondary = Blue700,
    // ...
)

หลังจากกำหนด Colors แล้ว คุณจะส่งไปยัง MaterialTheme ได้โดยทำดังนี้

MaterialTheme(
    colors = if (darkTheme) DarkColors else LightColors
) {
    // app content
}

ใช้สีธีม

คุณเรียกข้อมูล Colors ที่ระบุให้กับ Composable MaterialTheme ได้โดยใช้ MaterialTheme.colors

Text(
    text = "Hello theming",
    color = MaterialTheme.colors.primary
)

สีพื้นผิวและเนื้อหา

คอมโพเนนต์หลายรายการยอมรับคู่สีและสีเนื้อหา

Surface(
    color = MaterialTheme.colors.surface,
    contentColor = contentColorFor(color),
    // ...
) { /* ... */ }

TopAppBar(
    backgroundColor = MaterialTheme.colors.primarySurface,
    contentColor = contentColorFor(backgroundColor),
    // ...
) { /* ... */ }

ซึ่งไม่เพียงแต่ช่วยให้คุณตั้งค่าสีของ Composable ได้เท่านั้น แต่ยังช่วยให้คุณระบุ สีเริ่มต้นสำหรับเนื้อหาและ Composable ที่อยู่ในนั้นได้ด้วย Composable หลายรายการ ใช้สีเนื้อหานี้โดยค่าเริ่มต้น เช่น Text จะอิงสีตามสีเนื้อหาขององค์ประกอบหลัก และ Icon จะใช้สีนั้นเพื่อกำหนดเฉดสี

ตัวอย่างแบนเนอร์เดียวกัน 2 รายการที่มีสีต่างกัน
รูปที่ 3 การตั้งค่าสีพื้นหลังที่แตกต่างกันจะทำให้ข้อความและไอคอนมีสีแตกต่างกัน

เมธอด contentColorFor() จะดึงสี "เปิด" ที่เหมาะสมสำหรับ สีธีมใดก็ได้ เช่น หากคุณตั้งค่าสีพื้นหลัง primary ใน Surface ฟังก์ชันนี้จะใช้เพื่อตั้งค่า onPrimary เป็นสีเนื้อหา หากตั้งค่าสีพื้นหลังที่ไม่ใช่ธีม คุณควรระบุ สีเนื้อหาที่เหมาะสมด้วย ใช้ LocalContentColor เพื่อดึงสีเนื้อหาที่ต้องการ สำหรับพื้นหลังปัจจุบัน ณ ตำแหน่งที่กำหนดในลำดับชั้น

เวอร์ชันอัลฟ่าของเนื้อหา

บ่อยครั้งที่คุณต้องการเปลี่ยนระดับการเน้นเนื้อหาเพื่อสื่อถึงความสำคัญ และจัดลำดับชั้นของภาพ คำแนะนำด้านความสามารถในการอ่านข้อความของ Material Design แนะนำให้ใช้ระดับความทึบแสงที่แตกต่างกัน เพื่อสื่อถึงระดับความสำคัญที่แตกต่างกัน

Jetpack Compose ใช้ LocalContentAlpha เพื่อดำเนินการนี้ คุณระบุ ค่าอัลฟ่าของเนื้อหาสำหรับลำดับชั้นได้โดยระบุค่าสำหรับ CompositionLocal นี้ Composable ที่ซ้อนกันจะใช้ค่านี้เพื่อใช้ การปรับอัลฟ่ากับเนื้อหาของตนได้ เช่น Text และ Icon จะใช้ชุดค่าผสมของ LocalContentColor ที่ปรับให้ใช้ LocalContentAlpha โดยค่าเริ่มต้น Material ระบุค่าอัลฟ่ามาตรฐานบางค่า (high, medium, disabled) ซึ่งสร้างแบบจำลองโดยออบเจ็กต์ ContentAlpha

// By default, both Icon & Text use the combination of LocalContentColor &
// LocalContentAlpha. De-emphasize content by setting content alpha
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
    Text(
        // ...
    )
}
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) {
    Icon(
        // ...
    )
    Text(
        // ...
    )
}

ดูข้อมูลเพิ่มเติมเกี่ยวกับ CompositionLocal ได้ที่ข้อมูลที่กำหนดขอบเขตในเครื่องด้วย CompositionLocal

ภาพหน้าจอของชื่อบทความที่แสดงระดับการเน้นข้อความต่างๆ
รูปที่ 4 ใช้ระดับการเน้นที่แตกต่างกันกับข้อความเพื่อสื่อสารลำดับชั้นของข้อมูลด้วยภาพ ข้อความบรรทัดแรกคือชื่อและมีข้อมูลที่สำคัญที่สุด จึงใช้ ContentAlpha.high บรรทัดที่ 2 มีข้อมูลเมตาที่สําคัญน้อยกว่า จึงใช้ ContentAlpha.medium

ธีมมืด

ใน Compose คุณจะใช้ธีมสว่างและธีมมืดได้โดยระบุชุด Colors ที่แตกต่างกันให้กับ Composable MaterialTheme ดังนี้

@Composable
fun MyTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    MaterialTheme(
        colors = if (darkTheme) DarkColors else LightColors,
        /*...*/
        content = content
    )
}

ในตัวอย่างนี้ MaterialTheme จะรวมอยู่ในฟังก์ชันที่ประกอบกันได้ของตัวเอง ซึ่งรับพารามิเตอร์ที่ระบุว่าจะใช้ธีมมืดหรือไม่ ใน กรณีนี้ ฟังก์ชันจะรับค่าเริ่มต้นสำหรับ darkTheme โดยการค้นหา การตั้งค่าธีมของอุปกรณ์

คุณใช้โค้ดแบบนี้เพื่อตรวจสอบว่าColorsปัจจุบันเป็นแบบสว่างหรือมืดได้

val isLightTheme = MaterialTheme.colors.isLight
Icon(
    painterResource(
        id = if (isLightTheme) {
            R.drawable.ic_sun_24
        } else {
            R.drawable.ic_moon_24
        }
    ),
    contentDescription = "Theme"
)

การวางซ้อนระดับความสูง

ใน Material พื้นผิวในธีมมืดที่มีระดับความสูงมากขึ้นจะได้รับการซ้อนทับระดับความสูง ซึ่งจะทำให้พื้นหลังสว่างขึ้น ยิ่งพื้นผิวมีระดับความสูงมากเท่าใด (ยิ่งยกพื้นผิวให้เข้าใกล้แหล่งกำเนิดแสงโดยนัยมากเท่าใด) พื้นผิวก็จะยิ่งสว่างมากขึ้นเท่านั้น

Surface Composable จะใช้การวางซ้อนเหล่านี้โดยอัตโนมัติเมื่อใช้ สีเข้ม เช่นเดียวกับ Material Composable อื่นๆ ที่ใช้ Surface

Surface(
    elevation = 2.dp,
    color = MaterialTheme.colors.surface, // color will be adjusted for elevation
    /*...*/
) { /*...*/ }

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

สำหรับสถานการณ์ที่กำหนดเองซึ่งไม่มี Surface ให้ใช้ LocalElevationOverlay ซึ่งเป็น CompositionLocal ที่มี ElevationOverlay ที่ใช้โดยคอมโพเนนต์ Surface

// Elevation overlays
// Implemented in Surface (and any components that use it)
val color = MaterialTheme.colors.surface
val elevation = 4.dp
val overlaidColor = LocalElevationOverlay.current?.apply(
    color, elevation
)

หากต้องการปิดใช้ภาพซ้อนทับระดับความสูง ให้ระบุ null ที่จุดที่เลือกใน ลำดับชั้นที่ประกอบได้

MyTheme {
    CompositionLocalProvider(LocalElevationOverlay provides null) {
        // Content without elevation overlays
    }
}

สีเฉพาะจุดที่จำกัด

Material ขอแนะนำให้ใช้สีเฉพาะจุดแบบจำกัดสำหรับธีมมืด โดยให้ใช้สี surface มากกว่าสี primary ในกรณีส่วนใหญ่ Composable ของ Material เช่น TopAppBar และ BottomNavigation จะใช้ลักษณะการทำงานนี้โดยค่าเริ่มต้น

ภาพหน้าจอของธีมมืด Material ซึ่งแสดงแถบแอปด้านบนที่ใช้สีพื้นผิวแทนสีหลักสำหรับสีเฉพาะจุดที่จำกัด
รูปที่ 6 ธีมมืดของ Material ที่มีสีเน้นแบบจำกัด แถบแอปด้านบนใช้สีหลักในธีมสว่าง และสีพื้นผิวในธีมมืด

สําหรับสถานการณ์ที่กําหนดเอง ให้ใช้พร็อพเพอร์ตี้ส่วนขยาย primarySurface ดังนี้

Surface(
    // Switches between primary in light theme and surface in dark theme
    color = MaterialTheme.colors.primarySurface,
    /*...*/
) { /*...*/ }

การพิมพ์

Material กำหนดระบบประเภท ซึ่งแนะนำให้คุณใช้สไตล์ที่มีชื่อเชิงความหมายจำนวนเล็กน้อย

ตัวอย่างแบบอักษรหลายแบบในสไตล์ต่างๆ
รูปที่ 7 ระบบประเภทวัสดุ

Compose ใช้ระบบประเภทที่มีคลาส Typography, TextStyle และที่เกี่ยวข้องกับแบบอักษร Typography Constructor มีค่าเริ่มต้นสำหรับแต่ละสไตล์ คุณจึงละเว้นสไตล์ที่ไม่ต้องการปรับแต่งได้

val raleway = FontFamily(
    Font(R.font.raleway_regular),
    Font(R.font.raleway_medium, FontWeight.W500),
    Font(R.font.raleway_semibold, FontWeight.SemiBold)
)

val myTypography = Typography(
    h1 = TextStyle(
        fontFamily = raleway,
        fontWeight = FontWeight.W300,
        fontSize = 96.sp
    ),
    body1 = TextStyle(
        fontFamily = raleway,
        fontWeight = FontWeight.W600,
        fontSize = 16.sp
    )
    /*...*/
)
MaterialTheme(typography = myTypography, /*...*/) {
    /*...*/
}

หากต้องการใช้แบบอักษรเดียวกันตลอด ให้ระบุพารามิเตอร์ defaultFontFamily และละเว้น fontFamily ขององค์ประกอบ TextStyle

val typography = Typography(defaultFontFamily = raleway)
MaterialTheme(typography = typography, /*...*/) {
    /*...*/
}

ใช้รูปแบบข้อความ

เข้าถึงองค์ประกอบ TextStyle ได้โดยใช้ MaterialTheme.typography เรียกข้อมูล องค์ประกอบ TextStyle ดังนี้

Text(
    text = "Subtitle2 styled",
    style = MaterialTheme.typography.subtitle2
)

ภาพหน้าจอที่แสดงการผสมผสานแบบอักษรต่างๆ สำหรับวัตถุประสงค์ที่แตกต่างกัน
รูปที่ 8 ใช้แบบอักษรและสไตล์ที่เลือกเพื่อแสดงออกถึงแบรนด์

รูปร่าง

Material กำหนดระบบรูปร่างที่ช่วยให้คุณกำหนดรูปร่างสำหรับคอมโพเนนต์ขนาดใหญ่ กลาง และเล็กได้

แสดงรูปร่าง Material Design ที่หลากหลาย
รูปที่ 9 ระบบรูปร่างของ Material

Compose ใช้ระบบรูปร่างด้วยคลาส Shapes ซึ่งช่วยให้คุณระบุ CornerBasedShape สำหรับหมวดหมู่ขนาดแต่ละหมวดหมู่ได้ ดังนี้

val shapes = Shapes(
    small = RoundedCornerShape(percent = 50),
    medium = RoundedCornerShape(0f),
    large = CutCornerShape(
        topStart = 16.dp,
        topEnd = 0.dp,
        bottomEnd = 0.dp,
        bottomStart = 16.dp
    )
)

MaterialTheme(shapes = shapes, /*...*/) {
    /*...*/
}

คอมโพเนนต์หลายรายการใช้รูปร่างเหล่านี้โดยค่าเริ่มต้น เช่น Button, TextField และ FloatingActionButton จะมีค่าเริ่มต้นเป็นเล็ก AlertDialog จะมีค่าเริ่มต้นเป็นปานกลาง และ ModalDrawer จะมีค่าเริ่มต้นเป็น ใหญ่ ดูการแมปทั้งหมดได้ในการอ้างอิงรูปแบบรูปร่าง

ใช้รูปร่าง

เข้าถึงองค์ประกอบ Shape ได้โดยใช้ MaterialTheme.shapes เรียกข้อมูลองค์ประกอบ Shape ด้วยโค้ดดังนี้

Surface(
    shape = MaterialTheme.shapes.medium, /*...*/
) {
    /*...*/
}

ภาพหน้าจอของแอปที่ใช้รูปร่าง Material เพื่อสื่อถึงสถานะขององค์ประกอบ
รูปที่ 10 ใช้รูปร่างเพื่อแสดงแบรนด์หรือรัฐ

รูปแบบเริ่มต้น

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

@Composable
fun MyButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Button(
        colors = ButtonDefaults.buttonColors(
            backgroundColor = MaterialTheme.colors.secondary
        ),
        onClick = onClick,
        modifier = modifier,
        content = content
    )
}

การซ้อนทับธีม

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

นอกจากนี้ เมื่อย้ายข้อมูลหน้าจอที่อิงตาม View ไปยัง Compose ให้ระวังการใช้งานแอตทริบิวต์ android:theme คุณอาจต้องใช้ MaterialTheme ในส่วนนั้นของทรี UI ของ Compose

ในตัวอย่างนี้ หน้าจอรายละเอียดใช้ PinkTheme สำหรับส่วนใหญ่ของหน้าจอ และใช้ BlueTheme สำหรับส่วนที่เกี่ยวข้อง ภาพหน้าจอและโค้ดต่อไปนี้แสดงแนวคิดนี้

ภาพหน้าจอของแอปที่แสดงธีมที่ซ้อนกัน โดยมีธีมสีชมพูสำหรับหน้าจอหลักและธีมสีน้ำเงินสำหรับส่วนที่เกี่ยวข้อง
รูปที่ 11 ธีมที่ซ้อนกัน

@Composable
fun DetailsScreen(/* ... */) {
    PinkTheme {
        // other content
        RelatedSection()
    }
}

@Composable
fun RelatedSection(/* ... */) {
    BlueTheme {
        // content
    }
}

สถานะของคอมโพเนนต์

คอมโพเนนต์ Material ที่โต้ตอบได้ (คลิก เปิด/ปิด ฯลฯ) อาจมี สถานะภาพที่แตกต่างกัน สถานะต่างๆ ได้แก่ เปิดใช้ ปิดใช้ กด ฯลฯ

Composable มักมีพารามิเตอร์ enabled การตั้งค่าเป็น false จะป้องกัน การโต้ตอบ และเปลี่ยนพร็อพเพอร์ตี้ เช่น สีและความสูง เพื่อสื่อถึง สถานะของคอมโพเนนต์ด้วยภาพ

ภาพหน้าจอของปุ่ม 2 ปุ่ม โดยปุ่มหนึ่งเปิดใช้และอีกปุ่มหนึ่งปิดใช้ แสดงสถานะภาพที่แตกต่างกัน
รูปที่ 12 ปุ่มที่มี enabled = true (ซ้าย) และ enabled = false (ขวา)

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

Button(
    onClick = { /* ... */ },
    enabled = true,
    // Custom colors for different states
    colors = ButtonDefaults.buttonColors(
        backgroundColor = MaterialTheme.colors.secondary,
        disabledBackgroundColor = MaterialTheme.colors.onBackground
            .copy(alpha = 0.2f)
            .compositeOver(MaterialTheme.colors.background)
        // Also contentColor and disabledContentColor
    ),
    // Custom elevation for different states
    elevation = ButtonDefaults.elevation(
        defaultElevation = 8.dp,
        disabledElevation = 2.dp,
        // Also pressedElevation
    )
) { /* ... */ }

ภาพหน้าจอของปุ่ม 2 ปุ่มที่มีการปรับสีและระดับความสูงสำหรับสถานะที่เปิดและปิดใช้
รูปที่ 13 ปุ่มที่มี enabled = true (ซ้าย) และ enabled = false (ขวา) พร้อมค่าสีและความสูงที่ปรับแล้ว

ระลอกคลื่น

คอมโพเนนต์ Material ใช้เอฟเฟกต์ระลอกเพื่อระบุว่ามีการโต้ตอบกับคอมโพเนนต์ หากคุณใช้ MaterialTheme ในลำดับชั้น ระบบจะใช้ Ripple เป็นIndication เริ่มต้นภายในตัวแก้ไข เช่น clickable และ indication

ในกรณีส่วนใหญ่ คุณสามารถใช้Rippleเริ่มต้นได้ หากต้องการกำหนดค่าลักษณะที่ปรากฏ คุณสามารถใช้ RippleTheme เพื่อเปลี่ยนพร็อพเพอร์ตี้ เช่น สีและอัลฟ่า

คุณสามารถขยาย RippleTheme และใช้ฟังก์ชันยูทิลิตี defaultRippleColor และ defaultRippleAlpha ได้ จากนั้นคุณจะระบุธีม Ripple ที่กำหนดเองในลำดับชั้นได้โดยใช้ LocalRippleTheme ดังนี้

@Composable
fun MyApp() {
    MaterialTheme {
        CompositionLocalProvider(
            LocalRippleTheme provides SecondaryRippleTheme
        ) {
            // App content
        }
    }
}

@Immutable
private object SecondaryRippleTheme : RippleTheme {
    @Composable
    override fun defaultColor() = RippleTheme.defaultRippleColor(
        contentColor = MaterialTheme.colors.secondary,
        lightTheme = MaterialTheme.colors.isLight
    )

    @Composable
    override fun rippleAlpha() = RippleTheme.defaultRippleAlpha(
        contentColor = MaterialTheme.colors.secondary,
        lightTheme = MaterialTheme.colors.isLight
    )
}

GIF ภาพเคลื่อนไหวที่แสดงปุ่มที่มีเอฟเฟกต์การกระเพื่อมที่แตกต่างกันเมื่อแตะ
รูปที่ 14 ปุ่มที่มีค่าการกระเพื่อมที่แตกต่างกันซึ่งระบุโดยใช้ RippleConfiguration

ดูข้อมูลเพิ่มเติม

ดูข้อมูลเพิ่มเติมเกี่ยวกับการกำหนดธีม Material ใน Compose ได้ที่แหล่งข้อมูลเพิ่มเติมต่อไปนี้

Codelab

วิดีโอ