ดีไซน์ 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 มีฟังก์ชันตัวสร้างเพื่อสร้างชุดสีอ่อนหรือเข้ม ดังนี้

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 ที่ส่งไปยัง MaterialTheme Composable ได้โดย ใช้ 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 ที่อยู่ในนั้นได้ด้วย Composables หลายรายการใช้สีเนื้อหานี้โดยค่าเริ่มต้น เช่น 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 จะใช้การวางซ้อนเหล่านี้โดยอัตโนมัติเมื่อใช้ สีเข้ม และ 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 ในกรณีส่วนใหญ่ 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
รูปที่ 9 ระบบรูปร่างของวัสดุ

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 จะตั้งค่าเริ่มต้นของสี ตัวอักษร และรูปร่างเป็นค่าธีมปัจจุบัน พารามิเตอร์อื่นๆ ทั้งหมดจึงยังคงมีค่าเริ่มต้นเมื่อธีมตั้งค่าพารามิเตอร์เหล่านั้นเพียง 1 รายการ

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

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

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

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

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

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

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

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

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

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

Codelabs

วิดีโอ