summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/text/style/TextAppearanceSpan.java18
-rw-r--r--core/java/android/widget/TextView.java6
-rw-r--r--core/java/com/android/internal/graphics/fonts/DynamicMetrics.java129
-rw-r--r--graphics/java/android/graphics/Typeface.java30
4 files changed, 173 insertions, 10 deletions
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<FontVariationAxis> 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<String, FontFamily[]> 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<String, Typeface> 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);