Material Design 2 בכתיבה

‫Jetpack Compose מציע הטמעה של Material Design, מערכת עיצוב מקיפה ליצירת ממשקים דיגיטליים. הרכיבים של Material Design (כפתורים, כרטיסים, מתגים וכו') מבוססים על התאמת העיצוב של Google Material Design, שהיא שיטה שיטתית להתאמה אישית של Material Design כדי לשקף טוב יותר את המותג של המוצר. עיצוב Material Theme מכיל מאפיינים של צבע, טיפוגרפיה וצורה. כשאתם מתאימים אישית את המאפיינים האלה, השינויים משתקפים באופן אוטומטי ברכיבים שבהם אתם משתמשים כדי ליצור את האפליקציה.

ב-Jetpack Compose, המושגים האלה מיושמים באמצעות הפונקציה הקומפוזבילית MaterialTheme:

MaterialTheme(
    colors = // ...
    typography = // ...
    shapes = // ...
) {
    // app content
}

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

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

צבע

הצבעים מעוצבים ב-Compose באמצעות המחלקה Color, מחלקה שמכילה נתונים.

val Red = Color(0xffff0000)
val Blue = Color(red = 0f, green = 0f, blue = 1f)

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

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

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

‫Compose מטמיע את מערכת הסוגים באמצעות המחלקות Typography,‏ TextStyle ומחלקות שקשורות לגופנים. ה-constructor של Typography מציע ברירות מחדל לכל סגנון, כך שאפשר להשמיט את הסגנונות שלא רוצים להתאים אישית:

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 Design
איור 9. מערכת הצורות של Material.

ב-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 Design כדי להעביר את המצב של רכיב מסוים
איור 10. אפשר להשתמש בצורות כדי להביע את המותג או את המצב.

סגנונות ברירת מחדל

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

בנוסף, כשמעבירים מסכים מבוססי-View לפיתוח נייטיב, חשוב לשים לב לשימושים במאפיין android:theme. סביר להניח שצריך MaterialTheme חדש בחלק הזה של עץ ממשק המשתמש של Compose.

בדוגמה הזו, מסך הפרטים משתמש ב-PinkTheme ברוב המסך, וב-BlueTheme בקטע הקשור. צילום המסך והקוד הבאים ממחישים את הרעיון הזה:

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

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

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

מצבי הרכיבים

רכיבי Material שאפשר לבצע איתם אינטראקציה (ללחוץ, להחליף מצב וכו') יכולים להיות במצבים חזותיים שונים. המצבים כוללים: מופעל, מושבת, לחוץ וכו'.

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

צילום מסך של שני כפתורים: אחד מופעל ואחד מושבת, שמראה את המצבים החזותיים השונים שלהם
איור 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
    )
) { /* ... */ }

צילום מסך של שני כפתורים עם צבע וגובה מותאמים למצבים מופעלים ומושבתים
איור 13. כפתור עם enabled = true (משמאל) ו-enabled = false (מימין), עם ערכים מותאמים של צבע וגובה.

אדוות

רכיבי Material משתמשים באפקט של אדוות כדי לציין שמתבצעת איתם אינטראקציה. אם אתם משתמשים ב-MaterialTheme בהיררכיה, Ripple משמש כברירת המחדל של Indication בתוך מגבילי גישה כמו clickable ו-indication.

ברוב המקרים אפשר להסתמך על ברירת המחדל Ripple. כדי לשנות את המראה שלהם, אתם יכולים להשתמש ב-RippleTheme כדי לשנות מאפיינים כמו צבע ואלפא.

אפשר להרחיב את RippleTheme ולהשתמש בפונקציות הבסיסיות defaultRippleColor ו-defaultRippleAlpha. אחרי זה, אתם יכולים לספק את העיצוב המותאם אישית של האפקט בהיררכיה באמצעות 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. כפתורים עם ערכי אפקט אדווה שונים שסופקו באמצעות RippleConfiguration.

מידע נוסף

כדי לקבל מידע נוסף על התאמת העיצוב של Google Material Design ב-Compose, אפשר לעיין במקורות המידע הנוספים הבאים.

Codelabs

סרטונים