ResourceHelper.java revision fb93ce9684120a36862b5b5e67b1865a652907e9
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.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 * @throw NumberFormatException if the conversion failed. 68 */ 69 public static int getColor(String value) { 70 if (value != null) { 71 if (value.startsWith("#") == false) { 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) == false) { 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 KXmlParser parser = new KXmlParser(); 125 parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); 126 parser.setInput(new FileInputStream(f), "UTF-8"); //$NON-NLS-1$); 127 128 BridgeXmlBlockParser blockParser = new BridgeXmlBlockParser( 129 parser, context, resValue.isFramework()); 130 try { 131 return ColorStateList.createFromXml(context.getResources(), blockParser); 132 } finally { 133 blockParser.ensurePopped(); 134 } 135 } catch (XmlPullParserException e) { 136 Bridge.getLog().error(LayoutLog.TAG_BROKEN, 137 "Failed to configure parser for " + value, e, null /*data*/); 138 // we'll return null below. 139 } catch (Exception e) { 140 // this is an error and not warning since the file existence is 141 // checked before attempting to parse it. 142 Bridge.getLog().error(LayoutLog.TAG_RESOURCES_READ, 143 "Failed to parse file " + value, e, null /*data*/); 144 145 return null; 146 } 147 } else { 148 // try to load the color state list from an int 149 try { 150 int color = ResourceHelper.getColor(value); 151 return ColorStateList.valueOf(color); 152 } catch (NumberFormatException e) { 153 Bridge.getLog().error(LayoutLog.TAG_RESOURCES_FORMAT, 154 "Failed to convert " + value + " into a ColorStateList", e, 155 null /*data*/); 156 return null; 157 } 158 } 159 } 160 161 return null; 162 } 163 164 /** 165 * Returns a drawable from the given value. 166 * @param value The value that contains a path to a 9 patch, a bitmap or a xml based drawable, 167 * or an hexadecimal color 168 * @param context the current context 169 */ 170 public static Drawable getDrawable(ResourceValue value, BridgeContext context) { 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 KXmlParser parser = new KXmlParser(); 207 parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); 208 parser.setInput(new FileInputStream(f), "UTF-8"); //$NON-NLS-1$); 209 210 BridgeXmlBlockParser blockParser = new BridgeXmlBlockParser( 211 parser, context, value.isFramework()); 212 try { 213 return Drawable.createFromXml(context.getResources(), blockParser); 214 } finally { 215 blockParser.ensurePopped(); 216 } 217 } catch (Exception e) { 218 // this is an error and not warning since the file existence is checked before 219 // attempting to parse it. 220 Bridge.getLog().error(null, "Failed to parse file " + stringValue, 221 e, null /*data*/); 222 } 223 } else { 224 Bridge.getLog().error(LayoutLog.TAG_BROKEN, 225 String.format("File %s does not exist (or is not a file)", stringValue), 226 null /*data*/); 227 } 228 229 return null; 230 } else { 231 File bmpFile = new File(stringValue); 232 if (bmpFile.isFile()) { 233 try { 234 Bitmap bitmap = Bridge.getCachedBitmap(stringValue, 235 value.isFramework() ? null : context.getProjectKey()); 236 237 if (bitmap == null) { 238 bitmap = Bitmap_Delegate.createBitmap(bmpFile, false /*isMutable*/, 239 density); 240 Bridge.setCachedBitmap(stringValue, bitmap, 241 value.isFramework() ? null : context.getProjectKey()); 242 } 243 244 return new BitmapDrawable(context.getResources(), bitmap); 245 } catch (IOException e) { 246 // we'll return null below 247 Bridge.getLog().error(LayoutLog.TAG_RESOURCES_READ, 248 "Failed lot load " + bmpFile.getAbsolutePath(), e, null /*data*/); 249 } 250 } else { 251 // attempt to get a color from the value 252 try { 253 int color = getColor(stringValue); 254 return new ColorDrawable(color); 255 } catch (NumberFormatException e) { 256 // we'll return null below. 257 Bridge.getLog().error(LayoutLog.TAG_RESOURCES_FORMAT, 258 "Failed to convert " + stringValue + " into a drawable", e, 259 null /*data*/); 260 } 261 } 262 } 263 264 return null; 265 } 266 267 private static Drawable getNinePatchDrawable(InputStream inputStream, Density density, 268 boolean isFramework, String cacheKey, BridgeContext context) throws IOException { 269 // see if we still have both the chunk and the bitmap in the caches 270 NinePatchChunk chunk = Bridge.getCached9Patch(cacheKey, 271 isFramework ? null : context.getProjectKey()); 272 Bitmap bitmap = Bridge.getCachedBitmap(cacheKey, 273 isFramework ? null : context.getProjectKey()); 274 275 // if either chunk or bitmap is null, then we reload the 9-patch file. 276 if (chunk == null || bitmap == null) { 277 try { 278 NinePatch ninePatch = NinePatch.load(inputStream, true /*is9Patch*/, 279 false /* convert */); 280 if (ninePatch != null) { 281 if (chunk == null) { 282 chunk = ninePatch.getChunk(); 283 284 Bridge.setCached9Patch(cacheKey, chunk, 285 isFramework ? null : context.getProjectKey()); 286 } 287 288 if (bitmap == null) { 289 bitmap = Bitmap_Delegate.createBitmap(ninePatch.getImage(), 290 false /*isMutable*/, 291 density); 292 293 Bridge.setCachedBitmap(cacheKey, bitmap, 294 isFramework ? null : context.getProjectKey()); 295 } 296 } 297 } catch (MalformedURLException e) { 298 // URL is wrong, we'll return null below 299 } 300 } 301 302 if (chunk != null && bitmap != null) { 303 int[] padding = chunk.getPadding(); 304 Rect paddingRect = new Rect(padding[0], padding[1], padding[2], padding[3]); 305 306 return new NinePatchDrawable(context.getResources(), bitmap, 307 NinePatch_Delegate.serialize(chunk), 308 paddingRect, null); 309 } 310 311 return null; 312 } 313 314 // ------- TypedValue stuff 315 // This is taken from //device/libs/utils/ResourceTypes.cpp 316 317 private static final class UnitEntry { 318 String name; 319 int type; 320 int unit; 321 float scale; 322 323 UnitEntry(String name, int type, int unit, float scale) { 324 this.name = name; 325 this.type = type; 326 this.unit = unit; 327 this.scale = scale; 328 } 329 } 330 331 private final static UnitEntry[] sUnitNames = new UnitEntry[] { 332 new UnitEntry("px", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_PX, 1.0f), 333 new UnitEntry("dip", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_DIP, 1.0f), 334 new UnitEntry("dp", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_DIP, 1.0f), 335 new UnitEntry("sp", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_SP, 1.0f), 336 new UnitEntry("pt", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_PT, 1.0f), 337 new UnitEntry("in", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_IN, 1.0f), 338 new UnitEntry("mm", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_MM, 1.0f), 339 new UnitEntry("%", TypedValue.TYPE_FRACTION, TypedValue.COMPLEX_UNIT_FRACTION, 1.0f/100), 340 new UnitEntry("%p", TypedValue.TYPE_FRACTION, TypedValue.COMPLEX_UNIT_FRACTION_PARENT, 1.0f/100), 341 }; 342 343 /** 344 * Returns the raw value from the given string. 345 * This object is only valid until the next call on to {@link ResourceHelper}. 346 */ 347 public static TypedValue getValue(String s) { 348 if (stringToFloat(s, mValue)) { 349 return mValue; 350 } 351 352 return null; 353 } 354 355 /** 356 * Convert the string into a {@link TypedValue}. 357 * @param s 358 * @param outValue 359 * @return true if success. 360 */ 361 public static boolean stringToFloat(String s, TypedValue outValue) { 362 // remove the space before and after 363 s = s.trim(); 364 int len = s.length(); 365 366 if (len <= 0) { 367 return false; 368 } 369 370 // check that there's no non ascii characters. 371 char[] buf = s.toCharArray(); 372 for (int i = 0 ; i < len ; i++) { 373 if (buf[i] > 255) { 374 return false; 375 } 376 } 377 378 // check the first character 379 if (buf[0] < '0' && buf[0] > '9' && buf[0] != '.' && buf[0] != '-') { 380 return false; 381 } 382 383 // now look for the string that is after the float... 384 Matcher m = sFloatPattern.matcher(s); 385 if (m.matches()) { 386 String f_str = m.group(1); 387 String end = m.group(2); 388 389 float f; 390 try { 391 f = Float.parseFloat(f_str); 392 } catch (NumberFormatException e) { 393 // this shouldn't happen with the regexp above. 394 return false; 395 } 396 397 if (end.length() > 0 && end.charAt(0) != ' ') { 398 // Might be a unit... 399 if (parseUnit(end, outValue, sFloatOut)) { 400 401 f *= sFloatOut[0]; 402 boolean neg = f < 0; 403 if (neg) { 404 f = -f; 405 } 406 long bits = (long)(f*(1<<23)+.5f); 407 int radix; 408 int shift; 409 if ((bits&0x7fffff) == 0) { 410 // Always use 23p0 if there is no fraction, just to make 411 // things easier to read. 412 radix = TypedValue.COMPLEX_RADIX_23p0; 413 shift = 23; 414 } else if ((bits&0xffffffffff800000L) == 0) { 415 // Magnitude is zero -- can fit in 0 bits of precision. 416 radix = TypedValue.COMPLEX_RADIX_0p23; 417 shift = 0; 418 } else if ((bits&0xffffffff80000000L) == 0) { 419 // Magnitude can fit in 8 bits of precision. 420 radix = TypedValue.COMPLEX_RADIX_8p15; 421 shift = 8; 422 } else if ((bits&0xffffff8000000000L) == 0) { 423 // Magnitude can fit in 16 bits of precision. 424 radix = TypedValue.COMPLEX_RADIX_16p7; 425 shift = 16; 426 } else { 427 // Magnitude needs entire range, so no fractional part. 428 radix = TypedValue.COMPLEX_RADIX_23p0; 429 shift = 23; 430 } 431 int mantissa = (int)( 432 (bits>>shift) & TypedValue.COMPLEX_MANTISSA_MASK); 433 if (neg) { 434 mantissa = (-mantissa) & TypedValue.COMPLEX_MANTISSA_MASK; 435 } 436 outValue.data |= 437 (radix<<TypedValue.COMPLEX_RADIX_SHIFT) 438 | (mantissa<<TypedValue.COMPLEX_MANTISSA_SHIFT); 439 return true; 440 } 441 return false; 442 } 443 444 // make sure it's only spaces at the end. 445 end = end.trim(); 446 447 if (end.length() == 0) { 448 if (outValue != null) { 449 outValue.type = TypedValue.TYPE_FLOAT; 450 outValue.data = Float.floatToIntBits(f); 451 return true; 452 } 453 } 454 } 455 456 return false; 457 } 458 459 private static boolean parseUnit(String str, TypedValue outValue, float[] outScale) { 460 str = str.trim(); 461 462 for (UnitEntry unit : sUnitNames) { 463 if (unit.name.equals(str)) { 464 outValue.type = unit.type; 465 outValue.data = unit.unit << TypedValue.COMPLEX_UNIT_SHIFT; 466 outScale[0] = unit.scale; 467 468 return true; 469 } 470 } 471 472 return false; 473 } 474} 475 476