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 */ getManagedUserDrawable(Context context)82 public static Drawable getManagedUserDrawable(Context context) { 83 return getDrawableForDisplayDensity 84 (context, com.android.internal.R.drawable.ic_corp_user_badge); 85 } 86 getDrawableForDisplayDensity( Context context, @DrawableRes int drawable)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 */ getSizeForList(Context context)99 public static int getSizeForList(Context context) { 100 return (int) context.getResources().getDimension(R.dimen.circle_avatar_size); 101 } 102 UserIconDrawable()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 */ UserIconDrawable(int intrinsicSize)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 setIcon(Bitmap icon)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 setIconDrawable(Drawable icon)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 setBadge(Drawable badge)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 setBadgeIfManagedUser(Context context, int userId)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 /** 187 * Sets the managed badge to this user icon if the device has a device owner. 188 */ setBadgeIfManagedDevice(Context context)189 public UserIconDrawable setBadgeIfManagedDevice(Context context) { 190 Drawable badge = null; 191 boolean deviceOwnerExists = context.getSystemService(DevicePolicyManager.class) 192 .getDeviceOwnerComponentOnAnyUser() != null; 193 if (deviceOwnerExists) { 194 badge = getDrawableForDisplayDensity( 195 context, com.android.internal.R.drawable.ic_corp_badge_case); 196 } 197 return setBadge(badge); 198 } 199 setBadgeRadius(float radius)200 public void setBadgeRadius(float radius) { 201 mBadgeRadius = radius; 202 onBoundsChange(getBounds()); 203 } 204 setBadgeMargin(float margin)205 public void setBadgeMargin(float margin) { 206 mBadgeMargin = margin; 207 onBoundsChange(getBounds()); 208 } 209 210 /** 211 * Sets global padding of icon/frame. Doesn't effect the badge. 212 * @param padding 213 */ setPadding(float padding)214 public void setPadding(float padding) { 215 mPadding = padding; 216 onBoundsChange(getBounds()); 217 } 218 initFramePaint()219 private void initFramePaint() { 220 if (mFramePaint == null) { 221 mFramePaint = new Paint(); 222 mFramePaint.setStyle(Paint.Style.STROKE); 223 mFramePaint.setAntiAlias(true); 224 } 225 } 226 setFrameWidth(float width)227 public void setFrameWidth(float width) { 228 initFramePaint(); 229 mFrameWidth = width; 230 mFramePaint.setStrokeWidth(width); 231 onBoundsChange(getBounds()); 232 } 233 setFramePadding(float padding)234 public void setFramePadding(float padding) { 235 initFramePaint(); 236 mFramePadding = padding; 237 onBoundsChange(getBounds()); 238 } 239 setFrameColor(int color)240 public void setFrameColor(int color) { 241 initFramePaint(); 242 mFramePaint.setColor(color); 243 invalidateSelf(); 244 } 245 setFrameColor(ColorStateList colorList)246 public void setFrameColor(ColorStateList colorList) { 247 initFramePaint(); 248 mFrameColor = colorList; 249 invalidateSelf(); 250 } 251 252 /** 253 * This sets the "intrinsic" size of this drawable. Useful for views which use the drawable's 254 * intrinsic size for layout. It is independent of the bounds. 255 * @param size if 0, the intrinsic size will be set to the displayed icon's size 256 */ setIntrinsicSize(int size)257 public void setIntrinsicSize(int size) { 258 mSize = size; 259 } 260 261 @Override draw(Canvas canvas)262 public void draw(Canvas canvas) { 263 if (mInvalidated) { 264 rebake(); 265 } 266 if (mBitmap != null) { 267 if (mTintColor == null) { 268 mPaint.setColorFilter(null); 269 } else { 270 int color = mTintColor.getColorForState(getState(), mTintColor.getDefaultColor()); 271 if (shouldUpdateColorFilter(color, mTintMode)) { 272 mPaint.setColorFilter(new PorterDuffColorFilter(color, mTintMode)); 273 } 274 } 275 276 canvas.drawBitmap(mBitmap, 0, 0, mPaint); 277 } 278 } 279 shouldUpdateColorFilter(@olorInt int color, PorterDuff.Mode mode)280 private boolean shouldUpdateColorFilter(@ColorInt int color, PorterDuff.Mode mode) { 281 ColorFilter colorFilter = mPaint.getColorFilter(); 282 if (colorFilter instanceof PorterDuffColorFilter) { 283 PorterDuffColorFilter porterDuffColorFilter = (PorterDuffColorFilter) colorFilter; 284 int currentColor = porterDuffColorFilter.getColor(); 285 PorterDuff.Mode currentMode = porterDuffColorFilter.getMode(); 286 return currentColor != color || currentMode != mode; 287 } else { 288 return true; 289 } 290 } 291 292 @Override setAlpha(int alpha)293 public void setAlpha(int alpha) { 294 mPaint.setAlpha(alpha); 295 super.invalidateSelf(); 296 } 297 298 @Override setColorFilter(ColorFilter colorFilter)299 public void setColorFilter(ColorFilter colorFilter) { 300 } 301 302 @Override setTintList(ColorStateList tintList)303 public void setTintList(ColorStateList tintList) { 304 mTintColor = tintList; 305 super.invalidateSelf(); 306 } 307 308 @Override setTintMode(@onNull PorterDuff.Mode mode)309 public void setTintMode(@NonNull PorterDuff.Mode mode) { 310 mTintMode = mode; 311 super.invalidateSelf(); 312 } 313 314 @Override getConstantState()315 public ConstantState getConstantState() { 316 return new BitmapDrawable(mBitmap).getConstantState(); 317 } 318 319 /** 320 * This 'bakes' the current state of this icon into a bitmap and removes/recycles the source 321 * bitmap/drawable. Use this when no more changes will be made and an intrinsic size is set. 322 * This effectively turns this into a static drawable. 323 */ bake()324 public UserIconDrawable bake() { 325 if (mSize <= 0) { 326 throw new IllegalStateException("Baking requires an explicit intrinsic size"); 327 } 328 onBoundsChange(new Rect(0, 0, mSize, mSize)); 329 rebake(); 330 mFrameColor = null; 331 mFramePaint = null; 332 mClearPaint = null; 333 if (mUserDrawable != null) { 334 mUserDrawable.setCallback(null); 335 mUserDrawable = null; 336 } else if (mUserIcon != null) { 337 mUserIcon.recycle(); 338 mUserIcon = null; 339 } 340 return this; 341 } 342 rebake()343 private void rebake() { 344 mInvalidated = false; 345 346 if (mBitmap == null || (mUserDrawable == null && mUserIcon == null)) { 347 return; 348 } 349 350 final Canvas canvas = new Canvas(mBitmap); 351 canvas.drawColor(0, PorterDuff.Mode.CLEAR); 352 353 if(mUserDrawable != null) { 354 mUserDrawable.draw(canvas); 355 } else if (mUserIcon != null) { 356 int saveId = canvas.save(); 357 canvas.concat(mIconMatrix); 358 canvas.drawCircle(mUserIcon.getWidth() * 0.5f, mUserIcon.getHeight() * 0.5f, 359 mIntrinsicRadius, mIconPaint); 360 canvas.restoreToCount(saveId); 361 } 362 if (mFrameColor != null) { 363 mFramePaint.setColor(mFrameColor.getColorForState(getState(), Color.TRANSPARENT)); 364 } 365 if ((mFrameWidth + mFramePadding) > 0.001f) { 366 float radius = mDisplayRadius - mPadding - mFrameWidth * 0.5f; 367 canvas.drawCircle(getBounds().exactCenterX(), getBounds().exactCenterY(), 368 radius, mFramePaint); 369 } 370 371 if ((mBadge != null) && (mBadgeRadius > 0.001f)) { 372 final float badgeDiameter = mBadgeRadius * 2f; 373 final float badgeTop = mBitmap.getHeight() - badgeDiameter; 374 float badgeLeft = mBitmap.getWidth() - badgeDiameter; 375 376 mBadge.setBounds((int) badgeLeft, (int) badgeTop, 377 (int) (badgeLeft + badgeDiameter), (int) (badgeTop + badgeDiameter)); 378 379 final float borderRadius = mBadge.getBounds().width() * 0.5f + mBadgeMargin; 380 canvas.drawCircle(badgeLeft + mBadgeRadius, badgeTop + mBadgeRadius, 381 borderRadius, mClearPaint); 382 mBadge.draw(canvas); 383 } 384 } 385 386 @Override onBoundsChange(Rect bounds)387 protected void onBoundsChange(Rect bounds) { 388 if (bounds.isEmpty() || (mUserIcon == null && mUserDrawable == null)) { 389 return; 390 } 391 392 // re-create bitmap if applicable 393 float newDisplayRadius = Math.min(bounds.width(), bounds.height()) * 0.5f; 394 int size = (int) (newDisplayRadius * 2); 395 if (mBitmap == null || size != ((int) (mDisplayRadius * 2))) { 396 mDisplayRadius = newDisplayRadius; 397 if (mBitmap != null) { 398 mBitmap.recycle(); 399 } 400 mBitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); 401 } 402 403 // update metrics 404 mDisplayRadius = Math.min(bounds.width(), bounds.height()) * 0.5f; 405 final float iconRadius = mDisplayRadius - mFrameWidth - mFramePadding - mPadding; 406 RectF dstRect = new RectF(bounds.exactCenterX() - iconRadius, 407 bounds.exactCenterY() - iconRadius, 408 bounds.exactCenterX() + iconRadius, 409 bounds.exactCenterY() + iconRadius); 410 if (mUserDrawable != null) { 411 Rect rounded = new Rect(); 412 dstRect.round(rounded); 413 mIntrinsicRadius = Math.min(mUserDrawable.getIntrinsicWidth(), 414 mUserDrawable.getIntrinsicHeight()) * 0.5f; 415 mUserDrawable.setBounds(rounded); 416 } else if (mUserIcon != null) { 417 // Build square-to-square transformation matrix 418 final float iconCX = mUserIcon.getWidth() * 0.5f; 419 final float iconCY = mUserIcon.getHeight() * 0.5f; 420 mIntrinsicRadius = Math.min(iconCX, iconCY); 421 RectF srcRect = new RectF(iconCX - mIntrinsicRadius, iconCY - mIntrinsicRadius, 422 iconCX + mIntrinsicRadius, iconCY + mIntrinsicRadius); 423 mIconMatrix.setRectToRect(srcRect, dstRect, Matrix.ScaleToFit.FILL); 424 } 425 426 invalidateSelf(); 427 } 428 429 @Override invalidateSelf()430 public void invalidateSelf() { 431 super.invalidateSelf(); 432 mInvalidated = true; 433 } 434 435 @Override isStateful()436 public boolean isStateful() { 437 return mFrameColor != null && mFrameColor.isStateful(); 438 } 439 440 @Override getOpacity()441 public int getOpacity() { 442 return PixelFormat.TRANSLUCENT; 443 } 444 445 @Override getIntrinsicWidth()446 public int getIntrinsicWidth() { 447 return (mSize <= 0 ? (int) mIntrinsicRadius * 2 : mSize); 448 } 449 450 @Override getIntrinsicHeight()451 public int getIntrinsicHeight() { 452 return getIntrinsicWidth(); 453 } 454 455 @Override invalidateDrawable(@onNull Drawable who)456 public void invalidateDrawable(@NonNull Drawable who) { 457 invalidateSelf(); 458 } 459 460 @Override scheduleDrawable(@onNull Drawable who, @NonNull Runnable what, long when)461 public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) { 462 scheduleSelf(what, when); 463 } 464 465 @Override unscheduleDrawable(@onNull Drawable who, @NonNull Runnable what)466 public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) { 467 unscheduleSelf(what); 468 } 469 } 470