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