Compose 中的文字

文字是所有使用者介面的核心,Jetpack Compose 可讓系統以更簡單的方式顯示或撰寫文字。Compose 可以善用其建構塊的組合,這意味著您不需要覆寫屬性和方法,也不需要擴充大型類別,即可擁有特定可組合項設計以及按照您想要的方式執行的邏輯。

Compose 提供了 BasicTextBasicTextField 基本元素,做為顯示文字及處理使用者輸入內容的基本方式。Compose 在更高級別提供 TextTextField,這是根據質感設計準則所構成的組件。建議您使用它們,因為這樣一來,Android 使用者才能獲得良好的外觀和風格,同時提供其他選項簡化自訂程序,而且完全不須編寫大量的程式碼。

顯示文字

顯示文字最基本的方法,就是以 String 做為引數的方式使用 Text 組件:

@Composable
fun SimpleText() {
    Text("Hello World")
}

以黑色純文字顯示「Hello World」字詞

顯示資源中的文字

我們建議您使用字串資源,而非針對 Text 值進行硬式編碼,因為這樣您可以和 Android 檢視畫面共用相同的字串,以及做好應用程式國際化準備工作:

@Composable
fun StringResourceText() {
    Text(stringResource(R.string.hello_world))
}

樣式文字

Text 組件提供多個選用參數,可用以設定其內容樣式。以下我們列出涵蓋最常見的文字使用方式參數。若要查看 Text 的所有參數,建議您查看 Compose 文字原始碼

每當您設定其中一個參數,您就會套用樣式至整個文字值。如果您需要在同一行或同一段落中套用多種樣式,請參閱多種內嵌樣式一節。

變更文字顏色

@Composable
fun BlueText() {
    Text("Hello World", color = Color.Blue)
}

以藍色文字顯示「Hello World」字詞

變更文字大小

@Composable
fun BigText() {
  Text("Hello World", fontSize = 30.sp)
}

字型尺寸較大的「Hello World」字詞

將文字設定為斜體

使用 fontStyle 參數將文字設為斜體 (或設定另一個 FontStyle)。

@Composable
fun ItalicText() {
  Text("Hello World", fontStyle = FontStyle.Italic)
}

斜體字型的「Hello World」字詞

將文字設定為粗體

使用 fontWeight 參數將文字設為粗體 (或設定其他 FontWeight)。

@Composable
fun BoldText() {
    Text("Hello World", fontWeight = FontWeight.Bold)
}

粗體字型的「Hello World」字詞

文字對齊

textAlign 參數允許您在 Text 組件途徑區中設定文字的對齊方式

根據預設,Text 會根據內容值選擇自然文字對齊方式:

  • Text 容器的左側邊緣為由左至右的字母,例如拉丁文、斯拉夫文或韓文
  • Text 容器的右側邊緣為由右至左的字母,例如阿拉伯文或希伯來文
@Preview(showBackground = true)
@Composable
fun CenterText() {
    Text("Hello World", textAlign = TextAlign.Center,
                modifier = Modifier.width(150.dp))
}

「Hello World」字詞在其所包含的元素內置中

若要手動設定 Text 組件的文字對齊方式,建議個別使用 TextAlign.StartTextAlign.End,而不要使用 TextAlign.LeftTextAlign.Right,因為它們會根據偏好語言文字方向解析 Text 組件的右側邊緣。舉例來說,TextAlign.End 會對齊法文的右側以及阿拉伯文的左側,但無論使用何種字母,TextAlign.Right 都會對齊右側。

陰影

您可以透過 style 參數設定 TextStyle 類型的物件,並設定多個參數,例如陰影。Shadow 會接收陰影色彩、偏移值或與 Text 的相對位置,以及模糊半徑 (即模糊程度)。

@Preview(showBackground = true)
@Composable
fun TextShadow() {
    val offset = Offset(5.0f, 10.0f)
    Text(
        text = "Hello world!",
        style = TextStyle(
            fontSize = 24.sp,
            shadow = Shadow(
                color = Color.Blue,
                offset = offset,
                blurRadius = 3f
            )
        )
    )
}

有藍色陰影的「Hello World」字樣

使用字型

TextfontFamily 參數,可設定在組件中使用的字型。系統預設包含 Serif、Sans Serif、等寬和草寫字型系列:

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

兩個不同字型的「Hello World」字詞 (有襯線及沒有襯線)

您可以使用 fontFamily 屬性搭配 res/font 資料夾中定義的自訂字型和字體:

字型資料夾的圖形描述" class="l10n-absolute-url-src screenshot" l10n-attrs-original-order="src,alt,width,class" src="https://developer.android.com/static/images/jetpack/compose/text-font-folder.png" width="400" />

以下範例說明如何根據這些字型檔案以及使用 Font 函式定義 fontFamily

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(..., fontFamily = firaSansFamily, fontWeight = FontWeight.Light)
    Text(..., fontFamily = firaSansFamily, fontWeight = FontWeight.Normal)
    Text(
        ..., fontFamily = firaSansFamily, fontWeight = FontWeight.Normal,
        fontStyle = FontStyle.Italic
    )
    Text(..., fontFamily = firaSansFamily, fontWeight = FontWeight.Medium)
    Text(..., fontFamily = firaSansFamily, fontWeight = FontWeight.Bold)
}

幾種不同類型粗細和樣式的「Hello World」字詞

若要瞭解如何在整個應用程式中設定字型樣式,請參閱主題說明文件

文字中的多種樣式

如要在相同的 Text 組件中設定不同樣式,您必須使用可使用任意註解樣式加上註解的字串 AnnotatedString

AnnotatedString 是包含以下的資料類別:

  • Text
  • SpanStyleRangeList,等同於文字值內位置範圍的內嵌樣式
  • ParagraphStyleRangeList,用於指定文字對齊方式、文字方向、行高和文字縮排樣式

TextStyle 用於 Text 組件中,其中 SpanStyleParagraphStyle 適用於 AnnotatedString

SpanStyleParagraphStyle 的差異在於 ParagraphStyle 可以套用至整個段落,SpanStyle 則可套用字元層級。如果部分文字有加上 ParagraphStyle 標記,則該部分會與其餘部分分開,如同在開頭和結尾部分加上換行字元。

AnnotatedString 具有類型安全建構工具,可讓您輕鬆建立:buildAnnotatedString

@Composable
fun MultipleStylesInText() {
    Text(
        buildAnnotatedString {
            withStyle(style = SpanStyle(color = Color.Blue)) {
                append("H")
            }
            append("ello ")

            withStyle(style = SpanStyle(fontWeight = FontWeight.Bold, color = Color.Red)) {
                append("W")
            }
            append("orld")
        }
    )
}

顯示有多種樣式變更內嵌的「Hello World」字詞;H 是藍色,W 是紅色和粗體

我們能夠以相同方式設定 ParagraphStyle

@Composable
fun ParagraphStyle() {
    Text(
        buildAnnotatedString {
            withStyle(style = ParagraphStyle(lineHeight = 30.sp)) {
                withStyle(style = SpanStyle(color = Color.Blue)) {
                    append("Hello\n")
                }
                withStyle(
                    style = SpanStyle(
                        fontWeight = FontWeight.Bold,
                        color = Color.Red
                    )
                ) {
                    append("World\n")
                }
                append("Compose")
            }
        }
    )
}

三個段落分成三種樣式:藍色、紅色、粗體和純黑色

行數上限

如要限制 Text 組件的顯示行數,請設定 maxLines 參數:

@Composable
fun LongText() {
    Text("hello ".repeat(50), maxLines = 2)
}

長文字段落在兩行之後截斷

文字溢位

限制長文字時,您可能需要指定 TextOverflow;這只有在顯示的文字遭到截斷時才會顯示。如要執行此操作,請設定 textOverflow 參數:

@Composable
fun OverflowedText() {
    Text("Hello Compose ".repeat(50), maxLines = 2, overflow = TextOverflow.Ellipsis)
}

文字的長段落在三行後截斷,結尾是省略符號

includeFontPadding 和 lineHeight API

includeFontPadding 是舊版屬性,並會根據文字第一行頂端和最後一行的字型指標新增額外的邊框間距。在 Compose 1.2.0 中,includeFontPadding 預設為 True。

建議您從 Compose 1.2.0 版本中使用已淘汰的 API PlatformTextStyle,將 includeFontPadding 設為 False (這會移除額外邊框間距),並進一步調整文字。

設定 lineHeight 的功能並非全新功能,這項功能自 Android Q 起即可使用。您可以使用 lineHeight 參數將 Text 設定為 lineHeight,這會在每行文字中分配行高。然後,您就可以使用新的 LineHeightStyle API,進一步設定文字在聊天室中對齊的方式,並移除空白字元。

建議使用文字單位「em」(相對字型大小),而不使用「sp」(縮放像素) 來調整 lineHeight,以提升精確度。如要進一步瞭解如何選取適當的文字單元,請參閱此處的說明文件

根據正上方和正下方線條,將 lineHeight 作為測量顯示的圖片。
使用對齊和剪輯功能調整「行高」集合中的文字,並視需要修剪額外空間
@Composable
fun AlignedText() {
    Text(
        text = myText,
        style = LocalTextStyle.current.merge(
            TextStyle(
                lineHeight = 2.5.em,
                platformStyle = PlatformTextStyle(
                    includeFontPadding = false
                ),
                lineHeightStyle = LineHeightStyle(
                    alignment = LineHeightStyle.Alignment.Center,
                    trim = LineHeightStyle.Trim.None
                )
            )
        )
    )
}

除了調整 lineHeight,您現在可以透過LineHeightStyle實驗性 API 使用設定來進一步使文字置中和設定樣式:LineHeightStyle.AlignmentLineHeightStyle.Trim (includeFontPadding必須設定為 False,剪輯才能正常運作)。對齊和剪輯會將文字行間測量到的空間,更妥善地分配至所有行,包括單行文字和文字區塊的頂行文字。

LineHeightStyle.Alignment 定義如何利用行高所提供的空間將行對齊。在每行中,您可以將文字與頂端、底部、置中或依比例對齊。LineHeightStyle.Trim 則允許您保留或刪除文字第一行頂部和最後一行底部的額外空間,這些空間是由任何 lineHeight 和對齊調整所產生。以下範例顯示了置中對齊時 (LineHeightStyle.Alignment.Center),多行文字搭配各種 LineHeightStyle.Trim 設定的樣式。

示範 LineHeightStyle.Trim.None 的圖片 示範 LineHeightStyle.Trim.Both 的圖片
LineHeightStyle.Trim.None LineHeightStyle.Trim.Both
示範 LineHeightStyle.Trim.FirstLineTop 的圖片 示範 LineHeightStyle.Trim.LastLineBottom 的圖片
LineHeightStyle.Trim.FirstLineTop LineHeightStyle.Trim.LastLineBottom
一起使用時才會有效

請參閱「修正 Compose 文字中的字型邊框間距」網誌文章,進一步瞭解這項變更的前後內容,並瞭解 includeFontPadding 在 View 系統中的運作方式,以及 Compose 的變更和新的 LineHeightStyle API。

主題設定

如要使用應用程式主題設定文字樣式,請參閱主題說明文件

使用者互動

Jetpack Compose 可在 Text 中進行精細的互動。文字選取功能現在更具彈性,並且可以在可組合項的版面配置中完成。文字中的使用者互動與其他可組合項的版面配置不同,因為您無法在一部分 Text 可組合項中新增修飾詞。本節重點介紹各種可於啟用使用者互動的 API。

選取文字

根據預設,可組合項是無法選取的,也就是說,在預設情況下,使用者無法選取及複製應用程式中的文字。如要啟用文字選取功能,必須使用 SelectionContainer 可組合項包裝文字元素:

@Composable
fun SelectableText() {
    SelectionContainer {
        Text("This text is selectable")
    }
}

由使用者選取的簡短文字段落。

您可能需要針對可選取區域的特定部分停用選取功能。如要執行此操作,您必須使用 DisableSelection 組件納入無法選取的部分:

@Composable
fun PartiallySelectableText() {
    SelectionContainer {
        Column {
            Text("This text is selectable")
            Text("This one too")
            Text("This one as well")
            DisableSelection {
                Text("But not this one")
                Text("Neither this one")
            }
            Text("But again, you can select this one")
            Text("And this one too")
        }
    }
}

較長的文字段落。使用者嘗試選取整個段落,但因為有兩行已套用 DisableSelection,因此並未選取該選項。

取得點擊文字的位置

如要監聽 Text 上的點擊,您可以新增 clickable 輔助鍵。但是,如果您想在 Text 可組合項中取得點選位置,在對文字的不同部分執行不同動作的情況下,就必須改用 ClickableText

@Composable
fun SimpleClickableText() {
    ClickableText(
        text = AnnotatedString("Click Me"),
        onClick = { offset ->
            Log.d("ClickableText", "$offset -th character is clicked.")
        }
    )
}

附帶備註的點擊

當使用者按一下 Text 組件時,您可能需要在部分 Text 值中附加其他資訊,例如附加至特定字詞,以便在瀏覽器中開啟的網址:若要執行此操作,您必須附加註解,以標記 (String)、項目 (String) 和文字範圍做為參數。使用 AnnotatedString 即可依標記或文字範圍篩選這些註解。範例如下:

@Composable
fun AnnotatedClickableText() {
    val annotatedText = buildAnnotatedString {
        append("Click ")

        // We attach this *URL* annotation to the following content
        // until `pop()` is called
        pushStringAnnotation(tag = "URL",
                             annotation = "https://developer.android.com")
        withStyle(style = SpanStyle(color = Color.Blue,
                                    fontWeight = FontWeight.Bold)) {
            append("here")
        }

        pop()
    }

    ClickableText(
        text = annotatedText,
        onClick = { offset ->
            // We check if there is an *URL* annotation attached to the text
            // at the clicked position
            annotatedText.getStringAnnotations(tag = "URL", start = offset,
                                                    end = offset)
                .firstOrNull()?.let { annotation ->
                    // If yes, we log its value
                    Log.d("Clicked URL", annotation.item)
                }
        }
    )
}

輸入並修改文字

TextField 可讓使用者輸入及修改文字。TextField 實作分為兩個層級:

  1. TextField 為質感設計實作。建議您選擇此實作程序,因為符合質感設計準則
    • 填入預設樣式
    • OutlinedTextField外框樣式版本
  2. BasicTextField 可讓使用者透過硬體或螢幕鍵盤編輯文字,但無法提供提示或預留位置等裝飾。
@Composable
fun SimpleFilledTextFieldSample() {
    var text by remember { mutableStateOf("Hello") }

    TextField(
        value = text,
        onValueChange = { text = it },
        label = { Text("Label") }
    )
}

含有「Hello」字詞的可編輯文字欄位。這個欄位含有不可編輯的標籤「label」。

@Composable
fun SimpleOutlinedTextFieldSample() {
    var text by remember { mutableStateOf("") }

    OutlinedTextField(
        value = text,
        onValueChange = { text = it },
        label = { Text("Label") }
    )
}

可編輯文字欄位,帶有紫色邊框和標籤。

設定文字欄位樣式

TextFieldBasicTextField 會共用許多常見的參數以進行自訂。TextField 原始碼內提供 TextField 的完整清單。這份清單僅列舉部分有用參數的內容:

  • singleLine
  • maxLines
  • textStyle
@Composable
fun StyledTextField() {
    var value by remember { mutableStateOf("Hello\nWorld\nInvisible") }

    TextField(
        value = value,
        onValueChange = { value = it },
        label = { Text("Enter text") },
        maxLines = 2,
        textStyle = TextStyle(color = Color.Blue, fontWeight = FontWeight.Bold),
        modifier = Modifier.padding(20.dp)
    )
}

多行 TextField,包含可編輯的兩行加上標籤

如果設計需要 Material TextField 或 OutlineTextField,建議使用 BasicTextField 而非 TextField。然而,如果建構的設計不需要使用材質規格的裝飾,則應使用 BasicTextField

鍵盤選項

TextField 可讓您設定鍵盤設定選項 (例如鍵盤配置);或是啟用自動更正功能 (如果鍵盤有支援)。如果螢幕鍵盤不符合此處提供的選項,則可能無法保證某些選項可以使用。以下是支援的鍵盤選項清單:

  • capitalization
  • autoCorrect
  • keyboardType
  • imeAction

格式設定

TextField 可讓您為輸入值設定 VisualTransformation,例如將密碼中的字符更換為 *,或是在信用卡號碼中每隔 4 位數字插入一個連字號:

@Composable
fun PasswordTextField() {
    var password by rememberSaveable { mutableStateOf("") }

    TextField(
        value = password,
        onValueChange = { password = it },
        label = { Text("Enter password") },
        visualTransformation = PasswordVisualTransformation(),
        keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password)
    )
}

密碼文字輸入欄位,有遮罩文字

您可以在 VisualTransformSamples 原始碼中找到更多範例。

清理輸入

編輯文字的常見工作是移除前置字元,或在每次變更文字時轉換輸入字串。

做為模型,您應該假設每個 onValueChange 都可進行任意和大規模編輯。舉例來說,如果使用者使用自動更正功能,並以表情符號取代字詞或其他智慧編輯功能,就可能發生這種情況。為了正確處理這種情況,請在編寫任何轉換邏輯時假設,傳遞至 onValueChange 的目前文字與將被傳遞至 onValueChange 的前一個或下一個值無關。

如要實作不允許內容開頭為零的文字欄位,可以在每次值變更時移除所有開頭的零。

@Composable
fun NoLeadingZeroes() {
  var input by rememberSaveable { mutableStateOf("") }
  TextField(
      value = input,
      onValueChange = { newText ->
          input = newText.trimStart { it == '0' }
      }
  )
}

如要在清除文字時控制遊標位置,請使用 TextFieldTextFieldValue 超載做為狀態的一部分。

可下載的字型

Compose 1.2-alpha07 開始,您可以使用 Compose 應用程式中的可下載字型 API,以非同步方式下載 Google 字型,然後在應用程式中使用。

目前暫不支援自訂提供者提供的可下載字型。

透過程式輔助方式使用可下載字型

如要透過程式輔助方式下載字型,請執行下列步驟:

  1. 新增依附元件:

    Groovy

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

    Kotlin

    dependencies {
        ...
        implementation("androidx.compose.ui:ui-text-google-fonts:1.3.0")
    }
  2. 使用 Google Fonts 的憑證初始化 GoogleFont.Provider
    @OptIn(ExperimentalTextApi::class)
    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 的字型提供者授權。
    • 用於辨識供應者身分的字型供應者套件。
    • 憑證的雜湊組合,用於驗證供應者的身分。您可以在 JetChat 範例應用程式的 font_certs.xml 檔案中找到 Google Fonts 供應者所需的雜湊。
    請注意,您必須新增 ExperimentalTextApi 註解,才能在您的應用程式中使用可下載的字型 API。
  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)
    )
    
    您可以個別使用 FontWeightFontStyle 查詢字型使用的其他參數 (例如粗細和樣式):
    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(
        fontFamily = fontFamily,
        text = "Hello World!"
    )
    
    您還可以使用你的 FontFamily 定義字型
    val MyTypography = Typography(
       body1 = TextStyle(
       fontFamily = fontFamily,
       fontWeight = FontWeight.Normal,
       fontSize = ...
    ),
       body2 = TextStyle(
       fontFamily = fontFamily,
       fontWeight = FontWeight.Bold,
       letterSpacing = ...
    ),
       h4 = 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.body1
            )
        }
    }

您也可以使用提供者的 isAvailableOnDevice API 來測試提供者是否可用,以及憑證設定是否正確無誤。若要執行此操作,您可以呼叫 isAvailableOnDevice 方法,若提供者設定錯誤,則會傳回 false。

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

注意事項

Google Fonts 需要數個月的時間才能在 Android 上提供新字型。當字型在 fonts.google.com 中新增後,需要一段時間才可透過可下載字型 API 提供 (在檢視系統或 Compose 中)。使用 IllegalStateException 載入的新字型可能無法在應用程式中載入。為了協助開發人員判斷該錯誤,並與其他類型的字型載入錯誤進行比較,我們已針對 Compose 使用這裡的變更新增例外狀況訊息。