עבודה עם גופנים

בדף הזה מוסבר איך להגדיר גופנים באפליקציית Compose.

הגדרת גופן

Text יש פרמטר fontFamily שמאפשר להגדיר את הגופן שבו משתמשים ב-composable. כברירת מחדל, משפחות הגופנים serif, ‏ sans-serif, ‏ monospace ו-cursive כלולות:

@Composable
fun DifferentFonts() {
    Column {
        Text("Hello World", fontFamily = FontFamily.Serif)
        Text("Hello World", fontFamily = FontFamily.SansSerif)
    }
}

המילים

אפשר להשתמש במאפיין fontFamily כדי לעבוד עם גופנים מותאמים אישית ועם סוגי גופנים שהוגדרו בתיקייה res/font:

איור שבו מוצגת התיקייה res > font בסביבת הפיתוח

בדוגמה הזו מוסבר איך להגדיר fontFamily על סמך קובצי הגופן האלה באמצעות הפונקציה Font:

val firaSansFamily = FontFamily(
    Font(R.font.firasans_light, FontWeight.Light),
    Font(R.font.firasans_regular, FontWeight.Normal),
    Font(R.font.firasans_italic, FontWeight.Normal, FontStyle.Italic),
    Font(R.font.firasans_medium, FontWeight.Medium),
    Font(R.font.firasans_bold, FontWeight.Bold)
)

אפשר להעביר את fontFamily הזה לרכיב Text שאפשר להרכיב. מכיוון ש-fontFamily יכול לכלול משקלים שונים, אפשר להגדיר ידנית את fontWeight כדי לבחור את המשקל המתאים לטקסט:

Column {
    Text(text = "text", fontFamily = firaSansFamily, fontWeight = FontWeight.Light)
    Text(text = "text", fontFamily = firaSansFamily, fontWeight = FontWeight.Normal)
    Text(
        text = "text",
        fontFamily = firaSansFamily,
        fontWeight = FontWeight.Normal,
        fontStyle = FontStyle.Italic
    )
    Text(text = "text", fontFamily = firaSansFamily, fontWeight = FontWeight.Medium)
    Text(text = "text", fontFamily = firaSansFamily, fontWeight = FontWeight.Bold)
}

המילים

במאמר מערכות עיצוב בהתאמה אישית ב-Compose מוסבר איך להגדיר את הטיפוגרפיה באפליקציה כולה.

גופנים שאפשר להוריד

החל מ-Compose 1.2.0, אפשר להשתמש בממשק ה-API של גופנים להורדה באפליקציית Compose כדי להוריד גופנים של Google באופן אסינכרוני ולהשתמש בהם באפליקציה.

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

שימוש בגופנים להורדה באופן פרוגרמטי

כדי להוריד גופן באופן פרוגרמטי מתוך האפליקציה:

  1. מוסיפים את יחסי התלות:

    מגניב

    dependencies {
        ...
        implementation "androidx.compose.ui:ui-text-google-fonts:1.8.1"
    }

    Kotlin

    dependencies {
        ...
        implementation("androidx.compose.ui:ui-text-google-fonts:1.8.1")
    }
  2. מאתחלים את GoogleFont.Provider עם פרטי הכניסה לגופנים של Google:
    val provider = GoogleFont.Provider(
        providerAuthority = "com.google.android.gms.fonts",
        providerPackage = "com.google.android.gms",
        certificates = R.array.com_google_android_gms_fonts_certs
    )
    הפרמטרים שהספק מקבל הם:
    • רשות ספק הגופנים של גופנים של Google.
    • חבילת ספק הגופנים לאימות הזהות של הספק.
    • רשימה של קבוצות גיבוב לאימות הזהות של הספק. אפשר למצוא את הגיבובים שנדרשים לספק Google Fonts בקובץ font_certs.xml באפליקציית הדוגמה Jetchat.
  3. הגדרת FontFamily:
    // ...
     import androidx.compose.ui.text.googlefonts.GoogleFont
     import androidx.compose.ui.text.font.FontFamily
     import androidx.compose.ui.text.googlefonts.Font
     // ...
    
    val fontName = GoogleFont("Lobster Two")
    
    val fontFamily = FontFamily(
        Font(googleFont = fontName, fontProvider = provider)
    )
    אפשר לשלוח שאילתה לפרמטרים אחרים של הגופן, כמו עובי וסגנון, באמצעות FontWeight ו-FontStyle בהתאמה:
    // ...
     import androidx.compose.ui.text.googlefonts.GoogleFont
     import androidx.compose.ui.text.font.FontFamily
     import androidx.compose.ui.text.googlefonts.Font
     // ...
    
    val fontName = GoogleFont("Lobster Two")
    
    val fontFamily = FontFamily(
        Font(
            googleFont = fontName,
            fontProvider = provider,
            weight = FontWeight.Bold,
            style = FontStyle.Italic
        )
    )
  4. מגדירים את FontFamily לשימוש בפונקציה Text composable:

Text(
    fontFamily = fontFamily, text = "Hello World!"
)

אפשר גם להגדיר טיפוגרפיה לשימוש בFontFamily:

val MyTypography = Typography(
    bodyMedium = TextStyle(
        fontFamily = fontFamily, fontWeight = FontWeight.Normal, fontSize = 12.sp/*...*/
    ),
    bodyLarge = TextStyle(
        fontFamily = fontFamily,
        fontWeight = FontWeight.Bold,
        letterSpacing = 2.sp,
        /*...*/
    ),
    headlineMedium = TextStyle(
        fontFamily = fontFamily, fontWeight = FontWeight.SemiBold/*...*/
    ),
    /*...*/
)

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

MyAppTheme(
    typography = MyTypography
)/*...*/

דוגמה לאפליקציה שמטמיעה גופנים להורדה ב-Compose יחד עם Material3 מופיעה באפליקציית הדוגמה Jetchat.

הוספת גופנים חלופיים

אתם יכולים להגדיר שרשרת של חלופות לגופן, למקרה שההורדה שלו תיכשל. לדוגמה, אם הגדרתם גופן להורדה כך:

// ...
 import androidx.compose.ui.text.googlefonts.Font
 // ...

val fontName = GoogleFont("Lobster Two")

val fontFamily = FontFamily(
    Font(googleFont = fontName, fontProvider = provider),
    Font(googleFont = fontName, fontProvider = provider, weight = FontWeight.Bold)
)

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

// ...
 import androidx.compose.ui.text.font.Font
 import androidx.compose.ui.text.googlefonts.Font
 // ...

val fontName = GoogleFont("Lobster Two")

val fontFamily = FontFamily(
    Font(googleFont = fontName, fontProvider = provider),
    Font(resId = R.font.my_font_regular),
    Font(googleFont = fontName, fontProvider = provider, weight = FontWeight.Bold),
    Font(resId = R.font.my_font_regular_bold, weight = FontWeight.Bold)
)

חשוב לוודא שאתם מוסיפים את הייבוא הנכון.

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

ניפוי באגים בהטמעה

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

קודם כול צריך ליצור CoroutineExceptionHandler:

val handler = CoroutineExceptionHandler { _, throwable ->
    // process the Throwable
    Log.e(TAG, "There has been an issue: ", throwable)
}

מעבירים אותו לשיטה createFontFamilyResolver כדי שהפותר ישתמש במטפל החדש:

CompositionLocalProvider(
    LocalFontFamilyResolver provides createFontFamilyResolver(LocalContext.current, handler)
) {
    Column {
        Text(
            text = "Hello World!", style = MaterialTheme.typography.bodyMedium
        )
    }
}

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

val context = LocalContext.current
LaunchedEffect(Unit) {
    if (provider.isAvailableOnDevice(context)) {
        Log.d(TAG, "Success!")
    }
}

נקודות שחשוב לדעת

לוקח כמה חודשים עד שגופנים חדשים יהיו זמינים ב-Android דרך Google Fonts. יש פער זמן בין הרגע שבו גופן מתווסף ל-fonts.google.com לבין הרגע שבו הוא זמין דרך ה-API של גופנים להורדה (במערכת View או ב-Compose). יכול להיות שהגופנים החדשים שנוספו לא ייטענו באפליקציה עם השגיאה IllegalStateException. כדי לעזור למפתחים לזהות את השגיאה הזו מבין סוגים אחרים של שגיאות בטעינת גופנים, הוספנו הודעה תיאורית לחריגה ב-Compose עם השינויים שמופיעים כאן. אם נתקלים בבעיות, אפשר לדווח עליהן באמצעות הכלי למעקב אחר בעיות.

שימוש בגופנים משתנים

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

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

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

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

במסמך הזה מוסבר איך להטמיע גופן משתנה באפליקציית Compose.

טעינת גופן משתנה

  1. מורידים את הגופן המשתנה שרוצים להשתמש בו (לדוגמה, Roboto Flex) וממקמים אותו בתיקייה app/res/font באפליקציה. מוודאים ש .ttf הקובץ שמוסיפים הוא גרסת הגופן המשתנה של הגופן, וששם קובץ הגופן הוא באותיות קטנות בלבד ולא מכיל תווים מיוחדים.

  2. כדי לטעון גופן משתנה, מגדירים FontFamily באמצעות הגופן שמוצב בספרייה res/font/:

    // In Typography.kt
    @OptIn(ExperimentalTextApi::class)
    val displayLargeFontFamily =
        FontFamily(
            Font(
                R.font.robotoflex_variable,
                variationSettings = FontVariation.Settings(
                    FontVariation.weight(950),
                    FontVariation.width(30f),
                    FontVariation.slant(-6f),
                )
            )
        )

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

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

    // In Typography.kt
    val default = FontFamily(
        /*
        * This can be any font that makes sense
        */
        Font(
            R.font.robotoflex_static_regular
        )
    )
    @OptIn(ExperimentalTextApi::class)
    val displayLargeFontFamily = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        FontFamily(
            Font(
                R.font.robotoflex_variable,
                variationSettings = FontVariation.Settings(
                    FontVariation.weight(950),
                    FontVariation.width(30f),
                    FontVariation.slant(-6f),
                )
            )
        )
    } else {
        default
    }

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

    // VariableFontDimension.kt
    object DisplayLargeVFConfig {
        const val WEIGHT = 950
        const val WIDTH = 30f
        const val SLANT = -6f
        const val ASCENDER_HEIGHT = 800f
        const val COUNTER_WIDTH = 500
    }
    
    @OptIn(ExperimentalTextApi::class)
    val displayLargeFontFamily = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        FontFamily(
            Font(
                R.font.robotoflex_variable,
                variationSettings = FontVariation.Settings(
                    FontVariation.weight(DisplayLargeVFConfig.WEIGHT),
                    FontVariation.width(DisplayLargeVFConfig.WIDTH),
                    FontVariation.slant(DisplayLargeVFConfig.SLANT),
                )
            )
        )
    } else {
        default
    }

  5. מגדירים את הטיפוגרפיה של Material Design 3 כך שתשתמש ב-FontFamily:

    // Type.kt
    val Typography = Typography(
        displayLarge = TextStyle(
            fontFamily = displayLargeFontFamily,
            fontSize = 50.sp,
            lineHeight = 64.sp,
            letterSpacing = 0.sp,
            /***/
        )
    )

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

    ב-Material 3, אפשר לשנות את ערכי ברירת המחדל של TextStyle ושל fontFamily כדי להתאים אישית את הטיפוגרפיה. בקטע הקוד שלמעלה, מגדירים מופעים של TextStyle כדי להתאים אישית את הגדרות הגופן לכל משפחת גופנים.

  6. אחרי שמגדירים את הטיפוגרפיה, מעבירים אותה אל M3 MaterialTheme:

    MaterialTheme(
        colorScheme = MaterialTheme.colorScheme,
        typography = Typography,
        content = content
    )

  7. לבסוף, משתמשים בקומפוזבילי Text ומציינים את הסגנון לאחד מסגנונות הטיפוגרפיה המוגדרים, MaterialTheme.typography.displayLarge:

    @Composable
    @Preview
    fun CardDetails() {
        MyCustomTheme {
            Card(
                shape = RoundedCornerShape(8.dp),
                elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(16.dp)
            ) {
                Column(
                    modifier = Modifier.padding(16.dp)
                ) {
                    Text(
                        text = "Compose",
                        style = MaterialTheme.typography.displayLarge,
                        modifier = Modifier.padding(bottom = 8.dp),
                        maxLines = 1
                    )
                    Text(
                        text = "Beautiful UIs on Android",
                        style = MaterialTheme.typography.headlineMedium,
                        modifier = Modifier.padding(bottom = 8.dp),
                        maxLines = 2
                    )
                    Text(
                        text = "Jetpack Compose is Android’s recommended modern toolkit for building native UI. It simplifies and accelerates UI development on Android. Quickly bring your app to life with less code, powerful tools, and intuitive Kotlin APIs.",
                        style = MaterialTheme.typography.bodyLarge,
                        modifier = Modifier.padding(bottom = 8.dp),
                        maxLines = 3
                    )
                }
            }
        }
    }

    כל רכיב Text ניתן להגדרה באמצעות הסגנון של ערכת העיצוב Material שלו, והוא מכיל הגדרת גופן משתנה שונה. אפשר להשתמש ב-MaterialTheme.typography כדי לאחזר את הטיפוגרפיה שסופקה לרכיב הקומפוזבילי M3 MaterialTheme.

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

שימוש בצירים בהתאמה אישית

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

אפשר לשנות את הערך של הצירים האלה באמצעות ההגדרות של FontVariation.

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

  1. כדי להשתמש בצירים בהתאמה אישית, צריך להגדיר פונקציות לצירים ascenderHeight ו-counterWidth בהתאמה אישית:

    fun ascenderHeight(ascenderHeight: Float): FontVariation.Setting {
        require(ascenderHeight in 649f..854f) { "'Ascender Height' must be in 649f..854f" }
        return FontVariation.Setting("YTAS", ascenderHeight)
    }
    
    fun counterWidth(counterWidth: Int): FontVariation.Setting {
        require(counterWidth in 323..603) { "'Counter width' must be in 323..603" }
        return FontVariation.Setting("XTRA", counterWidth.toFloat())
    }

    הפונקציות האלה מבצעות את הפעולות הבאות:

    • הגדרת אמצעי הגנה לערכים שהם יכולים לקבל. כפי שאפשר לראות בקטלוג הגופנים המשתנים, ל-ascenderHeight (YTAS) יש ערך מינימלי של 649f וערך מקסימלי של 854f.
    • מחזירים את הגדרת הגופן, כך שההגדרה מוכנה להוספה לגופן. בשיטה FontVariation.Setting(), שם הציר (YTAS, XTRA) מוצפן, והערך מועבר כפרמטר.
  2. באמצעות הצירים עם הגדרת הגופן, מעבירים פרמטרים נוספים לכל Font שנטען:

    @OptIn(ExperimentalTextApi::class)
    val displayLargeFontFamily = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        FontFamily(
            Font(
                R.font.robotoflex_variable,
                variationSettings = FontVariation.Settings(
                    FontVariation.weight(DisplayLargeVFConfig.WEIGHT),
                    FontVariation.width(DisplayLargeVFConfig.WIDTH),
                    FontVariation.slant(DisplayLargeVFConfig.SLANT),
                    ascenderHeight(DisplayLargeVFConfig.ASCENDER_HEIGHT),
                    counterWidth(DisplayLargeVFConfig.COUNTER_WIDTH)
                )
            )
        )
    } else {
        default
    }

    שימו לב שהגובה של האותיות הקטנות שעולות מעל השורה גדל, והטקסט האחר רחב יותר:

שלושה טקסטים שונים שמציגים הגדרות שונות של גופנים משתנים, עם הגדרות מותאמות אישית של צירים – לחלק מהגופנים יש קווים עולים גבוהים יותר באותיות קטנות והם רחבים יותר מבעבר.
איור 3. טקסט שמציג צירים מותאמים אישית שהוגדרו בגופנים משתנים.

מקורות מידע נוספים

למידע נוסף, אפשר לקרוא את הפוסט הבא בבלוג בנושא גופנים משתנים: