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.getEnum(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 bitmap = BitmapFactory.decodeStream(stream, null, options); 425 if (bitmap != null && bitmap.getNinePatchChunk() == null && 426 lowerCaseValue.endsWith(NinePatch.EXTENSION_9PATCH)) { 427 //We are dealing with a non-compiled nine patch. 428 stream = repository.openNonAsset(0, stringValue, ACCESS_STREAMING); 429 NinePatch ninePatch = NinePatch.load(stream, true /*is9Patch*/, false /* convert */); 430 BufferedImage image = ninePatch.getImage(); 431 432 // width and height of the nine patch without the special border. 433 int width = image.getWidth(); 434 int height = image.getHeight(); 435 436 // Get pixel data from image independently of its type. 437 int[] imageData = GraphicsUtilities.getPixels(image, 0, 0, width, 438 height, null); 439 440 bitmap = Bitmap.createBitmap(imageData, width, height, Config.ARGB_8888); 441 442 bitmap.setDensity(options.inDensity); 443 bitmap.setNinePatchChunk(ninePatch.getChunk().getSerializedChunk()); 444 } 445 Bridge.setCachedBitmap(stringValue, bitmap, 446 value.isFramework() ? null : context.getProjectKey()); 447 } 448 449 if (bitmap != null && bitmap.getNinePatchChunk() != null) { 450 return new NinePatchDrawable(context.getResources(), bitmap, bitmap 451 .getNinePatchChunk(), new Rect(), lowerCaseValue); 452 } else { 453 return new BitmapDrawable(context.getResources(), bitmap); 454 } 455 } catch (IOException e) { 456 // we'll return null below 457 Bridge.getLog().error(ILayoutLog.TAG_RESOURCES_READ, 458 "Failed to load " + stringValue, e, null, null /*data*/); 459 } 460 } 461 } 462 463 return null; 464 } 465 getAssetRepository(@onNull BridgeContext context)466 private static AssetRepository getAssetRepository(@NonNull BridgeContext context) { 467 BridgeAssetManager assetManager = context.getAssets(); 468 return assetManager.getAssetRepository(); 469 } 470 471 /** 472 * Returns a {@link Typeface} given a font name. The font name, can be a system font family 473 * (like sans-serif) or a full path if the font is to be loaded from resources. 474 */ getFont(String fontName, BridgeContext context, Theme theme, boolean isFramework)475 public static Typeface getFont(String fontName, BridgeContext context, Theme theme, boolean 476 isFramework) { 477 if (fontName == null) { 478 return null; 479 } 480 481 if (Typeface_Accessor.isSystemFont(fontName)) { 482 // Shortcut for the case where we are asking for a system font name. Those are not 483 // loaded using external resources. 484 return null; 485 } 486 487 488 return Typeface_Delegate.createFromDisk(context, fontName, isFramework); 489 } 490 491 /** 492 * Returns a {@link Typeface} given a font name. The font name, can be a system font family 493 * (like sans-serif) or a full path if the font is to be loaded from resources. 494 */ getFont(ResourceValue value, BridgeContext context, Theme theme)495 public static Typeface getFont(ResourceValue value, BridgeContext context, Theme theme) { 496 if (value == null) { 497 return null; 498 } 499 500 return getFont(value.getValue(), context, theme, value.isFramework()); 501 } 502 503 /** 504 * Looks for an attribute in the current theme. 505 * 506 * @param resources the render resources 507 * @param attr the attribute reference 508 * @param defaultValue the default value. 509 * @return the value of the attribute or the default one if not found. 510 */ getBooleanThemeValue(@onNull RenderResources resources, @NonNull ResourceReference attr, boolean defaultValue)511 public static boolean getBooleanThemeValue(@NonNull RenderResources resources, 512 @NonNull ResourceReference attr, boolean defaultValue) { 513 ResourceValue value = resources.findItemInTheme(attr); 514 value = resources.resolveResValue(value); 515 if (value == null) { 516 return defaultValue; 517 } 518 return XmlUtils.convertValueToBoolean(value.getValue(), defaultValue); 519 } 520 521 /** 522 * Looks for a framework attribute in the current theme. 523 * 524 * @param resources the render resources 525 * @param name the name of the attribute 526 * @param defaultValue the default value. 527 * @return the value of the attribute or the default one if not found. 528 */ getBooleanThemeFrameworkAttrValue(@onNull RenderResources resources, @NonNull String name, boolean defaultValue)529 public static boolean getBooleanThemeFrameworkAttrValue(@NonNull RenderResources resources, 530 @NonNull String name, boolean defaultValue) { 531 ResourceReference attrRef = BridgeContext.createFrameworkAttrReference(name); 532 return getBooleanThemeValue(resources, attrRef, defaultValue); 533 } 534 535 /** 536 * This takes a resource string containing HTML tags for styling, 537 * and returns it correctly formatted to be displayed. 538 */ parseHtml(String string)539 public static CharSequence parseHtml(String string) { 540 // The parser requires <li> tags to be surrounded by <ul> tags to handle whitespace 541 // correctly, though Android does not support <ul> tags. 542 String str = string.replaceAll("<li>", "<ul><li>") 543 .replaceAll("</li>","</li></ul>"); 544 int firstTagIndex = str.indexOf('<'); 545 int lastTagIndex = str.lastIndexOf('>'); 546 StringBuilder stringBuilder = new StringBuilder(str.substring(0, firstTagIndex)); 547 List<Tag> tagList = new ArrayList<>(); 548 Map<String, Deque<Tag>> startStacks = new HashMap<>(); 549 Parser parser = new Parser(); 550 parser.setContentHandler(new DefaultHandler() { 551 @Override 552 public void startElement(String uri, String localName, String qName, 553 Attributes attributes) { 554 if (!Strings.isNullOrEmpty(localName)) { 555 Tag tag = new Tag(localName); 556 tag.mStart = stringBuilder.length(); 557 tag.mAttributes = attributes; 558 startStacks.computeIfAbsent(localName, key -> new ArrayDeque<>()).addFirst(tag); 559 } 560 } 561 562 @Override 563 public void endElement(String uri, String localName, String qName) { 564 if (!Strings.isNullOrEmpty(localName)) { 565 Tag tag = startStacks.get(localName).removeFirst(); 566 tag.mEnd = stringBuilder.length(); 567 tagList.add(tag); 568 } 569 } 570 571 @Override 572 public void characters(char[] ch, int start, int length) { 573 stringBuilder.append(ch, start, length); 574 } 575 }); 576 try { 577 parser.setProperty(Parser.schemaProperty, new HTMLSchema()); 578 parser.parse(new InputSource( 579 new StringReader(str.substring(firstTagIndex, lastTagIndex + 1)))); 580 } catch (SAXException | IOException e) { 581 Bridge.getLog().warning(ILayoutLog.TAG_RESOURCES_FORMAT, 582 "The string " + str + " is not valid HTML", null, null); 583 return str; 584 } 585 stringBuilder.append(str.substring(lastTagIndex + 1)); 586 return applyStyles(stringBuilder, tagList); 587 } 588 589 /** 590 * This applies the styles from tagList that are supported by Android 591 * and returns a {@link SpannedString}. 592 * This should mirror {@link StringBlock#applyStyles} 593 */ 594 @NonNull applyStyles(@onNull StringBuilder stringBuilder, @NonNull List<Tag> tagList)595 private static SpannedString applyStyles(@NonNull StringBuilder stringBuilder, 596 @NonNull List<Tag> tagList) { 597 SpannableString spannableString = new SpannableString(stringBuilder); 598 for (Tag tag : tagList) { 599 int start = tag.mStart; 600 int end = tag.mEnd; 601 Attributes attrs = tag.mAttributes; 602 switch (tag.mLabel) { 603 case "b": 604 spannableString.setSpan(new StyleSpan(Typeface.BOLD), start, end, 605 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 606 break; 607 case "i": 608 spannableString.setSpan(new StyleSpan(Typeface.ITALIC), start, end, 609 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 610 break; 611 case "u": 612 spannableString.setSpan(new UnderlineSpan(), start, end, 613 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 614 break; 615 case "tt": 616 spannableString.setSpan(new TypefaceSpan("monospace"), start, end, 617 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 618 break; 619 case "big": 620 spannableString.setSpan(new RelativeSizeSpan(1.25f), start, end, 621 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 622 break; 623 case "small": 624 spannableString.setSpan(new RelativeSizeSpan(0.8f), start, end, 625 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 626 break; 627 case "sup": 628 spannableString.setSpan(new SuperscriptSpan(), start, end, 629 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 630 break; 631 case "sub": 632 spannableString.setSpan(new SubscriptSpan(), start, end, 633 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 634 break; 635 case "strike": 636 spannableString.setSpan(new StrikethroughSpan(), start, end, 637 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 638 break; 639 case "li": 640 StringBlock.addParagraphSpan(spannableString, new BulletSpan(10), start, end); 641 break; 642 case "marquee": 643 spannableString.setSpan(TextUtils.TruncateAt.MARQUEE, start, end, 644 Spanned.SPAN_INCLUSIVE_INCLUSIVE); 645 break; 646 case "font": 647 String heightAttr = attrs.getValue("height"); 648 if (heightAttr != null) { 649 int height = Integer.parseInt(heightAttr); 650 StringBlock.addParagraphSpan(spannableString, new Height(height), start, 651 end); 652 } 653 654 String sizeAttr = attrs.getValue("size"); 655 if (sizeAttr != null) { 656 int size = Integer.parseInt(sizeAttr); 657 spannableString.setSpan(new AbsoluteSizeSpan(size, true), start, end, 658 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 659 } 660 661 String fgcolorAttr = attrs.getValue("fgcolor"); 662 if (fgcolorAttr != null) { 663 spannableString.setSpan(StringBlock.getColor(fgcolorAttr, true), start, end, 664 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 665 } 666 667 String colorAttr = attrs.getValue("color"); 668 if (colorAttr != null) { 669 spannableString.setSpan(StringBlock.getColor(colorAttr, true), start, end, 670 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 671 } 672 673 String bgcolorAttr = attrs.getValue("bgcolor"); 674 if (bgcolorAttr != null) { 675 spannableString.setSpan(StringBlock.getColor(bgcolorAttr, false), start, 676 end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 677 } 678 679 String faceAttr = attrs.getValue("face"); 680 if (faceAttr != null) { 681 spannableString.setSpan(new TypefaceSpan(faceAttr), start, end, 682 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 683 } 684 break; 685 case "a": 686 String href = tag.mAttributes.getValue("href"); 687 if (href != null) { 688 spannableString.setSpan(new URLSpan(href), start, end, 689 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 690 } 691 break; 692 case "annotation": 693 for (int i = 0; i < attrs.getLength(); i++) { 694 String key = attrs.getLocalName(i); 695 String value = attrs.getValue(i); 696 spannableString.setSpan(new Annotation(key, value), start, end, 697 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 698 } 699 } 700 } 701 return new SpannedString(spannableString); 702 } 703 704 // ------- TypedValue stuff 705 // This is taken from //device/libs/utils/ResourceTypes.cpp 706 707 private static final class UnitEntry { 708 String name; 709 int type; 710 int unit; 711 float scale; 712 UnitEntry(String name, int type, int unit, float scale)713 UnitEntry(String name, int type, int unit, float scale) { 714 this.name = name; 715 this.type = type; 716 this.unit = unit; 717 this.scale = scale; 718 } 719 } 720 721 private static final UnitEntry[] sUnitNames = new UnitEntry[] { 722 new UnitEntry("px", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_PX, 1.0f), 723 new UnitEntry("dip", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_DIP, 1.0f), 724 new UnitEntry("dp", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_DIP, 1.0f), 725 new UnitEntry("sp", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_SP, 1.0f), 726 new UnitEntry("pt", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_PT, 1.0f), 727 new UnitEntry("in", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_IN, 1.0f), 728 new UnitEntry("mm", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_MM, 1.0f), 729 new UnitEntry("%", TypedValue.TYPE_FRACTION, TypedValue.COMPLEX_UNIT_FRACTION, 1.0f/100), 730 new UnitEntry("%p", TypedValue.TYPE_FRACTION, TypedValue.COMPLEX_UNIT_FRACTION_PARENT, 1.0f/100), 731 }; 732 733 /** 734 * Returns the raw value from the given attribute float-type value string. 735 * This object is only valid until the next call on to {@link ResourceHelper}. 736 */ getValue(String attribute, String value, boolean requireUnit)737 public static TypedValue getValue(String attribute, String value, boolean requireUnit) { 738 if (parseFloatAttribute(attribute, value, mValue, requireUnit)) { 739 return mValue; 740 } 741 742 return null; 743 } 744 745 /** 746 * Parse a float attribute and return the parsed value into a given TypedValue. 747 * @param attribute the name of the attribute. Can be null if <var>requireUnit</var> is false. 748 * @param value the string value of the attribute 749 * @param outValue the TypedValue to receive the parsed value 750 * @param requireUnit whether the value is expected to contain a unit. 751 * @return true if success. 752 */ parseFloatAttribute(String attribute, @NonNull String value, TypedValue outValue, boolean requireUnit)753 public static boolean parseFloatAttribute(String attribute, @NonNull String value, 754 TypedValue outValue, boolean requireUnit) { 755 assert !requireUnit || attribute != null; 756 757 // remove the space before and after 758 value = value.trim(); 759 int len = value.length(); 760 761 if (len <= 0) { 762 return false; 763 } 764 765 // check that there's no non ascii characters. 766 char[] buf = value.toCharArray(); 767 for (int i = 0 ; i < len ; i++) { 768 if (buf[i] > 255) { 769 return false; 770 } 771 } 772 773 // check the first character 774 if ((buf[0] < '0' || buf[0] > '9') && buf[0] != '.' && buf[0] != '-' && buf[0] != '+') { 775 return false; 776 } 777 778 // now look for the string that is after the float... 779 Matcher m = sFloatPattern.matcher(value); 780 if (m.matches()) { 781 String f_str = m.group(1); 782 String end = m.group(2); 783 784 float f; 785 try { 786 f = Float.parseFloat(f_str); 787 } catch (NumberFormatException e) { 788 // this shouldn't happen with the regexp above. 789 return false; 790 } 791 792 if (end.length() > 0 && end.charAt(0) != ' ') { 793 // Might be a unit... 794 if (parseUnit(end, outValue, sFloatOut)) { 795 computeTypedValue(outValue, f, sFloatOut[0]); 796 return true; 797 } 798 return false; 799 } 800 801 // make sure it's only spaces at the end. 802 end = end.trim(); 803 804 if (end.length() == 0) { 805 if (outValue != null) { 806 if (!requireUnit) { 807 outValue.type = TypedValue.TYPE_FLOAT; 808 outValue.data = Float.floatToIntBits(f); 809 } else { 810 // no unit when required? Use dp and out an error. 811 applyUnit(sUnitNames[1], outValue, sFloatOut); 812 computeTypedValue(outValue, f, sFloatOut[0]); 813 814 Bridge.getLog().error(ILayoutLog.TAG_RESOURCES_RESOLVE, 815 String.format( 816 "Dimension \"%1$s\" in attribute \"%2$s\" is missing unit!", 817 value, attribute), 818 null, null); 819 } 820 return true; 821 } 822 } 823 } 824 825 return false; 826 } 827 computeTypedValue(TypedValue outValue, float value, float scale)828 private static void computeTypedValue(TypedValue outValue, float value, float scale) { 829 value *= scale; 830 boolean neg = value < 0; 831 if (neg) { 832 value = -value; 833 } 834 long bits = (long)(value*(1<<23)+.5f); 835 int radix; 836 int shift; 837 if ((bits&0x7fffff) == 0) { 838 // Always use 23p0 if there is no fraction, just to make 839 // things easier to read. 840 radix = TypedValue.COMPLEX_RADIX_23p0; 841 shift = 23; 842 } else if ((bits&0xffffffffff800000L) == 0) { 843 // Magnitude is zero -- can fit in 0 bits of precision. 844 radix = TypedValue.COMPLEX_RADIX_0p23; 845 shift = 0; 846 } else if ((bits&0xffffffff80000000L) == 0) { 847 // Magnitude can fit in 8 bits of precision. 848 radix = TypedValue.COMPLEX_RADIX_8p15; 849 shift = 8; 850 } else if ((bits&0xffffff8000000000L) == 0) { 851 // Magnitude can fit in 16 bits of precision. 852 radix = TypedValue.COMPLEX_RADIX_16p7; 853 shift = 16; 854 } else { 855 // Magnitude needs entire range, so no fractional part. 856 radix = TypedValue.COMPLEX_RADIX_23p0; 857 shift = 23; 858 } 859 int mantissa = (int)( 860 (bits>>shift) & TypedValue.COMPLEX_MANTISSA_MASK); 861 if (neg) { 862 mantissa = (-mantissa) & TypedValue.COMPLEX_MANTISSA_MASK; 863 } 864 outValue.data |= 865 (radix<<TypedValue.COMPLEX_RADIX_SHIFT) 866 | (mantissa<<TypedValue.COMPLEX_MANTISSA_SHIFT); 867 } 868 869 private static boolean parseUnit(String str, TypedValue outValue, float[] outScale) { 870 str = str.trim(); 871 872 for (UnitEntry unit : sUnitNames) { 873 if (unit.name.equals(str)) { 874 applyUnit(unit, outValue, outScale); 875 return true; 876 } 877 } 878 879 return false; 880 } 881 882 private static void applyUnit(UnitEntry unit, TypedValue outValue, float[] outScale) { 883 outValue.type = unit.type; 884 // COMPLEX_UNIT_SHIFT is 0 and hence intelliJ complains about it. Suppress the warning. 885 //noinspection PointlessBitwiseExpression 886 outValue.data = unit.unit << TypedValue.COMPLEX_UNIT_SHIFT; 887 outScale[0] = unit.scale; 888 } 889 890 private static class Tag { 891 private String mLabel; 892 private int mStart; 893 private int mEnd; 894 private Attributes mAttributes; 895 896 private Tag(String label) { 897 mLabel = label; 898 } 899 } 900 } 901 902