ดีไซน์ Material 2 ใน Compose

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

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)

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

ตัวอย่างชุดสีของธีม
รูปที่ 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 เพื่อดึงสีเนื้อหาที่ต้องการสำหรับพื้นหลังปัจจุบัน ณ ตำแหน่งที่ระบุในลำดับชั้น

Content alpha

บ่อยครั้งที่คุณต้องการปรับเปลี่ยนระดับการเน้นเนื้อหาเพื่อสื่อสารความสำคัญและสร้างลำดับชั้นของภาพ คำแนะนำเกี่ยวกับความอ่านง่ายของข้อความใน 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 จะใช้การวางซ้อนเหล่านี้โดยอัตโนมัติเมื่อใช้สีเข้ม และ Composable ของ Material อื่นๆ ที่ใช้พื้นผิวก็เช่นกัน

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 ในกรณีส่วนใหญ่ คอมโพสิเบิล 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 ระบบประเภทของ Material

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

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 ที่มีอยู่

@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 ได้โดยการฝังคอมโพสิเบิล 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 ที่โต้ตอบได้ (คลิก สลับ ฯลฯ) อาจอยู่ในสถานะภาพที่แตกต่างกัน สถานะต่างๆ ได้แก่ เปิดใช้ ปิดใช้ กด ฯลฯ

คอมโพสิเบิลมักมีพารามิเตอร์ 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 ปุ่มที่มีค่า Ripple ต่างกันซึ่งระบุโดยใช้ RippleConfiguration

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

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

Codelab

วิดีโอ