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.SdkConstants; 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.NinePatch; 34 import com.android.ninepatch.NinePatchChunk; 35 import com.android.resources.Density; 36 import com.android.resources.ResourceType; 37 38 import org.xmlpull.v1.XmlPullParser; 39 import org.xmlpull.v1.XmlPullParserException; 40 41 import android.annotation.NonNull; 42 import android.annotation.Nullable; 43 import android.content.res.BridgeAssetManager; 44 import android.content.res.ColorStateList; 45 import android.content.res.ComplexColor; 46 import android.content.res.ComplexColor_Accessor; 47 import android.content.res.GradientColor; 48 import android.content.res.Resources; 49 import android.content.res.Resources.Theme; 50 import android.graphics.Bitmap; 51 import android.graphics.Bitmap_Delegate; 52 import android.graphics.NinePatch_Delegate; 53 import android.graphics.Rect; 54 import android.graphics.Typeface; 55 import android.graphics.Typeface_Accessor; 56 import android.graphics.Typeface_Delegate; 57 import android.graphics.drawable.BitmapDrawable; 58 import android.graphics.drawable.ColorDrawable; 59 import android.graphics.drawable.Drawable; 60 import android.graphics.drawable.NinePatchDrawable; 61 import android.util.TypedValue; 62 63 import java.io.FileNotFoundException; 64 import java.io.IOException; 65 import java.io.InputStream; 66 import java.net.MalformedURLException; 67 import java.util.HashSet; 68 import java.util.Set; 69 import java.util.regex.Matcher; 70 import java.util.regex.Pattern; 71 72 import static android.content.res.AssetManager.ACCESS_STREAMING; 73 74 /** 75 * Helper class to provide various conversion method used in handling android resources. 76 */ 77 public final class ResourceHelper { 78 private static final Key<Set<ResourceValue>> KEY_GET_DRAWABLE = 79 Key.create("ResourceHelper.getDrawable"); 80 private static final Pattern sFloatPattern = Pattern.compile("(-?[0-9]+(?:\\.[0-9]+)?)(.*)"); 81 private static final float[] sFloatOut = new float[1]; 82 83 private static final TypedValue mValue = new TypedValue(); 84 85 /** 86 * Returns the color value represented by the given string value. 87 * 88 * @param value the color value 89 * @return the color as an int 90 * @throws NumberFormatException if the conversion failed. 91 */ getColor(@ullable String value)92 public static int getColor(@Nullable String value) { 93 if (value == null) { 94 throw new NumberFormatException("null value"); 95 } 96 97 value = value.trim(); 98 int len = value.length(); 99 100 // make sure it's not longer than 32bit or smaller than the RGB format 101 if (len < 2 || len > 9) { 102 throw new NumberFormatException(String.format( 103 "Color value '%s' has wrong size. Format is either" + 104 "#AARRGGBB, #RRGGBB, #RGB, or #ARGB", 105 value)); 106 } 107 108 if (value.charAt(0) != '#') { 109 if (value.startsWith(SdkConstants.PREFIX_THEME_REF)) { 110 throw new NumberFormatException(String.format( 111 "Attribute '%s' not found. Are you using the right theme?", value)); 112 } 113 throw new NumberFormatException( 114 String.format("Color value '%s' must start with #", value)); 115 } 116 117 value = value.substring(1); 118 119 if (len == 4) { // RGB format 120 char[] color = new char[8]; 121 color[0] = color[1] = 'F'; 122 color[2] = color[3] = value.charAt(0); 123 color[4] = color[5] = value.charAt(1); 124 color[6] = color[7] = value.charAt(2); 125 value = new String(color); 126 } else if (len == 5) { // ARGB format 127 char[] color = new char[8]; 128 color[0] = color[1] = value.charAt(0); 129 color[2] = color[3] = value.charAt(1); 130 color[4] = color[5] = value.charAt(2); 131 color[6] = color[7] = value.charAt(3); 132 value = new String(color); 133 } else if (len == 7) { 134 value = "FF" + value; 135 } 136 137 // this is a RRGGBB or AARRGGBB value 138 139 // Integer.parseInt will fail to parse strings like "ff191919", so we use 140 // a Long, but cast the result back into an int, since we know that we're only 141 // dealing with 32 bit values. 142 return (int)Long.parseLong(value, 16); 143 } 144 145 /** 146 * Returns a {@link ComplexColor} from the given {@link ResourceValue} 147 * 148 * @param resValue the value containing a color value or a file path to a complex color 149 * definition 150 * @param context the current context 151 * @param theme the theme to use when resolving the complex color 152 * @param allowGradients when false, only {@link ColorStateList} will be returned. If a {@link 153 * GradientColor} is found, null will be returned. 154 */ 155 @Nullable getInternalComplexColor(@onNull ResourceValue resValue, @NonNull BridgeContext context, @Nullable Theme theme, boolean allowGradients)156 private static ComplexColor getInternalComplexColor(@NonNull ResourceValue resValue, 157 @NonNull BridgeContext context, @Nullable Theme theme, boolean allowGradients) { 158 String value = resValue.getValue(); 159 if (value == null || RenderResources.REFERENCE_NULL.equals(value)) { 160 return null; 161 } 162 163 // try to load the color state list from an int 164 if (value.trim().startsWith("#")) { 165 try { 166 int color = getColor(value); 167 return ColorStateList.valueOf(color); 168 } catch (NumberFormatException e) { 169 Bridge.getLog().warning(ILayoutLog.TAG_RESOURCES_FORMAT, 170 String.format("\"%1$s\" cannot be interpreted as a color.", value), 171 null, null); 172 return null; 173 } 174 } 175 176 try { 177 BridgeXmlBlockParser blockParser = getXmlBlockParser(context, resValue); 178 if (blockParser != null) { 179 try { 180 // Advance the parser to the first element so we can detect if it's a 181 // color list or a gradient color 182 int type; 183 //noinspection StatementWithEmptyBody 184 while ((type = blockParser.next()) != XmlPullParser.START_TAG 185 && type != XmlPullParser.END_DOCUMENT) { 186 // Seek parser to start tag. 187 } 188 189 if (type != XmlPullParser.START_TAG) { 190 assert false : "No start tag found"; 191 return null; 192 } 193 194 final String name = blockParser.getName(); 195 if (allowGradients && "gradient".equals(name)) { 196 return ComplexColor_Accessor.createGradientColorFromXmlInner( 197 context.getResources(), 198 blockParser, blockParser, 199 theme); 200 } else if ("selector".equals(name)) { 201 return ComplexColor_Accessor.createColorStateListFromXmlInner( 202 context.getResources(), 203 blockParser, blockParser, 204 theme); 205 } 206 } finally { 207 blockParser.ensurePopped(); 208 } 209 } 210 } catch (XmlPullParserException e) { 211 Bridge.getLog().error(ILayoutLog.TAG_BROKEN, 212 "Failed to configure parser for " + value, e, null,null /*data*/); 213 // we'll return null below. 214 } catch (Exception e) { 215 // this is an error and not warning since the file existence is 216 // checked before attempting to parse it. 217 Bridge.getLog().error(ILayoutLog.TAG_RESOURCES_READ, 218 "Failed to parse file " + value, e, null, null /*data*/); 219 220 return null; 221 } 222 223 return null; 224 } 225 226 /** 227 * Returns a {@link ColorStateList} from the given {@link ResourceValue} 228 * 229 * @param resValue the value containing a color value or a file path to a complex color 230 * definition 231 * @param context the current context 232 */ 233 @Nullable getColorStateList(@onNull ResourceValue resValue, @NonNull BridgeContext context, @Nullable Resources.Theme theme)234 public static ColorStateList getColorStateList(@NonNull ResourceValue resValue, 235 @NonNull BridgeContext context, @Nullable Resources.Theme theme) { 236 return (ColorStateList) getInternalComplexColor(resValue, context, 237 theme != null ? theme : context.getTheme(), 238 false); 239 } 240 241 /** 242 * Returns a {@link ComplexColor} from the given {@link ResourceValue} 243 * 244 * @param resValue the value containing a color value or a file path to a complex color 245 * definition 246 * @param context the current context 247 */ 248 @Nullable getComplexColor(@onNull ResourceValue resValue, @NonNull BridgeContext context, @Nullable Resources.Theme theme)249 public static ComplexColor getComplexColor(@NonNull ResourceValue resValue, 250 @NonNull BridgeContext context, @Nullable Resources.Theme theme) { 251 return getInternalComplexColor(resValue, context, 252 theme != null ? theme : context.getTheme(), 253 true); 254 } 255 256 /** 257 * Returns a drawable from the given value. 258 * 259 * @param value The value that contains a path to a 9 patch, a bitmap or a xml based drawable, 260 * or an hexadecimal color 261 * @param context the current context 262 */ 263 @Nullable getDrawable(ResourceValue value, BridgeContext context)264 public static Drawable getDrawable(ResourceValue value, BridgeContext context) { 265 return getDrawable(value, context, null); 266 } 267 268 /** 269 * Returns a {@link BridgeXmlBlockParser} to parse the given {@link ResourceValue}. The passed 270 * value must point to an XML resource. 271 */ 272 @Nullable getXmlBlockParser(@onNull BridgeContext context, @NonNull ResourceValue value)273 public static BridgeXmlBlockParser getXmlBlockParser(@NonNull BridgeContext context, 274 @NonNull ResourceValue value) throws XmlPullParserException { 275 String stringValue = value.getValue(); 276 if (RenderResources.REFERENCE_NULL.equals(stringValue)) { 277 return null; 278 } 279 280 XmlPullParser parser = null; 281 ResourceNamespace namespace; 282 283 LayoutlibCallback layoutlibCallback = context.getLayoutlibCallback(); 284 // Framework values never need a PSI parser. They do not change and the do not contain 285 // aapt:attr attributes. 286 if (!value.isFramework()) { 287 parser = layoutlibCallback.getParser(value); 288 } 289 290 if (parser != null) { 291 namespace = ((ILayoutPullParser) parser).getLayoutNamespace(); 292 } else { 293 parser = ParserFactory.create(stringValue); 294 namespace = value.getNamespace(); 295 } 296 297 return parser == null 298 ? null 299 : new BridgeXmlBlockParser(parser, context, namespace); 300 } 301 302 /** 303 * Returns a drawable from the given value. 304 * 305 * @param value The value that contains a path to a 9 patch, a bitmap or a xml based drawable, 306 * or an hexadecimal color 307 * @param context the current context 308 * @param theme the theme to be used to inflate the drawable. 309 */ 310 @Nullable getDrawable(ResourceValue value, BridgeContext context, Theme theme)311 public static Drawable getDrawable(ResourceValue value, BridgeContext context, Theme theme) { 312 if (value == null) { 313 return null; 314 } 315 String stringValue = value.getValue(); 316 if (RenderResources.REFERENCE_NULL.equals(stringValue)) { 317 return null; 318 } 319 320 // try the simple case first. Attempt to get a color from the value 321 if (stringValue.trim().startsWith("#")) { 322 try { 323 int color = getColor(stringValue); 324 return new ColorDrawable(color); 325 } catch (NumberFormatException e) { 326 Bridge.getLog().warning(ILayoutLog.TAG_RESOURCES_FORMAT, 327 String.format("\"%1$s\" cannot be interpreted as a color.", stringValue), 328 null, null); 329 return null; 330 } 331 } 332 333 Density density = Density.MEDIUM; 334 if (value instanceof DensityBasedResourceValue) { 335 density = ((DensityBasedResourceValue) value).getResourceDensity(); 336 if (density == Density.NODPI || density == Density.ANYDPI) { 337 density = Density.getEnum(context.getConfiguration().densityDpi); 338 } 339 } 340 341 String lowerCaseValue = stringValue.toLowerCase(); 342 if (lowerCaseValue.endsWith(NinePatch.EXTENSION_9PATCH)) { 343 try { 344 return getNinePatchDrawable(density, value.isFramework(), stringValue, context); 345 } catch (IOException e) { 346 // failed to read the file, we'll return null below. 347 Bridge.getLog().error(ILayoutLog.TAG_RESOURCES_READ, 348 "Failed to load " + stringValue, e, null, null /*data*/); 349 } 350 351 return null; 352 } else if (lowerCaseValue.endsWith(".xml") || 353 value.getResourceType() == ResourceType.AAPT) { 354 // create a block parser for the file 355 try { 356 BridgeXmlBlockParser blockParser = getXmlBlockParser(context, value); 357 if (blockParser != null) { 358 Set<ResourceValue> visitedValues = context.getUserData(KEY_GET_DRAWABLE); 359 if (visitedValues == null) { 360 visitedValues = new HashSet<>(); 361 context.putUserData(KEY_GET_DRAWABLE, visitedValues); 362 } 363 if (!visitedValues.add(value)) { 364 Bridge.getLog().error(null, "Cyclic dependency in " + stringValue, null, 365 null); 366 return null; 367 } 368 369 try { 370 return Drawable.createFromXml(context.getResources(), blockParser, theme); 371 } finally { 372 visitedValues.remove(value); 373 blockParser.ensurePopped(); 374 } 375 } 376 } catch (Exception e) { 377 // this is an error and not warning since the file existence is checked before 378 // attempting to parse it. 379 Bridge.getLog().error(null, "Failed to parse file " + stringValue, e, 380 null, null /*data*/); 381 } 382 383 return null; 384 } else { 385 AssetRepository repository = getAssetRepository(context); 386 if (repository.isFileResource(stringValue)) { 387 try { 388 Bitmap bitmap = Bridge.getCachedBitmap(stringValue, 389 value.isFramework() ? null : context.getProjectKey()); 390 391 if (bitmap == null) { 392 InputStream stream; 393 try { 394 stream = repository.openNonAsset(0, stringValue, ACCESS_STREAMING); 395 396 } catch (FileNotFoundException e) { 397 stream = null; 398 } 399 bitmap = 400 Bitmap_Delegate.createBitmap(stream, false /*isMutable*/, density); 401 Bridge.setCachedBitmap(stringValue, bitmap, 402 value.isFramework() ? null : context.getProjectKey()); 403 } 404 405 return new BitmapDrawable(context.getResources(), bitmap); 406 } catch (IOException e) { 407 // we'll return null below 408 Bridge.getLog().error(ILayoutLog.TAG_RESOURCES_READ, 409 "Failed to load " + stringValue, e, null, null /*data*/); 410 } 411 } 412 } 413 414 return null; 415 } 416 getAssetRepository(@onNull BridgeContext context)417 private static AssetRepository getAssetRepository(@NonNull BridgeContext context) { 418 BridgeAssetManager assetManager = context.getAssets(); 419 return assetManager.getAssetRepository(); 420 } 421 422 /** 423 * Returns a {@link Typeface} given a font name. The font name, can be a system font family 424 * (like sans-serif) or a full path if the font is to be loaded from resources. 425 */ getFont(String fontName, BridgeContext context, Theme theme, boolean isFramework)426 public static Typeface getFont(String fontName, BridgeContext context, Theme theme, boolean 427 isFramework) { 428 if (fontName == null) { 429 return null; 430 } 431 432 if (Typeface_Accessor.isSystemFont(fontName)) { 433 // Shortcut for the case where we are asking for a system font name. Those are not 434 // loaded using external resources. 435 return null; 436 } 437 438 439 return Typeface_Delegate.createFromDisk(context, fontName, isFramework); 440 } 441 442 /** 443 * Returns a {@link Typeface} given a font name. The font name, can be a system font family 444 * (like sans-serif) or a full path if the font is to be loaded from resources. 445 */ getFont(ResourceValue value, BridgeContext context, Theme theme)446 public static Typeface getFont(ResourceValue value, BridgeContext context, Theme theme) { 447 if (value == null) { 448 return null; 449 } 450 451 return getFont(value.getValue(), context, theme, value.isFramework()); 452 } 453 getNinePatchDrawable(Density density, boolean isFramework, String path, BridgeContext context)454 private static Drawable getNinePatchDrawable(Density density, boolean isFramework, 455 String path, BridgeContext context) throws IOException { 456 // see if we still have both the chunk and the bitmap in the caches 457 NinePatchChunk chunk = Bridge.getCached9Patch(path, 458 isFramework ? null : context.getProjectKey()); 459 Bitmap bitmap = Bridge.getCachedBitmap(path, 460 isFramework ? null : context.getProjectKey()); 461 462 // if either chunk or bitmap is null, then we reload the 9-patch file. 463 if (chunk == null || bitmap == null) { 464 try { 465 AssetRepository repository = getAssetRepository(context); 466 if (!repository.isFileResource(path)) { 467 return null; 468 } 469 InputStream stream = repository.openNonAsset(0, path, ACCESS_STREAMING); 470 NinePatch ninePatch = NinePatch.load(stream, true /*is9Patch*/, 471 false /* convert */); 472 if (ninePatch != null) { 473 if (chunk == null) { 474 chunk = ninePatch.getChunk(); 475 476 Bridge.setCached9Patch(path, chunk, 477 isFramework ? null : context.getProjectKey()); 478 } 479 480 if (bitmap == null) { 481 bitmap = Bitmap_Delegate.createBitmap(ninePatch.getImage(), 482 false /*isMutable*/, 483 density); 484 485 Bridge.setCachedBitmap(path, bitmap, 486 isFramework ? null : context.getProjectKey()); 487 } 488 } 489 } catch (MalformedURLException e) { 490 // URL is wrong, we'll return null below 491 } 492 } 493 494 if (chunk != null && bitmap != null) { 495 int[] padding = chunk.getPadding(); 496 Rect paddingRect = new Rect(padding[0], padding[1], padding[2], padding[3]); 497 498 return new NinePatchDrawable(context.getResources(), bitmap, 499 NinePatch_Delegate.serialize(chunk), 500 paddingRect, null); 501 } 502 503 return null; 504 } 505 506 /** 507 * Looks for an attribute in the current theme. 508 * 509 * @param resources the render resources 510 * @param attr the attribute reference 511 * @param defaultValue the default value. 512 * @return the value of the attribute or the default one if not found. 513 */ getBooleanThemeValue(@onNull RenderResources resources, @NonNull ResourceReference attr, boolean defaultValue)514 public static boolean getBooleanThemeValue(@NonNull RenderResources resources, 515 @NonNull ResourceReference attr, boolean defaultValue) { 516 ResourceValue value = resources.findItemInTheme(attr); 517 value = resources.resolveResValue(value); 518 if (value == null) { 519 return defaultValue; 520 } 521 return XmlUtils.convertValueToBoolean(value.getValue(), defaultValue); 522 } 523 524 /** 525 * Looks for a framework attribute in the current theme. 526 * 527 * @param resources the render resources 528 * @param name the name of the attribute 529 * @param defaultValue the default value. 530 * @return the value of the attribute or the default one if not found. 531 */ getBooleanThemeFrameworkAttrValue(@onNull RenderResources resources, @NonNull String name, boolean defaultValue)532 public static boolean getBooleanThemeFrameworkAttrValue(@NonNull RenderResources resources, 533 @NonNull String name, boolean defaultValue) { 534 ResourceReference attrRef = BridgeContext.createFrameworkAttrReference(name); 535 return getBooleanThemeValue(resources, attrRef, defaultValue); 536 } 537 538 // ------- TypedValue stuff 539 // This is taken from //device/libs/utils/ResourceTypes.cpp 540 541 private static final class UnitEntry { 542 String name; 543 int type; 544 int unit; 545 float scale; 546 UnitEntry(String name, int type, int unit, float scale)547 UnitEntry(String name, int type, int unit, float scale) { 548 this.name = name; 549 this.type = type; 550 this.unit = unit; 551 this.scale = scale; 552 } 553 } 554 555 private static final UnitEntry[] sUnitNames = new UnitEntry[] { 556 new UnitEntry("px", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_PX, 1.0f), 557 new UnitEntry("dip", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_DIP, 1.0f), 558 new UnitEntry("dp", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_DIP, 1.0f), 559 new UnitEntry("sp", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_SP, 1.0f), 560 new UnitEntry("pt", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_PT, 1.0f), 561 new UnitEntry("in", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_IN, 1.0f), 562 new UnitEntry("mm", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_MM, 1.0f), 563 new UnitEntry("%", TypedValue.TYPE_FRACTION, TypedValue.COMPLEX_UNIT_FRACTION, 1.0f/100), 564 new UnitEntry("%p", TypedValue.TYPE_FRACTION, TypedValue.COMPLEX_UNIT_FRACTION_PARENT, 1.0f/100), 565 }; 566 567 /** 568 * Returns the raw value from the given attribute float-type value string. 569 * This object is only valid until the next call on to {@link ResourceHelper}. 570 */ getValue(String attribute, String value, boolean requireUnit)571 public static TypedValue getValue(String attribute, String value, boolean requireUnit) { 572 if (parseFloatAttribute(attribute, value, mValue, requireUnit)) { 573 return mValue; 574 } 575 576 return null; 577 } 578 579 /** 580 * Parse a float attribute and return the parsed value into a given TypedValue. 581 * @param attribute the name of the attribute. Can be null if <var>requireUnit</var> is false. 582 * @param value the string value of the attribute 583 * @param outValue the TypedValue to receive the parsed value 584 * @param requireUnit whether the value is expected to contain a unit. 585 * @return true if success. 586 */ parseFloatAttribute(String attribute, @NonNull String value, TypedValue outValue, boolean requireUnit)587 public static boolean parseFloatAttribute(String attribute, @NonNull String value, 588 TypedValue outValue, boolean requireUnit) { 589 assert !requireUnit || attribute != null; 590 591 // remove the space before and after 592 value = value.trim(); 593 int len = value.length(); 594 595 if (len <= 0) { 596 return false; 597 } 598 599 // check that there's no non ascii characters. 600 char[] buf = value.toCharArray(); 601 for (int i = 0 ; i < len ; i++) { 602 if (buf[i] > 255) { 603 return false; 604 } 605 } 606 607 // check the first character 608 if ((buf[0] < '0' || buf[0] > '9') && buf[0] != '.' && buf[0] != '-' && buf[0] != '+') { 609 return false; 610 } 611 612 // now look for the string that is after the float... 613 Matcher m = sFloatPattern.matcher(value); 614 if (m.matches()) { 615 String f_str = m.group(1); 616 String end = m.group(2); 617 618 float f; 619 try { 620 f = Float.parseFloat(f_str); 621 } catch (NumberFormatException e) { 622 // this shouldn't happen with the regexp above. 623 return false; 624 } 625 626 if (end.length() > 0 && end.charAt(0) != ' ') { 627 // Might be a unit... 628 if (parseUnit(end, outValue, sFloatOut)) { 629 computeTypedValue(outValue, f, sFloatOut[0]); 630 return true; 631 } 632 return false; 633 } 634 635 // make sure it's only spaces at the end. 636 end = end.trim(); 637 638 if (end.length() == 0) { 639 if (outValue != null) { 640 if (!requireUnit) { 641 outValue.type = TypedValue.TYPE_FLOAT; 642 outValue.data = Float.floatToIntBits(f); 643 } else { 644 // no unit when required? Use dp and out an error. 645 applyUnit(sUnitNames[1], outValue, sFloatOut); 646 computeTypedValue(outValue, f, sFloatOut[0]); 647 648 Bridge.getLog().error(ILayoutLog.TAG_RESOURCES_RESOLVE, 649 String.format( 650 "Dimension \"%1$s\" in attribute \"%2$s\" is missing unit!", 651 value, attribute), 652 null, null); 653 } 654 return true; 655 } 656 } 657 } 658 659 return false; 660 } 661 computeTypedValue(TypedValue outValue, float value, float scale)662 private static void computeTypedValue(TypedValue outValue, float value, float scale) { 663 value *= scale; 664 boolean neg = value < 0; 665 if (neg) { 666 value = -value; 667 } 668 long bits = (long)(value*(1<<23)+.5f); 669 int radix; 670 int shift; 671 if ((bits&0x7fffff) == 0) { 672 // Always use 23p0 if there is no fraction, just to make 673 // things easier to read. 674 radix = TypedValue.COMPLEX_RADIX_23p0; 675 shift = 23; 676 } else if ((bits&0xffffffffff800000L) == 0) { 677 // Magnitude is zero -- can fit in 0 bits of precision. 678 radix = TypedValue.COMPLEX_RADIX_0p23; 679 shift = 0; 680 } else if ((bits&0xffffffff80000000L) == 0) { 681 // Magnitude can fit in 8 bits of precision. 682 radix = TypedValue.COMPLEX_RADIX_8p15; 683 shift = 8; 684 } else if ((bits&0xffffff8000000000L) == 0) { 685 // Magnitude can fit in 16 bits of precision. 686 radix = TypedValue.COMPLEX_RADIX_16p7; 687 shift = 16; 688 } else { 689 // Magnitude needs entire range, so no fractional part. 690 radix = TypedValue.COMPLEX_RADIX_23p0; 691 shift = 23; 692 } 693 int mantissa = (int)( 694 (bits>>shift) & TypedValue.COMPLEX_MANTISSA_MASK); 695 if (neg) { 696 mantissa = (-mantissa) & TypedValue.COMPLEX_MANTISSA_MASK; 697 } 698 outValue.data |= 699 (radix<<TypedValue.COMPLEX_RADIX_SHIFT) 700 | (mantissa<<TypedValue.COMPLEX_MANTISSA_SHIFT); 701 } 702 703 private static boolean parseUnit(String str, TypedValue outValue, float[] outScale) { 704 str = str.trim(); 705 706 for (UnitEntry unit : sUnitNames) { 707 if (unit.name.equals(str)) { 708 applyUnit(unit, outValue, outScale); 709 return true; 710 } 711 } 712 713 return false; 714 } 715 716 private static void applyUnit(UnitEntry unit, TypedValue outValue, float[] outScale) { 717 outValue.type = unit.type; 718 // COMPLEX_UNIT_SHIFT is 0 and hence intelliJ complains about it. Suppress the warning. 719 //noinspection PointlessBitwiseExpression 720 outValue.data = unit.unit << TypedValue.COMPLEX_UNIT_SHIFT; 721 outScale[0] = unit.scale; 722 } 723 } 724 725