From ed00bfdfae5d5cbc9e13f4e8affdece48f4c5b7f Mon Sep 17 00:00:00 2001 From: Clara Bayarri Date: Fri, 20 Jan 2017 14:58:21 +0000 Subject: [PATCH] Implement .xml font resource support This change implements the loading and parsing of xml type font resources, and makes sure it is used properly by TextView styles. Test: run cts -m CtsContentTestCases -t android.content.res.cts.ResourcesTest run cts -m CtsWidgetTestCases -t android.widget.cts.TextViewTest#testFontResources* Change-Id: I5a2930b3ba7dad67d9607e9036a5dde6bab0c5a4 --- api/current.txt | 1 + api/system-current.txt | 1 + api/test-current.txt | 1 + core/java/android/annotation/FontRes.java | 37 +++++++ .../content/res/FontResourcesParser.java | 104 ++++++++++++++++++ core/java/android/content/res/Resources.java | 10 +- .../android/content/res/ResourcesImpl.java | 23 ++-- core/java/android/content/res/TypedArray.java | 31 ++++++ core/java/android/text/FontConfig.java | 9 +- core/java/android/widget/TextView.java | 55 ++++++--- graphics/java/android/graphics/Typeface.java | 63 ++++++++++- 11 files changed, 307 insertions(+), 28 deletions(-) create mode 100644 core/java/android/annotation/FontRes.java create mode 100644 core/java/android/content/res/FontResourcesParser.java diff --git a/api/current.txt b/api/current.txt index e1b13ae39bcae..6bc90aea634b5 100644 --- a/api/current.txt +++ b/api/current.txt @@ -10813,6 +10813,7 @@ package android.content.res { method public int getDimensionPixelSize(int, int); method public android.graphics.drawable.Drawable getDrawable(int); method public float getFloat(int, float); + method public android.graphics.Typeface getFont(int); method public float getFraction(int, int, int, float); method public int getIndex(int); method public int getIndexCount(); diff --git a/api/system-current.txt b/api/system-current.txt index b448647eac09d..34a55711f7e1f 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -11371,6 +11371,7 @@ package android.content.res { method public int getDimensionPixelSize(int, int); method public android.graphics.drawable.Drawable getDrawable(int); method public float getFloat(int, float); + method public android.graphics.Typeface getFont(int); method public float getFraction(int, int, int, float); method public int getIndex(int); method public int getIndexCount(); diff --git a/api/test-current.txt b/api/test-current.txt index 7b1032261fe49..0a62f41cfb11e 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -10845,6 +10845,7 @@ package android.content.res { method public int getDimensionPixelSize(int, int); method public android.graphics.drawable.Drawable getDrawable(int); method public float getFloat(int, float); + method public android.graphics.Typeface getFont(int); method public float getFraction(int, int, int, float); method public int getIndex(int); method public int getIndexCount(); diff --git a/core/java/android/annotation/FontRes.java b/core/java/android/annotation/FontRes.java new file mode 100644 index 0000000000000..dbacb5876dce2 --- /dev/null +++ b/core/java/android/annotation/FontRes.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2017 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 android.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * Denotes that an integer parameter, field or method return value is expected + * to be a Font resource reference (e.g. R.font.myfont). + * + * @hide + */ +@Documented +@Retention(SOURCE) +@Target({METHOD, PARAMETER, FIELD}) +public @interface FontRes { +} diff --git a/core/java/android/content/res/FontResourcesParser.java b/core/java/android/content/res/FontResourcesParser.java new file mode 100644 index 0000000000000..15f3a099c1632 --- /dev/null +++ b/core/java/android/content/res/FontResourcesParser.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2017 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 android.content.res; + +import com.android.internal.R; +import android.text.FontConfig; +import android.util.AttributeSet; +import android.util.Xml; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Parser for xml type font resources. + * @hide + */ +public class FontResourcesParser { + private static final int NORMAL_WEIGHT = 400; + private static final String ITALIC = "italic"; + + public static FontConfig parse(XmlPullParser parser, Resources resources) + throws XmlPullParserException, IOException { + int type; + while ((type=parser.next()) != XmlPullParser.START_TAG + && type != XmlPullParser.END_DOCUMENT) { + // Empty loop. + } + + if (type != XmlPullParser.START_TAG) { + throw new XmlPullParserException("No start tag found"); + } + return readFamilies(parser, resources); + } + + private static FontConfig readFamilies(XmlPullParser parser, Resources resources) + throws XmlPullParserException, IOException { + FontConfig config = new FontConfig(); + parser.require(XmlPullParser.START_TAG, null, "font-family"); + String tag = parser.getName(); + if (tag.equals("font-family")) { + config.getFamilies().add(readFamily(parser, resources)); + } else { + skip(parser); + } + return config; + } + + private static FontConfig.Family readFamily(XmlPullParser parser, Resources resources) + throws XmlPullParserException, IOException { + List fonts = new ArrayList<>(); + while (parser.next() != XmlPullParser.END_TAG) { + if (parser.getEventType() != XmlPullParser.START_TAG) continue; + String tag = parser.getName(); + if (tag.equals("font")) { + fonts.add(readFont(parser, resources)); + } else { + skip(parser); + } + } + return new FontConfig.Family(null, fonts, null, null); + } + + private static FontConfig.Font readFont(XmlPullParser parser, Resources resources) + throws XmlPullParserException, IOException { + AttributeSet attrs = Xml.asAttributeSet(parser); + TypedArray array = resources.obtainAttributes(attrs, R.styleable.FontFamilyFont); + int weight = array.getInt(R.styleable.FontFamilyFont_fontWeight, NORMAL_WEIGHT); + boolean isItalic = ITALIC.equals(array.getString(R.styleable.FontFamilyFont_fontStyle)); + String filename = array.getString(R.styleable.FontFamilyFont_font); + array.recycle(); + return new FontConfig.Font(filename, 0, null, weight, isItalic); + } + + private static void skip(XmlPullParser parser) throws XmlPullParserException, IOException { + int depth = 1; + while (depth > 0) { + switch (parser.next()) { + case XmlPullParser.START_TAG: + depth++; + break; + case XmlPullParser.END_TAG: + depth--; + break; + } + } + } +} diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java index c3185a7cad057..1bdf667af8798 100644 --- a/core/java/android/content/res/Resources.java +++ b/core/java/android/content/res/Resources.java @@ -27,6 +27,7 @@ import android.annotation.ColorInt; import android.annotation.ColorRes; import android.annotation.DimenRes; import android.annotation.DrawableRes; +import android.annotation.FontRes; import android.annotation.FractionRes; import android.annotation.IntegerRes; import android.annotation.LayoutRes; @@ -349,12 +350,12 @@ public class Resources { * * @return Typeface The Typeface data associated with the resource. */ - @NonNull public Typeface getFont(@StringRes int id) throws NotFoundException { + @NonNull public Typeface getFont(@FontRes int id) throws NotFoundException { final TypedValue value = obtainTempTypedValue(); try { final ResourcesImpl impl = mResourcesImpl; impl.getValue(id, value, true); - Typeface typeface = impl.loadFont(value, id); + Typeface typeface = impl.loadFont(this, value, id); if (typeface != null) { return typeface; } @@ -365,6 +366,11 @@ public class Resources { + Integer.toHexString(id)); } + @NonNull + Typeface getFont(@NonNull TypedValue value, @FontRes int id) throws NotFoundException { + return mResourcesImpl.loadFont(this, value, id); + } + /** * Returns the character sequence necessary for grammatically correct pluralization * of the given resource ID for the given quantity. diff --git a/core/java/android/content/res/ResourcesImpl.java b/core/java/android/content/res/ResourcesImpl.java index 05892e0ad6602..dc6780d2aeb11 100644 --- a/core/java/android/content/res/ResourcesImpl.java +++ b/core/java/android/content/res/ResourcesImpl.java @@ -38,6 +38,7 @@ import android.icu.text.PluralRules; import android.os.Build; import android.os.LocaleList; import android.os.Trace; +import android.text.FontConfig; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.Log; @@ -45,7 +46,6 @@ import android.util.LongSparseArray; import android.util.Slog; import android.util.TypedValue; import android.util.Xml; -import android.view.Display; import android.view.DisplayAdjustments; import java.io.IOException; @@ -745,13 +745,17 @@ public class ResourcesImpl { * Loads a font from XML or resources stream. */ @Nullable - public Typeface loadFont(TypedValue value, int id) { + public Typeface loadFont(Resources wrapper, TypedValue value, int id) { if (value.string == null) { throw new NotFoundException("Resource \"" + getResourceName(id) + "\" (" + Integer.toHexString(id) + ") is not a Font: " + value); } final String file = value.string.toString(); + Typeface cached = Typeface.createFromCache(mAssets, file); + if (cached != null) { + return cached; + } if (DEBUG_LOAD) { Log.v(TAG, "Loading font for cookie " + value.assetCookie + ": " + file); @@ -759,12 +763,17 @@ public class ResourcesImpl { Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file); try { - if (file.endsWith(".xml")) { - // TODO handle xml type font definitions - } else { - return Typeface.createFromResources( - mAssets, value.string.toString(), value.assetCookie); + if (file.endsWith("xml")) { + final XmlResourceParser rp = loadXmlResourceParser( + file, id, value.assetCookie, "font"); + final FontConfig config = FontResourcesParser.parse(rp, wrapper); + return Typeface.createFromResources(config, mAssets, file); } + return Typeface.createFromResources(mAssets, file, value.assetCookie); + } catch (XmlPullParserException e) { + Log.e(TAG, "Failed to parse xml resource " + file, e); + } catch (IOException e) { + Log.e(TAG, "Failed to read xml resource " + file, e); } finally { Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); } diff --git a/core/java/android/content/res/TypedArray.java b/core/java/android/content/res/TypedArray.java index 391220134adeb..f48afb55c58ae 100644 --- a/core/java/android/content/res/TypedArray.java +++ b/core/java/android/content/res/TypedArray.java @@ -22,6 +22,7 @@ import android.annotation.Nullable; import android.annotation.StyleableRes; import android.content.pm.ActivityInfo; import android.content.pm.ActivityInfo.Config; +import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.os.StrictMode; import android.util.AttributeSet; @@ -936,6 +937,36 @@ public class TypedArray { return null; } + /** + * Retrieve the Typeface for the attribute at index. + *

+ * This method will throw an exception if the attribute is defined but is + * not a font. + * + * @param index Index of attribute to retrieve. + * + * @return Typeface for the attribute, or {@code null} if not defined. + * @throws RuntimeException if the TypedArray has already been recycled. + * @throws UnsupportedOperationException if the attribute is defined but is + * not a font resource. + */ + @Nullable + public Typeface getFont(@StyleableRes int index) { + if (mRecycled) { + throw new RuntimeException("Cannot make calls to a recycled instance!"); + } + + final TypedValue value = mValue; + if (getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, value)) { + if (value.type == TypedValue.TYPE_ATTRIBUTE) { + throw new UnsupportedOperationException( + "Failed to resolve attribute at index " + index + ": " + value); + } + return mResources.getFont(value, value.resourceId); + } + return null; + } + /** * Retrieve the CharSequence[] for the attribute at index. * This gets the resource ID of the selected attribute, and uses diff --git a/core/java/android/text/FontConfig.java b/core/java/android/text/FontConfig.java index df694ff6af313..3048a38820100 100644 --- a/core/java/android/text/FontConfig.java +++ b/core/java/android/text/FontConfig.java @@ -164,7 +164,7 @@ public final class FontConfig implements Parcelable { * Class that holds information about a Font. */ public static final class Font implements Parcelable { - private final String mFontName; + private String mFontName; private final int mTtcIndex; private final List mAxes; private final int mWeight; @@ -202,6 +202,13 @@ public final class FontConfig implements Parcelable { return mFontName; } + /** + * @hide + */ + public void setFontName(String fontName) { + mFontName = fontName; + } + /** * Returns the index to be used to access this font when accessing a TTC file. */ diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index a11ece6608d25..e3fb32b9254e7 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -783,6 +783,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener ColorStateList textColorLink = null; int textSize = 15; String fontFamily = null; + Typeface fontTypeface = null; boolean fontFamilyExplicit = false; int typefaceIndex = -1; int styleIndex = -1; @@ -845,7 +846,14 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener break; case com.android.internal.R.styleable.TextAppearance_fontFamily: - fontFamily = appearance.getString(attr); + try { + fontTypeface = appearance.getFont(attr); + } catch (UnsupportedOperationException e) { + // Expected if it is not a font resource. + } + if (fontTypeface == null) { + fontFamily = appearance.getString(attr); + } break; case com.android.internal.R.styleable.TextAppearance_textStyle: @@ -1149,7 +1157,14 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener break; case com.android.internal.R.styleable.TextView_fontFamily: - fontFamily = a.getString(attr); + try { + fontTypeface = appearance.getFont(attr); + } catch (UnsupportedOperationException e) { + // Expected if it is not a font resource. + } + if (fontTypeface == null) { + fontFamily = appearance.getString(attr); + } fontFamilyExplicit = true; break; @@ -1499,7 +1514,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (typefaceIndex != -1 && !fontFamilyExplicit) { fontFamily = null; } - setTypefaceFromAttrs(fontFamily, typefaceIndex, styleIndex); + setTypefaceFromAttrs(fontTypeface, fontFamily, typefaceIndex, styleIndex); if (shadowcolor != 0) { setShadowLayer(r, dx, dy, shadowcolor); @@ -1792,14 +1807,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } - private void setTypefaceFromAttrs(String familyName, int typefaceIndex, int styleIndex) { - Typeface tf = null; - if (familyName != null) { + private void setTypefaceFromAttrs(Typeface fontTypeface, String familyName, int typefaceIndex, + int styleIndex) { + Typeface tf = fontTypeface; + if (tf == null && familyName != null) { tf = Typeface.create(familyName, styleIndex); - if (tf != null) { - setTypeface(tf); - return; - } + } + if (tf != null) { + setTypeface(tf); + return; } switch (typefaceIndex) { case SANS: @@ -3097,10 +3113,19 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener setLinkTextColor(textColorLink); } - final String fontFamily = ta.getString(R.styleable.TextAppearance_fontFamily); + Typeface fontTypeface = null; + String fontFamily = null; + try { + fontTypeface = ta.getFont(R.styleable.TextAppearance_fontFamily); + } catch (UnsupportedOperationException e) { + // Expected if it is not a font resource. + } + if (fontTypeface == null) { + fontFamily = ta.getString(R.styleable.TextAppearance_fontFamily); + } final int typefaceIndex = ta.getInt(R.styleable.TextAppearance_typeface, -1); final int styleIndex = ta.getInt(R.styleable.TextAppearance_textStyle, -1); - setTypefaceFromAttrs(fontFamily, typefaceIndex, styleIndex); + setTypefaceFromAttrs(fontTypeface, fontFamily, typefaceIndex, styleIndex); final int shadowColor = ta.getInt(R.styleable.TextAppearance_shadowColor, 0); if (shadowColor != 0) { @@ -5161,15 +5186,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener boolean forceUpdate = false; if (isPassword) { setTransformationMethod(PasswordTransformationMethod.getInstance()); - setTypefaceFromAttrs(null /* fontFamily */, MONOSPACE, 0); + setTypefaceFromAttrs(null/* fontTypeface */, null /* fontFamily */, MONOSPACE, 0); } else if (isVisiblePassword) { if (mTransformation == PasswordTransformationMethod.getInstance()) { forceUpdate = true; } - setTypefaceFromAttrs(null /* fontFamily */, MONOSPACE, 0); + setTypefaceFromAttrs(null/* fontTypeface */, null /* fontFamily */, MONOSPACE, 0); } else if (wasPassword || wasVisiblePassword) { // not in password mode, clean up typeface and transformation - setTypefaceFromAttrs(null /* fontFamily */, -1, -1); + setTypefaceFromAttrs(null/* fontTypeface */, null /* fontFamily */, -1, -1); if (mTransformation == PasswordTransformationMethod.getInstance()) { forceUpdate = true; } diff --git a/graphics/java/android/graphics/Typeface.java b/graphics/java/android/graphics/Typeface.java index 7a7c7afb21f1c..62ac42d324af6 100644 --- a/graphics/java/android/graphics/Typeface.java +++ b/graphics/java/android/graphics/Typeface.java @@ -18,6 +18,7 @@ package android.graphics; import android.annotation.IntDef; import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.res.AssetManager; import android.graphics.fonts.FontRequest; import android.graphics.fonts.FontResult; @@ -131,9 +132,9 @@ public class Typeface { /** * @hide - * Used by Resources. + * Used by Resources to load a font resource of type font file. */ - @NonNull + @Nullable public static Typeface createFromResources(AssetManager mgr, String path, int cookie) { if (sFallbackFonts != null) { synchronized (sDynamicTypefaceCache) { @@ -143,6 +144,7 @@ public class Typeface { FontFamily fontFamily = new FontFamily(); if (fontFamily.addFontFromAssetManager(mgr, path, cookie, false /* isAsset */)) { + fontFamily.freeze(); FontFamily[] families = {fontFamily}; typeface = createFromFamiliesWithDefault(families); sDynamicTypefaceCache.put(key, typeface); @@ -150,7 +152,62 @@ public class Typeface { } } } - throw new RuntimeException("Font resource not found " + path); + return null; + } + + /** + * @hide + * Used by Resources to load a font resource of type xml. + */ + @Nullable + public static Typeface createFromResources(FontConfig config, AssetManager mgr, String path) { + if (sFallbackFonts != null) { + synchronized (sDynamicTypefaceCache) { + final String key = createAssetUid(mgr, path); + Typeface typeface = sDynamicTypefaceCache.get(key); + if (typeface != null) return typeface; + + List families = config.getFamilies(); + if (families == null || families.isEmpty()) { + throw new RuntimeException("Font resource contained no fonts."); + } + if (families.size() > 1) { + throw new RuntimeException("Font resource contained more than one family."); + } + FontConfig.Family family = families.get(0); + + FontFamily fontFamily = new FontFamily(); + List fonts = family.getFonts(); + for (int i = 0; i < fonts.size(); i++) { + FontConfig.Font font = fonts.get(i); + // TODO: Use style and weight info + if (!fontFamily.addFontFromAssetManager(mgr, font.getFontName(), + 0 /* resourceCookie */, false /* isAsset */)) { + return null; + } + } + fontFamily.freeze(); + FontFamily[] familyChain = { fontFamily }; + typeface = createFromFamiliesWithDefault(familyChain); + sDynamicTypefaceCache.put(key, typeface); + return typeface; + } + } + return null; + } + + /** + * @hide + */ + public static Typeface createFromCache(AssetManager mgr, String path) { + synchronized (sDynamicTypefaceCache) { + final String key = createAssetUid(mgr, path); + Typeface typeface = sDynamicTypefaceCache.get(key); + if (typeface != null) { + return typeface; + } + } + return null; } /**