FontFamily_Delegate.java revision 6dac0d4b7c55da32cb5deb9b589b1acb43cb536d
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 17package android.graphics; 18 19import com.android.ide.common.rendering.api.AssetRepository; 20import com.android.ide.common.rendering.api.LayoutLog; 21import com.android.layoutlib.bridge.Bridge; 22import com.android.layoutlib.bridge.impl.DelegateManager; 23import com.android.tools.layoutlib.annotations.LayoutlibDelegate; 24 25import android.annotation.NonNull; 26import android.annotation.Nullable; 27import android.content.res.AssetManager; 28import android.content.res.BridgeAssetManager; 29 30import java.awt.Font; 31import java.awt.FontFormatException; 32import java.io.File; 33import java.io.FileNotFoundException; 34import java.io.IOException; 35import java.io.InputStream; 36import java.util.ArrayList; 37import java.util.Collections; 38import java.util.HashSet; 39import java.util.LinkedHashMap; 40import java.util.List; 41import java.util.Map; 42import java.util.Map.Entry; 43import java.util.Scanner; 44import java.util.Set; 45 46import static android.graphics.Typeface_Delegate.SYSTEM_FONTS; 47 48/** 49 * Delegate implementing the native methods of android.graphics.FontFamily 50 * 51 * Through the layoutlib_create tool, the original native methods of FontFamily have been replaced 52 * by calls to methods of the same name in this delegate class. 53 * 54 * This class behaves like the original native implementation, but in Java, keeping previously 55 * native data into its own objects and mapping them to int that are sent back and forth between 56 * it and the original FontFamily class. 57 * 58 * @see DelegateManager 59 */ 60public class FontFamily_Delegate { 61 62 public static final int DEFAULT_FONT_WEIGHT = 400; 63 public static final int BOLD_FONT_WEIGHT_DELTA = 300; 64 public static final int BOLD_FONT_WEIGHT = 700; 65 66 private static final String FONT_SUFFIX_ITALIC = "Italic.ttf"; 67 private static final String FN_ALL_FONTS_LIST = "fontsInSdk.txt"; 68 private static final String EXTENSION_OTF = ".otf"; 69 70 private static final int CACHE_SIZE = 10; 71 // The cache has a drawback that if the font file changed after the font object was created, 72 // we will not update it. 73 private static final Map<String, FontInfo> sCache = 74 new LinkedHashMap<String, FontInfo>(CACHE_SIZE) { 75 @Override 76 protected boolean removeEldestEntry(Entry<String, FontInfo> eldest) { 77 return size() > CACHE_SIZE; 78 } 79 80 @Override 81 public FontInfo put(String key, FontInfo value) { 82 // renew this entry. 83 FontInfo removed = remove(key); 84 super.put(key, value); 85 return removed; 86 } 87 }; 88 89 /** 90 * A class associating {@link Font} with its metadata. 91 */ 92 private static final class FontInfo { 93 @Nullable 94 Font mFont; 95 int mWeight; 96 boolean mIsItalic; 97 } 98 99 // ---- delegate manager ---- 100 private static final DelegateManager<FontFamily_Delegate> sManager = 101 new DelegateManager<FontFamily_Delegate>(FontFamily_Delegate.class); 102 103 // ---- delegate helper data ---- 104 private static String sFontLocation; 105 private static final List<FontFamily_Delegate> sPostInitDelegate = new 106 ArrayList<FontFamily_Delegate>(); 107 private static Set<String> SDK_FONTS; 108 109 110 // ---- delegate data ---- 111 private List<FontInfo> mFonts = new ArrayList<FontInfo>(); 112 113 /** 114 * The variant of the Font Family - compact or elegant. 115 * <p/> 116 * 0 is unspecified, 1 is compact and 2 is elegant. This needs to be kept in sync with values in 117 * android.graphics.FontFamily 118 * 119 * @see Paint#setElegantTextHeight(boolean) 120 */ 121 private FontVariant mVariant; 122 // List of runnables to process fonts after sFontLoader is initialized. 123 private List<Runnable> mPostInitRunnables = new ArrayList<Runnable>(); 124 /** @see #isValid() */ 125 private boolean mValid = false; 126 127 128 // ---- Public helper class ---- 129 130 public enum FontVariant { 131 // The order needs to be kept in sync with android.graphics.FontFamily. 132 NONE, COMPACT, ELEGANT 133 } 134 135 // ---- Public Helper methods ---- 136 137 public static FontFamily_Delegate getDelegate(long nativeFontFamily) { 138 return sManager.getDelegate(nativeFontFamily); 139 } 140 141 public static synchronized void setFontLocation(String fontLocation) { 142 sFontLocation = fontLocation; 143 // init list of bundled fonts. 144 File allFonts = new File(fontLocation, FN_ALL_FONTS_LIST); 145 // Current number of fonts is 103. Use the next round number to leave scope for more fonts 146 // in the future. 147 Set<String> allFontsList = new HashSet<String>(128); 148 Scanner scanner = null; 149 try { 150 scanner = new Scanner(allFonts); 151 while (scanner.hasNext()) { 152 String name = scanner.next(); 153 // Skip font configuration files. 154 if (!name.endsWith(".xml")) { 155 allFontsList.add(name); 156 } 157 } 158 } catch (FileNotFoundException e) { 159 Bridge.getLog().error(LayoutLog.TAG_BROKEN, 160 "Unable to load the list of fonts. Try re-installing the SDK Platform from the SDK Manager.", 161 e, null); 162 } finally { 163 if (scanner != null) { 164 scanner.close(); 165 } 166 } 167 SDK_FONTS = Collections.unmodifiableSet(allFontsList); 168 for (FontFamily_Delegate fontFamily : sPostInitDelegate) { 169 fontFamily.init(); 170 } 171 sPostInitDelegate.clear(); 172 } 173 174 @Nullable 175 public Font getFont(int desiredWeight, boolean isItalic) { 176 FontInfo desiredStyle = new FontInfo(); 177 desiredStyle.mWeight = desiredWeight; 178 desiredStyle.mIsItalic = isItalic; 179 FontInfo bestFont = null; 180 int bestMatch = Integer.MAX_VALUE; 181 //noinspection ForLoopReplaceableByForEach (avoid iterator instantiation) 182 for (int i = 0, n = mFonts.size(); i < n; i++) { 183 FontInfo font = mFonts.get(i); 184 int match = computeMatch(font, desiredStyle); 185 if (match < bestMatch) { 186 bestMatch = match; 187 bestFont = font; 188 } 189 } 190 if (bestFont == null) { 191 return null; 192 } 193 if (bestMatch == 0) { 194 return bestFont.mFont; 195 } 196 // Derive the font as required and add it to the list of Fonts. 197 deriveFont(bestFont, desiredStyle); 198 addFont(desiredStyle); 199 return desiredStyle.mFont; 200 } 201 202 public FontVariant getVariant() { 203 return mVariant; 204 } 205 206 /** 207 * Returns if the FontFamily should contain any fonts. If this returns true and 208 * {@link #getFont(int, boolean)} returns an empty list, it means that an error occurred while 209 * loading the fonts. However, some fonts are deliberately skipped, for example they are not 210 * bundled with the SDK. In such a case, this method returns false. 211 */ 212 public boolean isValid() { 213 return mValid; 214 } 215 216 /*package*/ static Font loadFont(String path) { 217 if (path.startsWith(SYSTEM_FONTS) ) { 218 String relativePath = path.substring(SYSTEM_FONTS.length()); 219 File f = new File(sFontLocation, relativePath); 220 221 try { 222 return Font.createFont(Font.TRUETYPE_FONT, f); 223 } catch (Exception e) { 224 if (path.endsWith(EXTENSION_OTF) && e instanceof FontFormatException) { 225 // If we aren't able to load an Open Type font, don't log a warning just yet. 226 // We wait for a case where font is being used. Only then we try to log the 227 // warning. 228 return null; 229 } 230 Bridge.getLog().fidelityWarning(LayoutLog.TAG_BROKEN, 231 String.format("Unable to load font %1$s", relativePath), 232 e, null); 233 } 234 } else { 235 Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED, 236 "Only platform fonts located in " + SYSTEM_FONTS + "can be loaded.", 237 null, null); 238 } 239 240 return null; 241 } 242 243 @Nullable 244 /*package*/ static String getFontLocation() { 245 return sFontLocation; 246 } 247 248 // ---- native methods ---- 249 250 @LayoutlibDelegate 251 /*package*/ static long nCreateFamily(String lang, int variant) { 252 // TODO: support lang. This is required for japanese locale. 253 FontFamily_Delegate delegate = new FontFamily_Delegate(); 254 // variant can be 0, 1 or 2. 255 assert variant < 3; 256 delegate.mVariant = FontVariant.values()[variant]; 257 if (sFontLocation != null) { 258 delegate.init(); 259 } else { 260 sPostInitDelegate.add(delegate); 261 } 262 return sManager.addNewDelegate(delegate); 263 } 264 265 @LayoutlibDelegate 266 /*package*/ static void nUnrefFamily(long nativePtr) { 267 // Removing the java reference for the object doesn't mean that it's freed for garbage 268 // collection. Typeface_Delegate may still hold a reference for it. 269 sManager.removeJavaReferenceFor(nativePtr); 270 } 271 272 @LayoutlibDelegate 273 /*package*/ static boolean nAddFont(long nativeFamily, final String path) { 274 final FontFamily_Delegate delegate = getDelegate(nativeFamily); 275 if (delegate != null) { 276 if (sFontLocation == null) { 277 delegate.mPostInitRunnables.add(new Runnable() { 278 @Override 279 public void run() { 280 delegate.addFont(path); 281 } 282 }); 283 return true; 284 } 285 return delegate.addFont(path); 286 } 287 return false; 288 } 289 290 @LayoutlibDelegate 291 /*package*/ static boolean nAddFontWeightStyle(long nativeFamily, final String path, 292 final int weight, final boolean isItalic) { 293 final FontFamily_Delegate delegate = getDelegate(nativeFamily); 294 if (delegate != null) { 295 if (sFontLocation == null) { 296 delegate.mPostInitRunnables.add(new Runnable() { 297 @Override 298 public void run() { 299 delegate.addFont(path, weight, isItalic); 300 } 301 }); 302 return true; 303 } 304 return delegate.addFont(path, weight, isItalic); 305 } 306 return false; 307 } 308 309 @LayoutlibDelegate 310 /*package*/ static boolean nAddFontFromAsset(long nativeFamily, AssetManager mgr, String path) { 311 FontFamily_Delegate ffd = sManager.getDelegate(nativeFamily); 312 ffd.mValid = true; 313 if (mgr == null) { 314 return false; 315 } 316 if (mgr instanceof BridgeAssetManager) { 317 InputStream fontStream = null; 318 try { 319 AssetRepository assetRepository = ((BridgeAssetManager) mgr).getAssetRepository(); 320 if (assetRepository == null) { 321 Bridge.getLog().error(LayoutLog.TAG_MISSING_ASSET, "Asset not found: " + path, 322 null); 323 return false; 324 } 325 if (!assetRepository.isSupported()) { 326 // Don't log any warnings on unsupported IDEs. 327 return false; 328 } 329 // Check cache 330 FontInfo fontInfo = sCache.get(path); 331 if (fontInfo != null) { 332 // renew the font's lease. 333 sCache.put(path, fontInfo); 334 ffd.addFont(fontInfo); 335 return true; 336 } 337 fontStream = assetRepository.openAsset(path, AssetManager.ACCESS_STREAMING); 338 if (fontStream == null) { 339 Bridge.getLog().error(LayoutLog.TAG_MISSING_ASSET, "Asset not found: " + path, 340 path); 341 return false; 342 } 343 Font font = Font.createFont(Font.TRUETYPE_FONT, fontStream); 344 fontInfo = new FontInfo(); 345 fontInfo.mFont = font; 346 fontInfo.mWeight = font.isBold() ? BOLD_FONT_WEIGHT : DEFAULT_FONT_WEIGHT; 347 fontInfo.mIsItalic = font.isItalic(); 348 ffd.addFont(fontInfo); 349 return true; 350 } catch (IOException e) { 351 Bridge.getLog().error(LayoutLog.TAG_MISSING_ASSET, "Unable to load font " + path, e, 352 path); 353 } catch (FontFormatException e) { 354 if (path.endsWith(EXTENSION_OTF)) { 355 // otf fonts are not supported on the user's config (JRE version + OS) 356 Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED, 357 "OpenType fonts are not supported yet: " + path, null, path); 358 } else { 359 Bridge.getLog().error(LayoutLog.TAG_BROKEN, 360 "Unable to load font " + path, e, path); 361 } 362 } finally { 363 if (fontStream != null) { 364 try { 365 fontStream.close(); 366 } catch (IOException ignored) { 367 } 368 } 369 } 370 return false; 371 } 372 // This should never happen. AssetManager is a final class (from user's perspective), and 373 // we've replaced every creation of AssetManager with our implementation. We create an 374 // exception and log it, but continue with rest of the rendering, without loading this font. 375 Bridge.getLog().error(LayoutLog.TAG_BROKEN, 376 "You have found a bug in the rendering library. Please file a bug at b.android.com.", 377 new RuntimeException("Asset Manager is not an instance of BridgeAssetManager"), 378 null); 379 return false; 380 } 381 382 383 // ---- private helper methods ---- 384 385 private void init() { 386 for (Runnable postInitRunnable : mPostInitRunnables) { 387 postInitRunnable.run(); 388 } 389 mPostInitRunnables = null; 390 } 391 392 private boolean addFont(@NonNull String path) { 393 return addFont(path, DEFAULT_FONT_WEIGHT, path.endsWith(FONT_SUFFIX_ITALIC)); 394 } 395 396 private boolean addFont(@NonNull String path, int weight, boolean isItalic) { 397 if (path.startsWith(SYSTEM_FONTS) && 398 !SDK_FONTS.contains(path.substring(SYSTEM_FONTS.length()))) { 399 return mValid = false; 400 } 401 // Set valid to true, even if the font fails to load. 402 mValid = true; 403 Font font = loadFont(path); 404 if (font == null) { 405 return false; 406 } 407 FontInfo fontInfo = new FontInfo(); 408 fontInfo.mFont = font; 409 fontInfo.mWeight = weight; 410 fontInfo.mIsItalic = isItalic; 411 addFont(fontInfo); 412 return true; 413 } 414 415 private boolean addFont(@NonNull FontInfo fontInfo) { 416 int weight = fontInfo.mWeight; 417 boolean isItalic = fontInfo.mIsItalic; 418 // The list is usually just two fonts big. So iterating over all isn't as bad as it looks. 419 // It's biggest for roboto where the size is 12. 420 //noinspection ForLoopReplaceableByForEach (avoid iterator instantiation) 421 for (int i = 0, n = mFonts.size(); i < n; i++) { 422 FontInfo font = mFonts.get(i); 423 if (font.mWeight == weight && font.mIsItalic == isItalic) { 424 return false; 425 } 426 } 427 mFonts.add(fontInfo); 428 return true; 429 } 430 431 /** 432 * Compute matching metric between two styles - 0 is an exact match. 433 */ 434 private static int computeMatch(@NonNull FontInfo font1, @NonNull FontInfo font2) { 435 int score = Math.abs(font1.mWeight - font2.mWeight); 436 if (font1.mIsItalic != font2.mIsItalic) { 437 score += 200; 438 } 439 return score; 440 } 441 442 /** 443 * Try to derive a font from {@code srcFont} for the style in {@code outFont}. 444 * <p/> 445 * {@code outFont} is updated to reflect the style of the derived font. 446 * @param srcFont the source font 447 * @param outFont contains the desired font style. Updated to contain the derived font and 448 * its style 449 * @return outFont 450 */ 451 @NonNull 452 private FontInfo deriveFont(@NonNull FontInfo srcFont, @NonNull FontInfo outFont) { 453 int desiredWeight = outFont.mWeight; 454 int srcWeight = srcFont.mWeight; 455 Font derivedFont = srcFont.mFont; 456 // Embolden the font if required. 457 if (desiredWeight >= BOLD_FONT_WEIGHT && desiredWeight - srcWeight > BOLD_FONT_WEIGHT_DELTA / 2) { 458 derivedFont = derivedFont.deriveFont(Font.BOLD); 459 srcWeight += BOLD_FONT_WEIGHT_DELTA; 460 } 461 // Italicize the font if required. 462 if (outFont.mIsItalic && !srcFont.mIsItalic) { 463 derivedFont = derivedFont.deriveFont(Font.ITALIC); 464 } else if (outFont.mIsItalic != srcFont.mIsItalic) { 465 // The desired font is plain, but the src font is italics. We can't convert it back. So 466 // we update the value to reflect the true style of the font we're deriving. 467 outFont.mIsItalic = srcFont.mIsItalic; 468 } 469 outFont.mFont = derivedFont; 470 outFont.mWeight = srcWeight; 471 // No need to update mIsItalics, as it's already been handled above. 472 return outFont; 473 } 474} 475