ContactDrawable.java revision a71d756a0e80f63b8b4ebd91451c3a79929e1a4e
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.Canvas; 23import android.graphics.ColorFilter; 24import android.graphics.Paint; 25import android.graphics.Paint.Align; 26import android.graphics.Rect; 27import android.graphics.Typeface; 28import android.graphics.drawable.Drawable; 29 30import com.android.oldbitmap.BitmapCache; 31import com.android.oldbitmap.DecodeTask.Request; 32import com.android.oldbitmap.ReusableBitmap; 33import com.android.mail.R; 34import com.android.mail.bitmap.ContactResolver.ContactDrawableInterface; 35 36/** 37 * A drawable that encapsulates all the functionality needed to display a contact image, 38 * including request creation/cancelling and data unbinding/re-binding. While no contact images 39 * can be shown, a default letter tile will be shown instead. 40 * 41 * <p/> 42 * The actual contact resolving and decoding is handled by {@link ContactResolver}. 43 */ 44public class ContactDrawable extends Drawable implements ContactDrawableInterface { 45 46 private final BitmapCache mCache; 47 private final ContactResolver mContactResolver; 48 49 private ContactRequest mContactRequest; 50 private ReusableBitmap mBitmap; 51 private final Paint mPaint; 52 private int mScale; 53 54 /** Letter tile */ 55 private static TypedArray sColors; 56 private static int sDefaultColor; 57 private static int sTileLetterFontSize; 58 private static int sTileLetterFontSizeSmall; 59 private static int sTileFontColor; 60 private static Bitmap DEFAULT_AVATAR; 61 /** Reusable components to avoid new allocations */ 62 private static final Paint sPaint = new Paint(); 63 private static final Rect sRect = new Rect(); 64 private static final char[] sFirstChar = new char[1]; 65 66 /** This should match the total number of colors defined in colors.xml for letter_tile_color */ 67 private static final int NUM_OF_TILE_COLORS = 8; 68 69 public ContactDrawable(final Resources res, final BitmapCache cache, 70 final ContactResolver contactResolver) { 71 mCache = cache; 72 mContactResolver = contactResolver; 73 mPaint = new Paint(); 74 mPaint.setFilterBitmap(true); 75 mPaint.setDither(true); 76 77 if (sColors == null) { 78 sColors = res.obtainTypedArray(R.array.letter_tile_colors); 79 sDefaultColor = res.getColor(R.color.letter_tile_default_color); 80 sTileLetterFontSize = res.getDimensionPixelSize(R.dimen.tile_letter_font_size); 81 sTileLetterFontSizeSmall = res 82 .getDimensionPixelSize(R.dimen.tile_letter_font_size_small); 83 sTileFontColor = res.getColor(R.color.letter_tile_font_color); 84 DEFAULT_AVATAR = BitmapFactory.decodeResource(res, R.drawable.ic_generic_man); 85 86 sPaint.setTypeface(Typeface.create("sans-serif-light", Typeface.NORMAL)); 87 sPaint.setTextAlign(Align.CENTER); 88 sPaint.setAntiAlias(true); 89 } 90 } 91 92 @Override 93 public void draw(final Canvas canvas) { 94 final Rect bounds = getBounds(); 95 if (!isVisible() || bounds.isEmpty()) { 96 return; 97 } 98 99 if (mBitmap != null && mBitmap.bmp != null) { 100 // Draw sender image. 101 drawBitmap(mBitmap.bmp, mBitmap.getLogicalWidth(), mBitmap.getLogicalHeight(), canvas); 102 } else { 103 // Draw letter tile. 104 drawLetterTile(canvas); 105 } 106 } 107 108 /** 109 * Draw the bitmap onto the canvas at the current bounds taking into account the current scale. 110 */ 111 private void drawBitmap(final Bitmap bitmap, final int width, final int height, 112 final Canvas canvas) { 113 final Rect bounds = getBounds(); 114 115 if (mScale != ContactGridDrawable.SCALE_TYPE_HALF) { 116 sRect.set(0, 0, width, height); 117 } else { 118 // For skinny bounds, draw the middle two quarters. 119 sRect.set(width / 4, 0, width / 4 * 3, height); 120 } 121 canvas.drawBitmap(bitmap, sRect, bounds, mPaint); 122 } 123 124 private void drawLetterTile(final Canvas canvas) { 125 if (mContactRequest == null) { 126 return; 127 } 128 129 // Draw background color. 130 final String email = mContactRequest.getEmail(); 131 sPaint.setColor(pickColor(email)); 132 sPaint.setAlpha(mPaint.getAlpha()); 133 canvas.drawRect(getBounds(), sPaint); 134 135 // Draw letter/digit or generic avatar. 136 final String displayName = mContactRequest.getDisplayName(); 137 final char firstChar = displayName.charAt(0); 138 final Rect bounds = getBounds(); 139 if (isEnglishLetterOrDigit(firstChar)) { 140 // Draw letter or digit. 141 sFirstChar[0] = Character.toUpperCase(firstChar); 142 sPaint.setTextSize(mScale == ContactGridDrawable.SCALE_TYPE_ONE ? sTileLetterFontSize 143 : sTileLetterFontSizeSmall); 144 sPaint.getTextBounds(sFirstChar, 0, 1, sRect); 145 sPaint.setColor(sTileFontColor); 146 canvas.drawText(sFirstChar, 0, 1, bounds.centerX(), 147 bounds.centerY() + sRect.height() / 2, sPaint); 148 } else { 149 drawBitmap(DEFAULT_AVATAR, DEFAULT_AVATAR.getWidth(), DEFAULT_AVATAR.getHeight(), 150 canvas); 151 } 152 } 153 154 private static int pickColor(final String email) { 155 // String.hashCode() implementation is not supposed to change across java versions, so 156 // this should guarantee the same email address always maps to the same color. 157 // The email should already have been normalized by the ContactRequest. 158 final int color = Math.abs(email.hashCode()) % NUM_OF_TILE_COLORS; 159 return sColors.getColor(color, sDefaultColor); 160 } 161 162 private static boolean isEnglishLetterOrDigit(final char c) { 163 return ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z') || ('0' <= c && c <= '9'); 164 } 165 166 @Override 167 public void setAlpha(final int alpha) { 168 mPaint.setAlpha(alpha); 169 } 170 171 @Override 172 public void setColorFilter(final ColorFilter cf) { 173 mPaint.setColorFilter(cf); 174 } 175 176 @Override 177 public int getOpacity() { 178 return 0; 179 } 180 181 public void setDecodeDimensions(final int decodeWidth, final int decodeHeight) { 182 mCache.setPoolDimensions(decodeWidth, decodeHeight); 183 } 184 185 public void setScale(final int scale) { 186 mScale = scale; 187 } 188 189 public void unbind() { 190 setImage(null); 191 } 192 193 public void bind(final String name, final String email) { 194 setImage(new ContactRequest(name, email)); 195 } 196 197 private void setImage(final ContactRequest contactRequest) { 198 if (mContactRequest != null && mContactRequest.equals(contactRequest)) { 199 return; 200 } 201 202 if (mBitmap != null) { 203 mBitmap.releaseReference(); 204 mBitmap = null; 205 } 206 207 mContactResolver.remove(mContactRequest, this); 208 mContactRequest = contactRequest; 209 210 if (contactRequest == null) { 211 invalidateSelf(); 212 return; 213 } 214 215 final ReusableBitmap cached = mCache.get(contactRequest, true /* incrementRefCount */); 216 if (cached != null) { 217 setBitmap(cached); 218 } else { 219 decode(); 220 } 221 } 222 223 private void setBitmap(final ReusableBitmap bmp) { 224 if (mBitmap != null && mBitmap != bmp) { 225 mBitmap.releaseReference(); 226 } 227 mBitmap = bmp; 228 invalidateSelf(); 229 } 230 231 private void decode() { 232 if (mContactRequest == null) { 233 return; 234 } 235 // Add to batch. 236 mContactResolver.add(mContactRequest, this); 237 } 238 239 public void onDecodeComplete(final Request key, final ReusableBitmap result) { 240 final ContactRequest request = (ContactRequest) key; 241 // Remove from batch. 242 mContactResolver.remove(request, this); 243 if (request.equals(mContactRequest)) { 244 setBitmap(result); 245 } else { 246 // if the requests don't match (i.e. this request is stale), decrement the 247 // ref count to allow the bitmap to be pooled 248 if (result != null) { 249 result.releaseReference(); 250 } 251 } 252 } 253} 254