diff options
-rw-r--r-- | core/java/android/text/style/TextAppearanceSpan.java | 18 | ||||
-rw-r--r-- | core/java/android/widget/TextView.java | 6 | ||||
-rw-r--r-- | core/java/com/android/internal/graphics/fonts/DynamicMetrics.java | 129 | ||||
-rw-r--r-- | graphics/java/android/graphics/Typeface.java | 30 |
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); |