Home | History | Annotate | Download | only in drawable
      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 
     17 package com.android.settingslib.drawable;
     18 
     19 import android.annotation.ColorInt;
     20 import android.annotation.DrawableRes;
     21 import android.annotation.NonNull;
     22 import android.app.admin.DevicePolicyManager;
     23 import android.content.Context;
     24 import android.content.res.ColorStateList;
     25 import android.graphics.Bitmap;
     26 import android.graphics.BitmapShader;
     27 import android.graphics.Canvas;
     28 import android.graphics.Color;
     29 import android.graphics.ColorFilter;
     30 import android.graphics.Matrix;
     31 import android.graphics.Paint;
     32 import android.graphics.PixelFormat;
     33 import android.graphics.PorterDuff;
     34 import android.graphics.PorterDuffColorFilter;
     35 import android.graphics.PorterDuffXfermode;
     36 import android.graphics.Rect;
     37 import android.graphics.RectF;
     38 import android.graphics.Shader;
     39 import android.graphics.drawable.BitmapDrawable;
     40 import android.graphics.drawable.Drawable;
     41 import android.os.UserHandle;
     42 
     43 import com.android.settingslib.R;
     44 
     45 /**
     46  * Converts the user avatar icon to a circularly clipped one with an optional badge and frame
     47  */
     48 public class UserIconDrawable extends Drawable implements Drawable.Callback {
     49 
     50     private Drawable mUserDrawable;
     51     private Bitmap mUserIcon;
     52     private Bitmap mBitmap; // baked representation. Required for transparent border around badge
     53     private final Paint mIconPaint = new Paint();
     54     private final Paint mPaint = new Paint();
     55     private final Matrix mIconMatrix = new Matrix();
     56     private float mIntrinsicRadius;
     57     private float mDisplayRadius;
     58     private float mPadding = 0;
     59     private int mSize = 0; // custom "intrinsic" size for this drawable if non-zero
     60     private boolean mInvalidated = true;
     61     private ColorStateList mTintColor = null;
     62     private PorterDuff.Mode mTintMode = PorterDuff.Mode.SRC_ATOP;
     63 
     64     private float mFrameWidth;
     65     private float mFramePadding;
     66     private ColorStateList mFrameColor = null;
     67     private Paint mFramePaint;
     68 
     69     private Drawable mBadge;
     70     private Paint mClearPaint;
     71     private float mBadgeRadius;
     72     private float mBadgeMargin;
     73 
     74     /**
     75      * Gets the system default managed-user badge as a drawable. This drawable is tint-able.
     76      * For badging purpose, consider
     77      * {@link android.content.pm.PackageManager#getUserBadgedDrawableForDensity(Drawable, UserHandle, Rect, int)}.
     78      *
     79      * @param context
     80      * @return drawable containing just the badge
     81      */
     82     public static Drawable getManagedUserDrawable(Context context) {
     83         return getDrawableForDisplayDensity
     84                 (context, com.android.internal.R.drawable.ic_corp_user_badge);
     85     }
     86 
     87     private static Drawable getDrawableForDisplayDensity(
     88             Context context, @DrawableRes int drawable) {
     89         int density = context.getResources().getDisplayMetrics().densityDpi;
     90         return context.getResources().getDrawableForDensity(
     91                 drawable, density, context.getTheme());
     92     }
     93 
     94     /**
     95      * Gets the preferred list-item size of this drawable.
     96      * @param context
     97      * @return size in pixels
     98      */
     99     public static int getSizeForList(Context context) {
    100         return (int) context.getResources().getDimension(R.dimen.circle_avatar_size);
    101     }
    102 
    103     public UserIconDrawable() {
    104         this(0);
    105     }
    106 
    107     /**
    108      * Use this constructor if the drawable is intended to be placed in listviews
    109      * @param intrinsicSize if 0, the intrinsic size will come from the icon itself
    110      */
    111     public UserIconDrawable(int intrinsicSize) {
    112         super();
    113         mIconPaint.setAntiAlias(true);
    114         mIconPaint.setFilterBitmap(true);
    115         mPaint.setFilterBitmap(true);
    116         mPaint.setAntiAlias(true);
    117         if (intrinsicSize > 0) {
    118             setBounds(0, 0, intrinsicSize, intrinsicSize);
    119             setIntrinsicSize(intrinsicSize);
    120         }
    121         setIcon(null);
    122     }
    123 
    124     public UserIconDrawable setIcon(Bitmap icon) {
    125         if (mUserDrawable != null) {
    126             mUserDrawable.setCallback(null);
    127             mUserDrawable = null;
    128         }
    129         mUserIcon = icon;
    130         if (mUserIcon == null) {
    131             mIconPaint.setShader(null);
    132             mBitmap = null;
    133         } else {
    134             mIconPaint.setShader(new BitmapShader(icon, Shader.TileMode.CLAMP,
    135                     Shader.TileMode.CLAMP));
    136         }
    137         onBoundsChange(getBounds());
    138         return this;
    139     }
    140 
    141     public UserIconDrawable setIconDrawable(Drawable icon) {
    142         if (mUserDrawable != null) {
    143             mUserDrawable.setCallback(null);
    144         }
    145         mUserIcon = null;
    146         mUserDrawable = icon;
    147         if (mUserDrawable == null) {
    148             mBitmap = null;
    149         } else {
    150             mUserDrawable.setCallback(this);
    151         }
    152         onBoundsChange(getBounds());
    153         return this;
    154     }
    155 
    156     public UserIconDrawable setBadge(Drawable badge) {
    157         mBadge = badge;
    158         if (mBadge != null) {
    159             if (mClearPaint == null) {
    160                 mClearPaint = new Paint();
    161                 mClearPaint.setAntiAlias(true);
    162                 mClearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
    163                 mClearPaint.setStyle(Paint.Style.FILL);
    164             }
    165             // update metrics
    166             onBoundsChange(getBounds());
    167         } else {
    168             invalidateSelf();
    169         }
    170         return this;
    171     }
    172 
    173     public UserIconDrawable setBadgeIfManagedUser(Context context, int userId) {
    174         Drawable badge = null;
    175         if (userId != UserHandle.USER_NULL) {
    176             boolean isManaged = context.getSystemService(DevicePolicyManager.class)
    177                     .getProfileOwnerAsUser(userId) != null;
    178             if (isManaged) {
    179                 badge = getDrawableForDisplayDensity(
    180                         context, com.android.internal.R.drawable.ic_corp_badge_case);
    181             }
    182         }
    183         return setBadge(badge);
    184     }
    185 
    186     public void setBadgeRadius(float radius) {
    187         mBadgeRadius = radius;
    188         onBoundsChange(getBounds());
    189     }
    190 
    191     public void setBadgeMargin(float margin) {
    192         mBadgeMargin = margin;
    193         onBoundsChange(getBounds());
    194     }
    195 
    196     /**
    197      * Sets global padding of icon/frame. Doesn't effect the badge.
    198      * @param padding
    199      */
    200     public void setPadding(float padding) {
    201         mPadding = padding;
    202         onBoundsChange(getBounds());
    203     }
    204 
    205     private void initFramePaint() {
    206         if (mFramePaint == null) {
    207             mFramePaint = new Paint();
    208             mFramePaint.setStyle(Paint.Style.STROKE);
    209             mFramePaint.setAntiAlias(true);
    210         }
    211     }
    212 
    213     public void setFrameWidth(float width) {
    214         initFramePaint();
    215         mFrameWidth = width;
    216         mFramePaint.setStrokeWidth(width);
    217         onBoundsChange(getBounds());
    218     }
    219 
    220     public void setFramePadding(float padding) {
    221         initFramePaint();
    222         mFramePadding = padding;
    223         onBoundsChange(getBounds());
    224     }
    225 
    226     public void setFrameColor(int color) {
    227         initFramePaint();
    228         mFramePaint.setColor(color);
    229         invalidateSelf();
    230     }
    231 
    232     public void setFrameColor(ColorStateList colorList) {
    233         initFramePaint();
    234         mFrameColor = colorList;
    235         invalidateSelf();
    236     }
    237 
    238     /**
    239      * This sets the "intrinsic" size of this drawable. Useful for views which use the drawable's
    240      * intrinsic size for layout. It is independent of the bounds.
    241      * @param size if 0, the intrinsic size will be set to the displayed icon's size
    242      */
    243     public void setIntrinsicSize(int size) {
    244         mSize = size;
    245     }
    246 
    247     @Override
    248     public void draw(Canvas canvas) {
    249         if (mInvalidated) {
    250             rebake();
    251         }
    252         if (mBitmap != null) {
    253             if (mTintColor == null) {
    254                 mPaint.setColorFilter(null);
    255             } else {
    256                 int color = mTintColor.getColorForState(getState(), mTintColor.getDefaultColor());
    257                 if (shouldUpdateColorFilter(color, mTintMode)) {
    258                     mPaint.setColorFilter(new PorterDuffColorFilter(color, mTintMode));
    259                 }
    260             }
    261 
    262             canvas.drawBitmap(mBitmap, 0, 0, mPaint);
    263         }
    264     }
    265 
    266     private boolean shouldUpdateColorFilter(@ColorInt int color, PorterDuff.Mode mode) {
    267         ColorFilter colorFilter = mPaint.getColorFilter();
    268         if (colorFilter instanceof PorterDuffColorFilter) {
    269             PorterDuffColorFilter porterDuffColorFilter = (PorterDuffColorFilter) colorFilter;
    270             int currentColor = porterDuffColorFilter.getColor();
    271             PorterDuff.Mode currentMode = porterDuffColorFilter.getMode();
    272             return currentColor != color || currentMode != mode;
    273         } else {
    274             return true;
    275         }
    276     }
    277 
    278     @Override
    279     public void setAlpha(int alpha) {
    280         mPaint.setAlpha(alpha);
    281         super.invalidateSelf();
    282     }
    283 
    284     @Override
    285     public void setColorFilter(ColorFilter colorFilter) {
    286     }
    287 
    288     @Override
    289     public void setTintList(ColorStateList tintList) {
    290         mTintColor = tintList;
    291         super.invalidateSelf();
    292     }
    293 
    294     @Override
    295     public void setTintMode(@NonNull PorterDuff.Mode mode) {
    296         mTintMode = mode;
    297         super.invalidateSelf();
    298     }
    299 
    300     @Override
    301     public ConstantState getConstantState() {
    302         return new BitmapDrawable(mBitmap).getConstantState();
    303     }
    304 
    305     /**
    306      * This 'bakes' the current state of this icon into a bitmap and removes/recycles the source
    307      * bitmap/drawable. Use this when no more changes will be made and an intrinsic size is set.
    308      * This effectively turns this into a static drawable.
    309      */
    310     public UserIconDrawable bake() {
    311         if (mSize <= 0) {
    312             throw new IllegalStateException("Baking requires an explicit intrinsic size");
    313         }
    314         onBoundsChange(new Rect(0, 0, mSize, mSize));
    315         rebake();
    316         mFrameColor = null;
    317         mFramePaint = null;
    318         mClearPaint = null;
    319         if (mUserDrawable != null) {
    320             mUserDrawable.setCallback(null);
    321             mUserDrawable = null;
    322         } else if (mUserIcon != null) {
    323             mUserIcon.recycle();
    324             mUserIcon = null;
    325         }
    326         return this;
    327     }
    328 
    329     private void rebake() {
    330         mInvalidated = false;
    331 
    332         if (mBitmap == null || (mUserDrawable == null && mUserIcon == null)) {
    333             return;
    334         }
    335 
    336         final Canvas canvas = new Canvas(mBitmap);
    337         canvas.drawColor(0, PorterDuff.Mode.CLEAR);
    338 
    339         if(mUserDrawable != null) {
    340             mUserDrawable.draw(canvas);
    341         } else if (mUserIcon != null) {
    342             int saveId = canvas.save();
    343             canvas.concat(mIconMatrix);
    344             canvas.drawCircle(mUserIcon.getWidth() * 0.5f, mUserIcon.getHeight() * 0.5f,
    345                     mIntrinsicRadius, mIconPaint);
    346             canvas.restoreToCount(saveId);
    347         }
    348         if (mFrameColor != null) {
    349             mFramePaint.setColor(mFrameColor.getColorForState(getState(), Color.TRANSPARENT));
    350         }
    351         if ((mFrameWidth + mFramePadding) > 0.001f) {
    352             float radius = mDisplayRadius - mPadding - mFrameWidth * 0.5f;
    353             canvas.drawCircle(getBounds().exactCenterX(), getBounds().exactCenterY(),
    354                     radius, mFramePaint);
    355         }
    356 
    357         if ((mBadge != null) && (mBadgeRadius > 0.001f)) {
    358             final float badgeDiameter = mBadgeRadius * 2f;
    359             final float badgeTop = mBitmap.getHeight() - badgeDiameter;
    360             float badgeLeft = mBitmap.getWidth() - badgeDiameter;
    361 
    362             mBadge.setBounds((int) badgeLeft, (int) badgeTop,
    363                     (int) (badgeLeft + badgeDiameter), (int) (badgeTop + badgeDiameter));
    364 
    365             final float borderRadius = mBadge.getBounds().width() * 0.5f + mBadgeMargin;
    366             canvas.drawCircle(badgeLeft + mBadgeRadius, badgeTop + mBadgeRadius,
    367                     borderRadius, mClearPaint);
    368             mBadge.draw(canvas);
    369         }
    370     }
    371 
    372     @Override
    373     protected void onBoundsChange(Rect bounds) {
    374         if (bounds.isEmpty() || (mUserIcon == null && mUserDrawable == null)) {
    375             return;
    376         }
    377 
    378         // re-create bitmap if applicable
    379         float newDisplayRadius = Math.min(bounds.width(), bounds.height()) * 0.5f;
    380         int size = (int) (newDisplayRadius * 2);
    381         if (mBitmap == null || size != ((int) (mDisplayRadius * 2))) {
    382             mDisplayRadius = newDisplayRadius;
    383             if (mBitmap != null) {
    384                 mBitmap.recycle();
    385             }
    386             mBitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
    387         }
    388 
    389         // update metrics
    390         mDisplayRadius = Math.min(bounds.width(), bounds.height()) * 0.5f;
    391         final float iconRadius = mDisplayRadius - mFrameWidth - mFramePadding - mPadding;
    392         RectF dstRect = new RectF(bounds.exactCenterX() - iconRadius,
    393                                   bounds.exactCenterY() - iconRadius,
    394                                   bounds.exactCenterX() + iconRadius,
    395                                   bounds.exactCenterY() + iconRadius);
    396         if (mUserDrawable != null) {
    397             Rect rounded = new Rect();
    398             dstRect.round(rounded);
    399             mIntrinsicRadius = Math.min(mUserDrawable.getIntrinsicWidth(),
    400                                         mUserDrawable.getIntrinsicHeight()) * 0.5f;
    401             mUserDrawable.setBounds(rounded);
    402         } else if (mUserIcon != null) {
    403             // Build square-to-square transformation matrix
    404             final float iconCX = mUserIcon.getWidth() * 0.5f;
    405             final float iconCY = mUserIcon.getHeight() * 0.5f;
    406             mIntrinsicRadius = Math.min(iconCX, iconCY);
    407             RectF srcRect = new RectF(iconCX - mIntrinsicRadius, iconCY - mIntrinsicRadius,
    408                                       iconCX + mIntrinsicRadius, iconCY + mIntrinsicRadius);
    409             mIconMatrix.setRectToRect(srcRect, dstRect, Matrix.ScaleToFit.FILL);
    410         }
    411 
    412         invalidateSelf();
    413     }
    414 
    415     @Override
    416     public void invalidateSelf() {
    417         super.invalidateSelf();
    418         mInvalidated = true;
    419     }
    420 
    421     @Override
    422     public boolean isStateful() {
    423         return mFrameColor != null && mFrameColor.isStateful();
    424     }
    425 
    426     @Override
    427     public int getOpacity() {
    428         return PixelFormat.TRANSLUCENT;
    429     }
    430 
    431     @Override
    432     public int getIntrinsicWidth() {
    433         return (mSize <= 0 ? (int) mIntrinsicRadius * 2 : mSize);
    434     }
    435 
    436     @Override
    437     public int getIntrinsicHeight() {
    438         return getIntrinsicWidth();
    439     }
    440 
    441     @Override
    442     public void invalidateDrawable(@NonNull Drawable who) {
    443         invalidateSelf();
    444     }
    445 
    446     @Override
    447     public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
    448         scheduleSelf(what, when);
    449     }
    450 
    451     @Override
    452     public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
    453         unscheduleSelf(what);
    454     }
    455 }
    456