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 217 // ---- native methods ---- 218 219 @LayoutlibDelegate 220 /*package*/ static long nCreateFamily(String lang, int variant) { 221 // TODO: support lang. This is required for japanese locale. 222 FontFamily_Delegate delegate = new FontFamily_Delegate(); 223 // variant can be 0, 1 or 2. 224 assert variant < 3; 225 delegate.mVariant = FontVariant.values()[variant]; 226 if (sFontLocation != null) { 227 delegate.init(); 228 } else { 229 sPostInitDelegate.add(delegate); 230 } 231 return sManager.addNewDelegate(delegate); 232 } 233 234 @LayoutlibDelegate 235 /*package*/ static void nUnrefFamily(long nativePtr) { 236 // Removing the java reference for the object doesn't mean that it's freed for garbage 237 // collection. Typeface_Delegate may still hold a reference for it. 238 sManager.removeJavaReferenceFor(nativePtr); 239 } 240 241 @LayoutlibDelegate 242 /*package*/ static boolean nAddFont(long nativeFamily, final String path) { 243 final FontFamily_Delegate delegate = getDelegate(nativeFamily); 244 if (delegate != null) { 245 if (sFontLocation == null) { 246 delegate.mPostInitRunnables.add(new Runnable() { 247 @Override 248 public void run() { 249 delegate.addFont(path); 250 } 251 }); 252 return true; 253 } 254 return delegate.addFont(path); 255 } 256 return false; 257 } 258 259 @LayoutlibDelegate 260 /*package*/ static boolean nAddFontWeightStyle(long nativeFamily, final String path, 261 final int weight, final boolean isItalic) { 262 final FontFamily_Delegate delegate = getDelegate(nativeFamily); 263 if (delegate != null) { 264 if (sFontLocation == null) { 265 delegate.mPostInitRunnables.add(new Runnable() { 266 @Override 267 public void run() { 268 delegate.addFont(path, weight, isItalic); 269 } 270 }); 271 return true; 272 } 273 return delegate.addFont(path, weight, isItalic); 274 } 275 return false; 276 } 277 278 @LayoutlibDelegate 279 /*package*/ static boolean nAddFontFromAsset(long nativeFamily, AssetManager mgr, String path) { 280 Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED, 281 "FontFamily.addFontFromAsset is not supported.", null, null); 282 return false; 283 } 284 285 286 // ---- private helper methods ---- 287 288 private void init() { 289 for (Runnable postInitRunnable : mPostInitRunnables) { 290 postInitRunnable.run(); 291 } 292 mPostInitRunnables = null; 293 } 294 295 private boolean addFont(@NonNull String path) { 296 return addFont(path, DEFAULT_FONT_WEIGHT, path.endsWith(FONT_SUFFIX_ITALIC)); 297 } 298 299 private boolean addFont(@NonNull String path, int weight, boolean isItalic) { 300 if (path.startsWith(SYSTEM_FONTS) && 301 !SDK_FONTS.contains(path.substring(SYSTEM_FONTS.length()))) { 302 return mValid = false; 303 } 304 // Set valid to true, even if the font fails to load. 305 mValid = true; 306 Font font = loadFont(path); 307 if (font == null) { 308 return false; 309 } 310 FontInfo fontInfo = new FontInfo(); 311 fontInfo.mFont = font; 312 fontInfo.mWeight = weight; 313 fontInfo.mIsItalic = isItalic; 314 addFont(fontInfo); 315 return true; 316 } 317 318 private boolean addFont(@NonNull FontInfo fontInfo) { 319 int weight = fontInfo.mWeight; 320 boolean isItalic = fontInfo.mIsItalic; 321 // The list is usually just two fonts big. So iterating over all isn't as bad as it looks. 322 // It's biggest for roboto where the size is 12. 323 for (FontInfo font : mFonts) { 324 if (font.mWeight == weight && font.mIsItalic == isItalic) { 325 return false; 326 } 327 } 328 mFonts.add(fontInfo); 329 return true; 330 } 331 332 /** 333 * Compute matching metric between two styles - 0 is an exact match. 334 */ 335 private static int computeMatch(@NonNull FontInfo font1, @NonNull FontInfo font2) { 336 int score = Math.abs(font1.mWeight - font2.mWeight); 337 if (font1.mIsItalic != font2.mIsItalic) { 338 score += 200; 339 } 340 return score; 341 } 342 343 /** 344 * Try to derive a font from {@code srcFont} for the style in {@code outFont}. 345 * <p/> 346 * {@code outFont} is updated to reflect the style of the derived font. 347 * @param srcFont the source font 348 * @param outFont contains the desired font style. Updated to contain the derived font and 349 * its style 350 * @return outFont 351 */ 352 @NonNull 353 private FontInfo deriveFont(@NonNull FontInfo srcFont, @NonNull FontInfo outFont) { 354 int desiredWeight = outFont.mWeight; 355 int srcWeight = srcFont.mWeight; 356 Font derivedFont = srcFont.mFont; 357 // Embolden the font if required. 358 if (desiredWeight >= BOLD_FONT_WEIGHT && desiredWeight - srcWeight > BOLD_FONT_WEIGHT_DELTA / 2) { 359 derivedFont = derivedFont.deriveFont(Font.BOLD); 360 srcWeight += BOLD_FONT_WEIGHT_DELTA; 361 } 362 // Italicize the font if required. 363 if (outFont.mIsItalic && !srcFont.mIsItalic) { 364 derivedFont = derivedFont.deriveFont(Font.ITALIC); 365 } else if (outFont.mIsItalic != srcFont.mIsItalic) { 366 // The desired font is plain, but the src font is italics. We can't convert it back. So 367 // we update the value to reflect the true style of the font we're deriving. 368 outFont.mIsItalic = srcFont.mIsItalic; 369 } 370 outFont.mFont = derivedFont; 371 outFont.mWeight = srcWeight; 372 // No need to update mIsItalics, as it's already been handled above. 373 return outFont; 374 } 375 } 376