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