일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 안드로이드 라이선스 종류
- 2022 플러터 설치
- Rxjava Observable
- 안드로이드 os 구조
- jvm 작동 원리
- 안드로이드 레트로핏 crud
- 클래스
- 멤버변수
- 큐 자바 코드
- 서비스 쓰레드 차이
- android ar 개발
- 플러터 설치 2022
- rxjava hot observable
- ANR이란
- 안드로이드 유닛 테스트
- 스택 자바 코드
- android retrofit login
- 서비스 vs 쓰레드
- 안드로이드 라이선스
- jvm이란
- 자바 다형성
- 2022 플러터 안드로이드 스튜디오
- 스택 큐 차이
- ar vr 차이
- 안드로이드 유닛테스트란
- 안드로이드 유닛 테스트 예시
- rxjava cold observable
- 안드로이드 레트로핏 사용법
- 객체
- rxjava disposable
- Today
- Total
나만을 위한 블로그
[Android Compose] 폰트 적용하기 본문
이 포스팅은 아래 디벨로퍼 문서를 바탕으로 작성했다.
https://developer.android.com/develop/ui/compose/text/fonts?hl=ko
글꼴 작업 | Jetpack Compose | Android Developers
이 페이지는 Cloud Translation API를 통해 번역되었습니다. 글꼴 작업 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 이 페이지에서는 Compose 앱에서 글꼴을 설정
developer.android.com
이 문서에서 사용하는 폰트를 다운로드하려면 아래 링크로 들어가면 된다. 그러나 첫 부분에서만 쓰고 이후부턴 라이브러리를 통해 폰트를 다운받아 쓰기 때문에 대충 하는 법만 봐도 될 것이다.
https://fonts.google.com/selection
Google Fonts
Making the web more beautiful, fast, and open through great typography
fonts.google.com
폰트 설정
Text의 매개변수 중 fontFamily를 쓰면 컴포저블에 쓰이는 폰트를 설정할 수 있다. 기본적으로 Serif, Sans Serif, 고정폭, 필기체 폰트 모음이 포함된다.
fontFamily를 써서 res/font 폴더에 정의된 커스텀 폰트를 쓸 수 있다. font 폴더는 프로젝트 생성 시 기본 생성되지 않기 때문에 res 폴더를 우클릭해서 폴더를 직접 만들어야 한다.
그리고 font 폴더 안에 들어가는 폰트 파일에는 대문자와 대시(-)가 없어야 한다. 소문자와 언더바를 써서 대충 구분할 수 있게만 넣어주면 된다. 프로젝트 폴더 안에 넣고 나서 바꾸려면 여간 귀찮은 게 아니니 그나마 덜 귀찮게 폴더에 넣기 전에 이름을 바꿔서 넣는 게 좋다.
넣고 나면 Type.kt에 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),
)
그리고 Text 컴포저블에서 방금 선언한 fontFamily를 갖다 쓸 수 있다. fontFamily에 여러 FontWeight가 있기 때문에 수동으로 fontWeight를 설정해서 원하는 가중치를 설정할 수 있다.
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import com.example.composepractice.ui.theme.ComposePracticeTheme
import com.example.composepractice.ui.theme.firaSansFamily
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
ComposePracticeTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
TestText(innerPadding)
}
}
}
}
}
@Composable
fun TestText(
innerPadding: PaddingValues,
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
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
)
}
}
다운로드 가능한 폰트
컴포즈 1.2.0부터 다운로드 가능한 폰트 api를 써서 구글 폰트를 비동기식으로 다운로드하고 앱에서 쓸 수 있다. 커스텀 제공업체에서 제공하는 다운로드 가능한 폰트는 현재 미지원이다.
라이브러리 형태로 제공되기 때문에 앱 gradle에 1줄 추가해야 한다.
[versions]
uiTextGoogleFonts = "1.9.0"
[libraries]
androidx-ui-text-google-fonts = { module = "androidx.compose.ui:ui-text-google-fonts", version.ref = "uiTextGoogleFonts" }
implementation(libs.androidx.ui.text.google.fonts)
이후 구글 폰트의 유저 인증 정보를 써서 GoogleFont.Provider를 초기화한다.
val provider = GoogleFont.Provider(
providerAuthority = "com.google.android.gms.fonts",
providerPackage = "com.google.android.gms",
certificates = R.array.com_google_android_gms_fonts_certs
)
R.array.com 어쩌고의 값은 아래 링크에서 가져왔다.
compose-samples/Jetchat/app/src/main/res/values-v23/font_certs.xml at main · android/compose-samples
Official Jetpack Compose samples. Contribute to android/compose-samples development by creating an account on GitHub.
github.com
<?xml version="1.0" encoding="utf-8"?>
<resources>
<array name="com_google_android_gms_fonts_certs">
<item>@array/com_google_android_gms_fonts_certs_dev</item>
<item>@array/com_google_android_gms_fonts_certs_prod</item>
</array>
<string-array name="com_google_android_gms_fonts_certs_dev">
<item>
MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAeFw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpFeVyxW0qMBujb8X8ETrWy550NaFtI6t9+u7hZeTfHwqNvacKhp1RbE6dBRGWynwMVX8XW8N1+UjFaq6GCJukT4qmpN2afb8sCjUigq0GuMwYXrFVee74bQgLHWGJwPmvmLHC69EH6kWr22ijx4OKXlSIx2xT1AsSHee70w5iDBiK4aph27yH3TxkXy9V89TDdexAcKk/cVHYNnDBapcavl7y0RiQ4biu8ymM8Ga/nmzhRKya6G0cGw8CAQOjgfwwgfkwHQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0cxb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrPzgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXclaXjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05aIskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+aayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUWEv9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs=
</item>
</string-array>
<string-array name="com_google_android_gms_fonts_certs_prod">
<item>
MIIEQzCCAyugAwIBAgIJAMLgh0ZkSjCNMA0GCSqGSIb3DQEBBAUAMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDAeFw0wODA4MjEyMzEzMzRaFw0zNjAxMDcyMzEzMzRaMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBAKtWLgDYO6IIrgqWbxJOKdoR8qtW0I9Y4sypEwPpt1TTcvZApxsdyxMJZ2JORland2qSGT2y5b+3JKkedxiLDmpHpDsz2WCbdxgxRczfey5YZnTJ4VZbH0xqWVW/8lGmPav5xVwnIiJS6HXk+BVKZF+JcWjAsb/GEuq/eFdpuzSqeYTcfi6idkyugwfYwXFU1+5fZKUaRKYCwkkFQVfcAs1fXA5V+++FGfvjJ/CxURaSxaBvGdGDhfXE28LWuT9ozCl5xw4Yq5OGazvV24mZVSoOO0yZ31j7kYvtwYK6NeADwbSxDdJEqO4k//0zOHKrUiGYXtqw/A0LFFtqoZKFjnkCAQOjgdkwgdYwHQYDVR0OBBYEFMd9jMIhF1Ylmn/Tgt9r45jk14alMIGmBgNVHSMEgZ4wgZuAFMd9jMIhF1Ylmn/Tgt9r45jk14aloXikdjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChMLR29vZ2xlIEluYy4xEDAOBgNVBAsTB0FuZHJvaWQxEDAOBgNVBAMTB0FuZHJvaWSCCQDC4IdGZEowjTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBAUAA4IBAQBt0lLO74UwLDYKqs6Tm8/yzKkEu116FmH4rkaymUIE0P9KaMftGlMexFlaYjzmB2OxZyl6euNXEsQH8gjwyxCUKRJNexBiGcCEyj6z+a1fuHHvkiaai+KL8W1EyNmgjmyy8AW7P+LLlkR+ho5zEHatRbM/YAnqGcFh5iZBqpknHf1SKMXFh4dd239FJ1jWYfbMDMy3NS5CTMQ2XFI1MvcyUTdZPErjQfTbQe3aDQsQcafEQPD+nqActifKZ0Np0IS9L9kR/wbNvyz6ENwPiTrjV2KRkEjH78ZMcUQXg0L3BYHJ3lc69Vs5Ddf9uUGGMYldX3WfMBEmh/9iFBDAaTCK
</item>
</string-array>
</resources>
제공업체에서 받는 매개변수는 아래와 같다.
- 구글 폰트 제공업체 권한
- 제공업체 ID를 확인하기 위한 폰트 제공업체 패키지
- 제공업체 ID를 확인하는 인증서의 해시 세트 목록. JetChat 샘플 앱의 font_certs.xml 파일에서 구글 폰트 제공업체에 필요한 해시를 볼 수 있다
이제 Type.kt 등에 FontFamily를 정의하고 FontWeight, FontStyle을 정의하면 폰트 두께 등 다른 매개변수를 쿼리할 수 있다.
val provider = GoogleFont.Provider(
providerAuthority = "com.google.android.gms.fonts",
providerPackage = "com.google.android.gms",
certificates = R.array.com_google_android_gms_fonts_certs
)
val fontName = GoogleFont("Lobster Two")
val fontFamily = FontFamily(
Font(
googleFont = fontName,
fontProvider = provider,
weight = FontWeight.Bold,
style = FontStyle.Italic
)
)
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.example.composepractice.ui.theme.ComposePracticeTheme
import com.example.composepractice.ui.theme.fontFamily
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
ComposePracticeTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
TestText(innerPadding)
}
}
}
}
}
@Composable
fun TestText(
innerPadding: PaddingValues,
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = "text",
fontFamily = fontFamily,
)
}
}
안드로이드 스튜디오 Narwhal Feature Drop 2025.1.2 Patch 1 버전 기준으로 프리뷰에선 위 수정사항이 표시되지 않아서 빌드한후에 확인할 수 있다. 필기체 같은 글자체가 사선으로 표시된다.
FontFamily를 사용하게 설정할 수도 있다.
androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.googlefonts.Font
import androidx.compose.ui.text.googlefonts.GoogleFont
import androidx.compose.ui.unit.sp
import com.example.composepractice.R
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)
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),
)
val provider = GoogleFont.Provider(
providerAuthority = "com.google.android.gms.fonts",
providerPackage = "com.google.android.gms",
certificates = R.array.com_google_android_gms_fonts_certs
)
val fontName = GoogleFont("Lobster Two")
val fontFamily = FontFamily(
Font(
googleFont = fontName,
fontProvider = provider,
weight = FontWeight.Bold,
style = FontStyle.Italic
)
)
val MyTypoGraphy = Typography(
bodyMedium = TextStyle(
fontFamily = fontFamily,
fontWeight = FontWeight.Normal,
fontSize = 12.sp,
),
bodyLarge = TextStyle(
fontFamily = fontFamily,
fontWeight = FontWeight.Bold,
fontSize = 2.sp,
),
headlineMedium = TextStyle(
fontFamily = fontFamily,
fontWeight = FontWeight.Bold,
)
)
그리고 Theme.kt에서 위에서 만든 MyTypoGraphy를 설정하면 된다.
@Composable
fun ComposePracticeTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = MyTypoGraphy,
content = content
)
}
Material3을 써서 컴포즈에서 다운로드 가능한 폰트를 구현한 앱의 예시는 JetChat 앱을 참고한다.
https://github.com/android/compose-samples/tree/main/Jetchat
compose-samples/Jetchat at main · android/compose-samples
Official Jetpack Compose samples. Contribute to android/compose-samples development by creating an account on GitHub.
github.com
대체 폰트 추가
폰트가 제대로 다운되지 않는 경우를 대비해 폰트의 대체 체인을 결정할 수 있다. 아래처럼 폰트가 정의된 경우를 가정한다.
val fontName = GoogleFont("Lobster Two")
val fontFamily = FontFamily(
Font(googleFont = fontName, fontProvider = provider),
Font(googleFont = fontName, fontProvider = provider, weight = FontWeight.Bold)
)
두 가중치의 기본 폰트를 아래처럼 정의할 수 있다.
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를 이렇게 정의하면 1개의 가중치 별 2개의 체인이 포함된 FontFamily가 만들어진다. 불러오기 메커니즘은 먼저 온라인 폰트를 결정하려고 시도한 다음 로컬의 R.font 폴더에 있는 폰트를 결정하려고 한다.
구현 디버깅
폰트가 잘 다운되는지 확인하려면 디버그 코루틴 핸들러를 정의한다. 핸들은 폰트가 비동기적으로 로드되지 않는 경우 실행할 작업을 제공한다.
먼저 CoroutineExceptionHandler를 만든다.
val handler = CoroutineExceptionHandler { _, t ->
Log.e("test", "## [코루틴 예외 핸들러] t : $t")
}
리졸버가 새 핸들러를 사용하게 createFontFamilyResolver 메서드에 전달한다.
CompositionLocalProvider(
LocalFontFamilyResolver provides createFontFamilyResolver(LocalContext.current, handler)
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = "text",
style = MaterialTheme.typography.bodyMedium
)
}
}
제공업체의 isAvailableOnDevice()를 써서 제공업체를 쓸 수 있는지, 인증서가 올바르게 구성됐는지 테스트할 수 있다. 이렇게 하려면 제공업체가 잘못 구성됐을 때 false를 리턴하는 isAvailableOnDevice()를 호출한다.
@Composable
fun TestText(
innerPadding: PaddingValues,
) {
val handler = CoroutineExceptionHandler { _, t ->
Log.e("test", "## [코루틴 예외 핸들러] t : $t")
}
val provider = GoogleFont.Provider(
providerAuthority = "com.google.android.gms.fonts",
providerPackage = "com.google.android.gms",
certificates = R.array.com_google_android_gms_fonts_certs
)
val context = LocalContext.current
LaunchedEffect(Unit) {
if (provider.isAvailableOnDevice(context)) {
Log.d("test", "Success!")
}
}
CompositionLocalProvider(
LocalFontFamilyResolver provides createFontFamilyResolver(LocalContext.current, handler)
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = "text",
style = MaterialTheme.typography.bodyMedium
)
}
}
}
구글 폰트가 안드로이드에서 새 폰트를 제공하려면 몇 달 정도 걸린다. 그래서 폰트가 fonts.goolge.com에 추가되는 시점, 다운 가능한 폰트 API를 통해 뷰 시스템 or 컴포즈에 제공되는 시점 사이 차이가 있다. IllegalStateException을 써도 새로 추가한 폰트가 앱에서 로드되지 않을 수 있으니 주의한다.
가변 폰트 사용
가변 폰트는 하나의 폰트 파일이 여러 스타일을 포함할 수 있는 폰트 형식이다. 가변 폰트를 쓰면 축(또는 매개변수)을 수정해서 원하는 스타일을 생성할 수 있다.
이런 축은 가중치, 너비, 기울기, 이탤릭체 같은 표준 축일 수 있고 가변 폰트마다 다른 축일 수 있다.
사용할 가변 폰트(여기선 Roboto Flex)를 다운로드해서 싱글 모듈 기준 app/res/font 폴더에 배치한다. ttf 파일이 폰트의 가변 폰트 버전이고 폰트 파일 이름이 모두 소문자면서 특수문자가 없다.
추가로 현재 다운 가능한 폰트를 통한 가변 폰트는 아직 미지원이다. 이슈 트래커에서 최신 업데이트를 확인할 수 있다.
https://issuetracker.google.com/issues/223262013?hl=ko
Google Issue Tracker
issuetracker.google.com
가변 폰트를 로드하려면 font 폴더에 담긴 폰트를 써서 FontFamily를 정의한다.
import androidx.compose.material3.Typography
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontVariation
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.googlefonts.Font
import androidx.compose.ui.text.googlefonts.GoogleFont
import androidx.compose.ui.unit.sp
import com.example.composepractice.R
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)
...
@OptIn(ExperimentalTextApi::class)
val displayLargeFontFamily = FontFamily(
Font(
resId = R.font.roboto_variable,
variationSettings = FontVariation.Settings(
FontVariation.weight(950),
FontVariation.width(30f),
FontVariation.slant(-6f)
)
)
)
FontVariation API를 쓰면 weight, width, slant(기울기) 같은 표준 폰트 축을 구성할 수 있다. 이건 모든 가변 폰트에서 쓸 수 있는 표준 축이다. 폰트가 쓰일 위치에 따라 폰트의 여러 구성을 만들 수 있다.
가변 폰트는 안드로이드 오레오(api 26) 이상에서만 쓸 수 있어서 버전 분기를 추가하고 대체 폰트를 구성해야 한다.
val default = FontFamily(
Font(resId = R.font.firasans_regular)
)
@OptIn(ExperimentalTextApi::class)
val displayLargeFontFamily = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
FontFamily(
Font(
R.font.roboto_variable,
variationSettings = FontVariation.Settings(
FontVariation.weight(950),
FontVariation.width(30f),
FontVariation.slant(-6f),
)
)
)
} else {
default
}
현재 카톡의 minSdk가 안드로이드 9(파이, api 28)라서 26을 지원하는 경우는 드물 것 같다. 그러나 26보다 높은 버전을 지원한다는 사실이 몰라도 되는 이유는 되지 않으니 알아두기라도 하면 좋을 것이다.
이제 더 쉽게 재사용할 수 있게 상수 집합으로 추출하고 폰트 설정을 이 상수로 대체할 수 있다.
object DisplayLargeVFConfig {
const val WEIGHT = 950
const val WIDTH = 30f
const val SLANT = -6f
const val ASCENDER_HEIGHT = 800f
const val COUNTER_WIDTH = 500
}
val default = FontFamily(
Font(resId = R.font.firasans_regular)
)
@OptIn(ExperimentalTextApi::class)
val displayLargeFontFamily = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
FontFamily(
Font(
R.font.roboto_variable,
variationSettings = FontVariation.Settings(
FontVariation.weight(DisplayLargeVFConfig.WEIGHT),
FontVariation.width(DisplayLargeVFConfig.WIDTH),
FontVariation.slant(DisplayLargeVFConfig.SLANT),
)
)
)
} else {
default
}
그리고 FontFamily를 사용하게 Material Design 3 폰트를 구성한다.
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
),
// 추가
displayLarge = TextStyle(
fontFamily = displayLargeFontFamily,
fontSize = 50.sp,
lineHeight = 64.sp,
letterSpacing = 0.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)
이 문서에선 기본 폰트 설정과 권장 사용이 다른 displayLarge Material 3 폰트를 사용한다. 예를 들어 화면에서 가장 큰 텍스트인 displayLarge는 짧고 중요한 텍스트에 써야 한다.
Material 3을 쓰면 TextStyle, fontFamily의 기본값을 바꿔서 폰트를 커스텀할 수 있다. 위 예시에선 TextStyle 인스턴스를 구성해서 각 폰트 모음의 폰트 설정을 커스텀한다.
폰트를 정의했으니 M3 MaterialTheme에 전달한다. Theme.kt를 수정하면 된다.
@Composable
fun ComposePracticeTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
마지막으로 Text 컴포저블을 사용하고 폰트 스타일 중 하나인 MaterialTheme.typography.displayLarge에 스타일을 지정한다.
아래는 문서에 포함된 스니펫에서 가져온 Type.kt다.
import android.os.Build
import androidx.compose.material3.Typography
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontVariation
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.googlefonts.Font
import androidx.compose.ui.text.googlefonts.GoogleFont
import androidx.compose.ui.unit.sp
import com.example.composepractice.R
object DisplayLargeVFConfig {
const val WEIGHT = 950
const val WIDTH = 30f
const val SLANT = -6f
const val ASCENDER_HEIGHT = 800f
const val COUNTER_WIDTH = 500
}
object HeadlineMediumVFConfig {
const val WEIGHT = 800
const val WIDTH = 90f
const val SLANT = 0f
const val ASCENDER_HEIGHT = 750f
const val COUNTER_WIDTH = 393
}
object BodyLargeVFConfig {
const val WEIGHT = 400
const val WIDTH = 50f
const val SLANT = 0f
const val ASCENDER_HEIGHT = 750f
const val COUNTER_WIDTH = 603
}
val default = FontFamily(
Font(resId = R.font.firasans_regular)
)
@OptIn(ExperimentalTextApi::class)
val displayLargeFontFamily = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
FontFamily(
Font(
R.font.roboto_variable,
variationSettings = FontVariation.Settings(
FontVariation.weight(DisplayLargeVFConfig.WEIGHT),
FontVariation.width(DisplayLargeVFConfig.WIDTH),
FontVariation.slant(DisplayLargeVFConfig.SLANT),
)
)
)
} else {
default
}
@OptIn(ExperimentalTextApi::class)
val headlineMediumFontFamily = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
FontFamily(
Font(
R.font.roboto_variable,
variationSettings = FontVariation.Settings(
FontVariation.weight(HeadlineMediumVFConfig.WEIGHT),
FontVariation.width(HeadlineMediumVFConfig.WIDTH),
FontVariation.slant(HeadlineMediumVFConfig.SLANT)
)
)
)
} else {
default
}
@OptIn(ExperimentalTextApi::class)
val bodyLargeFontFamily = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
FontFamily(
Font(
R.font.roboto_variable,
variationSettings = FontVariation.Settings(
FontVariation.weight(BodyLargeVFConfig.WEIGHT),
FontVariation.width(BodyLargeVFConfig.WIDTH),
FontVariation.slant(BodyLargeVFConfig.SLANT)
)
)
)
} else {
default
}
// Set of Material typography styles to start with
val Typography = Typography(
// bodyLarge = TextStyle(
// fontFamily = FontFamily.Default,
// fontWeight = FontWeight.Normal,
// fontSize = 16.sp,
// lineHeight = 24.sp,
// letterSpacing = 0.5.sp
// ),
displayLarge = TextStyle(
fontFamily = displayLargeFontFamily,
fontSize = 50.sp,
lineHeight = 64.sp,
letterSpacing = 0.sp
),
headlineMedium = TextStyle(
fontFamily = headlineMediumFontFamily,
fontSize = 35.sp,
lineHeight = 37.sp
/***/
),
bodyLarge = TextStyle(
fontFamily = bodyLargeFontFamily,
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
lineHeight = 28.sp,
letterSpacing = 0.15.sp
/***/
),
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)
그리고 Text 컴포저블에 적용한다. 난 아래처럼 수정했다.
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.composepractice.ui.theme.ComposePracticeTheme
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
ComposePracticeTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
TestText(innerPadding)
}
}
}
}
}
@Composable
fun TestText(
innerPadding: PaddingValues,
) {
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,
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun MainPreview() {
ComposePracticeTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
TestText(innerPadding)
}
}
}
맞춤 폭 사용
폰트에 맞춤 축이 있을 수 있다. 폰트 파일 안에 정의돼 있는데 Roboto Flex 폰트엔 소문자 어센더의 높이를 조정하는 어센더 높이(YTAS) 축이 있고 카운터 너비 XTRA는 각 글자 너비를 조정한다.
FontVariation 설정을 써서 이런 축의 값을 바꿀 수 있다. 폰트에 대해 구성 가능한 맞춤 축의 자세한 내용은 지원되는 축 표를 확인한다. 내용이 많아서 로딩이 조금 걸릴 수 있다.
https://fonts.google.com/variablefonts?hl=ko#font-families
Variable Fonts - Google Fonts
Making the web more beautiful, fast, and open through great typography
fonts.google.com
Roboto Flex 기준으로 맞춤 축을 사용하려면 ascenderHeight, counterWidth 축의 함수를 정의한다.
fun ascenderHeight(ascenderHeight: Float): FontVariation.Setting {
require(ascenderHeight in 649f .. 854f) {
"Ascender Height는 649 이상 854 이하여야 한다"
}
return FontVariation.Setting("YTAS", ascenderHeight)
}
fun counterWidth(counterWidth: Int): FontVariation.Setting {
require(counterWidth in 323..603) {
"Counter width는 323 이상 603 이하여야 한다"
}
return FontVariation.Setting("XTRA", counterWidth.toFloat())
}
이런 함수는
- 허용 가능한 값에 대한 가이드라인을 정의한다. 가변 폰트 카탈로그(위의 지원되는 축 표)에서 볼 수 있듯 ascenderHeight(YTAS) 값은 최소 649f, 최대 854f다
- 폰트에 추가할 수 있게 FontVariation을 리턴한다. FontVariation.Setting()에서 축 이름 YTAS, XTRA는 하드코딩돼 있고 값을 매개변수로 쓴다
폰트 구성과 같이 축을 써서 로드된 각 Font에 추가 매개변수를 전달한다.
import android.os.Build
import androidx.compose.material3.Typography
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontVariation
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.googlefonts.Font
import androidx.compose.ui.text.googlefonts.GoogleFont
import androidx.compose.ui.unit.sp
import com.example.composepractice.R
object DisplayLargeVFConfig {
const val WEIGHT = 950
const val WIDTH = 30f
const val SLANT = -6f
const val ASCENDER_HEIGHT = 800f
const val COUNTER_WIDTH = 500
}
object HeadlineMediumVFConfig {
const val WEIGHT = 800
const val WIDTH = 90f
const val SLANT = 0f
const val ASCENDER_HEIGHT = 750f
const val COUNTER_WIDTH = 393
}
object BodyLargeVFConfig {
const val WEIGHT = 400
const val WIDTH = 50f
const val SLANT = 0f
const val ASCENDER_HEIGHT = 750f
const val COUNTER_WIDTH = 603
}
val default = FontFamily(
Font(resId = R.font.firasans_regular)
)
@OptIn(ExperimentalTextApi::class)
val displayLargeFontFamily = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
FontFamily(
Font(
R.font.roboto_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
}
@OptIn(ExperimentalTextApi::class)
val headlineMediumFontFamily = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
FontFamily(
Font(
R.font.roboto_variable,
variationSettings = FontVariation.Settings(
FontVariation.weight(HeadlineMediumVFConfig.WEIGHT),
FontVariation.width(HeadlineMediumVFConfig.WIDTH),
FontVariation.slant(HeadlineMediumVFConfig.SLANT),
ascenderHeight(HeadlineMediumVFConfig.ASCENDER_HEIGHT),
counterWidth(HeadlineMediumVFConfig.COUNTER_WIDTH)
)
)
)
} else {
default
}
@OptIn(ExperimentalTextApi::class)
val bodyLargeFontFamily = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
FontFamily(
Font(
R.font.roboto_variable,
variationSettings = FontVariation.Settings(
FontVariation.weight(BodyLargeVFConfig.WEIGHT),
FontVariation.width(BodyLargeVFConfig.WIDTH),
FontVariation.slant(BodyLargeVFConfig.SLANT),
ascenderHeight(BodyLargeVFConfig.ASCENDER_HEIGHT),
counterWidth(BodyLargeVFConfig.COUNTER_WIDTH)
)
)
)
} else {
default
}
이제 소문자 어센더의 높이가 증가하고 다른 텍스트가 넓어진다. 포스팅을 보고 있다면 위 사진을 클릭해서 이전 사진과 비교해 보면 차이를 알 수 있다.
어센더는 타이포그래피 용어 중 하나인데 자세한 내용은 아래 링크들처럼 어센더, 디센더를 검색하면 더 잘 알 수 있다.
타이포그래피 용어
타이포그래피에 대한 정보를 찾아보면 다양한 용어들을 만날 수 있다. 그중에는 우리에게 익숙한 용어들도 있고 생소한 용어들도 물론 많다. 하지만 알고 나면 그렇게 어려운 용어들이 아닐 것
tammist.tistory.com
https://www.yeonjoong.dev/posts/design/descender-and-ascender
디센더와 어센더
 [타이포그래피(typography)](/contents/design/typography) 에서 글자는 단순한 정보 전달 수단을 넘어 시각적 소통의 중요한 도구입니다. 글자의 모
www.yeonjoong.dev
'Android > Compose' 카테고리의 다른 글
[Android Compose] TextField 사용법 (0) | 2025.08.28 |
---|---|
[Android Compose] Text 사용법 (0) | 2025.07.27 |
[Android Compose] 드래그로 사진 선택 구현하는 법 (0) | 2025.01.26 |
[Android Compose] Supabase를 활용한 CRUD 구현 - 2 - (0) | 2025.01.11 |
[Android Compose] Supabase를 활용한 CRUD 구현 - 1 - (0) | 2025.01.06 |