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

בדף הזה מוסבר איך להגדיר גופנים באפליקציית 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 הזה ל-composable של 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. מוסיפים את יחסי התלות:

    Groovy

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

    Kotlin

    dependencies {
        ...
        implementation("androidx.compose.ui:ui-text-google-fonts:1.7.5")
    }
  2. מאתחלים את GoogleFont.Provider עם פרטי הכניסה של Google Fonts:
    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 Fonts.
    • חבילת ספק הגופן לאימות הזהות של הספק.
    • רשימה של קבוצות של גיבוב (hash) של האישורים לאימות הזהות של הספק. הגיבוב הנדרש לספק 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.

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

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

מתחילים ליצור 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!")
    }
}

נקודות שצריך לשים לב אליהן:

חולפים כמה חודשים עד שגופנים חדשים יהיו זמינים ב-Google Fonts ב-Android. יש פער זמן בין המועד שבו גופן מתווסף לכתובת fonts.google.com לבין המועד שבו הוא זמין דרך ה-API של הגופנים שניתן להורדה (במערכת התצוגה או בכלי ליצירת הודעות). יכול להיות שגופנים שנוספו לאחרונה לא ייטענו באפליקציה עם הודעת השגיאה 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),
                )
            )
        )

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

  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 כדי לאחזר את הגופנים שסופקו ל-M3MaterialTheme.

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

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

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