From 2264c25527258109f3c0b804c65f1b95da67e265 Mon Sep 17 00:00:00 2001 From: Danny Lin Date: Thu, 17 Feb 2022 17:38:19 -0800 Subject: Implement Inter Dynamic Metrics for system UI font Inter, our system UI font of choice, works best with the official Dynamic Metrics. The font's default spacing makes text relatively sparse compared to Roboto, Google Sans Text, and other common UI fonts. Dynamic Metrics optimizes tracking (letter spacing) based on font size to improve appearance. Spacing is increased for small text and decreased for larger text, making it generally more compact and closer to Apple's SF Pro. Math.exp is relatively expensive, so use a precalculated lookup table for common sizes (up to 32dp). The LUT follows steps of 0.5dp so we can safely use integer casting. For compatibility, only the default system UI font is affected, and only if spacing is unset (null or 0). Change-Id: I48642f05f4b60b07326f657aaac76d968465a5dd --- .../android/text/style/TextAppearanceSpan.java | 18 ++- core/java/android/widget/TextView.java | 6 + .../internal/graphics/fonts/DynamicMetrics.java | 129 +++++++++++++++++++++ graphics/java/android/graphics/Typeface.java | 30 ++++- 4 files changed, 173 insertions(+), 10 deletions(-) create mode 100644 core/java/com/android/internal/graphics/fonts/DynamicMetrics.java diff --git a/core/java/android/text/style/TextAppearanceSpan.java b/core/java/android/text/style/TextAppearanceSpan.java index 23557694a48d..464a20b9008c 100644 --- a/core/java/android/text/style/TextAppearanceSpan.java +++ b/core/java/android/text/style/TextAppearanceSpan.java @@ -29,6 +29,8 @@ import android.text.ParcelableSpan; import android.text.TextPaint; import android.text.TextUtils; +import com.android.internal.graphics.fonts.DynamicMetrics; + /** * Sets the text appearance using the given * {@link android.R.styleable#TextAppearance TextAppearance} attributes. @@ -487,17 +489,17 @@ public class TextAppearanceSpan extends MetricAffectingSpan implements Parcelabl styledTypeface = null; } + Typeface finalTypeface = null; if (styledTypeface != null) { - final Typeface readyTypeface; if (mTextFontWeight >= 0) { final int weight = Math.min(FontStyle.FONT_WEIGHT_MAX, mTextFontWeight); final boolean italic = (style & Typeface.ITALIC) != 0; - readyTypeface = ds.setTypeface(Typeface.create(styledTypeface, weight, italic)); + finalTypeface = ds.setTypeface(Typeface.create(styledTypeface, weight, italic)); } else { - readyTypeface = styledTypeface; + finalTypeface = styledTypeface; } - int fake = style & ~readyTypeface.getStyle(); + int fake = style & ~finalTypeface.getStyle(); if ((fake & Typeface.BOLD) != 0) { ds.setFakeBoldText(true); @@ -507,7 +509,7 @@ public class TextAppearanceSpan extends MetricAffectingSpan implements Parcelabl ds.setTextSkewX(-0.25f); } - ds.setTypeface(readyTypeface); + ds.setTypeface(finalTypeface); } if (mTextSize > 0) { @@ -526,6 +528,12 @@ public class TextAppearanceSpan extends MetricAffectingSpan implements Parcelabl ds.setLetterSpacing(mLetterSpacing); } + if ((!mHasLetterSpacing || mLetterSpacing == 0.0f) && + mTextSize > 0 && finalTypeface != null && + finalTypeface.isSystemFont()) { + ds.setLetterSpacing(DynamicMetrics.calcTracking(mTextSize)); + } + if (mFontFeatureSettings != null) { ds.setFontFeatureSettings(mFontFeatureSettings); } diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index f5c1bcf2de42..0ca1efe4d282 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -201,6 +201,7 @@ import android.view.translation.ViewTranslationRequest; import android.widget.RemoteViews.RemoteView; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.graphics.fonts.DynamicMetrics; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.util.ArrayUtils; @@ -4193,6 +4194,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener setLetterSpacing(attributes.mLetterSpacing); } + if ((!attributes.mHasLetterSpacing || attributes.mLetterSpacing == 0.0f) && + DynamicMetrics.shouldModifyFont(mTextPaint.getTypeface())) { + setLetterSpacing(DynamicMetrics.calcTracking(mTextPaint.getTextSize())); + } + if (attributes.mFontFeatureSettings != null) { setFontFeatureSettings(attributes.mFontFeatureSettings); } diff --git a/core/java/com/android/internal/graphics/fonts/DynamicMetrics.java b/core/java/com/android/internal/graphics/fonts/DynamicMetrics.java new file mode 100644 index 000000000000..a3adcdfc0b63 --- /dev/null +++ b/core/java/com/android/internal/graphics/fonts/DynamicMetrics.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.graphics.fonts; + +import android.app.ActivityThread; +import android.content.Context; +import android.graphics.Typeface; + +public class DynamicMetrics { + // https://rsms.me/inter/dynmetrics/ + private static final float A = -0.0223f; + private static final float B = 0.185f; + private static final float C = -0.1745f; + + private static float sDensity = 0.0f; + + // Precalculated tracking LUT up to 32 dp, in steps of 0.5 dp to minimize rounding errors. + // Sizes are close enough that we can cast them to ints for lookup. + // In most cases, we should never have to calculate tracking at runtime because of this. + private static final float[] TRACKING_LUT = { + /* 0.0dp */ 0.1627f, + /* 0.5dp */ 0.147242871675558f, + /* 1.0dp */ 0.1330772180324039f, + /* 1.5dp */ 0.12009513371985438f, + /* 2.0dp */ 0.10819772909994156f, + /* 2.5dp */ 0.09729437696617904f, + /* 3.0dp */ 0.08730202220051463f, + /* 3.5dp */ 0.0781445491098568f, + /* 4.0dp */ 0.06975220162292829f, + /* 4.5dp */ 0.062061051930857966f, + /* 5.0dp */ 0.055012513523938045f, + /* 5.5dp */ 0.04855289491515605f, + /* 6.0dp */ 0.04263299065103837f, + /* 6.5dp */ 0.03720770649437408f, + /* 7.0dp */ 0.032235715923688846f, + /* 7.5dp */ 0.027679145332890072f, + /* 8.0dp */ 0.02350328553312564f, + /* 8.5dp */ 0.019676327359252226f, + /* 9.0dp */ 0.016169119366923862f, + /* 9.5dp */ 0.012954945774584309f, + /* 10.0dp */ 0.010009322958860013f, + /* 10.5dp */ 0.007309812953179257f, + /* 11.0dp */ 0.004835852528962955f, + /* 11.5dp */ 0.002568596557431524f, + /* 12.0dp */ 0.0004907744588531701f, + /* 12.5dp */ -0.0014134413542490412f, + /* 13.0dp */ -0.0031585560420509667f, + /* 13.5dp */ -0.004757862828932768f, + /* 14.0dp */ -0.006223544263193034f, + /* 14.5dp */ -0.007566765016306747f, + /* 15.0dp */ -0.008797756928615424f, + /* 15.5dp */ -0.00992589694927596f, + /* 16.0dp */ -0.010959778564167369f, + /* 16.5dp */ -0.01190727725584982f, + /* 17.0dp */ -0.012775610494210232f, + /* 17.5dp */ -0.013571392714766779f, + /* 18.0dp */ -0.014300685703423587f, + /* 18.5dp */ -0.014969044771476151f, + /* 19.0dp */ -0.01558156107260065f, + /* 19.5dp */ -0.01614290038417221f, + /* 20.0dp */ -0.016657338648324763f, + /* 20.5dp */ -0.017128794543482675f, + /* 21.0dp */ -0.01756085933447426f, + /* 21.5dp */ -0.017956824228607303f, + /* 22.0dp */ -0.018319705446088512f, + /* 22.5dp */ -0.018652267195758174f, + /* 23.0dp */ -0.01895704273115516f, + /* 23.5dp */ -0.01923635364730468f, + /* 24.0dp */ -0.019492327565219923f, + /* 24.5dp */ -0.01972691433882746f, + /* 25.0dp */ -0.019941900907770843f, + /* 25.5dp */ -0.02013892490923212f, + /* 26.0dp */ -0.020319487152457818f, + /* 26.5dp */ -0.02048496305101277f, + /* 27.0dp */ -0.020636613099845737f, + /* 27.5dp */ -0.02077559247697482f, + /* 28.0dp */ -0.020902959842932358f, + /* 28.5dp */ -0.021019685404998267f, + /* 29.0dp */ -0.021126658307650148f, + /* 29.5dp */ -0.0212246934055262f, + /* 30.0dp */ -0.02131453747049323f, + /* 30.5dp */ -0.02139687488010142f, + /* 31.0dp */ -0.021472332830757092f, + /* 31.5dp */ -0.0215414861153242f, + /* 32.0dp */ -0.02160486150154747f, + }; + + private DynamicMetrics() {} + + public static float calcTracking(float sizePx) { + if (sDensity == 0.0f) { + Context context = ActivityThread.currentApplication(); + if (context == null) { + return 0.0f; + } + + sDensity = context.getResources().getDisplayMetrics().density; + } + + // Pixels -> sp + float sizeDp = sizePx / sDensity; + int lutIndex = (int) (sizeDp * 2); // 0.5dp steps + + // Precalculated lookup + if (lutIndex < TRACKING_LUT.length) { + return TRACKING_LUT[lutIndex]; + } + + return A + B * (float) Math.exp(C * sizeDp); + } + + public static boolean shouldModifyFont(Typeface typeface) { + return typeface == null || typeface.isSystemFont(); + } +} diff --git a/graphics/java/android/graphics/Typeface.java b/graphics/java/android/graphics/Typeface.java index 61f7facf0916..2e11253f9fb8 100644 --- a/graphics/java/android/graphics/Typeface.java +++ b/graphics/java/android/graphics/Typeface.java @@ -212,6 +212,8 @@ public class Typeface { private @IntRange(from = 0, to = FontStyle.FONT_WEIGHT_MAX) final int mWeight; + private boolean mIsSystemDefault; + // Value for weight and italic. Indicates the value is resolved by font metadata. // Must be the same as the C++ constant in core/jni/android/graphics/FontFamily.cpp /** @hide */ @@ -237,6 +239,7 @@ public class Typeface { */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) private static void setDefault(Typeface t) { + t.mIsSystemDefault = true; synchronized (SYSTEM_FONT_MAP_LOCK) { sDefaultTypeface = t; nativeSetDefault(t.native_instance); @@ -933,7 +936,7 @@ public class Typeface { } } - typeface = new Typeface(nativeCreateFromTypeface(ni, style)); + typeface = new Typeface(nativeCreateFromTypeface(ni, style), family.mIsSystemDefault); styles.put(style, typeface); } return typeface; @@ -1001,7 +1004,8 @@ public class Typeface { } typeface = new Typeface( - nativeCreateFromTypefaceWithExactStyle(base.native_instance, weight, italic)); + nativeCreateFromTypefaceWithExactStyle(base.native_instance, weight, italic), + base.mIsSystemDefault); innerCache.put(key, typeface); } return typeface; @@ -1011,7 +1015,8 @@ public class Typeface { public static Typeface createFromTypefaceWithVariation(@Nullable Typeface family, @NonNull List axes) { final Typeface base = family == null ? Typeface.DEFAULT : family; - return new Typeface(nativeCreateFromTypefaceWithVariation(base.native_instance, axes)); + return new Typeface(nativeCreateFromTypefaceWithVariation(base.native_instance, axes), + base.mIsSystemDefault); } /** @@ -1174,6 +1179,12 @@ public class Typeface { mCleaner = sRegistry.registerNativeAllocation(this, native_instance); mStyle = nativeGetStyle(ni); mWeight = nativeGetWeight(ni); + mIsSystemDefault = false; + } + + private Typeface(long ni, boolean isSystemDefault) { + this(ni); + mIsSystemDefault = isSystemDefault; } private static Typeface getSystemDefaultTypeface(@NonNull String familyName) { @@ -1189,6 +1200,7 @@ public class Typeface { for (Map.Entry entry : fallbacks.entrySet()) { outSystemFontMap.put(entry.getKey(), createFromFamilies(entry.getValue())); } + outSystemFontMap.get("sans-serif").mIsSystemDefault = true; for (int i = 0; i < aliases.size(); ++i) { final FontConfig.Alias alias = aliases.get(i); @@ -1203,7 +1215,8 @@ public class Typeface { } final int weight = alias.getWeight(); final Typeface newFace = weight == 400 ? base : - new Typeface(nativeCreateWeightAlias(base.native_instance, weight)); + new Typeface(nativeCreateWeightAlias(base.native_instance, weight), + base.mIsSystemDefault); outSystemFontMap.put(alias.getName(), newFace); } } @@ -1230,6 +1243,7 @@ public class Typeface { for (Map.Entry entry : fontMap.entrySet()) { nativePtrs[i++] = entry.getValue().native_instance; writeString(namesBytes, entry.getKey()); + writeInt(namesBytes, entry.getValue().mIsSystemDefault ? 1 : 0); } int typefacesBytesCount = nativeWriteTypefaces(null, nativePtrs); // int (typefacesBytesCount), typefaces, namesBytes @@ -1271,7 +1285,8 @@ public class Typeface { buffer.position(buffer.position() + typefacesBytesCount); for (long nativePtr : nativePtrs) { String name = readString(buffer); - out.put(name, new Typeface(nativePtr)); + boolean isSystemDefault = buffer.getInt() == 1; + out.put(name, new Typeface(nativePtr, isSystemDefault)); } return nativePtrs; } @@ -1496,6 +1511,11 @@ public class Typeface { return families; } + /** @hide */ + public boolean isSystemFont() { + return mIsSystemDefault; + } + private static native long nativeCreateFromTypeface(long native_instance, int style); private static native long nativeCreateFromTypefaceWithExactStyle( long native_instance, int weight, boolean italic); -- cgit v1.2.3