ContactDrawable.java revision ad215836214c524509aae0f6a1f6c6b1b740634c
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 */ 16package com.android.mail.bitmap; 17 18import android.content.res.Resources; 19import android.content.res.TypedArray; 20import android.graphics.Bitmap; 21import android.graphics.BitmapFactory; 22import android.graphics.BitmapShader; 23import android.graphics.Canvas; 24import android.graphics.Color; 25import android.graphics.ColorFilter; 26import android.graphics.Matrix; 27import android.graphics.Paint; 28import android.graphics.Paint.Align; 29import android.graphics.Rect; 30import android.graphics.Shader; 31import android.graphics.Typeface; 32import android.graphics.drawable.Drawable; 33 34import com.android.bitmap.BitmapCache; 35import com.android.bitmap.RequestKey; 36import com.android.bitmap.ReusableBitmap; 37import com.android.mail.R; 38import com.android.mail.bitmap.ContactResolver.ContactDrawableInterface; 39 40/** 41 * A drawable that encapsulates all the functionality needed to display a contact image, 42 * including request creation/cancelling and data unbinding/re-binding. While no contact images 43 * can be shown, a default letter tile will be shown instead. 44 * 45 * <p/> 46 * The actual contact resolving and decoding is handled by {@link ContactResolver}. 47 */ 48public class ContactDrawable extends Drawable implements ContactDrawableInterface { 49 50 private BitmapCache mCache; 51 private ContactResolver mContactResolver; 52 53 private ContactRequest mContactRequest; 54 private ReusableBitmap mBitmap; 55 private final Paint mPaint; 56 57 /** Letter tile */ 58 private static TypedArray sColors; 59 private static int sColorCount; 60 private static int sDefaultColor; 61 private static int sTileLetterFontSize; 62 private static int sTileFontColor; 63 private static Bitmap DEFAULT_AVATAR; 64 /** Reusable components to avoid new allocations */ 65 private static final Paint sPaint = new Paint(); 66 private static final Rect sRect = new Rect(); 67 private static final char[] sFirstChar = new char[1]; 68 69 private final float mBorderWidth; 70 private final Paint mBitmapPaint; 71 private final Paint mBorderPaint; 72 private final Matrix mMatrix; 73 74 private int mDecodeWidth; 75 private int mDecodeHeight; 76 77 public ContactDrawable(final Resources res) { 78 mPaint = new Paint(); 79 mPaint.setFilterBitmap(true); 80 mPaint.setDither(true); 81 82 mBitmapPaint = new Paint(); 83 mBitmapPaint.setAntiAlias(true); 84 mBitmapPaint.setFilterBitmap(true); 85 mBitmapPaint.setDither(true); 86 87 mBorderWidth = res.getDimensionPixelSize(R.dimen.avatar_border_width); 88 89 mBorderPaint = new Paint(); 90 mBorderPaint.setColor(Color.TRANSPARENT); 91 mBorderPaint.setStyle(Paint.Style.STROKE); 92 mBorderPaint.setStrokeWidth(mBorderWidth); 93 mBorderPaint.setAntiAlias(true); 94 95 mMatrix = new Matrix(); 96 97 if (sColors == null) { 98 sColors = res.obtainTypedArray(R.array.letter_tile_colors); 99 sColorCount = sColors.length(); 100 sDefaultColor = res.getColor(R.color.letter_tile_default_color); 101 sTileLetterFontSize = res.getDimensionPixelSize(R.dimen.tile_letter_font_size); 102 sTileFontColor = res.getColor(R.color.letter_tile_font_color); 103 DEFAULT_AVATAR = BitmapFactory.decodeResource(res, R.drawable.ic_generic_man); 104 105 sPaint.setTypeface(Typeface.create("sans-serif-light", Typeface.NORMAL)); 106 sPaint.setTextAlign(Align.CENTER); 107 sPaint.setAntiAlias(true); 108 } 109 } 110 111 public void setBitmapCache(final BitmapCache cache) { 112 mCache = cache; 113 } 114 115 public void setContactResolver(final ContactResolver contactResolver) { 116 mContactResolver = contactResolver; 117 } 118 119 @Override 120 public void draw(final Canvas canvas) { 121 final Rect bounds = getBounds(); 122 if (!isVisible() || bounds.isEmpty()) { 123 return; 124 } 125 126 if (mBitmap != null && mBitmap.bmp != null) { 127 // Draw sender image. 128 drawBitmap(mBitmap.bmp, mBitmap.getLogicalWidth(), mBitmap.getLogicalHeight(), canvas); 129 } else { 130 // Draw letter tile. 131 drawLetterTile(canvas); 132 } 133 } 134 135 /** 136 * Draw the bitmap onto the canvas at the current bounds taking into account the current scale. 137 */ 138 private void drawBitmap(final Bitmap bitmap, final int width, final int height, 139 final Canvas canvas) { 140 final Rect bounds = getBounds(); 141 // Draw bitmap through shader first. 142 final BitmapShader shader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, 143 Shader.TileMode.CLAMP); 144 mMatrix.reset(); 145 146 // Fit bitmap to bounds. 147 final float boundsWidth = (float) bounds.width(); 148 final float boundsHeight = (float) bounds.height(); 149 final float scale = Math.max(boundsWidth / width, boundsHeight / height); 150 mMatrix.postScale(scale, scale); 151 152 // Translate bitmap to dst bounds. 153 mMatrix.postTranslate(bounds.left, bounds.top); 154 155 shader.setLocalMatrix(mMatrix); 156 mBitmapPaint.setShader(shader); 157 drawCircle(canvas, bounds, mBitmapPaint); 158 159 // Then draw the border. 160 final float radius = bounds.width() / 2f - mBorderWidth / 2; 161 canvas.drawCircle(bounds.centerX(), bounds.centerY(), radius, mBorderPaint); 162 } 163 164 private void drawLetterTile(final Canvas canvas) { 165 if (mContactRequest == null) { 166 return; 167 } 168 169 final Rect bounds = getBounds(); 170 171 // Draw background color. 172 final String email = mContactRequest.getEmail(); 173 sPaint.setColor(pickColor(email)); 174 sPaint.setAlpha(mPaint.getAlpha()); 175 drawCircle(canvas, bounds, sPaint); 176 177 // Draw letter/digit or generic avatar. 178 final String displayName = mContactRequest.getDisplayName(); 179 final char firstChar = displayName.charAt(0); 180 if (isEnglishLetterOrDigit(firstChar)) { 181 // Draw letter or digit. 182 sFirstChar[0] = Character.toUpperCase(firstChar); 183 sPaint.setTextSize(sTileLetterFontSize); 184 sPaint.getTextBounds(sFirstChar, 0, 1, sRect); 185 sPaint.setColor(sTileFontColor); 186 canvas.drawText(sFirstChar, 0, 1, bounds.centerX(), 187 bounds.centerY() + sRect.height() / 2, sPaint); 188 } else { 189 drawBitmap(DEFAULT_AVATAR, DEFAULT_AVATAR.getWidth(), DEFAULT_AVATAR.getHeight(), 190 canvas); 191 } 192 } 193 194 /** 195 * Draws the largest circle that fits within the given <code>bounds</code>. 196 * 197 * @param canvas the canvas on which to draw 198 * @param bounds the bounding box of the circle 199 * @param paint the paint with which to draw 200 */ 201 private static void drawCircle(Canvas canvas, Rect bounds, Paint paint) { 202 canvas.drawCircle(bounds.centerX(), bounds.centerY(), bounds.width() / 2, paint); 203 } 204 205 private static int pickColor(final String email) { 206 // String.hashCode() implementation is not supposed to change across java versions, so 207 // this should guarantee the same email address always maps to the same color. 208 // The email should already have been normalized by the ContactRequest. 209 final int color = Math.abs(email.hashCode()) % sColorCount; 210 return sColors.getColor(color, sDefaultColor); 211 } 212 213 private static boolean isEnglishLetterOrDigit(final char c) { 214 return ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z') || ('0' <= c && c <= '9'); 215 } 216 217 @Override 218 public void setAlpha(final int alpha) { 219 mPaint.setAlpha(alpha); 220 } 221 222 @Override 223 public void setColorFilter(final ColorFilter cf) { 224 mPaint.setColorFilter(cf); 225 } 226 227 @Override 228 public int getOpacity() { 229 return 0; 230 } 231 232 public void setDecodeDimensions(final int decodeWidth, final int decodeHeight) { 233 mDecodeWidth = decodeWidth; 234 mDecodeHeight = decodeHeight; 235 } 236 237 public void unbind() { 238 setImage(null); 239 } 240 241 public void bind(final String name, final String email) { 242 setImage(new ContactRequest(name, email)); 243 } 244 245 private void setImage(final ContactRequest contactRequest) { 246 if (mContactRequest != null && mContactRequest.equals(contactRequest)) { 247 return; 248 } 249 250 if (mBitmap != null) { 251 mBitmap.releaseReference(); 252 mBitmap = null; 253 } 254 255 mContactResolver.remove(mContactRequest, this); 256 mContactRequest = contactRequest; 257 258 if (contactRequest == null) { 259 invalidateSelf(); 260 return; 261 } 262 263 final ReusableBitmap cached = mCache.get(contactRequest, true /* incrementRefCount */); 264 if (cached != null) { 265 setBitmap(cached); 266 } else { 267 decode(); 268 } 269 } 270 271 private void setBitmap(final ReusableBitmap bmp) { 272 if (mBitmap != null && mBitmap != bmp) { 273 mBitmap.releaseReference(); 274 } 275 mBitmap = bmp; 276 invalidateSelf(); 277 } 278 279 private void decode() { 280 if (mContactRequest == null) { 281 return; 282 } 283 // Add to batch. 284 mContactResolver.add(mContactRequest, this); 285 } 286 287 @Override 288 public int getDecodeWidth() { 289 return mDecodeWidth; 290 } 291 292 @Override 293 public int getDecodeHeight() { 294 return mDecodeHeight; 295 } 296 297 @Override 298 public void onDecodeComplete(final RequestKey key, final ReusableBitmap result) { 299 final ContactRequest request = (ContactRequest) key; 300 // Remove from batch. 301 mContactResolver.remove(request, this); 302 if (request.equals(mContactRequest)) { 303 setBitmap(result); 304 } else { 305 // if the requests don't match (i.e. this request is stale), decrement the 306 // ref count to allow the bitmap to be pooled 307 if (result != null) { 308 result.releaseReference(); 309 } 310 } 311 } 312} 313