FontFamily_Delegate.java revision a87b07d7fafd59ae26073a80cd742b17ea427ecd
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, 292 final String path, final int index, final List<FontListParser.Axis> axes, 293 final int weight, final boolean isItalic) { 294 // 'index' and 'axes' are not supported by java.awt.Font 295 final FontFamily_Delegate delegate = getDelegate(nativeFamily); 296 if (delegate != null) { 297 if (sFontLocation == null) { 298 delegate.mPostInitRunnables.add(new Runnable() { 299 @Override 300 public void run() { 301 delegate.addFont(path, weight, isItalic); 302 } 303 }); 304 return true; 305 } 306 return delegate.addFont(path, weight, isItalic); 307 } 308 return false; 309 } 310 311 @LayoutlibDelegate 312 /*package*/ static boolean nAddFontFromAsset(long nativeFamily, AssetManager mgr, String path) { 313 FontFamily_Delegate ffd = sManager.getDelegate(nativeFamily); 314 ffd.mValid = true; 315 if (mgr == null) { 316 return false; 317 } 318 if (mgr instanceof BridgeAssetManager) { 319 InputStream fontStream = null; 320 try { 321 AssetRepository assetRepository = ((BridgeAssetManager) mgr).getAssetRepository(); 322 if (assetRepository == null) { 323 Bridge.getLog().error(LayoutLog.TAG_MISSING_ASSET, "Asset not found: " + path, 324 null); 325 return false; 326 } 327 if (!assetRepository.isSupported()) { 328 // Don't log any warnings on unsupported IDEs. 329 return false; 330 } 331 // Check cache 332 FontInfo fontInfo = sCache.get(path); 333 if (fontInfo != null) { 334 // renew the font's lease. 335 sCache.put(path, fontInfo); 336 ffd.addFont(fontInfo); 337 return true; 338 } 339 fontStream = assetRepository.openAsset(path, AssetManager.ACCESS_STREAMING); 340 if (fontStream == null) { 341 Bridge.getLog().error(LayoutLog.TAG_MISSING_ASSET, "Asset not found: " + path, 342 path); 343 return false; 344 } 345 Font font = Font.createFont(Font.TRUETYPE_FONT, fontStream); 346 fontInfo = new FontInfo(); 347 fontInfo.mFont = font; 348 fontInfo.mWeight = font.isBold() ? BOLD_FONT_WEIGHT : DEFAULT_FONT_WEIGHT; 349 fontInfo.mIsItalic = font.isItalic(); 350 ffd.addFont(fontInfo); 351 return true; 352 } catch (IOException e) { 353 Bridge.getLog().error(LayoutLog.TAG_MISSING_ASSET, "Unable to load font " + path, e, 354 path); 355 } catch (FontFormatException e) { 356 if (path.endsWith(EXTENSION_OTF)) { 357 // otf fonts are not supported on the user's config (JRE version + OS) 358 Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED, 359 "OpenType fonts are not supported yet: " + path, null, path); 360 } else { 361 Bridge.getLog().error(LayoutLog.TAG_BROKEN, 362 "Unable to load font " + path, e, path); 363 } 364 } finally { 365 if (fontStream != null) { 366 try { 367 fontStream.close(); 368 } catch (IOException ignored) { 369 } 370 } 371 } 372 return false; 373 } 374 // This should never happen. AssetManager is a final class (from user's perspective), and 375 // we've replaced every creation of AssetManager with our implementation. We create an 376 // exception and log it, but continue with rest of the rendering, without loading this font. 377 Bridge.getLog().error(LayoutLog.TAG_BROKEN, 378 "You have found a bug in the rendering library. Please file a bug at b.android.com.", 379 new RuntimeException("Asset Manager is not an instance of BridgeAssetManager"), 380 null); 381 return false; 382 } 383 384 385 // ---- private helper methods ---- 386 387 private void init() { 388 for (Runnable postInitRunnable : mPostInitRunnables) { 389 postInitRunnable.run(); 390 } 391 mPostInitRunnables = null; 392 } 393 394 private boolean addFont(@NonNull String path) { 395 return addFont(path, DEFAULT_FONT_WEIGHT, path.endsWith(FONT_SUFFIX_ITALIC)); 396 } 397 398 private boolean addFont(@NonNull String path, int weight, boolean isItalic) { 399 if (path.startsWith(SYSTEM_FONTS) && 400 !SDK_FONTS.contains(path.substring(SYSTEM_FONTS.length()))) { 401 return mValid = false; 402 } 403 // Set valid to true, even if the font fails to load. 404 mValid = true; 405 Font font = loadFont(path); 406 if (font == null) { 407 return false; 408 } 409 FontInfo fontInfo = new FontInfo(); 410 fontInfo.mFont = font; 411 fontInfo.mWeight = weight; 412 fontInfo.mIsItalic = isItalic; 413 addFont(fontInfo); 414 return true; 415 } 416 417 private boolean addFont(@NonNull FontInfo fontInfo) { 418 int weight = fontInfo.mWeight; 419 boolean isItalic = fontInfo.mIsItalic; 420 // The list is usually just two fonts big. So iterating over all isn't as bad as it looks. 421 // It's biggest for roboto where the size is 12. 422 //noinspection ForLoopReplaceableByForEach (avoid iterator instantiation) 423 for (int i = 0, n = mFonts.size(); i < n; i++) { 424 FontInfo font = mFonts.get(i); 425 if (font.mWeight == weight && font.mIsItalic == isItalic) { 426 return false; 427 } 428 } 429 mFonts.add(fontInfo); 430 return true; 431 } 432 433 /** 434 * Compute matching metric between two styles - 0 is an exact match. 435 */ 436 private static int computeMatch(@NonNull FontInfo font1, @NonNull FontInfo font2) { 437 int score = Math.abs(font1.mWeight - font2.mWeight); 438 if (font1.mIsItalic != font2.mIsItalic) { 439 score += 200; 440 } 441 return score; 442 } 443 444 /** 445 * Try to derive a font from {@code srcFont} for the style in {@code outFont}. 446 * <p/> 447 * {@code outFont} is updated to reflect the style of the derived font. 448 * @param srcFont the source font 449 * @param outFont contains the desired font style. Updated to contain the derived font and 450 * its style 451 * @return outFont 452 */ 453 @NonNull 454 private FontInfo deriveFont(@NonNull FontInfo srcFont, @NonNull FontInfo outFont) { 455 int desiredWeight = outFont.mWeight; 456 int srcWeight = srcFont.mWeight; 457 Font derivedFont = srcFont.mFont; 458 // Embolden the font if required. 459 if (desiredWeight >= BOLD_FONT_WEIGHT && desiredWeight - srcWeight > BOLD_FONT_WEIGHT_DELTA / 2) { 460 derivedFont = derivedFont.deriveFont(Font.BOLD); 461 srcWeight += BOLD_FONT_WEIGHT_DELTA; 462 } 463 // Italicize the font if required. 464 if (outFont.mIsItalic && !srcFont.mIsItalic) { 465 derivedFont = derivedFont.deriveFont(Font.ITALIC); 466 } else if (outFont.mIsItalic != srcFont.mIsItalic) { 467 // The desired font is plain, but the src font is italics. We can't convert it back. So 468 // we update the value to reflect the true style of the font we're deriving. 469 outFont.mIsItalic = srcFont.mIsItalic; 470 } 471 outFont.mFont = derivedFont; 472 outFont.mWeight = srcWeight; 473 // No need to update mIsItalics, as it's already been handled above. 474 return outFont; 475 } 476} 477