Home | History | Annotate | Download | only in common
      1 /*
      2  * Copyright (C) 2016 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.car.apps.common;
     17 
     18 import android.annotation.Nullable;
     19 import android.content.res.Resources;
     20 import android.content.res.TypedArray;
     21 import android.graphics.Bitmap;
     22 import android.graphics.Canvas;
     23 import android.graphics.ColorFilter;
     24 import android.graphics.Paint;
     25 import android.graphics.Paint.Align;
     26 import android.graphics.PorterDuff;
     27 import android.graphics.Rect;
     28 import android.graphics.Typeface;
     29 import android.graphics.drawable.Drawable;
     30 import android.text.TextUtils;
     31 
     32 /**
     33  * A drawable that encapsulates all the functionality needed to display a letter tile to
     34  * represent a contact image.
     35  */
     36 public class LetterTileDrawable extends Drawable {
     37     /** Letter tile */
     38     private static int[] sColors;
     39     private static int sDefaultColor;
     40     private static int sTileFontColor;
     41     private static float sLetterToTileRatio;
     42     private static Drawable sDefaultPersonAvatar;
     43     private static Drawable sDefaultBusinessAvatar;
     44     private static Drawable sDefaultVoicemailAvatar;
     45 
     46     /** Reusable components to avoid new allocations */
     47     private static final Paint sPaint = new Paint();
     48     private static final Rect sRect = new Rect();
     49     private static final char[] sFirstChar = new char[1];
     50 
     51     /** Contact type constants */
     52     public static final int TYPE_PERSON = 1;
     53     public static final int TYPE_BUSINESS = 2;
     54     public static final int TYPE_VOICEMAIL = 3;
     55     public static final int TYPE_DEFAULT = TYPE_PERSON;
     56 
     57     private final Paint mPaint;
     58 
     59     @Nullable private String mDisplayName;
     60     private int mColor;
     61     private int mContactType = TYPE_DEFAULT;
     62     private float mScale = 1.0f;
     63     private float mOffset = 0.0f;
     64     private boolean mIsCircle = false;
     65 
     66     // TODO(rogerxue): the use pattern for this class is always:
     67     // create LTD, setContactDetails(), setIsCircular(true). merge them into ctor.
     68     public LetterTileDrawable(final Resources res) {
     69         mPaint = new Paint();
     70         mPaint.setFilterBitmap(true);
     71         mPaint.setDither(true);
     72         setScale(0.7f);
     73 
     74         if (sColors == null) {
     75             sDefaultColor = res.getColor(R.color.letter_tile_default_color);
     76             TypedArray ta = res.obtainTypedArray(R.array.letter_tile_colors);
     77             if (ta.length() == 0) {
     78                 // TODO(dnotario). Looks like robolectric shadow doesn't currently support
     79                 // obtainTypedArray and always returns length 0 array, which will make some code
     80                 // below that does a division by length of sColors choke. Workaround by creating
     81                 // an array of length 1. A more proper fix tracked by b/26518438.
     82                 sColors = new int[] { sDefaultColor };
     83 
     84             } else {
     85                 sColors = new int[ta.length()];
     86                 for (int i = ta.length() - 1; i >= 0; i--) {
     87                     sColors[i] = ta.getColor(i, sDefaultColor);
     88                 }
     89                 ta.recycle();
     90             }
     91 
     92             sTileFontColor = res.getColor(R.color.letter_tile_font_color);
     93             sLetterToTileRatio = res.getFraction(R.fraction.letter_to_tile_ratio, 1, 1);
     94             // TODO: get images for business and voicemail
     95             sDefaultPersonAvatar = res.getDrawable(R.drawable.ic_person, null /* theme */);
     96             sDefaultBusinessAvatar = res.getDrawable(R.drawable.ic_person, null /* theme */);
     97             sDefaultVoicemailAvatar = res.getDrawable(R.drawable.ic_person, null /* theme */);
     98             sPaint.setTypeface(Typeface.create("sans-serif-light", Typeface.NORMAL));
     99             sPaint.setTextAlign(Align.CENTER);
    100             sPaint.setAntiAlias(true);
    101         }
    102     }
    103 
    104     @Override
    105     public void draw(final Canvas canvas) {
    106         final Rect bounds = getBounds();
    107         if (!isVisible() || bounds.isEmpty()) {
    108             return;
    109         }
    110         // Draw letter tile.
    111         drawLetterTile(canvas);
    112     }
    113 
    114     /**
    115      * Draw the drawable onto the canvas at the current bounds taking into account the current
    116      * scale.
    117      */
    118     private void drawDrawableOnCanvas(final Drawable drawable, final Canvas canvas) {
    119         // The drawable should be drawn in the middle of the canvas without changing its width to
    120         // height ratio.
    121         final Rect destRect = copyBounds();
    122 
    123         // Crop the destination bounds into a square, scaled and offset as appropriate
    124         final int halfLength = (int) (mScale * Math.min(destRect.width(), destRect.height()) / 2);
    125 
    126         destRect.set(destRect.centerX() - halfLength,
    127                 (int) (destRect.centerY() - halfLength + mOffset * destRect.height()),
    128                 destRect.centerX() + halfLength,
    129                 (int) (destRect.centerY() + halfLength + mOffset * destRect.height()));
    130 
    131         drawable.setAlpha(mPaint.getAlpha());
    132         drawable.setColorFilter(sTileFontColor, PorterDuff.Mode.SRC_IN);
    133         drawable.setBounds(destRect);
    134         drawable.draw(canvas);
    135     }
    136 
    137     private void drawLetterTile(final Canvas canvas) {
    138         // Draw background color.
    139         sPaint.setColor(mColor);
    140 
    141         sPaint.setAlpha(mPaint.getAlpha());
    142         final Rect bounds = getBounds();
    143         final int minDimension = Math.min(bounds.width(), bounds.height());
    144 
    145         if (mIsCircle) {
    146             canvas.drawCircle(bounds.centerX(), bounds.centerY(), minDimension / 2, sPaint);
    147         } else {
    148             canvas.drawRect(bounds, sPaint);
    149         }
    150 
    151         // Draw letter/digit only if the first character is an english letter
    152         if (!TextUtils.isEmpty(mDisplayName) && isEnglishLetter(mDisplayName.charAt(0))) {
    153             // Draw letter or digit.
    154             sFirstChar[0] = Character.toUpperCase(mDisplayName.charAt(0));
    155 
    156             // Scale text by canvas bounds and user selected scaling factor
    157             sPaint.setTextSize(mScale * sLetterToTileRatio * minDimension);
    158             //sPaint.setTextSize(sTileLetterFontSize);
    159             sPaint.getTextBounds(sFirstChar, 0, 1, sRect);
    160             sPaint.setColor(sTileFontColor);
    161 
    162             // Draw the letter in the canvas, vertically shifted up or down by the user-defined
    163             // offset
    164             canvas.drawText(sFirstChar, 0, 1, bounds.centerX(),
    165                     bounds.centerY() + mOffset * bounds.height() + sRect.height() / 2,
    166                     sPaint);
    167         } else {
    168             // Draw the default image if there is no letter/digit to be drawn
    169             final Drawable drawable = getDrawablepForContactType(mContactType);
    170             drawDrawableOnCanvas(drawable, canvas);
    171         }
    172     }
    173 
    174     public int getColor() {
    175         return mColor;
    176     }
    177 
    178     /**
    179      * Returns a deterministic color based on the provided contact identifier string.
    180      */
    181     private int pickColor(final String identifier) {
    182         if (TextUtils.isEmpty(identifier) || mContactType == TYPE_VOICEMAIL) {
    183             return sDefaultColor;
    184         }
    185         // String.hashCode() implementation is not supposed to change across java versions, so
    186         // this should guarantee the same email address always maps to the same color.
    187         // The email should already have been normalized by the ContactRequest.
    188         final int color = Math.abs(identifier.hashCode()) % sColors.length;
    189         return sColors[color];
    190     }
    191 
    192     private static Drawable getDrawablepForContactType(int contactType) {
    193         switch (contactType) {
    194             case TYPE_PERSON:
    195                 return sDefaultPersonAvatar;
    196             case TYPE_BUSINESS:
    197                 return sDefaultBusinessAvatar;
    198             case TYPE_VOICEMAIL:
    199                 return sDefaultVoicemailAvatar;
    200             default:
    201                 return sDefaultPersonAvatar;
    202         }
    203     }
    204 
    205     private static boolean isEnglishLetter(final char c) {
    206         return ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z');
    207     }
    208 
    209     @Override
    210     public void setAlpha(final int alpha) {
    211         mPaint.setAlpha(alpha);
    212     }
    213 
    214     @Override
    215     public void setColorFilter(final ColorFilter cf) {
    216         mPaint.setColorFilter(cf);
    217     }
    218 
    219     @Override
    220     public int getOpacity() {
    221         return android.graphics.PixelFormat.OPAQUE;
    222     }
    223 
    224     /**
    225      * Scale the drawn letter tile to a ratio of its default size
    226      *
    227      * @param scale The ratio the letter tile should be scaled to as a percentage of its default
    228      * size, from a scale of 0 to 2.0f. The default is 1.0f.
    229      */
    230     public void setScale(float scale) {
    231         mScale = scale;
    232     }
    233 
    234     /**
    235      * Assigns the vertical offset of the position of the letter tile to the ContactDrawable
    236      *
    237      * @param offset The provided offset must be within the range of -0.5f to 0.5f.
    238      * If set to -0.5f, the letter will be shifted upwards by 0.5 times the height of the canvas
    239      * it is being drawn on, which means it will be drawn with the center of the letter starting
    240      * at the top edge of the canvas.
    241      * If set to 0.5f, the letter will be shifted downwards by 0.5 times the height of the canvas
    242      * it is being drawn on, which means it will be drawn with the center of the letter starting
    243      * at the bottom edge of the canvas.
    244      * The default is 0.0f.
    245      */
    246     public void setOffset(float offset) {
    247         mOffset = offset;
    248     }
    249 
    250     public void setContactDetails(@Nullable String displayName, String identifier) {
    251         mDisplayName = displayName;
    252         mColor = pickColor(identifier);
    253     }
    254 
    255     public void setContactType(int contactType) {
    256         mContactType = contactType;
    257     }
    258 
    259     public void setIsCircular(boolean isCircle) {
    260         mIsCircle = isCircle;
    261     }
    262 
    263     /**
    264      * Convert the drawable to a bitmap.
    265      * @param size The target size of the bitmap.
    266      * @return A bitmap representation of the drawable.
    267      */
    268     public Bitmap toBitmap(int size) {
    269         Bitmap largeIcon = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
    270         Canvas canvas = new Canvas(largeIcon);
    271         Rect bounds = getBounds();
    272         setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
    273         draw(canvas);
    274         setBounds(bounds);
    275         return largeIcon;
    276     }
    277 }