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 }