OSDN Git Service

Eleven: Add some caching logic to BitmapWithColors for perf optimization
[android-x86/packages-apps-Eleven.git] / src / com / cyanogenmod / eleven / widgets / LetterTileDrawable.java
1 /*
2  * Copyright (C) 2013 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
17 package com.cyanogenmod.eleven.widgets;
18
19 import android.content.Context;
20 import android.content.res.Resources;
21 import android.content.res.TypedArray;
22 import android.graphics.Bitmap;
23 import android.graphics.BitmapFactory;
24 import android.graphics.Canvas;
25 import android.graphics.ColorFilter;
26 import android.graphics.Paint;
27 import android.graphics.Paint.Align;
28 import android.graphics.Rect;
29 import android.graphics.Typeface;
30 import android.graphics.drawable.Drawable;
31 import android.text.TextUtils;
32
33 import junit.framework.Assert;
34
35 import com.cyanogenmod.eleven.R;
36 import com.cyanogenmod.eleven.cache.ImageWorker.ImageType;
37 import com.cyanogenmod.eleven.utils.BitmapWithColors;
38 import com.cyanogenmod.eleven.utils.MusicUtils;
39
40 /**
41  * A drawable that encapsulates all the functionality needed to display a letter tile to
42  * represent a artist/album/playlist image.
43  */
44 public class LetterTileDrawable extends Drawable {
45
46     private final String TAG = LetterTileDrawable.class.getSimpleName();
47
48     private final Paint mPaint;
49
50     /** Letter tile */
51     private static TypedArray sColors;
52     private static TypedArray sVibrantDarkColors;
53     private static int sDefaultColor;
54     private static int sTileFontColor;
55     private static float sLetterToTileRatio;
56     private static Bitmap DEFAULT_ARTIST;
57     private static Bitmap DEFAULT_ARTIST_LARGE;
58     private static Bitmap DEFAULT_ALBUM;
59     private static Bitmap DEFAULT_ALBUM_LARGE;
60     private static Bitmap DEFAULT_PLAYLIST;
61     private static Bitmap DEFAULT_PLAYLIST_LARGE;
62
63     /** Reusable components to avoid new allocations */
64     private static final Paint sPaint = new Paint();
65     private static final Rect sRect = new Rect();
66     private static final char[] sChars = new char[2];
67
68     private String mDisplayName;
69     private String mIdentifier;
70     private float mScale = 1.0f;
71     private float mOffset = 0.0f;
72     private Resources res;
73     private boolean mIsCircle = false;
74
75     private ImageType mImageType;
76
77     private static synchronized void initializeStaticVariables(final Resources res) {
78         if (sColors == null) {
79             sColors = res.obtainTypedArray(R.array.letter_tile_colors);
80             sVibrantDarkColors = res.obtainTypedArray(R.array.letter_tile_vibrant_dark_colors);
81             sDefaultColor = res.getColor(R.color.letter_tile_default_color);
82             sTileFontColor = res.getColor(R.color.letter_tile_font_color);
83             sLetterToTileRatio = res.getFraction(R.dimen.letter_to_tile_ratio, 1, 1);
84             DEFAULT_ARTIST = BitmapFactory.decodeResource(res, R.drawable.ic_artist);
85             DEFAULT_ARTIST_LARGE = BitmapFactory.decodeResource(res, R.drawable.ic_artist_lg);
86             DEFAULT_ALBUM = BitmapFactory.decodeResource(res, R.drawable.ic_album);
87             DEFAULT_ALBUM_LARGE = BitmapFactory.decodeResource(res, R.drawable.ic_album_lg);
88             DEFAULT_PLAYLIST = BitmapFactory.decodeResource(res, R.drawable.ic_playlist);
89             DEFAULT_PLAYLIST_LARGE = BitmapFactory.decodeResource(res, R.drawable.ic_playlist_lg);
90
91             sPaint.setTypeface(Typeface.create(
92                     res.getString(R.string.letter_tile_letter_font_family), Typeface.NORMAL));
93             sPaint.setTextAlign(Align.CENTER);
94             sPaint.setAntiAlias(true);
95         }
96     }
97
98     public LetterTileDrawable(final Context context) {
99         mPaint = new Paint();
100         mPaint.setFilterBitmap(true);
101         mPaint.setDither(true);
102         res = context.getResources();
103
104         initializeStaticVariables(res);
105     }
106
107     @Override
108     public void draw(final Canvas canvas) {
109         //setBounds(0, 0, 120, 120);
110         final Rect bounds = getBounds();
111         if (!isVisible() || bounds.isEmpty()) {
112             return;
113         }
114         // Draw letter tile.
115         drawLetterTile(canvas);
116     }
117
118     @Override
119     public void setBounds(Rect bounds) {
120         super.setBounds(bounds);
121     }
122
123     private void drawLetterTile(final Canvas canvas) {
124         // Draw background color.
125         sPaint.setColor(pickColor(mIdentifier));
126
127         sPaint.setAlpha(mPaint.getAlpha());
128         final Rect bounds = getBounds();
129         final int minDimension = Math.min(bounds.width(), bounds.height());
130
131         if (mIsCircle) {
132             canvas.drawCircle(bounds.centerX(), bounds.centerY(), minDimension / 2, sPaint);
133         } else {
134             canvas.drawRect(bounds, sPaint);
135         }
136
137         // Draw letter/digit only if the first character is an english letter
138         if (mDisplayName != null
139                 && isEnglishLetter(mDisplayName.charAt(0))) {
140             int numChars = 1;
141
142             // Draw letter or digit.
143             sChars[0] = Character.toUpperCase(mDisplayName.charAt(0));
144
145             if (mDisplayName.length() > 1 && isEnglishLetter(mDisplayName.charAt(1))) {
146                 sChars[1] = Character.toLowerCase(mDisplayName.charAt(1));
147                 numChars = 2;
148             }
149
150             // Scale text by canvas bounds and user selected scaling factor
151             sPaint.setTextSize(mScale * sLetterToTileRatio * minDimension);
152             //sPaint.setTextSize(sTileLetterFontSize);
153             sPaint.getTextBounds(sChars, 0, numChars, sRect);
154             sPaint.setColor(sTileFontColor);
155
156             // Draw the letter in the canvas, vertically shifted up or down by the user-defined
157             // offset
158             canvas.drawText(sChars, 0, numChars, bounds.centerX(),
159                     bounds.centerY() + mOffset * bounds.height() + sRect.height() / 2,
160                     sPaint);
161         } else {
162             // Draw the default image if there is no letter/digit to be drawn
163             final Bitmap bitmap = getDefaultBitmapForImageType(mImageType, bounds);
164
165             // The bitmap should be drawn in the middle of the canvas without changing its width to
166             // height ratio.
167             final Rect destRect = copyBounds();
168
169             drawBitmap(bitmap, bitmap.getWidth(), bitmap.getHeight(), canvas, destRect, mScale,
170                     mOffset, mPaint);
171         }
172     }
173
174     public int getColor() {
175         return pickColor(mIdentifier);
176     }
177
178     /**
179      * @return the corresponding index in the color palette based on the identifier
180      */
181     private static int getColorIndex(final String identifier) {
182         if (TextUtils.isEmpty(identifier)) {
183             return -1;
184         }
185
186         return Math.abs(identifier.hashCode()) % sColors.length();
187     }
188
189     /**
190      * Returns a deterministic color based on the provided contact identifier string.
191      */
192     private static int pickColor(final String identifier) {
193         final int idx = getColorIndex(identifier);
194         if (idx == -1) {
195             return sDefaultColor;
196         }
197
198         return sColors.getColor(idx, sDefaultColor);
199     }
200
201     /**
202      * Returns the vibrant matching color based on the provided contact identifier string.
203      */
204     private static int pickVibrantDarkColor(final String identifier) {
205         final int idx = getColorIndex(identifier);
206         if (idx == -1) {
207             return sDefaultColor;
208         }
209
210         return sVibrantDarkColors.getColor(idx, sDefaultColor);
211     }
212
213     /**
214      * Gets the default image to show for the image type.  If the bounds are large,
215      * it will use the large default bitmap
216      */
217     private static Bitmap getDefaultBitmapForImageType(ImageType type, Rect bounds) {
218         Bitmap ret = getDefaultBitmap(type, true);
219         if (Math.max(bounds.width(), bounds.height()) > Math.max(ret.getWidth(), ret.getHeight())) {
220             ret = getDefaultBitmap(type, false);
221         }
222
223         return ret;
224     }
225
226     private static Bitmap getDefaultBitmap(ImageType type, boolean small) {
227         switch (type) {
228             case ARTIST:
229                 return small ? DEFAULT_ARTIST : DEFAULT_ARTIST_LARGE;
230             case ALBUM:
231                 return small ? DEFAULT_ALBUM : DEFAULT_ALBUM_LARGE;
232              case PLAYLIST:
233                  return small ? DEFAULT_PLAYLIST : DEFAULT_PLAYLIST_LARGE;
234             default:
235                 throw new IllegalArgumentException("Unrecognized image type");
236         }
237     }
238
239     private static boolean isEnglishLetter(final char c) {
240         return ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z') || ('0' <= c && c <= '9');
241     }
242
243     @Override
244     public void setAlpha(final int alpha) {
245         mPaint.setAlpha(alpha);
246     }
247
248     @Override
249     public void setColorFilter(final ColorFilter cf) {
250         mPaint.setColorFilter(cf);
251     }
252
253     @Override
254     public int getOpacity() {
255         return android.graphics.PixelFormat.OPAQUE;
256     }
257
258     /**
259      * Scale the drawn letter tile to a ratio of its default size
260      *
261      * @param scale The ratio the letter tile should be scaled to as a percentage of its default
262      * size, from a scale of 0 to 2.0f. The default is 1.0f.
263      */
264     public void setScale(float scale) {
265         mScale = scale;
266     }
267
268     /**
269      * Assigns the vertical offset of the position of the letter tile to the ContactDrawable
270      *
271      * @param offset The provided offset must be within the range of -0.5f to 0.5f.
272      * If set to -0.5f, the letter will be shifted upwards by 0.5 times the height of the canvas
273      * it is being drawn on, which means it will be drawn with the center of the letter starting
274      * at the top edge of the canvas.
275      * If set to 0.5f, the letter will be shifted downwards by 0.5 times the height of the canvas
276      * it is being drawn on, which means it will be drawn with the center of the letter starting
277      * at the bottom edge of the canvas.
278      * The default is 0.0f.
279      */
280     public void setOffset(float offset) {
281         Assert.assertTrue(offset >= -0.5f && offset <= 0.5f);
282         mOffset = offset;
283     }
284
285     /**
286      * Sets the tile data used to determine the display text and color
287      * @param displayName the name to display - Some logic will be applied to do some trimming
288      *                    and up to the first two letters will be displayed
289      * @param identifier the identifier used to determine the color of the background.  For
290      *                   album, use albumId, for artist use artistName and for playlist use
291      *                   playlistId
292      * @param type the type of item that this tile drawable corresponds to
293      */
294     public void setTileDetails(final String displayName, final String identifier,
295                                final ImageType type) {
296         mDisplayName = MusicUtils.getTrimmedName(displayName);
297         mIdentifier = MusicUtils.getTrimmedName(identifier);
298         mImageType = type;
299         invalidateSelf();
300     }
301
302     public void setIsCircular(boolean isCircle) {
303         mIsCircle = isCircle;
304     }
305
306     /**
307      * Draw the bitmap onto the canvas at the current bounds taking into account the current scale.
308      */
309     private static void drawBitmap(final Bitmap bitmap, final int width, final int height,
310                                    final Canvas canvas, final Rect destRect, final float scale,
311                                    final float offset, final Paint paint) {
312         // Crop the destination bounds into a square, scaled and offset as appropriate
313         final int halfLength = (int) (scale * Math.min(destRect.width(), destRect.height()) / 2);
314
315         destRect.set(destRect.centerX() - halfLength,
316                 (int) (destRect.centerY() - halfLength + offset * destRect.height()),
317                 destRect.centerX() + halfLength,
318                 (int) (destRect.centerY() + halfLength + offset * destRect.height()));
319
320         // Source rectangle remains the entire bounds of the source bitmap.
321         sRect.set(0, 0, width, height);
322
323         canvas.drawBitmap(bitmap, sRect, destRect, paint);
324     }
325
326     /**
327      * Draws the default letter tile drawable for the image type to a bitmap
328      * @param identifier the identifier used to determine the color of the background.  For
329      *                   album, use albumId, for artist use artistName and for playlist use
330      *                   playlistId
331      * @param type the type of item that this tile drawable corresponds to
332      * @param isCircle whether to draw a circle or a square
333      * @param smallArtwork true if you want to draw a smaller version of the default bitmap for
334      *                     perf/memory reasons
335      */
336     public static BitmapWithColors createDefaultBitmap(Context context, String identifier,
337             ImageType type, boolean isCircle, boolean smallArtwork) {
338         initializeStaticVariables(context.getResources());
339         
340         identifier = MusicUtils.getTrimmedName(identifier);
341
342         // get the default bitmap to determine what to draw to
343         Bitmap defaultBitmap = getDefaultBitmap(type, smallArtwork);
344         final Rect bounds = new Rect(0, 0, defaultBitmap.getWidth(), defaultBitmap.getHeight());
345
346         // create a bitmap and canvas for drawing
347         Bitmap createdBitmap = Bitmap.createBitmap(defaultBitmap.getWidth(),
348                 defaultBitmap.getHeight(), defaultBitmap.getConfig());
349         Canvas canvas = new Canvas(createdBitmap);
350
351         int color = pickColor(identifier);
352         int vibrantDarkColor = pickVibrantDarkColor(identifier);
353
354         Paint paint = new Paint();
355         paint.setColor(color);
356
357         final int minDimension = Math.min(bounds.width(), bounds.height());
358
359         if (isCircle) {
360             canvas.drawCircle(bounds.centerX(), bounds.centerY(), minDimension / 2, paint);
361         } else {
362             canvas.drawRect(bounds, paint);
363         }
364
365         // draw to the bitmap
366         drawBitmap(defaultBitmap, defaultBitmap.getWidth(), defaultBitmap.getHeight(), canvas,
367                 bounds, 1, 0, paint);
368
369         return new BitmapWithColors(createdBitmap, identifier.hashCode(), color, vibrantDarkColor);
370     }
371 }