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