מערכות עיצוב בהתאמה אישית בכתיבה

אמנם מערכת העיצוב של Material היא המומלצת שלנו, ו-Jetpack Compose כולל הטמעה של Material, אבל אתם לא חייבים להשתמש בה. מערכת Material מבוססת כולה על ממשקי API ציבוריים, כך שאפשר ליצור מערכת עיצוב משלכם באותו אופן.

יש כמה גישות אפשריות:

אפשר גם להמשיך להשתמש ברכיבי Material עם מערכת עיצוב בהתאמה אישית. ניתן לעשות זאת, אבל יש דברים שכדאי לזכור בגישה שנקטתם.

מידע נוסף על המבנים ברמה נמוכה יותר ועל ממשקי ה-API שבהם MaterialTheme ומערכות עיצוב בהתאמה אישית משתמשים זמין במדריך Anatomy of a theme in Compose.

הרחבת העיצוב של Material

Compose Material מבוסס על עיצוב לפי נושאים ב-Material Design, כדי שיהיה קל ופרקטי לפעול בהתאם להנחיות של Material Design. עם זאת, שאפשר להרחיב את הצבעים, הטיפוגרפיה והצורות ערכים.

הגישה הפשוטה ביותר היא להוסיף נכסי תוספים:

// Use with MaterialTheme.colorScheme.snackbarAction
val ColorScheme.snackbarAction: Color
    @Composable
    get() = if (isSystemInDarkTheme()) 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. דוגמה לכך מוגדר על ידי 'פיתוח נייטיב' עצמו הוא surfaceColorAtElevation, שקובע את צבע פני השטח שבו יש להשתמש בהתאם לגובה.

גישה אחרת היא להגדיר נושא מורחב שמקיף את MaterialTheme ואת הערכים שלו.

נניח שרוצים להוסיף עוד שני צבעים: caution ו-onCaution, צבע צהוב המשמש לפעולות מסוכנות למחצה — תוך שמירה על צבעי החומר הקיימים:

@Immutable
data class ExtendedColors(
    val caution: Color,
    val onCaution: Color
)

val LocalExtendedColors = staticCompositionLocalOf {
    ExtendedColors(
        caution = Color.Unspecified,
        onCaution = Color.Unspecified
    )
}

@Composable
fun ExtendedTheme(
    /* ... */
    content: @Composable () -> Unit
) {
    val extendedColors = ExtendedColors(
        caution = Color(0xFFFFCC02),
        onCaution = Color(0xFF2C2D30)
    )
    CompositionLocalProvider(LocalExtendedColors provides extendedColors) {
        MaterialTheme(
            /* colors = ..., typography = ..., shapes = ... */
            content = content
        )
    }
}

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

זה דומה לממשקי API לדיווח על שימוש ב-MaterialTheme. הוא תומך גם בכמה עיצובים, כי אפשר להטמיע רכיבי ExtendedTheme באותו אופן שבו מטמיעים רכיבי MaterialTheme.

שימוש ברכיבי Material

כשמרחיבים את ערכת הנושא של Material, ערכי MaterialTheme הקיימים נשמרים ורכיבי Material עדיין כוללים ערכי ברירת מחדל סבירים.

אם רוצים להשתמש בערכים מורחבים ברכיבים, צריך לכלול אותם ברכיבים אחרים בפונקציות קומפוזביליות, להגדיר באופן ישיר את הערכים שרוצים לשנות, חשיפת אחרים כפרמטרים לתוכן הקומפוזבילי שמכיל:

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

לאחר מכן, צריך להחליף את השימוש ב-Button ב-ExtendedButton במקרים הרלוונטיים.

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

החלפת תת-מערכות של חומרים

במקום להרחיב את Material Theming, יכול להיות שתרצו להחליף מערכת אחת או יותר – 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, תוך שימוש ב'חומר' עלולים לגרום לערכים לא רצויים של צבע, סוג או צורה של החומר.

אם רוצים להשתמש בערכי החלפה ברכיבים, אפשר לשמור אותם בצורות שונות פונקציות קומפוזביליות, שמגדירים באופן ישיר את הערכים של המערכת הרלוונטית, חשיפת אחרים כפרמטרים לתוכן הקומפוזבילי שמכיל

@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 = { /* ... */ }) {
            /* ... */
        }
    }
}

הטמעת מערכת עיצוב בהתאמה אישית מלאה

כדאי להחליף את מערכת העיצוב של Material Theming במערכת עיצוב מותאמת אישית לחלוטין. נניח ש-MaterialTheme מספק את המערכות הבאות:

  • Colors,‏ Typography ו-Shapes: מערכות של התאמת עיצוב של Google Material Design
  • TextSelectionColors: הצבעים שבהם נעשה שימוש לבחירת טקסט על ידי Text ו-TextField
  • Ripple ו-RippleTheme: הטמעת חומר לימוד של Indication

אם אתם רוצים להמשיך להשתמש ברכיבי Material, תצטרכו להחליף חלק מהמערכות האלה בעיצוב או בעיצובים המותאמים אישית שלכם, או לטפל במערכות ברכיבים שלכם כדי למנוע התנהגות לא רצויה.

עם זאת, מערכות העיצוב לא מוגבלות למושגים שעליהם מתבססת Material (חומר לימוד). שלך יכול לשנות מערכות קיימות ולהציג מערכות חדשות לגמרי — באמצעות מחלקות חדשות כדי להתאים מושגים אחרים לנושאים.

בקוד הבא, אנחנו יוצרים מודל של מערכת צבעים מותאמת אישית שכוללת מעברים מדורגים (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 כפי שהם יוביל לערכי צבע, סוג וצורה לא רצויים של Material, וכן להתנהגות לא רצויה של אינדיקציה.

אם רוצים להשתמש בערכים מותאמים אישית ברכיבים, אפשר לארוז אותם בתוכן קומפוזבילי משלכם שמגדירים באופן ישיר את הערכים של המערכת הרלוונטית, אחרים כפרמטרים של התוכן הקומפוזבילי שמכיל.

מומלץ לגשת לערכים שהגדרתם מהעיצוב המותאם אישית. לחלופין, אם העיצוב לא מספק 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 = 0.38f)

        ),
        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.