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