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