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

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

יש כמה גישות שאפשר לנקוט:

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

כדי לקבל מידע נוסף על מבנים ועל ממשקי API ברמה נמוכה יותר שמשמשים את MaterialTheme ומערכות עיצוב בהתאמה אישית, כדאי לעיין במדריך ממה מורכב עיצוב ב-Compose.

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

‫Compose Material מבוסס על התאמת העיצוב של Google 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. דוגמה לכך שמוגדרת על ידי Compose עצמו היא surfaceColorAtElevation, שקובעת את צבע המשטח שבו צריך להשתמש בהתאם לגובה.

גישה אחרת היא להגדיר עיצוב מורחב ש "עוטף" את MaterialTheme ואת הערכים שלו.

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

@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. הוא גם תומך בכמה עיצובים, כי אפשר להוסיף ExtendedThemes באותו אופן כמו MaterialTheme.

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

כשמרחיבים את התאמת העיצוב של Google Material Design, הערכים הקיימים של 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

במקום להרחיב את התאמת העיצוב של Google Material Design, אתם יכולים להחליף מערכת אחת או יותר (ColorScheme,‏ 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, שימוש ברכיבי Material כמו שהם עלול לגרום לערכים לא רצויים של צבע, סוג או צורה של Material.

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

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

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

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

  • ‫ColorScheme,‏ Typography ו-Shapes: מערכות להתאמת העיצוב של Google Material Design
  • ‫TextSelectionColors: הצבעים שמשמשים לבחירת טקסט ב-Text וב-TextField
  • ‫ripple, שסופק בתור LocalIndication: הטמעה מהותית של Indication

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

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

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