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

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

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

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

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

הרחבת העיצוב של חומרים

הרכב מודלים קרובים של 'חומר' עיצוב חומרים כדי שיהיה פשוט ובטוח להקליד בהתאם להנחיות בנושא 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. דוגמה לכך מוגדר על ידי 'פיתוח נייטיב' עצמו הוא primarySurface שמשמש כשרת proxy בין primary ל-surface בהתאם Colors.isLight.

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

נניח שרוצים להוסיף עוד שני צבעים – tertiary ו-onTertiary תוך שמירה על צבעי החומר הקיימים:

@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

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

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

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

החלפה של מערכות Material

במקום להרחיב את עיצוב Material, כדאי להחליף אחד או יותר מערכות — 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 במערכת עיצוב בהתאמה אישית מלאה. חשוב לזכור שה-MaterialTheme מספק את המערכות הבאות:

  • Colors, Typography ו-Shapes: מערכות עיצוב של חומרים
  • ContentAlpha: רמות שקיפות להעברת הדגשה בText ובIcon
  • 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 כפי שהם התוצאה בערכים לא רצויים של צבע, סוג, צורה והתנהגות אינדיקציה של חומר.

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

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