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