blob: 274696bfec0ed5dce341f9f628daa47489c7c9d4 [file] [log] [blame]
Evan Roskyaa7f51f2016-03-16 13:15:53 -07001/*
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
17package com.android.settingslib.drawable;
18
Nader Jawad85ff47e2018-04-09 09:54:48 -070019import android.annotation.ColorInt;
Tony Mak34b5bd82018-03-06 11:24:51 +000020import android.annotation.DrawableRes;
Evan Roskyaa7f51f2016-03-16 13:15:53 -070021import android.annotation.NonNull;
22import android.app.admin.DevicePolicyManager;
23import android.content.Context;
24import android.content.res.ColorStateList;
25import android.graphics.Bitmap;
26import android.graphics.BitmapShader;
27import android.graphics.Canvas;
28import android.graphics.Color;
29import android.graphics.ColorFilter;
30import android.graphics.Matrix;
31import android.graphics.Paint;
32import android.graphics.PixelFormat;
33import android.graphics.PorterDuff;
34import android.graphics.PorterDuffColorFilter;
35import android.graphics.PorterDuffXfermode;
36import android.graphics.Rect;
37import android.graphics.RectF;
38import android.graphics.Shader;
Fan Zhang73b161d2017-02-08 11:46:54 -080039import android.graphics.drawable.BitmapDrawable;
Evan Roskyaa7f51f2016-03-16 13:15:53 -070040import android.graphics.drawable.Drawable;
Tony Mak34b5bd82018-03-06 11:24:51 +000041import android.os.UserHandle;
Evan Roskyaa7f51f2016-03-16 13:15:53 -070042
43import com.android.settingslib.R;
44
45/**
46 * Converts the user avatar icon to a circularly clipped one with an optional badge and frame
47 */
48public 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 /**
Tony Mak34b5bd82018-03-06 11:24:51 +000075 * 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 *
Evan Roskyaa7f51f2016-03-16 13:15:53 -070079 * @param context
80 * @return drawable containing just the badge
81 */
Tony Mak34b5bd82018-03-06 11:24:51 +000082 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;
Evan Roskyaa7f51f2016-03-16 13:15:53 -070090 return context.getResources().getDrawableForDensity(
Tony Mak34b5bd82018-03-06 11:24:51 +000091 drawable, density, context.getTheme());
Evan Roskyaa7f51f2016-03-16 13:15:53 -070092 }
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;
Evan Laird97eaacb2018-09-04 15:06:37 -0400175 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 }
Evan Roskyaa7f51f2016-03-16 13:15:53 -0700182 }
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());
Nader Jawad85ff47e2018-04-09 09:54:48 -0700257 if (shouldUpdateColorFilter(color, mTintMode)) {
Evan Roskyaa7f51f2016-03-16 13:15:53 -0700258 mPaint.setColorFilter(new PorterDuffColorFilter(color, mTintMode));
Evan Roskyaa7f51f2016-03-16 13:15:53 -0700259 }
260 }
261
262 canvas.drawBitmap(mBitmap, 0, 0, mPaint);
263 }
264 }
265
Nader Jawad85ff47e2018-04-09 09:54:48 -0700266 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 {
Nader Jawad6a8727e2018-04-11 10:41:10 -0700274 return true;
Nader Jawad85ff47e2018-04-09 09:54:48 -0700275 }
276 }
277
Evan Roskyaa7f51f2016-03-16 13:15:53 -0700278 @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
Fan Zhang73b161d2017-02-08 11:46:54 -0800300 @Override
301 public ConstantState getConstantState() {
302 return new BitmapDrawable(mBitmap).getConstantState();
303 }
304
Evan Roskyaa7f51f2016-03-16 13:15:53 -0700305 /**
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 }
Evan Roskyaa7f51f2016-03-16 13:15:53 -0700348 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);
Evan Roskyaa7f51f2016-03-16 13:15:53 -0700368 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}