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