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

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

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

החל מכתיבה 1.2.0, תוכלו להשתמש ב-API של הגופנים להורדה באפליקציית 'כתיבה' כדי להוריד גופנים של 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 בקובץ 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!")
    }
}

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

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

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

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

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

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

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

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

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

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

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

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