Home | History | Annotate | Download | only in graphics
      1 /*
      2  * Copyright (C) 2014 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.graphics;
     18 
     19 import com.android.annotations.NonNull;
     20 import com.android.annotations.Nullable;
     21 import com.android.ide.common.rendering.api.LayoutLog;
     22 import com.android.layoutlib.bridge.Bridge;
     23 import com.android.layoutlib.bridge.impl.DelegateManager;
     24 import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
     25 
     26 import android.content.res.AssetManager;
     27 
     28 import java.awt.Font;
     29 import java.awt.FontFormatException;
     30 import java.io.File;
     31 import java.io.FileNotFoundException;
     32 import java.util.ArrayList;
     33 import java.util.Collections;
     34 import java.util.HashSet;
     35 import java.util.List;
     36 import java.util.Scanner;
     37 import java.util.Set;
     38 
     39 import static android.graphics.Typeface_Delegate.SYSTEM_FONTS;
     40 
     41 /**
     42  * Delegate implementing the native methods of android.graphics.FontFamily
     43  *
     44  * Through the layoutlib_create tool, the original native methods of FontFamily have been replaced
     45  * by calls to methods of the same name in this delegate class.
     46  *
     47  * This class behaves like the original native implementation, but in Java, keeping previously
     48  * native data into its own objects and mapping them to int that are sent back and forth between
     49  * it and the original FontFamily class.
     50  *
     51  * @see DelegateManager
     52  */
     53 public class FontFamily_Delegate {
     54 
     55     public static final int DEFAULT_FONT_WEIGHT = 400;
     56     public static final int BOLD_FONT_WEIGHT_DELTA = 300;
     57     public static final int BOLD_FONT_WEIGHT = 700;
     58 
     59     // FONT_SUFFIX_ITALIC will always match FONT_SUFFIX_BOLDITALIC and hence it must be checked
     60     // separately.
     61     private static final String FONT_SUFFIX_ITALIC = "Italic.ttf";
     62     private static final String FN_ALL_FONTS_LIST = "fontsInSdk.txt";
     63 
     64     /**
     65      * A class associating {@link Font} with its metadata.
     66      */
     67     private static final class FontInfo {
     68         @Nullable
     69         Font mFont;
     70         int mWeight;
     71         boolean mIsItalic;
     72     }
     73 
     74     // ---- delegate manager ----
     75     private static final DelegateManager<FontFamily_Delegate> sManager =
     76             new DelegateManager<FontFamily_Delegate>(FontFamily_Delegate.class);
     77 
     78     // ---- delegate helper data ----
     79     private static String sFontLocation;
     80     private static final List<FontFamily_Delegate> sPostInitDelegate = new
     81             ArrayList<FontFamily_Delegate>();
     82     private static Set<String> SDK_FONTS;
     83 
     84 
     85     // ---- delegate data ----
     86     private List<FontInfo> mFonts = new ArrayList<FontInfo>();
     87 
     88     /**
     89      * The variant of the Font Family - compact or elegant.
     90      * <p/>
     91      * 0 is unspecified, 1 is compact and 2 is elegant. This needs to be kept in sync with values in
     92      * android.graphics.FontFamily
     93      *
     94      * @see Paint#setElegantTextHeight(boolean)
     95      */
     96     private FontVariant mVariant;
     97     // List of runnables to process fonts after sFontLoader is initialized.
     98     private List<Runnable> mPostInitRunnables = new ArrayList<Runnable>();
     99     /** @see #isValid() */
    100     private boolean mValid = false;
    101 
    102 
    103     // ---- Public helper class ----
    104 
    105     public enum FontVariant {
    106         // The order needs to be kept in sync with android.graphics.FontFamily.
    107         NONE, COMPACT, ELEGANT
    108     }
    109 
    110     // ---- Public Helper methods ----
    111 
    112     public static FontFamily_Delegate getDelegate(long nativeFontFamily) {
    113         return sManager.getDelegate(nativeFontFamily);
    114     }
    115 
    116     public static synchronized void setFontLocation(String fontLocation) {
    117         sFontLocation = fontLocation;
    118         // init list of bundled fonts.
    119         File allFonts = new File(fontLocation, FN_ALL_FONTS_LIST);
    120         // Current number of fonts is 103. Use the next round number to leave scope for more fonts
    121         // in the future.
    122         Set<String> allFontsList = new HashSet<String>(128);
    123         Scanner scanner = null;
    124         try {
    125             scanner = new Scanner(allFonts);
    126             while (scanner.hasNext()) {
    127                 String name = scanner.next();
    128                 // Skip font configuration files.
    129                 if (!name.endsWith(".xml")) {
    130                     allFontsList.add(name);
    131                 }
    132             }
    133         } catch (FileNotFoundException e) {
    134             Bridge.getLog().error(LayoutLog.TAG_BROKEN,
    135                     "Unable to load the list of fonts. Try re-installing the SDK Platform from the SDK Manager.",
    136                     e, null);
    137         } finally {
    138             if (scanner != null) {
    139                 scanner.close();
    140             }
    141         }
    142         SDK_FONTS = Collections.unmodifiableSet(allFontsList);
    143         for (FontFamily_Delegate fontFamily : sPostInitDelegate) {
    144             fontFamily.init();
    145         }
    146         sPostInitDelegate.clear();
    147     }
    148 
    149     @Nullable
    150     public Font getFont(int desiredWeight, boolean isItalic) {
    151         FontInfo desiredStyle = new FontInfo();
    152         desiredStyle.mWeight = desiredWeight;
    153         desiredStyle.mIsItalic = isItalic;
    154         FontInfo bestFont = null;
    155         int bestMatch = Integer.MAX_VALUE;
    156         for (FontInfo font : mFonts) {
    157             int match = computeMatch(font, desiredStyle);
    158             if (match < bestMatch) {
    159                 bestMatch = match;
    160                 bestFont = font;
    161             }
    162         }
    163         if (bestFont == null) {
    164             return null;
    165         }
    166         if (bestMatch == 0) {
    167             return bestFont.mFont;
    168         }
    169         // Derive the font as required and add it to the list of Fonts.
    170         deriveFont(bestFont, desiredStyle);
    171         addFont(desiredStyle);
    172         return desiredStyle.mFont;
    173     }
    174 
    175     public FontVariant getVariant() {
    176         return mVariant;
    177     }
    178 
    179     /**
    180      * Returns if the FontFamily should contain any fonts. If this returns true and
    181      * {@link #getFont(int, boolean)} returns an empty list, it means that an error occurred while
    182      * loading the fonts. However, some fonts are deliberately skipped, for example they are not
    183      * bundled with the SDK. In such a case, this method returns false.
    184      */
    185     public boolean isValid() {
    186         return mValid;
    187     }
    188 
    189     /*package*/ static Font loadFont(String path) {
    190         if (path.startsWith(SYSTEM_FONTS) ) {
    191             String relativePath = path.substring(SYSTEM_FONTS.length());
    192             File f = new File(sFontLocation, relativePath);
    193 
    194             try {
    195                 return Font.createFont(Font.TRUETYPE_FONT, f);
    196             } catch (Exception e) {
    197                 if (path.endsWith(".otf") && e instanceof FontFormatException) {
    198                     // If we aren't able to load an Open Type font, don't log a warning just yet.
    199                     // We wait for a case where font is being used. Only then we try to log the
    200                     // warning.
    201                     return null;
    202                 }
    203                 Bridge.getLog().fidelityWarning(LayoutLog.TAG_BROKEN,
    204                         String.format("Unable to load font %1$s", relativePath),
    205                         e, null);
    206             }
    207         } else {
    208             Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED,
    209                     "Only platform fonts located in " + SYSTEM_FONTS + "can be loaded.",
    210                     null, null);
    211         }
    212 
    213         return null;
    214     }
    215 
    216     @Nullable
    217     /*package*/ static String getFontLocation() {
    218         return sFontLocation;
    219     }
    220 
    221     // ---- native methods ----
    222 
    223     @LayoutlibDelegate
    224     /*package*/ static long nCreateFamily(String lang, int variant) {
    225         // TODO: support lang. This is required for japanese locale.
    226         FontFamily_Delegate delegate = new FontFamily_Delegate();
    227         // variant can be 0, 1 or 2.
    228         assert variant < 3;
    229         delegate.mVariant = FontVariant.values()[variant];
    230         if (sFontLocation != null) {
    231             delegate.init();
    232         } else {
    233             sPostInitDelegate.add(delegate);
    234         }
    235         return sManager.addNewDelegate(delegate);
    236     }
    237 
    238     @LayoutlibDelegate
    239     /*package*/ static void nUnrefFamily(long nativePtr) {
    240         // Removing the java reference for the object doesn't mean that it's freed for garbage
    241         // collection. Typeface_Delegate may still hold a reference for it.
    242         sManager.removeJavaReferenceFor(nativePtr);
    243     }
    244 
    245     @LayoutlibDelegate
    246     /*package*/ static boolean nAddFont(long nativeFamily, final String path) {
    247         final FontFamily_Delegate delegate = getDelegate(nativeFamily);
    248         if (delegate != null) {
    249             if (sFontLocation == null) {
    250                 delegate.mPostInitRunnables.add(new Runnable() {
    251                     @Override
    252                     public void run() {
    253                         delegate.addFont(path);
    254                     }
    255                 });
    256                 return true;
    257             }
    258             return delegate.addFont(path);
    259         }
    260         return false;
    261     }
    262 
    263     @LayoutlibDelegate
    264     /*package*/ static boolean nAddFontWeightStyle(long nativeFamily, final String path,
    265             final int weight, final boolean isItalic) {
    266         final FontFamily_Delegate delegate = getDelegate(nativeFamily);
    267         if (delegate != null) {
    268             if (sFontLocation == null) {
    269                 delegate.mPostInitRunnables.add(new Runnable() {
    270                     @Override
    271                     public void run() {
    272                         delegate.addFont(path, weight, isItalic);
    273                     }
    274                 });
    275                 return true;
    276             }
    277             return delegate.addFont(path, weight, isItalic);
    278         }
    279         return false;
    280     }
    281 
    282     @LayoutlibDelegate
    283     /*package*/ static boolean nAddFontFromAsset(long nativeFamily, AssetManager mgr, String path) {
    284         Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED,
    285                 "Typeface.createFromAsset is not supported.", null, null);
    286         return false;
    287     }
    288 
    289 
    290     // ---- private helper methods ----
    291 
    292     private void init() {
    293         for (Runnable postInitRunnable : mPostInitRunnables) {
    294             postInitRunnable.run();
    295         }
    296         mPostInitRunnables = null;
    297     }
    298 
    299      private boolean addFont(@NonNull String path) {
    300          return addFont(path, DEFAULT_FONT_WEIGHT, path.endsWith(FONT_SUFFIX_ITALIC));
    301      }
    302 
    303     private boolean addFont(@NonNull String path, int weight, boolean isItalic) {
    304         if (path.startsWith(SYSTEM_FONTS) &&
    305                 !SDK_FONTS.contains(path.substring(SYSTEM_FONTS.length()))) {
    306             return mValid = false;
    307         }
    308         // Set valid to true, even if the font fails to load.
    309         mValid = true;
    310         Font font = loadFont(path);
    311         if (font == null) {
    312             return false;
    313         }
    314         FontInfo fontInfo = new FontInfo();
    315         fontInfo.mFont = font;
    316         fontInfo.mWeight = weight;
    317         fontInfo.mIsItalic = isItalic;
    318         addFont(fontInfo);
    319         return true;
    320     }
    321 
    322     private boolean addFont(@NonNull FontInfo fontInfo) {
    323         int weight = fontInfo.mWeight;
    324         boolean isItalic = fontInfo.mIsItalic;
    325         // The list is usually just two fonts big. So iterating over all isn't as bad as it looks.
    326         // It's biggest for roboto where the size is 12.
    327         for (FontInfo font : mFonts) {
    328             if (font.mWeight == weight && font.mIsItalic == isItalic) {
    329                 return false;
    330             }
    331         }
    332         mFonts.add(fontInfo);
    333         return true;
    334     }
    335 
    336     /**
    337      * Compute matching metric between two styles - 0 is an exact match.
    338      */
    339     private static int computeMatch(@NonNull FontInfo font1, @NonNull FontInfo font2) {
    340         int score = Math.abs(font1.mWeight - font2.mWeight);
    341         if (font1.mIsItalic != font2.mIsItalic) {
    342             score += 200;
    343         }
    344         return score;
    345     }
    346 
    347     /**
    348      * Try to derive a font from {@code srcFont} for the style in {@code outFont}.
    349      * <p/>
    350      * {@code outFont} is updated to reflect the style of the derived font.
    351      * @param srcFont the source font
    352      * @param outFont contains the desired font style. Updated to contain the derived font and
    353      *                its style
    354      * @return outFont
    355      */
    356     @NonNull
    357     private FontInfo deriveFont(@NonNull FontInfo srcFont, @NonNull FontInfo outFont) {
    358         int desiredWeight = outFont.mWeight;
    359         int srcWeight = srcFont.mWeight;
    360         Font derivedFont = srcFont.mFont;
    361         // Embolden the font if required.
    362         if (desiredWeight >= BOLD_FONT_WEIGHT && desiredWeight - srcWeight > BOLD_FONT_WEIGHT_DELTA / 2) {
    363             derivedFont = derivedFont.deriveFont(Font.BOLD);
    364             srcWeight += BOLD_FONT_WEIGHT_DELTA;
    365         }
    366         // Italicize the font if required.
    367         if (outFont.mIsItalic && !srcFont.mIsItalic) {
    368             derivedFont = derivedFont.deriveFont(Font.ITALIC);
    369         } else if (outFont.mIsItalic != srcFont.mIsItalic) {
    370             // The desired font is plain, but the src font is italics. We can't convert it back. So
    371             // we update the value to reflect the true style of the font we're deriving.
    372             outFont.mIsItalic = srcFont.mIsItalic;
    373         }
    374         outFont.mFont = derivedFont;
    375         outFont.mWeight = srcWeight;
    376         // No need to update mIsItalics, as it's already been handled above.
    377         return outFont;
    378     }
    379 }
    380