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