2020년 2월 24일 월요일

[안드로이드] 폰트 리소스 적용

폰트 리소스 적용기

발단

현재 담당하고 있는 앱에서 앱의 모든 텍스트뷰에 시스템폰트 대신 커스텀 폰트를 적용해야 하는 스펙이 있었고, 이를 쉽게 적용하기 위해 Typekit 이라는 라이브러리를 사용하고 있었습니다. 그러나 targetApi를 29로 올리면 이 라이브러리에서 크래시가 발생합니다. 내부에서 리플렉션을 이용하여 모든 텍스트뷰에 폰트를 주입하고 있었는데, 이 리플렉션을 이용하는 부분이 문제가 되었습니다. 이 라이브러리는 2016년 7월16일 이후로 커밋이 올라오지 않아서 사실상 관리되지 않는 라이브러리로 개선을 기대할 수 없어서 다른 방법을 찾아야만 했습니다.

8.0 미만 버전에서 폰트를 적용하는 방법

초창기부터 안드로이드는 커스텀폰트를 적용하기 위해 Assets 공간을 활용했습니다. Assets 폴더에 커스텀 폰트 파일(.ttf, .otf 등등)을 넣어두고 Typeface.createFromAsset() 메서드를 이용하여 해당 폰트를 불러와서 텍스트뷰에 적용했습니다.
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        val assetManager = resources.assets
        val customTypeface = Typeface.createFromAsset(assetManager, "font/custom_font.ttf")
        
        val textView = ...
        textView.typeface = customTypeface
    }
}

그럼 Typekit은?

위의 방법으로 앱의 모든 TextView에 폰트를 적용하기란 매우 번거롭고 코드량도 많아집니다. Typekit은 Application.onCreate() 등 앱의 최초 초기화시에 폰트를 로드하고, BaseActivity를 만들어 attachBaseContext()메서드를 상속받아 새로운 Context를 등록해주면 끝입니다.
// 폰트 초기화
class MyApplication : Application() {
    override fun onCreate() {
        ...
        val assetManager = resources.assets
        val customTypeface = Typeface.createFromAsset(assetManager, "font/custom_font.ttf")
        Typekit.getInstace().addNormal(customTypeface)
    }
}
// 폰트 적용
class BaseActivity : AppCompatActivity() {
    override fun attachBaseContext(newBase: Context?) {
        super.attachBaseContext(TypekitContextWrapper(newBase))
    }
}
사용은 간단한데, 내부적으로 살펴보니 TypeContextWrapper 클래스는 getSystemService()를 오버라이드하여 TypekitLayoutInflater라는 커스텀 인플레이터를 반환하도록 되어 있었습니다. 이 인플레이터가 뷰 생성/인플레이팅시에 리플렉션으로 폰트를 주입해주는 핵심 역할을 담당하고 있었습니다.

8.0 이상에서 폰트 적용하기

8.0(API 26 오레오)부터 폰트를 리소스로 취급하도록 변경되었으며, font-family를 xml로 작성하여 쉽게 사용할 수 있도록 되었습니다. res/font/ 폴더 내에 사용할 폰트를 넣어두면 리소스로 인식됩니다. 리소스로 인식할 수 있는 폰트파일은 .ttf, .ttc, .otf, .xml 가 있습니다.

xml로 font-family 작성하기

<font-family xmlns:android="http://schemas.android.com/apk/res/android">
    <font
        android:fontStyle="normal"
        android:fontWeight="400"
        android:font="@font/lobster_regular" />
    <font
        android:fontStyle="italic"
        android:fontWeight="400"
        android:font="@font/lobster_italic" />
</font-family>
  • font-family : 폰트 xml은 반드시 이 루트 엘리먼트로 시작
  • font
    • fontStyle : 폰트의 스타일. italic / normal 상수값 대입 가능
    • fontWeight : 폰트의 두께. 100 ~ 900 사이의 100단위 양수
    • font : res/font 폴더 내에 있는 폰트 리소스

layout.xml에서 폰트를 직접 적용하기

<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    ...
    android:fontFamily="@font/my_font_family" />

Style에서 폰트를 적용하기

<style name="MyTextStyle" parent="@android:style/TextAppearance.Small">
    <item name="android:fontFamily">@font/my_font_family</item>
</style>

폰트 리소스를 코드에서 사용하기

  • Resources 클래스에 API 26부터 새로생긴 getFont() 메서드 이용
val typeface = resources.getFont(R.font.my_font_family)
textview.typeface = typeface
  • 코드에서의 하위호환을 유지하기 위해 AndroidX(구 SupportLibrary v26.x)의 ResourcesCompat.getFont()를 이용
  • API 16이상부터 사용가능 (minApi = 16)
  • font-family xml을 작성하는 경우 android 네임스페이스 대신, app 네임스페이스를 사용해야 함
<font-family xmlns:app="http://schemas.android.com/apk/res-auto">
    <font
        app:fontStyle="normal"
        app:fontWeight="400"
        app:font="@font/lobster_regular" />
    <font
        app:fontStyle="italic"
        app:fontWeight="400"
        app:font="@font/lobster_italic" />
</font-family>
val typeface = ResourcesCompat.getFont(context, R.font.my_font_family)

다운로더블 폰트

APK에 폰트파일을 포함하는 대신 Provider 애플리케이션으로부터 폰트를 요청하거나, 폰트를 다운로드 받을 수 있는 API가 안드로이드 8.0 (API 26) / Support Library 26에 추가되었습니다. 이 기능은 아이스크림 샌드위치(API 14)이상의 기기에서 Support Library 26 이상 버전을 이용하는 경우에 이용할 수 있습니다.

동작 방식

폰트를 관리해주는 Font Provider를 도입했습니다. 이 Provider는 폰트를 다운로드 받거나 캐싱하여 다른 앱에서 요청하는 폰트를 공유하도록 하는 역할을 담당합니다. 즉, 각각의 앱에서는 필요한 폰트를 FontsContract라는 API로 이 Provider로 요청하고, 요청한 결과를 콜백으로 받습니다.

사용방법

1. 리소스를 이용

res/font 리소스에 xml로 다운로더블 폰트의 제공자 정보를 작성하는 방법입니다. xml로 font-family를 추가하고, 태그에서 속성값을 주어야 합니다.
<font-family xmlns:android="http://schemas.android.com/apk/res/android"
    app:fontProviderAuthority="com.google.android.gms.fonts"
    app:fontProviderPackage="com.google.android.gms"
    app:fontProviderQuery="Goudy Bookletter 1911"
    app:fontProviderCerts="@array/com_google_android_gms_fonts_certs">
</font-family>
  • fontProviderAuthority : 폰트를 가져올 FontProvider의 소유자 정보. 매니페스트에서 Provider를 지정할 때 넣어주는 android:authority값을 의미함
  • fontProviderPackage : 해당 프로바이더의 패키지
  • fontProviderQuery : 프로바이더에게 요청할 폰트의 식별자(이름)
  • fontProviderCerts : 해당 폰트를 사용하기 위한 인증키(?)

2. 코드를 이용

FontsContract.requeestFont() 메서드를 이용하여 코드에서 Typeface를 바로 얻을 수 있습니다. FontRequest 객체에 Provider에 대한 정보를 추가하여, requestFont()메서드의 인자로 전달하면, 해당 폰트를 콜백메서드로 반환해 줍니다. AndroidX(Support Library v26)에서 하위호환을 지원하기 위해 FontsContractCompat 클래스를 지원합니다.
val request = FontRequest(
        "com.example.fontprovider.authority",
        "com.example.fontprovider",
        "my font",
        certs
)
val callback = object : FontsContract.FontRequestCallback() {

    override fun onTypefaceRetrieved(typeface: Typeface) {
        // Your code to use the font goes here
        ...
    }

    override fun onTypefaceRequestFailed(reason: Int) {
        // Your code to deal with the failure goes here
        ...
    }
}
FontsContract.requestFonts(context, request, handler, null, callback)
  • FontRequest 생성자 파라미터
    • String : 프로바이더 소유자
    • String : 프로바이더 패키지
    • String : 쿼리할 이름
    • List<List<byte[]> : 인증키
  • FontsContract.requestFont() 파라미터
    • Context : 프로바이더 사용을 위한 컨텍스트
    • Request : 프로바이더 정보를 담은 FontRequest 객체
    • Handler : 폰트조회를 수행할 핸들러
    • CancellationSignal : 폰트조회를 취소할 객체
    • FontRequestCallback : 폰트조회결과를 받을 콜백

3. 추가사항

1) 매니페스트에 사용할 폰트를 미리 선언하기(Optional)
뷰 인플레이션과 리소스 검색은 메인스레드에서 진행되는 동기 작업입니다. 그렇기에 앱 구동후에 사용할 폰트를 프로바이더로부터 쿼리하는 작업도 메인스레드에서 동작합니다. 이는 최초 레이아웃에 걸리는 시간을 증가시켜서 앱의 성능을 조금 저하시킵니다. 이를 피하기 위해 매니페스트에 미리 폰트를 선언해두면, 시스템이 미리 폰트를 쿼리해두어, 바로 사용가능하도록 준비해줍니다. 만약 쿼리한 폰트가 없다면 기본폰트로 지정됩니다.
res/values 폴더에 폰트의 목록을 array로 지정해 둡니다.
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <array name="preloaded_fonts" translatable="false">
        <item>@font/goudy_bookletter_1911</item>
    </array>
</resources>
그 다음, 매니페스트에 로 해당 array를 명시합니다.
<meta-data
    android:name="preloaded_fonts"
    android:resource="@array/preloaded_fonts" />
2) 인증 추가
사용할 FontProvider가 미리 설치되어있지 않거나 Support Library를 사용하여 구현하는 경우라면, 반드시 이 인증키를 명시해 주어야 합니다.
res/values 폴더에 인증키 목록을 string-array로 지정합니다.
<?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>
이 인증키를 리소스 / 코드에서 사용하면 됩니다.

참고