• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 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 package com.android.car.apps.common;
17 
18 import android.annotation.Nullable;
19 import android.content.res.Resources;
20 import android.content.res.TypedArray;
21 import android.graphics.Bitmap;
22 import android.graphics.Canvas;
23 import android.graphics.ColorFilter;
24 import android.graphics.Paint;
25 import android.graphics.Paint.Align;
26 import android.graphics.PorterDuff;
27 import android.graphics.Rect;
28 import android.graphics.Typeface;
29 import android.graphics.drawable.Drawable;
30 import android.text.TextUtils;
31 
32 /**
33  * A drawable that encapsulates all the functionality needed to display a letter tile to
34  * represent a contact image.
35  */
36 public class LetterTileDrawable extends Drawable {
37     /** Letter tile */
38     private static int[] sColors;
39     private static int sDefaultColor;
40     private static int sTileFontColor;
41     private static float sLetterToTileRatio;
42     private static Drawable sDefaultPersonAvatar;
43     private static Drawable sDefaultBusinessAvatar;
44     private static Drawable sDefaultVoicemailAvatar;
45 
46     /** Reusable components to avoid new allocations */
47     private static final Paint sPaint = new Paint();
48     private static final Rect sRect = new Rect();
49     private static final char[] sFirstChar = new char[1];
50 
51     /** Contact type constants */
52     public static final int TYPE_PERSON = 1;
53     public static final int TYPE_BUSINESS = 2;
54     public static final int TYPE_VOICEMAIL = 3;
55     public static final int TYPE_DEFAULT = TYPE_PERSON;
56 
57     private final Paint mPaint;
58 
59     @Nullable private String mDisplayName;
60     private int mColor;
61     private int mContactType = TYPE_DEFAULT;
62     private float mScale = 1.0f;
63     private float mOffset = 0.0f;
64     private boolean mIsCircle = false;
65 
66     // TODO(rogerxue): the use pattern for this class is always:
67     // create LTD, setContactDetails(), setIsCircular(true). merge them into ctor.
LetterTileDrawable(final Resources res)68     public LetterTileDrawable(final Resources res) {
69         mPaint = new Paint();
70         mPaint.setFilterBitmap(true);
71         mPaint.setDither(true);
72         setScale(0.7f);
73 
74         if (sColors == null) {
75             sDefaultColor = res.getColor(R.color.letter_tile_default_color);
76             TypedArray ta = res.obtainTypedArray(R.array.letter_tile_colors);
77             if (ta.length() == 0) {
78                 // TODO(dnotario). Looks like robolectric shadow doesn't currently support
79                 // obtainTypedArray and always returns length 0 array, which will make some code
80                 // below that does a division by length of sColors choke. Workaround by creating
81                 // an array of length 1. A more proper fix tracked by b/26518438.
82                 sColors = new int[] { sDefaultColor };
83 
84             } else {
85                 sColors = new int[ta.length()];
86                 for (int i = ta.length() - 1; i >= 0; i--) {
87                     sColors[i] = ta.getColor(i, sDefaultColor);
88                 }
89                 ta.recycle();
90             }
91 
92             sTileFontColor = res.getColor(R.color.letter_tile_font_color);
93             sLetterToTileRatio = res.getFraction(R.fraction.letter_to_tile_ratio, 1, 1);
94             // TODO: get images for business and voicemail
95             sDefaultPersonAvatar = res.getDrawable(R.drawable.ic_person, null /* theme */);
96             sDefaultBusinessAvatar = res.getDrawable(R.drawable.ic_person, null /* theme */);
97             sDefaultVoicemailAvatar = res.getDrawable(R.drawable.ic_person, null /* theme */);
98             sPaint.setTypeface(Typeface.create("sans-serif-light", Typeface.NORMAL));
99             sPaint.setTextAlign(Align.CENTER);
100             sPaint.setAntiAlias(true);
101         }
102     }
103 
104     @Override
draw(final Canvas canvas)105     public void draw(final Canvas canvas) {
106         final Rect bounds = getBounds();
107         if (!isVisible() || bounds.isEmpty()) {
108             return;
109         }
110         // Draw letter tile.
111         drawLetterTile(canvas);
112     }
113 
114     /**
115      * Draw the drawable onto the canvas at the current bounds taking into account the current
116      * scale.
117      */
drawDrawableOnCanvas(final Drawable drawable, final Canvas canvas)118     private void drawDrawableOnCanvas(final Drawable drawable, final Canvas canvas) {
119         // The drawable should be drawn in the middle of the canvas without changing its width to
120         // height ratio.
121         final Rect destRect = copyBounds();
122 
123         // Crop the destination bounds into a square, scaled and offset as appropriate
124         final int halfLength = (int) (mScale * Math.min(destRect.width(), destRect.height()) / 2);
125 
126         destRect.set(destRect.centerX() - halfLength,
127                 (int) (destRect.centerY() - halfLength + mOffset * destRect.height()),
128                 destRect.centerX() + halfLength,
129                 (int) (destRect.centerY() + halfLength + mOffset * destRect.height()));
130 
131         drawable.setAlpha(mPaint.getAlpha());
132         drawable.setColorFilter(sTileFontColor, PorterDuff.Mode.SRC_IN);
133         drawable.setBounds(destRect);
134         drawable.draw(canvas);
135     }
136 
drawLetterTile(final Canvas canvas)137     private void drawLetterTile(final Canvas canvas) {
138         // Draw background color.
139         sPaint.setColor(mColor);
140 
141         sPaint.setAlpha(mPaint.getAlpha());
142         final Rect bounds = getBounds();
143         final int minDimension = Math.min(bounds.width(), bounds.height());
144 
145         if (mIsCircle) {
146             canvas.drawCircle(bounds.centerX(), bounds.centerY(), minDimension / 2, sPaint);
147         } else {
148             canvas.drawRect(bounds, sPaint);
149         }
150 
151         // Draw letter/digit only if the first character is an english letter
152         if (!TextUtils.isEmpty(mDisplayName) && isEnglishLetter(mDisplayName.charAt(0))) {
153             // Draw letter or digit.
154             sFirstChar[0] = Character.toUpperCase(mDisplayName.charAt(0));
155 
156             // Scale text by canvas bounds and user selected scaling factor
157             sPaint.setTextSize(mScale * sLetterToTileRatio * minDimension);
158             //sPaint.setTextSize(sTileLetterFontSize);
159             sPaint.getTextBounds(sFirstChar, 0, 1, sRect);
160             sPaint.setColor(sTileFontColor);
161 
162             // Draw the letter in the canvas, vertically shifted up or down by the user-defined
163             // offset
164             canvas.drawText(sFirstChar, 0, 1, bounds.centerX(),
165                     bounds.centerY() + mOffset * bounds.height() + sRect.height() / 2,
166                     sPaint);
167         } else {
168             // Draw the default image if there is no letter/digit to be drawn
169             final Drawable drawable = getDrawablepForContactType(mContactType);
170             drawDrawableOnCanvas(drawable, canvas);
171         }
172     }
173 
getColor()174     public int getColor() {
175         return mColor;
176     }
177 
178     /**
179      * Returns a deterministic color based on the provided contact identifier string.
180      */
pickColor(final String identifier)181     private int pickColor(final String identifier) {
182         if (TextUtils.isEmpty(identifier) || mContactType == TYPE_VOICEMAIL) {
183             return sDefaultColor;
184         }
185         // String.hashCode() implementation is not supposed to change across java versions, so
186         // this should guarantee the same email address always maps to the same color.
187         // The email should already have been normalized by the ContactRequest.
188         final int color = Math.abs(identifier.hashCode()) % sColors.length;
189         return sColors[color];
190     }
191 
getDrawablepForContactType(int contactType)192     private static Drawable getDrawablepForContactType(int contactType) {
193         switch (contactType) {
194             case TYPE_PERSON:
195                 return sDefaultPersonAvatar;
196             case TYPE_BUSINESS:
197                 return sDefaultBusinessAvatar;
198             case TYPE_VOICEMAIL:
199                 return sDefaultVoicemailAvatar;
200             default:
201                 return sDefaultPersonAvatar;
202         }
203     }
204 
isEnglishLetter(final char c)205     private static boolean isEnglishLetter(final char c) {
206         return ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z');
207     }
208 
209     @Override
setAlpha(final int alpha)210     public void setAlpha(final int alpha) {
211         mPaint.setAlpha(alpha);
212     }
213 
214     @Override
setColorFilter(final ColorFilter cf)215     public void setColorFilter(final ColorFilter cf) {
216         mPaint.setColorFilter(cf);
217     }
218 
219     @Override
getOpacity()220     public int getOpacity() {
221         return android.graphics.PixelFormat.OPAQUE;
222     }
223 
224     /**
225      * Scale the drawn letter tile to a ratio of its default size
226      *
227      * @param scale The ratio the letter tile should be scaled to as a percentage of its default
228      * size, from a scale of 0 to 2.0f. The default is 1.0f.
229      */
setScale(float scale)230     public void setScale(float scale) {
231         mScale = scale;
232     }
233 
234     /**
235      * Assigns the vertical offset of the position of the letter tile to the ContactDrawable
236      *
237      * @param offset The provided offset must be within the range of -0.5f to 0.5f.
238      * If set to -0.5f, the letter will be shifted upwards by 0.5 times the height of the canvas
239      * it is being drawn on, which means it will be drawn with the center of the letter starting
240      * at the top edge of the canvas.
241      * If set to 0.5f, the letter will be shifted downwards by 0.5 times the height of the canvas
242      * it is being drawn on, which means it will be drawn with the center of the letter starting
243      * at the bottom edge of the canvas.
244      * The default is 0.0f.
245      */
setOffset(float offset)246     public void setOffset(float offset) {
247         mOffset = offset;
248     }
249 
setContactDetails(@ullable String displayName, String identifier)250     public void setContactDetails(@Nullable String displayName, String identifier) {
251         mDisplayName = displayName;
252         mColor = pickColor(identifier);
253     }
254 
setContactType(int contactType)255     public void setContactType(int contactType) {
256         mContactType = contactType;
257     }
258 
setIsCircular(boolean isCircle)259     public void setIsCircular(boolean isCircle) {
260         mIsCircle = isCircle;
261     }
262 
263     /**
264      * Convert the drawable to a bitmap.
265      * @param size The target size of the bitmap.
266      * @return A bitmap representation of the drawable.
267      */
toBitmap(int size)268     public Bitmap toBitmap(int size) {
269         Bitmap largeIcon = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
270         Canvas canvas = new Canvas(largeIcon);
271         Rect bounds = getBounds();
272         setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
273         draw(canvas);
274         setBounds(bounds);
275         return largeIcon;
276     }
277 }