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