ResourceHelper.java revision 171804201dd556f8ce7ee7618661a5c8ee71507a
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.annotations.NonNull; 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.layoutlib.bridge.Bridge; 25import com.android.layoutlib.bridge.android.BridgeContext; 26import com.android.layoutlib.bridge.android.BridgeXmlBlockParser; 27import com.android.ninepatch.NinePatch; 28import com.android.ninepatch.NinePatchChunk; 29import com.android.resources.Density; 30 31import org.xmlpull.v1.XmlPullParser; 32import org.xmlpull.v1.XmlPullParserException; 33 34import android.content.res.ColorStateList; 35import android.graphics.Bitmap; 36import android.graphics.Bitmap_Delegate; 37import android.graphics.NinePatch_Delegate; 38import android.graphics.Rect; 39import android.graphics.drawable.BitmapDrawable; 40import android.graphics.drawable.ColorDrawable; 41import android.graphics.drawable.Drawable; 42import android.graphics.drawable.NinePatchDrawable; 43import android.util.TypedValue; 44 45import java.io.File; 46import java.io.FileInputStream; 47import java.io.IOException; 48import java.io.InputStream; 49import java.net.MalformedURLException; 50import java.util.regex.Matcher; 51import java.util.regex.Pattern; 52 53/** 54 * Helper class to provide various conversion method used in handling android resources. 55 */ 56public final class ResourceHelper { 57 58 private final static Pattern sFloatPattern = Pattern.compile("(-?[0-9]+(?:\\.[0-9]+)?)(.*)"); 59 private final static float[] sFloatOut = new float[1]; 60 61 private final static TypedValue mValue = new TypedValue(); 62 63 /** 64 * Returns the color value represented by the given string value 65 * @param value the color value 66 * @return the color as an int 67 * @throws NumberFormatException if the conversion failed. 68 */ 69 public static int getColor(String value) { 70 if (value != null) { 71 if (!value.startsWith("#")) { 72 throw new NumberFormatException( 73 String.format("Color value '%s' must start with #", value)); 74 } 75 76 value = value.substring(1); 77 78 // make sure it's not longer than 32bit 79 if (value.length() > 8) { 80 throw new NumberFormatException(String.format( 81 "Color value '%s' is too long. Format is either" + 82 "#AARRGGBB, #RRGGBB, #RGB, or #ARGB", 83 value)); 84 } 85 86 if (value.length() == 3) { // RGB format 87 char[] color = new char[8]; 88 color[0] = color[1] = 'F'; 89 color[2] = color[3] = value.charAt(0); 90 color[4] = color[5] = value.charAt(1); 91 color[6] = color[7] = value.charAt(2); 92 value = new String(color); 93 } else if (value.length() == 4) { // ARGB format 94 char[] color = new char[8]; 95 color[0] = color[1] = value.charAt(0); 96 color[2] = color[3] = value.charAt(1); 97 color[4] = color[5] = value.charAt(2); 98 color[6] = color[7] = value.charAt(3); 99 value = new String(color); 100 } else if (value.length() == 6) { 101 value = "FF" + value; 102 } 103 104 // this is a RRGGBB or AARRGGBB value 105 106 // Integer.parseInt will fail to parse strings like "ff191919", so we use 107 // a Long, but cast the result back into an int, since we know that we're only 108 // dealing with 32 bit values. 109 return (int)Long.parseLong(value, 16); 110 } 111 112 throw new NumberFormatException(); 113 } 114 115 public static ColorStateList getColorStateList(ResourceValue resValue, BridgeContext context) { 116 String value = resValue.getValue(); 117 if (value != null && !RenderResources.REFERENCE_NULL.equals(value)) { 118 // first check if the value is a file (xml most likely) 119 File f = new File(value); 120 if (f.isFile()) { 121 try { 122 // let the framework inflate the ColorStateList from the XML file, by 123 // providing an XmlPullParser 124 XmlPullParser parser = ParserFactory.create(f); 125 126 BridgeXmlBlockParser blockParser = new BridgeXmlBlockParser( 127 parser, context, resValue.isFramework()); 128 try { 129 return ColorStateList.createFromXml(context.getResources(), blockParser); 130 } finally { 131 blockParser.ensurePopped(); 132 } 133 } catch (XmlPullParserException e) { 134 Bridge.getLog().error(LayoutLog.TAG_BROKEN, 135 "Failed to configure parser for " + value, e, null /*data*/); 136 // we'll return null below. 137 } catch (Exception e) { 138 // this is an error and not warning since the file existence is 139 // checked before attempting to parse it. 140 Bridge.getLog().error(LayoutLog.TAG_RESOURCES_READ, 141 "Failed to parse file " + value, e, null /*data*/); 142 143 return null; 144 } 145 } else { 146 // try to load the color state list from an int 147 try { 148 int color = ResourceHelper.getColor(value); 149 return ColorStateList.valueOf(color); 150 } catch (NumberFormatException e) { 151 Bridge.getLog().error(LayoutLog.TAG_RESOURCES_FORMAT, 152 "Failed to convert " + value + " into a ColorStateList", e, 153 null /*data*/); 154 return null; 155 } 156 } 157 } 158 159 return null; 160 } 161 162 /** 163 * Returns a drawable from the given value. 164 * @param value The value that contains a path to a 9 patch, a bitmap or a xml based drawable, 165 * or an hexadecimal color 166 * @param context the current context 167 */ 168 public static Drawable getDrawable(ResourceValue value, BridgeContext context) { 169 if (value == null) { 170 return null; 171 } 172 String stringValue = value.getValue(); 173 if (RenderResources.REFERENCE_NULL.equals(stringValue)) { 174 return null; 175 } 176 177 String lowerCaseValue = stringValue.toLowerCase(); 178 179 Density density = Density.MEDIUM; 180 if (value instanceof DensityBasedResourceValue) { 181 density = 182 ((DensityBasedResourceValue)value).getResourceDensity(); 183 } 184 185 186 if (lowerCaseValue.endsWith(NinePatch.EXTENSION_9PATCH)) { 187 File file = new File(stringValue); 188 if (file.isFile()) { 189 try { 190 return getNinePatchDrawable( 191 new FileInputStream(file), density, value.isFramework(), 192 stringValue, context); 193 } catch (IOException e) { 194 // failed to read the file, we'll return null below. 195 Bridge.getLog().error(LayoutLog.TAG_RESOURCES_READ, 196 "Failed lot load " + file.getAbsolutePath(), e, null /*data*/); 197 } 198 } 199 200 return null; 201 } else if (lowerCaseValue.endsWith(".xml")) { 202 // create a block parser for the file 203 File f = new File(stringValue); 204 if (f.isFile()) { 205 try { 206 // let the framework inflate the Drawable from the XML file. 207 XmlPullParser parser = ParserFactory.create(f); 208 209 BridgeXmlBlockParser blockParser = new BridgeXmlBlockParser( 210 parser, context, value.isFramework()); 211 try { 212 return Drawable.createFromXml(context.getResources(), blockParser); 213 } finally { 214 blockParser.ensurePopped(); 215 } 216 } catch (Exception e) { 217 // this is an error and not warning since the file existence is checked before 218 // attempting to parse it. 219 Bridge.getLog().error(null, "Failed to parse file " + stringValue, 220 e, null /*data*/); 221 } 222 } else { 223 Bridge.getLog().error(LayoutLog.TAG_BROKEN, 224 String.format("File %s does not exist (or is not a file)", stringValue), 225 null /*data*/); 226 } 227 228 return null; 229 } else { 230 File bmpFile = new File(stringValue); 231 if (bmpFile.isFile()) { 232 try { 233 Bitmap bitmap = Bridge.getCachedBitmap(stringValue, 234 value.isFramework() ? null : context.getProjectKey()); 235 236 if (bitmap == null) { 237 bitmap = Bitmap_Delegate.createBitmap(bmpFile, false /*isMutable*/, 238 density); 239 Bridge.setCachedBitmap(stringValue, bitmap, 240 value.isFramework() ? null : context.getProjectKey()); 241 } 242 243 return new BitmapDrawable(context.getResources(), bitmap); 244 } catch (IOException e) { 245 // we'll return null below 246 Bridge.getLog().error(LayoutLog.TAG_RESOURCES_READ, 247 "Failed lot load " + bmpFile.getAbsolutePath(), e, null /*data*/); 248 } 249 } else { 250 // attempt to get a color from the value 251 try { 252 int color = getColor(stringValue); 253 return new ColorDrawable(color); 254 } catch (NumberFormatException e) { 255 // we'll return null below. 256 Bridge.getLog().error(LayoutLog.TAG_RESOURCES_FORMAT, 257 "Failed to convert " + stringValue + " into a drawable", e, 258 null /*data*/); 259 } 260 } 261 } 262 263 return null; 264 } 265 266 private static Drawable getNinePatchDrawable(InputStream inputStream, Density density, 267 boolean isFramework, String cacheKey, BridgeContext context) throws IOException { 268 // see if we still have both the chunk and the bitmap in the caches 269 NinePatchChunk chunk = Bridge.getCached9Patch(cacheKey, 270 isFramework ? null : context.getProjectKey()); 271 Bitmap bitmap = Bridge.getCachedBitmap(cacheKey, 272 isFramework ? null : context.getProjectKey()); 273 274 // if either chunk or bitmap is null, then we reload the 9-patch file. 275 if (chunk == null || bitmap == null) { 276 try { 277 NinePatch ninePatch = NinePatch.load(inputStream, true /*is9Patch*/, 278 false /* convert */); 279 if (ninePatch != null) { 280 if (chunk == null) { 281 chunk = ninePatch.getChunk(); 282 283 Bridge.setCached9Patch(cacheKey, chunk, 284 isFramework ? null : context.getProjectKey()); 285 } 286 287 if (bitmap == null) { 288 bitmap = Bitmap_Delegate.createBitmap(ninePatch.getImage(), 289 false /*isMutable*/, 290 density); 291 292 Bridge.setCachedBitmap(cacheKey, bitmap, 293 isFramework ? null : context.getProjectKey()); 294 } 295 } 296 } catch (MalformedURLException e) { 297 // URL is wrong, we'll return null below 298 } 299 } 300 301 if (chunk != null && bitmap != null) { 302 int[] padding = chunk.getPadding(); 303 Rect paddingRect = new Rect(padding[0], padding[1], padding[2], padding[3]); 304 305 return new NinePatchDrawable(context.getResources(), bitmap, 306 NinePatch_Delegate.serialize(chunk), 307 paddingRect, null); 308 } 309 310 return null; 311 } 312 313 // ------- TypedValue stuff 314 // This is taken from //device/libs/utils/ResourceTypes.cpp 315 316 private static final class UnitEntry { 317 String name; 318 int type; 319 int unit; 320 float scale; 321 322 UnitEntry(String name, int type, int unit, float scale) { 323 this.name = name; 324 this.type = type; 325 this.unit = unit; 326 this.scale = scale; 327 } 328 } 329 330 private final static UnitEntry[] sUnitNames = new UnitEntry[] { 331 new UnitEntry("px", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_PX, 1.0f), 332 new UnitEntry("dip", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_DIP, 1.0f), 333 new UnitEntry("dp", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_DIP, 1.0f), 334 new UnitEntry("sp", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_SP, 1.0f), 335 new UnitEntry("pt", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_PT, 1.0f), 336 new UnitEntry("in", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_IN, 1.0f), 337 new UnitEntry("mm", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_MM, 1.0f), 338 new UnitEntry("%", TypedValue.TYPE_FRACTION, TypedValue.COMPLEX_UNIT_FRACTION, 1.0f/100), 339 new UnitEntry("%p", TypedValue.TYPE_FRACTION, TypedValue.COMPLEX_UNIT_FRACTION_PARENT, 1.0f/100), 340 }; 341 342 /** 343 * Returns the raw value from the given attribute float-type value string. 344 * This object is only valid until the next call on to {@link ResourceHelper}. 345 */ 346 public static TypedValue getValue(String attribute, String value, boolean requireUnit) { 347 if (parseFloatAttribute(attribute, value, mValue, requireUnit)) { 348 return mValue; 349 } 350 351 return null; 352 } 353 354 /** 355 * Parse a float attribute and return the parsed value into a given TypedValue. 356 * @param attribute the name of the attribute. Can be null if <var>requireUnit</var> is false. 357 * @param value the string value of the attribute 358 * @param outValue the TypedValue to receive the parsed value 359 * @param requireUnit whether the value is expected to contain a unit. 360 * @return true if success. 361 */ 362 public static boolean parseFloatAttribute(String attribute, @NonNull String value, 363 TypedValue outValue, boolean requireUnit) { 364 assert !requireUnit || attribute != null; 365 366 // remove the space before and after 367 value = value.trim(); 368 int len = value.length(); 369 370 if (len <= 0) { 371 return false; 372 } 373 374 // check that there's no non ascii characters. 375 char[] buf = value.toCharArray(); 376 for (int i = 0 ; i < len ; i++) { 377 if (buf[i] > 255) { 378 return false; 379 } 380 } 381 382 // check the first character 383 if ((buf[0] < '0' || buf[0] > '9') && buf[0] != '.' && buf[0] != '-' && buf[0] != '+') { 384 return false; 385 } 386 387 // now look for the string that is after the float... 388 Matcher m = sFloatPattern.matcher(value); 389 if (m.matches()) { 390 String f_str = m.group(1); 391 String end = m.group(2); 392 393 float f; 394 try { 395 f = Float.parseFloat(f_str); 396 } catch (NumberFormatException e) { 397 // this shouldn't happen with the regexp above. 398 return false; 399 } 400 401 if (end.length() > 0 && end.charAt(0) != ' ') { 402 // Might be a unit... 403 if (parseUnit(end, outValue, sFloatOut)) { 404 computeTypedValue(outValue, f, sFloatOut[0]); 405 return true; 406 } 407 return false; 408 } 409 410 // make sure it's only spaces at the end. 411 end = end.trim(); 412 413 if (end.length() == 0) { 414 if (outValue != null) { 415 if (!requireUnit) { 416 outValue.type = TypedValue.TYPE_FLOAT; 417 outValue.data = Float.floatToIntBits(f); 418 } else { 419 // no unit when required? Use dp and out an error. 420 applyUnit(sUnitNames[1], outValue, sFloatOut); 421 computeTypedValue(outValue, f, sFloatOut[0]); 422 423 Bridge.getLog().error(LayoutLog.TAG_RESOURCES_RESOLVE, 424 String.format( 425 "Dimension \"%1$s\" in attribute \"%2$s\" is missing unit!", 426 value, attribute), 427 null); 428 } 429 return true; 430 } 431 } 432 } 433 434 return false; 435 } 436 437 private static void computeTypedValue(TypedValue outValue, float value, float scale) { 438 value *= scale; 439 boolean neg = value < 0; 440 if (neg) { 441 value = -value; 442 } 443 long bits = (long)(value*(1<<23)+.5f); 444 int radix; 445 int shift; 446 if ((bits&0x7fffff) == 0) { 447 // Always use 23p0 if there is no fraction, just to make 448 // things easier to read. 449 radix = TypedValue.COMPLEX_RADIX_23p0; 450 shift = 23; 451 } else if ((bits&0xffffffffff800000L) == 0) { 452 // Magnitude is zero -- can fit in 0 bits of precision. 453 radix = TypedValue.COMPLEX_RADIX_0p23; 454 shift = 0; 455 } else if ((bits&0xffffffff80000000L) == 0) { 456 // Magnitude can fit in 8 bits of precision. 457 radix = TypedValue.COMPLEX_RADIX_8p15; 458 shift = 8; 459 } else if ((bits&0xffffff8000000000L) == 0) { 460 // Magnitude can fit in 16 bits of precision. 461 radix = TypedValue.COMPLEX_RADIX_16p7; 462 shift = 16; 463 } else { 464 // Magnitude needs entire range, so no fractional part. 465 radix = TypedValue.COMPLEX_RADIX_23p0; 466 shift = 23; 467 } 468 int mantissa = (int)( 469 (bits>>shift) & TypedValue.COMPLEX_MANTISSA_MASK); 470 if (neg) { 471 mantissa = (-mantissa) & TypedValue.COMPLEX_MANTISSA_MASK; 472 } 473 outValue.data |= 474 (radix<<TypedValue.COMPLEX_RADIX_SHIFT) 475 | (mantissa<<TypedValue.COMPLEX_MANTISSA_SHIFT); 476 } 477 478 private static boolean parseUnit(String str, TypedValue outValue, float[] outScale) { 479 str = str.trim(); 480 481 for (UnitEntry unit : sUnitNames) { 482 if (unit.name.equals(str)) { 483 applyUnit(unit, outValue, outScale); 484 return true; 485 } 486 } 487 488 return false; 489 } 490 491 private static void applyUnit(UnitEntry unit, TypedValue outValue, float[] outScale) { 492 outValue.type = unit.type; 493 // COMPLEX_UNIT_SHIFT is 0 and hence intelliJ complains about it. Suppress the warning. 494 //noinspection PointlessBitwiseExpression 495 outValue.data = unit.unit << TypedValue.COMPLEX_UNIT_SHIFT; 496 outScale[0] = unit.scale; 497 } 498} 499 500