Home | History | Annotate | Download | only in bitmap
      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  */
     16 package com.android.mail.bitmap;
     17 
     18 import android.content.res.Resources;
     19 import android.content.res.TypedArray;
     20 import android.graphics.Bitmap;
     21 import android.graphics.BitmapFactory;
     22 import android.graphics.BitmapShader;
     23 import android.graphics.Canvas;
     24 import android.graphics.Color;
     25 import android.graphics.ColorFilter;
     26 import android.graphics.Matrix;
     27 import android.graphics.Paint;
     28 import android.graphics.Paint.Align;
     29 import android.graphics.Rect;
     30 import android.graphics.Shader;
     31 import android.graphics.Typeface;
     32 import android.graphics.drawable.Drawable;
     33 
     34 import com.android.bitmap.BitmapCache;
     35 import com.android.bitmap.RequestKey;
     36 import com.android.bitmap.ReusableBitmap;
     37 import com.android.mail.R;
     38 import 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  */
     48 public 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