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