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 getDelegate(long nativeFontFamily)112 public static FontFamily_Delegate getDelegate(long nativeFontFamily) { 113 return sManager.getDelegate(nativeFontFamily); 114 } 115 setFontLocation(String fontLocation)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 getFont(int desiredWeight, boolean isItalic)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 getVariant()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 */ isValid()185 public boolean isValid() { 186 return mValid; 187 } 188 loadFont(String path)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 getFontLocation()217 /*package*/ static String getFontLocation() { 218 return sFontLocation; 219 } 220 221 // ---- native methods ---- 222 223 @LayoutlibDelegate nCreateFamily(String lang, int variant)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