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