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