1 /* 2 * Copyright (C) 2008 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 package com.android.layoutlib.bridge.impl; 17 18 import com.android.ide.common.rendering.api.AndroidConstants; 19 import com.android.ide.common.rendering.api.AssetRepository; 20 import com.android.ide.common.rendering.api.DensityBasedResourceValue; 21 import com.android.ide.common.rendering.api.ILayoutLog; 22 import com.android.ide.common.rendering.api.ILayoutPullParser; 23 import com.android.ide.common.rendering.api.LayoutlibCallback; 24 import com.android.ide.common.rendering.api.RenderResources; 25 import com.android.ide.common.rendering.api.ResourceNamespace; 26 import com.android.ide.common.rendering.api.ResourceReference; 27 import com.android.ide.common.rendering.api.ResourceValue; 28 import com.android.internal.util.XmlUtils; 29 import com.android.layoutlib.bridge.Bridge; 30 import com.android.layoutlib.bridge.android.BridgeContext; 31 import com.android.layoutlib.bridge.android.BridgeContext.Key; 32 import com.android.layoutlib.bridge.android.BridgeXmlBlockParser; 33 import com.android.ninepatch.GraphicsUtilities; 34 import com.android.ninepatch.NinePatch; 35 import com.android.resources.Density; 36 import com.android.resources.ResourceType; 37 38 import org.ccil.cowan.tagsoup.HTMLSchema; 39 import org.ccil.cowan.tagsoup.Parser; 40 import org.xml.sax.Attributes; 41 import org.xml.sax.InputSource; 42 import org.xml.sax.SAXException; 43 import org.xml.sax.helpers.DefaultHandler; 44 import org.xmlpull.v1.XmlPullParser; 45 import org.xmlpull.v1.XmlPullParserException; 46 47 import android.annotation.NonNull; 48 import android.annotation.Nullable; 49 import android.content.res.BridgeAssetManager; 50 import android.content.res.ColorStateList; 51 import android.content.res.ComplexColor; 52 import android.content.res.ComplexColor_Accessor; 53 import android.content.res.GradientColor; 54 import android.content.res.Resources; 55 import android.content.res.Resources.Theme; 56 import android.content.res.StringBlock; 57 import android.content.res.StringBlock.Height; 58 import android.graphics.Bitmap; 59 import android.graphics.Bitmap.Config; 60 import android.graphics.BitmapFactory; 61 import android.graphics.BitmapFactory.Options; 62 import android.graphics.Rect; 63 import android.graphics.Typeface; 64 import android.graphics.Typeface_Accessor; 65 import android.graphics.Typeface_Delegate; 66 import android.graphics.drawable.BitmapDrawable; 67 import android.graphics.drawable.ColorDrawable; 68 import android.graphics.drawable.Drawable; 69 import android.graphics.drawable.NinePatchDrawable; 70 import android.text.Annotation; 71 import android.text.Spannable; 72 import android.text.SpannableString; 73 import android.text.Spanned; 74 import android.text.SpannedString; 75 import android.text.TextUtils; 76 import android.text.style.AbsoluteSizeSpan; 77 import android.text.style.BulletSpan; 78 import android.text.style.RelativeSizeSpan; 79 import android.text.style.StrikethroughSpan; 80 import android.text.style.StyleSpan; 81 import android.text.style.SubscriptSpan; 82 import android.text.style.SuperscriptSpan; 83 import android.text.style.TypefaceSpan; 84 import android.text.style.URLSpan; 85 import android.text.style.UnderlineSpan; 86 import android.util.TypedValue; 87 88 import java.awt.image.BufferedImage; 89 import java.io.FileNotFoundException; 90 import java.io.IOException; 91 import java.io.InputStream; 92 import java.io.StringReader; 93 import java.util.ArrayDeque; 94 import java.util.ArrayList; 95 import java.util.Deque; 96 import java.util.HashMap; 97 import java.util.HashSet; 98 import java.util.List; 99 import java.util.Map; 100 import java.util.Set; 101 import java.util.regex.Matcher; 102 import java.util.regex.Pattern; 103 104 import com.google.common.base.Strings; 105 106 import static android.content.res.AssetManager.ACCESS_STREAMING; 107 108 /** 109 * Helper class to provide various conversion method used in handling android resources. 110 */ 111 public final class ResourceHelper { 112 private static final Key<Set<ResourceValue>> KEY_GET_DRAWABLE = 113 Key.create("ResourceHelper.getDrawable"); 114 private static final Pattern sFloatPattern = Pattern.compile("(-?[0-9]*(?:\\.[0-9]*)?)(.*)"); 115 private static final float[] sFloatOut = new float[1]; 116 117 private static final TypedValue mValue = new TypedValue(); 118 119 /** 120 * Returns the color value represented by the given string value. 121 * 122 * @param value the color value 123 * @return the color as an int 124 * @throws NumberFormatException if the conversion failed. 125 */ getColor(@ullable String value)126 public static int getColor(@Nullable String value) { 127 if (value == null) { 128 throw new NumberFormatException("null value"); 129 } 130 131 value = value.trim(); 132 int len = value.length(); 133 134 // make sure it's not longer than 32bit or smaller than the RGB format 135 if (len < 2 || len > 9) { 136 throw new NumberFormatException(String.format( 137 "Color value '%s' has wrong size. Format is either" + 138 "#AARRGGBB, #RRGGBB, #RGB, or #ARGB", 139 value)); 140 } 141 142 if (value.charAt(0) != '#') { 143 if (value.startsWith(AndroidConstants.PREFIX_THEME_REF)) { 144 throw new NumberFormatException(String.format( 145 "Attribute '%s' not found. Are you using the right theme?", value)); 146 } 147 throw new NumberFormatException( 148 String.format("Color value '%s' must start with #", value)); 149 } 150 151 value = value.substring(1); 152 153 if (len == 4) { // RGB format 154 char[] color = new char[8]; 155 color[0] = color[1] = 'F'; 156 color[2] = color[3] = value.charAt(0); 157 color[4] = color[5] = value.charAt(1); 158 color[6] = color[7] = value.charAt(2); 159 value = new String(color); 160 } else if (len == 5) { // ARGB format 161 char[] color = new char[8]; 162 color[0] = color[1] = value.charAt(0); 163 color[2] = color[3] = value.charAt(1); 164 color[4] = color[5] = value.charAt(2); 165 color[6] = color[7] = value.charAt(3); 166 value = new String(color); 167 } else if (len == 7) { 168 value = "FF" + value; 169 } 170 171 // this is a RRGGBB or AARRGGBB value 172 173 // Integer.parseInt will fail to parse strings like "ff191919", so we use 174 // a Long, but cast the result back into an int, since we know that we're only 175 // dealing with 32 bit values. 176 return (int)Long.parseLong(value, 16); 177 } 178 179 /** 180 * Returns a {@link ComplexColor} from the given {@link ResourceValue} 181 * 182 * @param resValue the value containing a color value or a file path to a complex color 183 * definition 184 * @param context the current context 185 * @param theme the theme to use when resolving the complex color 186 * @param allowGradients when false, only {@link ColorStateList} will be returned. If a {@link 187 * GradientColor} is found, null will be returned. 188 */ 189 @Nullable getInternalComplexColor(@onNull ResourceValue resValue, @NonNull BridgeContext context, @Nullable Theme theme, boolean allowGradients)190 private static ComplexColor getInternalComplexColor(@NonNull ResourceValue resValue, 191 @NonNull BridgeContext context, @Nullable Theme theme, boolean allowGradients) { 192 String value = resValue.getValue(); 193 if (value == null || RenderResources.REFERENCE_NULL.equals(value)) { 194 return null; 195 } 196 197 // try to load the color state list from an int 198 if (value.trim().startsWith("#")) { 199 try { 200 int color = getColor(value); 201 return ColorStateList.valueOf(color); 202 } catch (NumberFormatException e) { 203 Bridge.getLog().warning(ILayoutLog.TAG_RESOURCES_FORMAT, 204 String.format("\"%1$s\" cannot be interpreted as a color.", value), 205 null, null); 206 return null; 207 } 208 } 209 210 try { 211 BridgeXmlBlockParser blockParser = getXmlBlockParser(context, resValue); 212 if (blockParser != null) { 213 try { 214 // Advance the parser to the first element so we can detect if it's a 215 // color list or a gradient color 216 int type; 217 //noinspection StatementWithEmptyBody 218 while ((type = blockParser.next()) != XmlPullParser.START_TAG 219 && type != XmlPullParser.END_DOCUMENT) { 220 // Seek parser to start tag. 221 } 222 223 if (type != XmlPullParser.START_TAG) { 224 assert false : "No start tag found"; 225 return null; 226 } 227 228 final String name = blockParser.getName(); 229 if (allowGradients && "gradient".equals(name)) { 230 return ComplexColor_Accessor.createGradientColorFromXmlInner( 231 context.getResources(), 232 blockParser, blockParser, 233 theme); 234 } else if ("selector".equals(name)) { 235 return ComplexColor_Accessor.createColorStateListFromXmlInner( 236 context.getResources(), 237 blockParser, blockParser, 238 theme); 239 } 240 } finally { 241 blockParser.ensurePopped(); 242 } 243 } 244 } catch (XmlPullParserException e) { 245 Bridge.getLog().error(ILayoutLog.TAG_BROKEN, 246 "Failed to configure parser for " + value, e, null,null /*data*/); 247 // we'll return null below. 248 } catch (Exception e) { 249 // this is an error and not warning since the file existence is 250 // checked before attempting to parse it. 251 Bridge.getLog().error(ILayoutLog.TAG_RESOURCES_READ, 252 "Failed to parse file " + value, e, null, null /*data*/); 253 254 return null; 255 } 256 257 return null; 258 } 259 260 /** 261 * Returns a {@link ColorStateList} from the given {@link ResourceValue} 262 * 263 * @param resValue the value containing a color value or a file path to a complex color 264 * definition 265 * @param context the current context 266 */ 267 @Nullable getColorStateList(@onNull ResourceValue resValue, @NonNull BridgeContext context, @Nullable Resources.Theme theme)268 public static ColorStateList getColorStateList(@NonNull ResourceValue resValue, 269 @NonNull BridgeContext context, @Nullable Resources.Theme theme) { 270 return (ColorStateList) getInternalComplexColor(resValue, context, 271 theme != null ? theme : context.getTheme(), 272 false); 273 } 274 275 /** 276 * Returns a {@link ComplexColor} from the given {@link ResourceValue} 277 * 278 * @param resValue the value containing a color value or a file path to a complex color 279 * definition 280 * @param context the current context 281 */ 282 @Nullable getComplexColor(@onNull ResourceValue resValue, @NonNull BridgeContext context, @Nullable Resources.Theme theme)283 public static ComplexColor getComplexColor(@NonNull ResourceValue resValue, 284 @NonNull BridgeContext context, @Nullable Resources.Theme theme) { 285 return getInternalComplexColor(resValue, context, 286 theme != null ? theme : context.getTheme(), 287 true); 288 } 289 290 /** 291 * Returns a drawable from the given value. 292 * 293 * @param value The value that contains a path to a 9 patch, a bitmap or a xml based drawable, 294 * or an hexadecimal color 295 * @param context the current context 296 */ 297 @Nullable getDrawable(ResourceValue value, BridgeContext context)298 public static Drawable getDrawable(ResourceValue value, BridgeContext context) { 299 return getDrawable(value, context, null); 300 } 301 302 /** 303 * Returns a {@link BridgeXmlBlockParser} to parse the given {@link ResourceValue}. The passed 304 * value must point to an XML resource. 305 */ 306 @Nullable getXmlBlockParser(@onNull BridgeContext context, @NonNull ResourceValue value)307 public static BridgeXmlBlockParser getXmlBlockParser(@NonNull BridgeContext context, 308 @NonNull ResourceValue value) throws XmlPullParserException { 309 String stringValue = value.getValue(); 310 if (RenderResources.REFERENCE_NULL.equals(stringValue)) { 311 return null; 312 } 313 314 XmlPullParser parser = null; 315 ResourceNamespace namespace; 316 317 LayoutlibCallback layoutlibCallback = context.getLayoutlibCallback(); 318 // Framework values never need a PSI parser. They do not change and the do not contain 319 // aapt:attr attributes. 320 if (!value.isFramework()) { 321 parser = layoutlibCallback.getParser(value); 322 } 323 324 if (parser != null) { 325 namespace = ((ILayoutPullParser) parser).getLayoutNamespace(); 326 } else { 327 parser = ParserFactory.create(stringValue); 328 namespace = value.getNamespace(); 329 } 330 331 return parser == null 332 ? null 333 : new BridgeXmlBlockParser(parser, context, namespace); 334 } 335 336 /** 337 * Returns a drawable from the given value. 338 * 339 * @param value The value that contains a path to a 9 patch, a bitmap or a xml based drawable, 340 * or an hexadecimal color 341 * @param context the current context 342 * @param theme the theme to be used to inflate the drawable. 343 */ 344 @Nullable getDrawable(ResourceValue value, BridgeContext context, Theme theme)345 public static Drawable getDrawable(ResourceValue value, BridgeContext context, Theme theme) { 346 if (value == null) { 347 return null; 348 } 349 String stringValue = value.getValue(); 350 if (RenderResources.REFERENCE_NULL.equals(stringValue)) { 351 return null; 352 } 353 354 // try the simple case first. Attempt to get a color from the value 355 if (stringValue.trim().startsWith("#")) { 356 try { 357 int color = getColor(stringValue); 358 return new ColorDrawable(color); 359 } catch (NumberFormatException e) { 360 Bridge.getLog().warning(ILayoutLog.TAG_RESOURCES_FORMAT, 361 String.format("\"%1$s\" cannot be interpreted as a color.", stringValue), 362 null, null); 363 return null; 364 } 365 } 366 367 Density density = Density.MEDIUM; 368 if (value instanceof DensityBasedResourceValue) { 369 density = ((DensityBasedResourceValue) value).getResourceDensity(); 370 if (density == Density.NODPI || density == Density.ANYDPI) { 371 density = Density.create(context.getConfiguration().densityDpi); 372 } 373 } 374 375 String lowerCaseValue = stringValue.toLowerCase(); 376 if (lowerCaseValue.endsWith(".xml") || value.getResourceType() == ResourceType.AAPT) { 377 // create a block parser for the file 378 try { 379 BridgeXmlBlockParser blockParser = getXmlBlockParser(context, value); 380 if (blockParser != null) { 381 Set<ResourceValue> visitedValues = context.getUserData(KEY_GET_DRAWABLE); 382 if (visitedValues == null) { 383 visitedValues = new HashSet<>(); 384 context.putUserData(KEY_GET_DRAWABLE, visitedValues); 385 } 386 if (!visitedValues.add(value)) { 387 Bridge.getLog().error(null, "Cyclic dependency in " + stringValue, null, 388 null); 389 return null; 390 } 391 392 try { 393 return Drawable.createFromXml(context.getResources(), blockParser, theme); 394 } finally { 395 visitedValues.remove(value); 396 blockParser.ensurePopped(); 397 } 398 } 399 } catch (Exception e) { 400 // this is an error and not warning since the file existence is checked before 401 // attempting to parse it. 402 Bridge.getLog().error(null, "Failed to parse file " + stringValue, e, 403 null, null /*data*/); 404 } 405 406 return null; 407 } else { 408 AssetRepository repository = getAssetRepository(context); 409 if (repository.isFileResource(stringValue)) { 410 try { 411 Bitmap bitmap = Bridge.getCachedBitmap(stringValue, 412 value.isFramework() ? null : context.getProjectKey()); 413 414 if (bitmap == null) { 415 InputStream stream; 416 try { 417 stream = repository.openNonAsset(0, stringValue, ACCESS_STREAMING); 418 419 } catch (FileNotFoundException e) { 420 stream = null; 421 } 422 Options options = new Options(); 423 options.inDensity = density.getDpiValue(); 424 Rect padding = new Rect(); 425 bitmap = BitmapFactory.decodeStream(stream, padding, options); 426 if (bitmap != null && bitmap.getNinePatchChunk() == null && 427 lowerCaseValue.endsWith(NinePatch.EXTENSION_9PATCH)) { 428 //We are dealing with a non-compiled nine patch. 429 stream = repository.openNonAsset(0, stringValue, ACCESS_STREAMING); 430 NinePatch ninePatch = NinePatch.load(stream, true /*is9Patch*/, false /* convert */); 431 BufferedImage image = ninePatch.getImage(); 432 433 // width and height of the nine patch without the special border. 434 int width = image.getWidth(); 435 int height = image.getHeight(); 436 437 // Get pixel data from image independently of its type. 438 int[] imageData = GraphicsUtilities.getPixels(image, 0, 0, width, 439 height, null); 440 441 bitmap = Bitmap.createBitmap(imageData, width, height, Config.ARGB_8888); 442 443 bitmap.setDensity(options.inDensity); 444 bitmap.setNinePatchChunk(ninePatch.getChunk().getSerializedChunk()); 445 int[] padArray = ninePatch.getChunk().getPadding(); 446 padding.set(padArray[0], padArray[1], padArray[2], padArray[3]); 447 } 448 Bridge.setCachedBitmapPadding(stringValue, padding, 449 value.isFramework() ? null : context.getProjectKey()); 450 Bridge.setCachedBitmap(stringValue, bitmap, 451 value.isFramework() ? null : context.getProjectKey()); 452 } 453 454 if (bitmap != null && bitmap.getNinePatchChunk() != null) { 455 Rect padding = Bridge.getCachedBitmapPadding(stringValue, 456 value.isFramework() ? null : context.getProjectKey()); 457 return new NinePatchDrawable(context.getResources(), bitmap, bitmap 458 .getNinePatchChunk(), padding, lowerCaseValue); 459 } else { 460 return new BitmapDrawable(context.getResources(), bitmap); 461 } 462 } catch (IOException e) { 463 // we'll return null below 464 Bridge.getLog().error(ILayoutLog.TAG_RESOURCES_READ, 465 "Failed to load " + stringValue, e, null, null /*data*/); 466 } 467 } 468 } 469 470 return null; 471 } 472 getAssetRepository(@onNull BridgeContext context)473 private static AssetRepository getAssetRepository(@NonNull BridgeContext context) { 474 BridgeAssetManager assetManager = context.getAssets(); 475 return assetManager.getAssetRepository(); 476 } 477 478 /** 479 * Returns a {@link Typeface} given a font name. The font name, can be a system font family 480 * (like sans-serif) or a full path if the font is to be loaded from resources. 481 */ getFont(String fontName, BridgeContext context, Theme theme, boolean isFramework)482 public static Typeface getFont(String fontName, BridgeContext context, Theme theme, boolean 483 isFramework) { 484 if (fontName == null || fontName.isBlank()) { 485 return null; 486 } 487 488 if (Typeface_Accessor.isSystemFont(fontName)) { 489 // Shortcut for the case where we are asking for a system font name. Those are not 490 // loaded using external resources. 491 return null; 492 } 493 494 495 return Typeface_Delegate.createFromDisk(context, fontName, isFramework); 496 } 497 498 /** 499 * Returns a {@link Typeface} given a font name. The font name, can be a system font family 500 * (like sans-serif) or a full path if the font is to be loaded from resources. 501 */ getFont(ResourceValue value, BridgeContext context, Theme theme)502 public static Typeface getFont(ResourceValue value, BridgeContext context, Theme theme) { 503 if (value == null) { 504 return null; 505 } 506 507 return getFont(value.getValue(), context, theme, value.isFramework()); 508 } 509 510 /** 511 * Looks for an attribute in the current theme. 512 * 513 * @param resources the render resources 514 * @param attr the attribute reference 515 * @param defaultValue the default value. 516 * @return the value of the attribute or the default one if not found. 517 */ getBooleanThemeValue(@onNull RenderResources resources, @NonNull ResourceReference attr, boolean defaultValue)518 public static boolean getBooleanThemeValue(@NonNull RenderResources resources, 519 @NonNull ResourceReference attr, boolean defaultValue) { 520 ResourceValue value = resources.findItemInTheme(attr); 521 value = resources.resolveResValue(value); 522 if (value == null) { 523 return defaultValue; 524 } 525 return XmlUtils.convertValueToBoolean(value.getValue(), defaultValue); 526 } 527 528 /** 529 * Looks for a framework attribute in the current theme. 530 * 531 * @param resources the render resources 532 * @param name the name of the attribute 533 * @param defaultValue the default value. 534 * @return the value of the attribute or the default one if not found. 535 */ getBooleanThemeFrameworkAttrValue(@onNull RenderResources resources, @NonNull String name, boolean defaultValue)536 public static boolean getBooleanThemeFrameworkAttrValue(@NonNull RenderResources resources, 537 @NonNull String name, boolean defaultValue) { 538 ResourceReference attrRef = BridgeContext.createFrameworkAttrReference(name); 539 return getBooleanThemeValue(resources, attrRef, defaultValue); 540 } 541 542 /** 543 * This takes a resource string containing HTML tags for styling, 544 * and returns it correctly formatted to be displayed. 545 */ parseHtml(String string)546 public static CharSequence parseHtml(String string) { 547 // The parser requires <li> tags to be surrounded by <ul> tags to handle whitespace 548 // correctly, though Android does not support <ul> tags. 549 String str = string.replaceAll("<li>", "<ul><li>") 550 .replaceAll("</li>","</li></ul>"); 551 int firstTagIndex = str.indexOf('<'); 552 if (firstTagIndex == -1) { 553 return string; 554 } 555 int lastTagIndex = str.lastIndexOf('>'); 556 StringBuilder stringBuilder = new StringBuilder(str.substring(0, firstTagIndex)); 557 List<Tag> tagList = new ArrayList<>(); 558 Map<String, Deque<Tag>> startStacks = new HashMap<>(); 559 Parser parser = new Parser(); 560 parser.setContentHandler(new DefaultHandler() { 561 @Override 562 public void startElement(String uri, String localName, String qName, 563 Attributes attributes) { 564 if (!Strings.isNullOrEmpty(localName)) { 565 Tag tag = new Tag(localName); 566 tag.mStart = stringBuilder.length(); 567 tag.mAttributes = attributes; 568 startStacks.computeIfAbsent(localName, key -> new ArrayDeque<>()).addFirst(tag); 569 } 570 } 571 572 @Override 573 public void endElement(String uri, String localName, String qName) { 574 if (!Strings.isNullOrEmpty(localName)) { 575 Tag tag = startStacks.get(localName).removeFirst(); 576 tag.mEnd = stringBuilder.length(); 577 tagList.add(tag); 578 } 579 } 580 581 @Override 582 public void characters(char[] ch, int start, int length) { 583 stringBuilder.append(ch, start, length); 584 } 585 }); 586 try { 587 parser.setProperty(Parser.schemaProperty, new HTMLSchema()); 588 parser.parse(new InputSource( 589 new StringReader(str.substring(firstTagIndex, lastTagIndex + 1)))); 590 } catch (SAXException | IOException e) { 591 Bridge.getLog().warning(ILayoutLog.TAG_RESOURCES_FORMAT, 592 "The string " + str + " is not valid HTML", null, null); 593 return str; 594 } 595 stringBuilder.append(str.substring(lastTagIndex + 1)); 596 return applyStyles(stringBuilder, tagList); 597 } 598 599 /** 600 * This applies the styles from tagList that are supported by Android 601 * and returns a {@link SpannedString}. 602 * This should mirror {@link StringBlock#applyStyles} 603 */ 604 @NonNull applyStyles(@onNull StringBuilder stringBuilder, @NonNull List<Tag> tagList)605 private static SpannedString applyStyles(@NonNull StringBuilder stringBuilder, 606 @NonNull List<Tag> tagList) { 607 SpannableString spannableString = new SpannableString(stringBuilder); 608 for (Tag tag : tagList) { 609 int start = tag.mStart; 610 int end = tag.mEnd; 611 Attributes attrs = tag.mAttributes; 612 switch (tag.mLabel) { 613 case "b": 614 spannableString.setSpan(new StyleSpan(Typeface.BOLD), start, end, 615 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 616 break; 617 case "i": 618 spannableString.setSpan(new StyleSpan(Typeface.ITALIC), start, end, 619 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 620 break; 621 case "u": 622 spannableString.setSpan(new UnderlineSpan(), start, end, 623 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 624 break; 625 case "tt": 626 spannableString.setSpan(new TypefaceSpan("monospace"), start, end, 627 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 628 break; 629 case "big": 630 spannableString.setSpan(new RelativeSizeSpan(1.25f), start, end, 631 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 632 break; 633 case "small": 634 spannableString.setSpan(new RelativeSizeSpan(0.8f), start, end, 635 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 636 break; 637 case "sup": 638 spannableString.setSpan(new SuperscriptSpan(), start, end, 639 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 640 break; 641 case "sub": 642 spannableString.setSpan(new SubscriptSpan(), start, end, 643 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 644 break; 645 case "strike": 646 spannableString.setSpan(new StrikethroughSpan(), start, end, 647 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 648 break; 649 case "li": 650 StringBlock.addParagraphSpan(spannableString, new BulletSpan(10), start, end); 651 break; 652 case "marquee": 653 spannableString.setSpan(TextUtils.TruncateAt.MARQUEE, start, end, 654 Spanned.SPAN_INCLUSIVE_INCLUSIVE); 655 break; 656 case "font": 657 String heightAttr = attrs.getValue("height"); 658 if (heightAttr != null) { 659 int height = Integer.parseInt(heightAttr); 660 StringBlock.addParagraphSpan(spannableString, new Height(height), start, 661 end); 662 } 663 664 String sizeAttr = attrs.getValue("size"); 665 if (sizeAttr != null) { 666 int size = Integer.parseInt(sizeAttr); 667 spannableString.setSpan(new AbsoluteSizeSpan(size, true), start, end, 668 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 669 } 670 671 String fgcolorAttr = attrs.getValue("fgcolor"); 672 if (fgcolorAttr != null) { 673 spannableString.setSpan(StringBlock.getColor(fgcolorAttr, true), start, end, 674 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 675 } 676 677 String colorAttr = attrs.getValue("color"); 678 if (colorAttr != null) { 679 spannableString.setSpan(StringBlock.getColor(colorAttr, true), start, end, 680 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 681 } 682 683 String bgcolorAttr = attrs.getValue("bgcolor"); 684 if (bgcolorAttr != null) { 685 spannableString.setSpan(StringBlock.getColor(bgcolorAttr, false), start, 686 end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 687 } 688 689 String faceAttr = attrs.getValue("face"); 690 if (faceAttr != null) { 691 spannableString.setSpan(new TypefaceSpan(faceAttr), start, end, 692 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 693 } 694 break; 695 case "a": 696 String href = tag.mAttributes.getValue("href"); 697 if (href != null) { 698 spannableString.setSpan(new URLSpan(href), start, end, 699 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 700 } 701 break; 702 case "annotation": 703 for (int i = 0; i < attrs.getLength(); i++) { 704 String key = attrs.getLocalName(i); 705 String value = attrs.getValue(i); 706 spannableString.setSpan(new Annotation(key, value), start, end, 707 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 708 } 709 } 710 } 711 return new SpannedString(spannableString); 712 } 713 714 // ------- TypedValue stuff 715 // This is taken from //device/libs/utils/ResourceTypes.cpp 716 717 private static final class UnitEntry { 718 String name; 719 int type; 720 int unit; 721 float scale; 722 UnitEntry(String name, int type, int unit, float scale)723 UnitEntry(String name, int type, int unit, float scale) { 724 this.name = name; 725 this.type = type; 726 this.unit = unit; 727 this.scale = scale; 728 } 729 } 730 731 private static final UnitEntry[] sUnitNames = new UnitEntry[] { 732 new UnitEntry("px", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_PX, 1.0f), 733 new UnitEntry("dip", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_DIP, 1.0f), 734 new UnitEntry("dp", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_DIP, 1.0f), 735 new UnitEntry("sp", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_SP, 1.0f), 736 new UnitEntry("pt", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_PT, 1.0f), 737 new UnitEntry("in", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_IN, 1.0f), 738 new UnitEntry("mm", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_MM, 1.0f), 739 new UnitEntry("%", TypedValue.TYPE_FRACTION, TypedValue.COMPLEX_UNIT_FRACTION, 1.0f/100), 740 new UnitEntry("%p", TypedValue.TYPE_FRACTION, TypedValue.COMPLEX_UNIT_FRACTION_PARENT, 1.0f/100), 741 }; 742 743 /** 744 * Returns the raw value from the given attribute float-type value string. 745 * This object is only valid until the next call on to {@link ResourceHelper}. 746 */ getValue(String attribute, String value, boolean requireUnit)747 public static TypedValue getValue(String attribute, String value, boolean requireUnit) { 748 if (parseFloatAttribute(attribute, value, mValue, requireUnit)) { 749 return mValue; 750 } 751 752 return null; 753 } 754 755 /** 756 * Parse a float attribute and return the parsed value into a given TypedValue. 757 * @param attribute the name of the attribute. Can be null if <var>requireUnit</var> is false. 758 * @param value the string value of the attribute 759 * @param outValue the TypedValue to receive the parsed value 760 * @param requireUnit whether the value is expected to contain a unit. 761 * @return true if success. 762 */ parseFloatAttribute(String attribute, @NonNull String value, TypedValue outValue, boolean requireUnit)763 public static boolean parseFloatAttribute(String attribute, @NonNull String value, 764 TypedValue outValue, boolean requireUnit) { 765 assert !requireUnit || attribute != null; 766 767 // remove the space before and after 768 value = value.trim(); 769 int len = value.length(); 770 771 if (len <= 0) { 772 return false; 773 } 774 775 // check that there's no non ascii characters. 776 char[] buf = value.toCharArray(); 777 for (int i = 0 ; i < len ; i++) { 778 if (buf[i] > 255) { 779 return false; 780 } 781 } 782 783 // check the first character 784 if ((buf[0] < '0' || buf[0] > '9') && buf[0] != '.' && buf[0] != '-' && buf[0] != '+') { 785 return false; 786 } 787 788 // now look for the string that is after the float... 789 Matcher m = sFloatPattern.matcher(value); 790 if (m.matches()) { 791 String f_str = m.group(1); 792 String end = m.group(2); 793 794 float f; 795 try { 796 f = Float.parseFloat(f_str); 797 } catch (NumberFormatException e) { 798 // this shouldn't happen with the regexp above. 799 return false; 800 } 801 802 if (end.length() > 0 && end.charAt(0) != ' ') { 803 // Might be a unit... 804 if (parseUnit(end, outValue, sFloatOut)) { 805 computeTypedValue(outValue, f, sFloatOut[0]); 806 return true; 807 } 808 return false; 809 } 810 811 // make sure it's only spaces at the end. 812 end = end.trim(); 813 814 if (end.length() == 0) { 815 if (outValue != null) { 816 if (!requireUnit) { 817 outValue.type = TypedValue.TYPE_FLOAT; 818 outValue.data = Float.floatToIntBits(f); 819 } else { 820 // no unit when required? Use dp and out an error. 821 applyUnit(sUnitNames[1], outValue, sFloatOut); 822 computeTypedValue(outValue, f, sFloatOut[0]); 823 824 Bridge.getLog().error(ILayoutLog.TAG_RESOURCES_RESOLVE, 825 String.format( 826 "Dimension \"%1$s\" in attribute \"%2$s\" is missing unit!", 827 value, attribute), 828 null, null); 829 } 830 return true; 831 } 832 } 833 } 834 835 return false; 836 } 837 computeTypedValue(TypedValue outValue, float value, float scale)838 private static void computeTypedValue(TypedValue outValue, float value, float scale) { 839 value *= scale; 840 boolean neg = value < 0; 841 if (neg) { 842 value = -value; 843 } 844 long bits = (long)(value*(1<<23)+.5f); 845 int radix; 846 int shift; 847 if ((bits&0x7fffff) == 0) { 848 // Always use 23p0 if there is no fraction, just to make 849 // things easier to read. 850 radix = TypedValue.COMPLEX_RADIX_23p0; 851 shift = 23; 852 } else if ((bits&0xffffffffff800000L) == 0) { 853 // Magnitude is zero -- can fit in 0 bits of precision. 854 radix = TypedValue.COMPLEX_RADIX_0p23; 855 shift = 0; 856 } else if ((bits&0xffffffff80000000L) == 0) { 857 // Magnitude can fit in 8 bits of precision. 858 radix = TypedValue.COMPLEX_RADIX_8p15; 859 shift = 8; 860 } else if ((bits&0xffffff8000000000L) == 0) { 861 // Magnitude can fit in 16 bits of precision. 862 radix = TypedValue.COMPLEX_RADIX_16p7; 863 shift = 16; 864 } else { 865 // Magnitude needs entire range, so no fractional part. 866 radix = TypedValue.COMPLEX_RADIX_23p0; 867 shift = 23; 868 } 869 int mantissa = (int)( 870 (bits>>shift) & TypedValue.COMPLEX_MANTISSA_MASK); 871 if (neg) { 872 mantissa = (-mantissa) & TypedValue.COMPLEX_MANTISSA_MASK; 873 } 874 outValue.data |= 875 (radix<<TypedValue.COMPLEX_RADIX_SHIFT) 876 | (mantissa<<TypedValue.COMPLEX_MANTISSA_SHIFT); 877 } 878 879 private static boolean parseUnit(String str, TypedValue outValue, float[] outScale) { 880 str = str.trim(); 881 882 for (UnitEntry unit : sUnitNames) { 883 if (unit.name.equals(str)) { 884 applyUnit(unit, outValue, outScale); 885 return true; 886 } 887 } 888 889 return false; 890 } 891 892 private static void applyUnit(UnitEntry unit, TypedValue outValue, float[] outScale) { 893 outValue.type = unit.type; 894 // COMPLEX_UNIT_SHIFT is 0 and hence intelliJ complains about it. Suppress the warning. 895 //noinspection PointlessBitwiseExpression 896 outValue.data = unit.unit << TypedValue.COMPLEX_UNIT_SHIFT; 897 outScale[0] = unit.scale; 898 } 899 900 private static class Tag { 901 private String mLabel; 902 private int mStart; 903 private int mEnd; 904 private Attributes mAttributes; 905 906 private Tag(String label) { 907 mLabel = label; 908 } 909 } 910 } 911 912