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.android.contacts.common.lettertiles;
18 
19 import android.content.res.Resources;
20 import android.content.res.TypedArray;
21 import android.graphics.Bitmap;
22 import android.graphics.BitmapFactory;
23 import android.graphics.Canvas;
24 import android.graphics.ColorFilter;
25 import android.graphics.Paint;
26 import android.graphics.Paint.Align;
27 import android.graphics.Rect;
28 import android.graphics.Typeface;
29 import android.graphics.drawable.Drawable;
30 import android.text.TextUtils;
31 
32 import com.android.contacts.common.R;
33 
34 import junit.framework.Assert;
35 
36 /**
37  * A drawable that encapsulates all the functionality needed to display a letter tile to
38  * represent a contact image.
39  */
40 public class LetterTileDrawable extends Drawable {
41 
42     private final String TAG = LetterTileDrawable.class.getSimpleName();
43 
44     private final Paint mPaint;
45 
46     /** Letter tile */
47     private static TypedArray sColors;
48     private static int sDefaultColor;
49     private static int sTileFontColor;
50     private static float sLetterToTileRatio;
51     private static Bitmap DEFAULT_PERSON_AVATAR;
52     private static Bitmap DEFAULT_BUSINESS_AVATAR;
53     private static Bitmap DEFAULT_VOICEMAIL_AVATAR;
54 
55     /** Reusable components to avoid new allocations */
56     private static final Paint sPaint = new Paint();
57     private static final Rect sRect = new Rect();
58     private static final char[] sFirstChar = new char[1];
59 
60     /** Contact type constants */
61     public static final int TYPE_PERSON = 1;
62     public static final int TYPE_BUSINESS = 2;
63     public static final int TYPE_VOICEMAIL = 3;
64     public static final int TYPE_DEFAULT = TYPE_PERSON;
65 
66     private int mContactType = TYPE_DEFAULT;
67     private float mScale = 1.0f;
68     private float mOffset = 0.0f;
69     private boolean mIsCircle = false;
70 
71     private int mColor;
72     private Character mLetter = null;
73 
LetterTileDrawable(final Resources res)74     public LetterTileDrawable(final Resources res) {
75         if (sColors == null) {
76             sColors = res.obtainTypedArray(R.array.letter_tile_colors);
77             sDefaultColor = res.getColor(R.color.letter_tile_default_color);
78             sTileFontColor = res.getColor(R.color.letter_tile_font_color);
79             sLetterToTileRatio = res.getFraction(R.dimen.letter_to_tile_ratio, 1, 1);
80             DEFAULT_PERSON_AVATAR = BitmapFactory.decodeResource(res,
81                     R.drawable.ic_person_white_120dp);
82             DEFAULT_BUSINESS_AVATAR = BitmapFactory.decodeResource(res,
83                     R.drawable.ic_business_white_120dp);
84             DEFAULT_VOICEMAIL_AVATAR = BitmapFactory.decodeResource(res,
85                     R.drawable.ic_voicemail_avatar);
86             sPaint.setTypeface(Typeface.create(
87                     res.getString(R.string.letter_tile_letter_font_family), Typeface.NORMAL));
88             sPaint.setTextAlign(Align.CENTER);
89             sPaint.setAntiAlias(true);
90         }
91         mPaint = new Paint();
92         mPaint.setFilterBitmap(true);
93         mPaint.setDither(true);
94         mColor = sDefaultColor;
95     }
96 
97     @Override
draw(final Canvas canvas)98     public void draw(final Canvas canvas) {
99         final Rect bounds = getBounds();
100         if (!isVisible() || bounds.isEmpty()) {
101             return;
102         }
103         // Draw letter tile.
104         drawLetterTile(canvas);
105     }
106 
107     /**
108      * Draw the bitmap onto the canvas at the current bounds taking into account the current scale.
109      */
drawBitmap(final Bitmap bitmap, final int width, final int height, final Canvas canvas)110     private void drawBitmap(final Bitmap bitmap, final int width, final int height,
111             final Canvas canvas) {
112         // The bitmap should be drawn in the middle of the canvas without changing its width to
113         // height ratio.
114         final Rect destRect = copyBounds();
115 
116         // Crop the destination bounds into a square, scaled and offset as appropriate
117         final int halfLength = (int) (mScale * Math.min(destRect.width(), destRect.height()) / 2);
118 
119         destRect.set(destRect.centerX() - halfLength,
120                 (int) (destRect.centerY() - halfLength + mOffset * destRect.height()),
121                 destRect.centerX() + halfLength,
122                 (int) (destRect.centerY() + halfLength + mOffset * destRect.height()));
123 
124         // Source rectangle remains the entire bounds of the source bitmap.
125         sRect.set(0, 0, width, height);
126 
127         canvas.drawBitmap(bitmap, sRect, destRect, mPaint);
128     }
129 
drawLetterTile(final Canvas canvas)130     private void drawLetterTile(final Canvas canvas) {
131         // Draw background color.
132         sPaint.setColor(mColor);
133 
134         sPaint.setAlpha(mPaint.getAlpha());
135         final Rect bounds = getBounds();
136         final int minDimension = Math.min(bounds.width(), bounds.height());
137 
138         if (mIsCircle) {
139             canvas.drawCircle(bounds.centerX(), bounds.centerY(), minDimension / 2, sPaint);
140         } else {
141             canvas.drawRect(bounds, sPaint);
142         }
143 
144         // Draw letter/digit only if the first character is an english letter or there's a override
145 
146         if (mLetter != null) {
147             // Draw letter or digit.
148             sFirstChar[0] = mLetter;
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(sFirstChar, 0, 1, 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(sFirstChar, 0, 1, bounds.centerX(),
159                     bounds.centerY() + mOffset * bounds.height() - sRect.exactCenterY(),
160                     sPaint);
161         } else {
162             // Draw the default image if there is no letter/digit to be drawn
163             final Bitmap bitmap = getBitmapForContactType(mContactType);
164             drawBitmap(bitmap, bitmap.getWidth(), bitmap.getHeight(),
165                     canvas);
166         }
167     }
168 
getColor()169     public int getColor() {
170         return mColor;
171     }
172 
173     /**
174      * Returns a deterministic color based on the provided contact identifier string.
175      */
pickColor(final String identifier)176     private int pickColor(final String identifier) {
177         if (TextUtils.isEmpty(identifier) || mContactType == TYPE_VOICEMAIL) {
178             return sDefaultColor;
179         }
180         // String.hashCode() implementation is not supposed to change across java versions, so
181         // this should guarantee the same email address always maps to the same color.
182         // The email should already have been normalized by the ContactRequest.
183         final int color = Math.abs(identifier.hashCode()) % sColors.length();
184         return sColors.getColor(color, sDefaultColor);
185     }
186 
getBitmapForContactType(int contactType)187     private static Bitmap getBitmapForContactType(int contactType) {
188         switch (contactType) {
189             case TYPE_PERSON:
190                 return DEFAULT_PERSON_AVATAR;
191             case TYPE_BUSINESS:
192                 return DEFAULT_BUSINESS_AVATAR;
193             case TYPE_VOICEMAIL:
194                 return DEFAULT_VOICEMAIL_AVATAR;
195             default:
196                 return DEFAULT_PERSON_AVATAR;
197         }
198     }
199 
isEnglishLetter(final char c)200     private static boolean isEnglishLetter(final char c) {
201         return ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z');
202     }
203 
204     @Override
setAlpha(final int alpha)205     public void setAlpha(final int alpha) {
206         mPaint.setAlpha(alpha);
207     }
208 
209     @Override
setColorFilter(final ColorFilter cf)210     public void setColorFilter(final ColorFilter cf) {
211         mPaint.setColorFilter(cf);
212     }
213 
214     @Override
getOpacity()215     public int getOpacity() {
216         return android.graphics.PixelFormat.OPAQUE;
217     }
218 
219     /**
220      * Scale the drawn letter tile to a ratio of its default size
221      *
222      * @param scale The ratio the letter tile should be scaled to as a percentage of its default
223      * size, from a scale of 0 to 2.0f. The default is 1.0f.
224      */
setScale(float scale)225     public LetterTileDrawable setScale(float scale) {
226         mScale = scale;
227         return this;
228     }
229 
230     /**
231      * Assigns the vertical offset of the position of the letter tile to the ContactDrawable
232      *
233      * @param offset The provided offset must be within the range of -0.5f to 0.5f.
234      * If set to -0.5f, the letter will be shifted upwards by 0.5 times the height of the canvas
235      * it is being drawn on, which means it will be drawn with the center of the letter starting
236      * at the top edge of the canvas.
237      * If set to 0.5f, the letter will be shifted downwards by 0.5 times the height of the canvas
238      * it is being drawn on, which means it will be drawn with the center of the letter starting
239      * at the bottom edge of the canvas.
240      * The default is 0.0f.
241      */
setOffset(float offset)242     public LetterTileDrawable setOffset(float offset) {
243         Assert.assertTrue(offset >= -0.5f && offset <= 0.5f);
244         mOffset = offset;
245         return this;
246     }
247 
setLetter(Character letter)248     public LetterTileDrawable setLetter(Character letter){
249         mLetter = letter;
250         return this;
251     }
252 
setColor(int color)253     public LetterTileDrawable setColor(int color){
254         mColor = color;
255         return this;
256     }
257 
setLetterAndColorFromContactDetails(final String displayName, final String identifier)258     public LetterTileDrawable setLetterAndColorFromContactDetails(final String displayName,
259             final String identifier) {
260         if (displayName != null && displayName.length() > 0
261                 && isEnglishLetter(displayName.charAt(0))) {
262             mLetter = Character.toUpperCase(displayName.charAt(0));
263         }else{
264             mLetter = null;
265         }
266         mColor = pickColor(identifier);
267         return this;
268     }
269 
setContactType(int contactType)270     public LetterTileDrawable setContactType(int contactType) {
271         mContactType = contactType;
272         return this;
273     }
274 
setIsCircular(boolean isCircle)275     public LetterTileDrawable setIsCircular(boolean isCircle) {
276         mIsCircle = isCircle;
277         return this;
278     }
279 }
280