본 포스팅은 DroidKaigi 2018 ~ Support LibraryのDownloadable FontsやEmojiCompatに対応したアプリを作ろう 을 기본으로 번역하여 작성했습니다
제 일본어 실력으로 인하여 오역이나 오타가 발생할 수 있습니다.
DroidKaigi 2018
takahirom
AbemaTV Android 앱을 만들고 있습니다.
https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts 로부터
Downloadable Font 구조가 포함된 Font Resource에 대해서
res/font/hogehoge.ttf → R.font.hogehoge
<TextView android:fontFamily="@font/hogehoge"
Bundled Font와 Downloadable Font 2종류가 있다
<?xml version="1.0" encoding="utf-8"?>
<font-family xmlns:app="../apk/res-auto">
<font
app:font="@font/orbitron_regular"
app:fontStyle="normal"
/>
<font
app:font="@font/orbitron_bold"
app:fontStyle="normal"
app:fontWeight="700"
/>
</font-family>
<TextView
android:fontFamily="@font/orbitron"
android:textStyle="bold"
textStyle로 부터 사용할 Font를 변경한다
Downloadable Font를 적용하는 방법을 보겠습니다
Android Studio 의 레이아웃 내에서 fontFamily를 선택하고 More Fonts…를 선택
이미지는 발표 이미지와 다른, Android Developer 공식 사이트의 이미지가 사용되었습니다.
완성
https://speakerdeck.com/takahirom/support-libraryfalsedownloadable-fontsyaemojicompatnidui-ying-sitaapuriwozuo-rou?slide=34
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/days_one"
android:text="Hello World!"
/>
TextView에서 Font Resource를 지정
<?xml version="1.0" encoding="utf-8"?>
<font-family
xmlns:app="http://schemas.android.com/apk/res-auto"
android:fontProviderAuthority="com.google.android.gms.fonts"
android:fontProviderPackage="com.google.android.gms"
android:fontProviderQuery="Days One"
android:fontProviderCerts="@array/com_goolge_android_gms_fonts_certs">
</font-family>
아래를 지정한다
Google Play Services
<?xml version="1.0" encoding="utf-8"?>
<resources>
<array name="preloaded_fonts" translatable="false">
<item>@font/days_one</item>
</array>
</resources>
preloaded_fonts에는 Font Resource가 지정되어 이 Font가 preload 된다
<resources>
<array name="com_goolge_android_gms_fonts_certs">
<item>@array/com_goolge_android_gms_fonts_certs_dev</item>
<item>@array/com_goolge_android_gms_fonts_certs_prod</item>
</array>
<string-array name="com_goolge_android_gms_fonts_certs_dev">
<item>
MIIEqDCCA5C...
</item>
</string-array>
<string-array name="com_goolge_android_gms_fonts_certs_prod">
<item>
MIIEQzCCAyug...
</item>
</string-array>
</resources>
서명 파일로는 길다. 문자열이 포함되어 있다 (자세한 내용은 나중에 설명)
<application
android:allowBackup="true"
android:icon="@minmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@minmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
>
<meta-data
android:name="preloaded_fonts"
android:resource="@array/preloaded_fonts"
/>
manifest 파일에 preloaded_fonts 가 추가되어 있다
Support Library와 Platform 구현이 있어 기본적으로 같다
※ Support Library 27.0.2를 기준으로 이야기합니다
// AppCompatViewInflater.java
switch (name) {
case "TextView":
view = new AppCompatTextView(context, attrs);
break;
case "ImageView":
view = new AppCompatImageView(context, attrs);
break;
}
레이아웃의 “TextView” 대신에 AppCompatTextView 가 만들어진다
AppCompatTextView 생성자
→ AppCompatTextHelper#loadFromAttributes()
→ TintTypedArray#getFont()
여기에서
ResourceCompat#getFont()를 사용해 Font를 얻는다
ResourcesCompat.FontCallback replyCallback =
new ResourcesCompat.FontCallback replyCallback() {
@Override
public void onFontRetrieved(@NonNull Typeface typeface) {
onAsyncTypefaceReceived(textViewWeak, typeface);
}
@Override
public void onFontRetrievalFailed(int reason) {
// Do nothing.
}
};
try {
// Note the callback will be triggered on the UI thread.
mFontTypeface = a.getFont(fontFamilyId, mStyle, replyCallback);
mAsyncFontPending = mFontTypeface == null;
} catch (UnsupportedOperationException | Resource.NotFoundException e) {
// Expected if it is not a font resource.
}
private void onAsyncTypefaceReceived(WeakReference<TextView> textViewWeak, Typeface typeface) {
if (mAsyncFontPending) {
mFontTypeface = typeface;
final TextView textView = textViewWeak.get();
if (textView != null) {
textView.setTypeface(typeface, mStyle);
}
}
}
비동기로 처리해서 얻었다면 textView#setTypeface() 로 설정한다
Support Library와 Platform 구현이 있어 기본적으로 같다
취득처 앱의 서명 체크
※ Support Library 27.0.2를 기준으로 이야기합니다
FontsContractCompat
List<byte[]> signatures;
PackageInfo packageInfo = packageManager.getPackageInfo(info.packageName, PackageManager.GET_SIGNATURES); // info.packageName => "com.google.android.gms" = Google Play Services
// PackageManager로부터 Google Play Services 앱의 Signature를 얻어 바이트 배열로 변환한다
signatures = convertToByteArrayList(packageInfo.signatures);
List<List<byte[]>> requestCertificatesList = getCertificates(request, resources);
for (int i = 0; i < requestCertificatesList.size(); ++i) {
List<byte[]> requestSignatures = new ArrayList<>(requestCertificatesList.get(i));
Collections.sort(requestSignatures, sByteArrayComparator);
// 바이트 배열을 비교해서 동일하면 정보를 반환한다
if (equalsByteArrayList(signatures, requestSignatures)) {
return info;
}
}
return null;
Support Library와 Platform 구현이 있어 기본적으로 같다
ContentResolver를 통해 Google Play Services로부터 Font를 얻는다
※ Support Library 27.0.2를 기준으로 이야기합니다
폰트 취득은 3중으로 Wrapping 되어 구현되어 있다
ContentResolver 처리
- FontsContractCompat - ResourceCompat
ContentResolver와 ContentProvider는 Android에 오래전부터 있었다
애플리케이션간의 데이터를 주고받는 구조
실제로 ContentResolver를 통해 Goolge Play Service로부터 얻을 수 있습니다
val fileBaseUri = Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
.authority("com.google.android.gms.fonts")
.build()
var cursor = contentResolver.query(
uri,
arrayOf( // ContentResolver를 사용해 얻는다
FontsContractCompat.Columns._ID,
FontsContractCompat.Columns.FILE_ID,
FontsContractCompat.Columns.TTC_INDEX,
""
),
"query = ?",
arrayOf("Days One"), // 폰트 이름
null
)
폰트 취득은 3중으로 Wrapping 되어 구현되어 있다
ContentResolver 처리 - FontsContractCompat
- ResourceCompat
조금 전의 ContentResolver 방법에는 서명 체크가 포함되어 있지 않고, 안전하지 않습니다.
FontsContract를 사용하면 패키지 체크도 해주므로 안전하게 가져올 수 있습니다
val request = FontRequest(
"com.google.android.gms.fonts",
"com.google.android.gms",
"Days One",
R.array.com_google_android_gms_fonts_certs_prod
)
// FontRequest를 작성
// ContentResolver에 필요한 Authority 이름, 폰트명, 서명이 들어있는 리소스를 지정한다
FontsContractCompat.requestFont(
this,
request,
object : FontsContractCompat.FontRequestCallback() {
override fun onTypefaceRetrieved(typeface: Typeface?) {
// Callback을 통해 TextView의 typeface에 설정한다
textView.typeface = typeface
}
override fun onTypefaceRequestFailed(reason: Int) {
super.onTypefaceRequestFailed(reason)
}
}, Handler()
)
폰트 취득은 3중으로 Wrapping 되어 구현되어 있다
ContentResolver 처리 - FontsContractCompat - ResourceCompat
FontsContractCompat은 ContentProvider보다 간단하지만, Google Play Services의 패키지명 지정 등으로 어렵습니다.
조금 전 설명한 Font Resource를 사용해봅시다.
Font Resource로 조금 전 작성한 패키지명 등이 적혀있습니다.
<?xml version="1.0" encoding="utf-8"?>
<font-family xmlns:app="http://schemas.android.com/apk/res-auto"
app:fontProviderAuthority="com.google.android.gms.fonts"
app:fontProviderPackage="com.google.android.gms"
app:fontProviderQuery="Days One"
app:fontProviderCerts="@array/com_google_android_gms_fonts_certs">
</font-family>
ResourcesCompat을 사용해 폰트에 접근
상당히 깔끔해졌습니다.
fontText.typeface = ResourcesCompat.getFont(this, R.font.orbitron)
[X] MainThread를 Block해서 얻으므로 주의할 필요가 있다
비동기로 사용도 가능합니다. (Support Lib 27.0.0부터 추가된 메소드)
ResourcesCompat.getFont(
this,
R.font.orbitron,
object : ResourcesCompat.FontCallback() {
override fun onFontRetrieved(typeface: Typeface) {
fontText.typeface = typeface
}
override fun onFontRetrievalFailed(reason: Int) {
}
},
Handler())
어떻게 Downloadable Font를 사용할 것 인가?
아직 Downloadable Font에 일본어는 없습니다 😇
Get Started with the Google Fonts for Android라는 페이지에 아래와 같이 있습니다
Which fonts can I use?
The entire Google Fonts Open Source collection! Visit https://fonts.google.com to browse.
fonts.google.com에 있는 폰트를 사용할 수 있다!
fonts.google.com 내에는 일본어 font는 아직 없다
fonts.google.com 에는 Early Access라는 항목이 있고, 그 페이지에는 일본어 폰트가 있습니다.
그러므로, 그 Early Access가 끝나면
가능성이 있어보입니다 😭
googlesamples/android-DownloadableFonts 의 issue에 작은 저항 (무의미)을 했으니, ❤ 눌러주세요.
Downloadable Font는 아니지만 단순히 Bundled Font로 Font Resource를 이용해 사용할 수 있습니다.
그 때의 기술을 소개합니다.
머티리얼 디자인이라면 일본어는 고밀도(Dense Script)이며 Noto 폰트를 기준으로 작성됩니다.
일본어 머티리얼 디자인의 타이포그래피 인용
타이포그래피
Ice Cream Sandwich 출시 이후, Android 표준 폰트가 된 것이 Roboto입니다. 한편, Roboto에 대응하지 않는 언어용 Android 표준 폰트로 Froyo부터 도입된 것이 Noto입니다. Noto는 ChromeOS에서 모든 언어에에 대응하는 표준 폰트로도 사용되고 있습니다.
고밀도 : 커다란 글리프(Glyph)에 대응하도록 행간에 여유를 가질 필요가 있는 언어의 문자. 중국어, 일본어, 한국어가 해당합니다. Noto에는 해당 언어에 대해 7종류의 Weidth가 있습니다.
머티리얼 디자인의 Dense script 항목 인용
각각 크기와 폰트가 설정되어 있다
각각의 TextAppearance 가 준비되어 있어 fontFamily와 크기를 지정
여기에서 font/notosans_medium.otf 등 FontFamily 앱에 넣는다
DroidKaigi 2018 인용
<style name="TextAppearance.App.Title" parent="TextAppearance.AppCompat.Title">
<item name="android:textSize">21sp</item>
<item name="android:fontFamily">@font/notosans_medium</item>
</style>
<style name="TextAppearance.App.Subhead" parent="TextAppearance.AppCompat.Subhead">
<item name="android:textSize">17sp</item>
<item name="android:fontFamily">@font/notosans_regular</item>
</style>
TextView에 지정한다
<TextView
android:id="@+id/speaker_name"
android:textAppearance="@style/TextAppearance.App.Body1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
잘 움직인다 🙆♀️
X
Toolbar의 Title 등 기본 폰트도 app:textAppearance 등으로 설정해야 한다
테마쪽에서 어떻게 안될까?
<style name="AppTheme" parent="Theme.AppCompat.Light">
<item name="android:textAppearance">@style/TextAppearance.hogehoge</item>
<item name="android:textAppearanceSmall">@style/TextAppearance..</item>
<item name="android:textAppearanceButton">@style/TextAppearance</item>
<item name="android:textAppearanceMediumInverse">@style/TextAppearance</item>
X
가능하지만 많이 있다. 힘듬
christens/Calligraphy를 사용
Calligraphy란 Android Standard 라이브러리로 기본 폰트를 설정 가능
@Override
public void onCreate() {
super.onCreate();
CalligraphyConfig.initDefault(
new CalligraphyConfig.Builder()
.setDefaultFontPath("fonts/Roboto-RobotoRegular.ttf")
.setFontAttrId(R.attr.fontPath)
.build()
);
X
하지만 Calligraphy는 오래전부터 있던 라이브러리로 새로운 Font Resource는 지정할 수 없고, 호환성도 없다
https://twitter.com/chrisjenx/status/865273111129726977
어떤 구조로 움직이는가.
Downloadable Fonts의 일본어 폰트 대응 상황에 대해.
실제로 Calligraphy를 Downloadable Fonts등의 Font Resources로 교체하는 방법.
😇
EmojiCompat의 API level 19에 어떻게 대응하는가.
어떻게 테스트하는가.
등 실천적인 부분을 소개합니다.
라이브러리를 만들었습니다
Calligraphy를 구조를 이용한 라이브러리를 만들었습니다
Calligraphy와 같은 스펙으로 설정하는 것으로
CalligraphyConfig.initDefault(new CalligraphyConfig.Builder()
.setDefaultFontPath(R.font.roboto_reqular)
.build());
@Override
protected void attachBaseContext(Context newBase) {
super.attachBaseContext(CalligraphyContextWrapper.wrap(newBase));
}
DroidKaigi 앱에서 제대로 움직이고 있습니다
https://github.com/DroidKaigi/conference-app-2018/pull/39
여기에 있는 사람이 Start 해서 유명한 라이브러리가 된다면 안심해서 사용할 수 있을 터이다. 🙇🏻♂️
takahirom/DownloadableCalligraphy
https://github.com/takahirom/DownloadableCalligraphy
여기까지가 50~60%
14:15라면 페이스대로
EmojiCompat
tohu ☐ 를 막을 수 있다
https://developer.android.com/guide/topics/ui/look-and-feel/emoji-compat 인용
http://blog.unicode.org/2018/02/unicode-emoji-110-characters-now-final.html?m=1
Application#onCreate
val fontRequest = FontRequest(
"com.google.android.gms.fonts",
"com.google.android.gms",
"Noto Color EmojiCompat",
R.array.com_google_android_gms_fonts_certs)
val config = FontRequestEmojiCompatConfig(context, fontRequest)
EmojiCompat.init(config)
조금전의 Font Request를 사용해 EmojiCompat.init() 으로 초기화
implementation "com.android.support:support-emoji:27.0.2"
나머지는 기본적으로 아래와 같이 이모티콘이 사용되는 곳에 각각 View를 사용
<android.support.text.emoji.widget.EmojiAppCompatTextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<android.support.text.emoji.widget.EmojiAppCompatEditText
android:id="@+id/edit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
...
implementation "com.android.support:support-emoji-appcompat:27.0.2"
implementation "com.android.support:support-emoji:27.0.2"
implementation "com.android.support:support-emoji-appcompat:27.0.2"
implementation "com.android.support:support-emoji-bundled:27.0.2"
Download 버전
→ emoji-appcompat
Bundle 버전을 이용하는 경우
→ emoji-bundled
→ emoji-appcompat
.setEmojiSpanIndicatorEnabled(true)
.setEmojiSpanIndicatorColor(Color.GREEN)
EmojiCompat에 따라 교체가 이루어지는 부분을 알 수 있다. (디버그용)
.setReplaceAll(true)
시스템이 표시 불가능한 이모티콘만을 교체하는 것이 아니라 전부를 교체할지 여부
통일감을 가진다면 전부 교체하는 편이 좋다
Java는 통상, 16비트의 char 데이터 타입을 사용
그러나 16비트에 포함되지 못한 이모티콘 등은 32비트, 2개의 char을 사용해 표현된다.
이 방법을 Surrogate Pair라고 한다.
Surrogate Pair는 31비트 값으로 표현한다
그것을 Code Point
라고 한다
👨 = U+1F468 = \ud83d\udc68
↑ Code Point
어느 Code Point가 어떤 이모티콘에 대응하는지 Emoji Charts로 확인할 수 있습니다
http://unicode.org/emoji/charts/full-emoji-list.html
Code Point를 얻거나 문자열로 고칠 수 있습니다.
// String -> code point
val face = "\ud83d\udc68"
// Character.toCodePoint에 char를 2개 넘길 뿐뿐뿐뿐
val codePoint: Int = Character.toCodePoint(face[0], face[1])
println(Integer.toHexString(codePoint))
// code point -> String
// Character.toChars에 Code Point를 넘길 뿐
val charArray: CharArray = Character.toChars(codePoint)
val string = String(charArray)
println(string)
복수 Code Point로부터 만들어진 이모티콘이 존재한다
U+1F468 | U+1F3FB | U+200D | U+1F4BB |
---|---|---|---|
👨 | 🏻 | ZWJ | 💻 |
ZWJ = 문자를 결합시키는 제어문자
https://speakerdeck.com/takahirom/support-libraryfalsedownloadable-fontsyaemojicompatnidui-ying-sitaapuriwozuo-rou?slide=130
이 Code Point를 Code로 표현하기 위해서는?
U+1F468 | U+1F3FB | U+200D | U+1F4BB |
---|---|---|---|
👨 | 🏻 | ZWJ | 💻 |
val face = Character.toChars(0x1f468)
val lightSkin = Character.toChars(0x1f3fb)
val zwj = Character.toChars(0x200d)
val computer = Character.toChars(0x1f4bb)
val programmer = face + lightSkin + zwj + computer
programmer.forEach {
println(Integer.toHexString(it.toInt()))
}
이것을 먼저 char의 array를 얻어
다음 슬라이드에서 “programmer”를 사용
textView.typeface = ResourcesCompat.getFont(this, R.font.noto_color_emoji)
textView.text = String(programmer)
// or
textView.text = "\ud83d\udc68\ud83c\udffb\u200d\ud83d\udcbb"
EmojiCompat이 없어도 이모티콘은 동작하지만
→ 제대로 교체하면서 다른 폰트가 필요 (이후 EmojiCompat에서 이야기합니다)
TextView#setTransformationMethod를 사용
TextView가 표시할 문자를 변환한다
Password TransformationMethod 의 사례
<TextView
android:id="@+id/textView"
android:text="test"
editText.transformationMethod = PasswordTransformationMethod()
TransformationMethod로 ReplacementSpan라는 Class를 확장한 TypefaceEmojiSpan을 사용한다
ReplacementSpan에서는 draw 메소드를 override하면 다른 문자를 그리거나 할 수 있다
public final class TypefaceEmojiSpan extends EmojiSpan {
...
@Override
public void draw(@NonNull final Canvas canvas, ..., @NonNull final Paint paint) {
getMetadata().draw(canvas, x, y, paint);
}
...
}
// EmojiMetadata.java
public void draw(@NonNull final Canvas canvas, final float x, ..., @NonNull final Paint paint) {
final Typeface typeface = mMetadataRepo.getTypeface();
...
paint.setTypeface(typeface);
...
canvas.drawText(..., paint);
}
paint에 setTypeface해서 canvas.drawText() 하는 것 뿐
어떻게 이모티콘만을 TypefaceEmojiSpan으로 교체하는가
Font File을 FlatBuffer로 읽어 이모티콘의 트리 구조를 만든다
이모티콘의 트리 구조 (간략화)
class Node {
val children: Map<Int, Node>
val data
}
root
↓ + codepoint = U+1F468
data: F0048
↓ 🏻 ← codepoint = U+1F3FB (node.children.get(0x1F3FB))
data: F0606
↓ ZWJ (문자를 결합시키는 제어문자)
…
이 data의 Code Point는 EmojiCompat의 Font가 가지고 있는 Private Use Area에 맵핑되어있어 그곳을 참조하고 있다.
data: F0048 | <map code="0xf0048" name="u1F468" /> |
data: F0606 | <map code="0xf0606" name="uF0606" /> |
이 Tree를 사용해 문자열에서 하나씩 Code Point를 읽어 Tree에 있는 data를 적용해나간다
int currentOffset = start;
int codePoint = Character.codePointAt(charSequence, currentOffset);
while (currentOffset < end && addedCount < maxEmojiCount) {
final int action = sm.check(codePoint);
switch (action) {
case ACTION_ADVANCE_BOTH:
start += Character.charCount(Character.codePointAt(charSequence, start));
currentOffset = start;
if (currentOffset < end) {
codePoint = Character.codePointAt(charSequence, currentOffset);
}
...
Emoji Font가 이용할 수 있게 된 것은 API Level 19부터 있으므로,
EmojiCompat의 기능은 API Level 18 미만은 사용할 수 없다
EmojiAppCompatTextView의 코멘트
https://github.com/takahirom/droidkaigi-2018-fonts
comments powered by Disqus
Subscribe to this blog via RSS.