ระบบการออกแบบที่กำหนดเองใน Compose

แม้ว่า Material จะเป็นระบบการออกแบบที่เราแนะนำ และ Jetpack Compose ก็จัดส่งผลิตภัณฑ์ การใช้งาน Material คุณไม่ต้องบังคับให้มีการใช้ สร้างวัสดุ ทั้งหมดบน API สาธารณะ คุณจึงสร้างระบบการออกแบบของคุณเอง ในลักษณะเดียวกัน

โดยอาจใช้หลายวิธี ดังนี้

หรือต้องการใช้คอมโพเนนต์ Material กับการออกแบบที่กำหนดเองต่อไป ระบบ สามารถดำเนินการนี้ได้ แต่สิ่งที่ควรคำนึงถึงคือ วิธีการที่คุณใช้

ดูข้อมูลเพิ่มเติมเกี่ยวกับโครงสร้างระดับล่างและ API ที่ MaterialTheme ใช้ และระบบการออกแบบที่กำหนดเอง โปรดดูคู่มือโครงสร้างของธีมในการเขียน

การขยายธีมของวัสดุ

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

วิธีการที่ง่ายที่สุดคือการเพิ่มพร็อพเพอร์ตี้ของส่วนขยาย ดังนี้

// Use with MaterialTheme.colors.snackbarAction
val Colors.snackbarAction: Color
    get() = if (isLight) Red300 else Red700

// Use with MaterialTheme.typography.textFieldInput
val Typography.textFieldInput: TextStyle
    get() = TextStyle(/* ... */)

// Use with MaterialTheme.shapes.card
val Shapes.card: Shape
    get() = RoundedCornerShape(size = 20.dp)

การดำเนินการนี้จะสอดคล้องกับ API การใช้งานของ MaterialTheme ตัวอย่าง ที่ Compose เขียนเองคือ primarySurface ซึ่งทำหน้าที่เป็นพร็อกซีระหว่าง primary และ surface ขึ้นอยู่กับ Colors.isLight

อีกวิธีการหนึ่งคือการกำหนดธีมแบบขยายที่ "รวม" MaterialTheme และ คุณค่าของเนื้อหา

สมมติว่าคุณต้องการเพิ่มสีอีก 2 สี คือ tertiary และ onTertiary — ขณะเดียวกันก็ใช้สี Material แบบเดิม

@Immutable
data class ExtendedColors(
    val tertiary: Color,
    val onTertiary: Color
)

val LocalExtendedColors = staticCompositionLocalOf {
    ExtendedColors(
        tertiary = Color.Unspecified,
        onTertiary = Color.Unspecified
    )
}

@Composable
fun ExtendedTheme(
    /* ... */
    content: @Composable () -> Unit
) {
    val extendedColors = ExtendedColors(
        tertiary = Color(0xFFA8EFF0),
        onTertiary = Color(0xFF002021)
    )
    CompositionLocalProvider(LocalExtendedColors provides extendedColors) {
        MaterialTheme(
            /* colors = ..., typography = ..., shapes = ... */
            content = content
        )
    }
}

// Use with eg. ExtendedTheme.colors.tertiary
object ExtendedTheme {
    val colors: ExtendedColors
        @Composable
        get() = LocalExtendedColors.current
}

ซึ่งคล้ายกับ API การใช้งานของ MaterialTheme และยังรองรับหลายธีมอีกด้วย เนื่องจากคุณสามารถซ้อน ExtendedTheme ในลักษณะเดียวกับ MaterialTheme ได้

การใช้คอมโพเนนต์ Material

เมื่อมีการขยายธีมวัสดุ ค่า MaterialTheme ที่มีอยู่จะยังคงเดิม และคอมโพเนนต์เนื้อหายังคงมีค่าเริ่มต้นที่เหมาะสม

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

@Composable
fun ExtendedButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Button(
        colors = ButtonDefaults.buttonColors(
            containerColor = ExtendedTheme.colors.tertiary,
            contentColor = ExtendedTheme.colors.onTertiary
            /* Other colors use values from MaterialTheme */
        ),
        onClick = onClick,
        modifier = modifier,
        content = content
    )
}

จากนั้นแทนที่การใช้งาน Button ด้วย ExtendedButton โดยที่ เหมาะสม

@Composable
fun ExtendedApp() {
    ExtendedTheme {
        /*...*/
        ExtendedButton(onClick = { /* ... */ }) {
            /* ... */
        }
    }
}

การแทนที่ระบบวัสดุ

แทนที่จะขยายธีมเนื้อหา คุณอาจต้องการแทนที่ ระบบ — Colors, Typography หรือ Shapes — ที่มีการนำไปใช้แบบกำหนดเอง ขณะที่ยังคงรักษาสิ่งอื่นๆ ไว้

สมมติว่าคุณต้องการเปลี่ยนประเภทและระบบรูปร่างโดยที่ยังเก็บสีไว้ ระบบ:

@Immutable
data class ReplacementTypography(
    val body: TextStyle,
    val title: TextStyle
)

@Immutable
data class ReplacementShapes(
    val component: Shape,
    val surface: Shape
)

val LocalReplacementTypography = staticCompositionLocalOf {
    ReplacementTypography(
        body = TextStyle.Default,
        title = TextStyle.Default
    )
}
val LocalReplacementShapes = staticCompositionLocalOf {
    ReplacementShapes(
        component = RoundedCornerShape(ZeroCornerSize),
        surface = RoundedCornerShape(ZeroCornerSize)
    )
}

@Composable
fun ReplacementTheme(
    /* ... */
    content: @Composable () -> Unit
) {
    val replacementTypography = ReplacementTypography(
        body = TextStyle(fontSize = 16.sp),
        title = TextStyle(fontSize = 32.sp)
    )
    val replacementShapes = ReplacementShapes(
        component = RoundedCornerShape(percent = 50),
        surface = RoundedCornerShape(size = 40.dp)
    )
    CompositionLocalProvider(
        LocalReplacementTypography provides replacementTypography,
        LocalReplacementShapes provides replacementShapes
    ) {
        MaterialTheme(
            /* colors = ... */
            content = content
        )
    }
}

// Use with eg. ReplacementTheme.typography.body
object ReplacementTheme {
    val typography: ReplacementTypography
        @Composable
        get() = LocalReplacementTypography.current
    val shapes: ReplacementShapes
        @Composable
        get() = LocalReplacementShapes.current
}

การใช้คอมโพเนนต์ Material

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

หากต้องการใช้ค่าการแทนที่ในคอมโพเนนต์ ให้รวมค่าดังกล่าวด้วยตนเอง ฟังก์ชันที่ประกอบกันได้ การตั้งค่าโดยตรงสำหรับระบบที่เกี่ยวข้อง และ โดยแสดงให้ผู้อื่นเห็นเป็นพารามิเตอร์ใน Composable ที่มี

@Composable
fun ReplacementButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Button(
        shape = ReplacementTheme.shapes.component,
        onClick = onClick,
        modifier = modifier,
        content = {
            ProvideTextStyle(
                value = ReplacementTheme.typography.body
            ) {
                content()
            }
        }
    )
}

จากนั้นแทนที่การใช้งาน Button ด้วย ReplacementButton โดยที่ เหมาะสม

@Composable
fun ReplacementApp() {
    ReplacementTheme {
        /*...*/
        ReplacementButton(onClick = { /* ... */ }) {
            /* ... */
        }
    }
}

การใช้ระบบการออกแบบที่กำหนดเองอย่างสมบูรณ์

คุณอาจต้องการเปลี่ยนธีมวัสดุเป็นระบบการออกแบบที่กำหนดเองทั้งหมด พิจารณาว่า MaterialTheme มีระบบต่อไปนี้

  • Colors, Typography และ Shapes: ระบบธีมวัสดุ
  • ContentAlpha: ระดับความทึบแสงเพื่อบอกการเน้นใน Text และ Icon
  • TextSelectionColors: สีที่ใช้เพื่อเลือกข้อความโดย Text และ TextField
  • Ripple และ RippleTheme: การนำ Indication มาใช้อย่างเป็นรูปธรรม

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

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

ในโค้ดต่อไปนี้ เราสร้างแบบจำลองระบบสีแบบกำหนดเองซึ่งรวมถึงการไล่ระดับสี (List<Color>) รวมระบบประเภท แนะนำระบบระดับความสูงใหม่ และไม่รวมระบบอื่นๆ จาก MaterialTheme:

@Immutable
data class CustomColors(
    val content: Color,
    val component: Color,
    val background: List<Color>
)

@Immutable
data class CustomTypography(
    val body: TextStyle,
    val title: TextStyle
)

@Immutable
data class CustomElevation(
    val default: Dp,
    val pressed: Dp
)

val LocalCustomColors = staticCompositionLocalOf {
    CustomColors(
        content = Color.Unspecified,
        component = Color.Unspecified,
        background = emptyList()
    )
}
val LocalCustomTypography = staticCompositionLocalOf {
    CustomTypography(
        body = TextStyle.Default,
        title = TextStyle.Default
    )
}
val LocalCustomElevation = staticCompositionLocalOf {
    CustomElevation(
        default = Dp.Unspecified,
        pressed = Dp.Unspecified
    )
}

@Composable
fun CustomTheme(
    /* ... */
    content: @Composable () -> Unit
) {
    val customColors = CustomColors(
        content = Color(0xFFDD0D3C),
        component = Color(0xFFC20029),
        background = listOf(Color.White, Color(0xFFF8BBD0))
    )
    val customTypography = CustomTypography(
        body = TextStyle(fontSize = 16.sp),
        title = TextStyle(fontSize = 32.sp)
    )
    val customElevation = CustomElevation(
        default = 4.dp,
        pressed = 8.dp
    )
    CompositionLocalProvider(
        LocalCustomColors provides customColors,
        LocalCustomTypography provides customTypography,
        LocalCustomElevation provides customElevation,
        content = content
    )
}

// Use with eg. CustomTheme.elevation.small
object CustomTheme {
    val colors: CustomColors
        @Composable
        get() = LocalCustomColors.current
    val typography: CustomTypography
        @Composable
        get() = LocalCustomTypography.current
    val elevation: CustomElevation
        @Composable
        get() = LocalCustomElevation.current
}

การใช้คอมโพเนนต์ Material

หากไม่มี MaterialTheme อยู่ ระบบจะใช้คอมโพเนนต์ Material ตามที่มีอยู่ ในค่าสี ประเภท และรูปร่างของวัสดุที่ไม่ต้องการ รวมถึงลักษณะการทํางานของสิ่งบ่งชี้

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

เราขอแนะนําให้คุณเข้าถึงค่าที่ตั้งไว้จากธีมที่กําหนดเอง อีกวิธีหนึ่งหาก ธีมไม่มี Color, TextStyle, Shape หรือระบบอื่นๆ สามารถฮาร์ดโค้ดอุปกรณ์ได้

@Composable
fun CustomButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Button(
        colors = ButtonDefaults.buttonColors(
            containerColor = CustomTheme.colors.component,
            contentColor = CustomTheme.colors.content,
            disabledContainerColor = CustomTheme.colors.content
                .copy(alpha = 0.12f)
                .compositeOver(CustomTheme.colors.component),
            disabledContentColor = CustomTheme.colors.content
                .copy(alpha = ContentAlpha.disabled)
        ),
        shape = ButtonShape,
        elevation = ButtonDefaults.elevatedButtonElevation(
            defaultElevation = CustomTheme.elevation.default,
            pressedElevation = CustomTheme.elevation.pressed
            /* disabledElevation = 0.dp */
        ),
        onClick = onClick,
        modifier = modifier,
        content = {
            ProvideTextStyle(
                value = CustomTheme.typography.body
            ) {
                content()
            }
        }
    )
}

val ButtonShape = RoundedCornerShape(percent = 50)

หากคุณได้แนะนำประเภทคลาสใหม่ เช่น List<Color> เพื่อแสดงถึงการไล่ระดับสี การใช้คอมโพเนนต์ใหม่ตั้งแต่ต้นแทนการรวมไว้อาจดีกว่า ลองดูตัวอย่างต่อไปนี้ JetsnackButton จากตัวอย่างของ Jetsnack