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