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