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 setBadgeRadius(float radius)186 public void setBadgeRadius(float radius) { 187 mBadgeRadius = radius; 188 onBoundsChange(getBounds()); 189 } 190 setBadgeMargin(float margin)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 */ setPadding(float padding)200 public void setPadding(float padding) { 201 mPadding = padding; 202 onBoundsChange(getBounds()); 203 } 204 initFramePaint()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 setFrameWidth(float width)213 public void setFrameWidth(float width) { 214 initFramePaint(); 215 mFrameWidth = width; 216 mFramePaint.setStrokeWidth(width); 217 onBoundsChange(getBounds()); 218 } 219 setFramePadding(float padding)220 public void setFramePadding(float padding) { 221 initFramePaint(); 222 mFramePadding = padding; 223 onBoundsChange(getBounds()); 224 } 225 setFrameColor(int color)226 public void setFrameColor(int color) { 227 initFramePaint(); 228 mFramePaint.setColor(color); 229 invalidateSelf(); 230 } 231 setFrameColor(ColorStateList colorList)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 */ setIntrinsicSize(int size)243 public void setIntrinsicSize(int size) { 244 mSize = size; 245 } 246 247 @Override draw(Canvas canvas)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 shouldUpdateColorFilter(@olorInt int color, PorterDuff.Mode mode)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 setAlpha(int alpha)279 public void setAlpha(int alpha) { 280 mPaint.setAlpha(alpha); 281 super.invalidateSelf(); 282 } 283 284 @Override setColorFilter(ColorFilter colorFilter)285 public void setColorFilter(ColorFilter colorFilter) { 286 } 287 288 @Override setTintList(ColorStateList tintList)289 public void setTintList(ColorStateList tintList) { 290 mTintColor = tintList; 291 super.invalidateSelf(); 292 } 293 294 @Override setTintMode(@onNull PorterDuff.Mode mode)295 public void setTintMode(@NonNull PorterDuff.Mode mode) { 296 mTintMode = mode; 297 super.invalidateSelf(); 298 } 299 300 @Override getConstantState()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 */ bake()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 rebake()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 onBoundsChange(Rect bounds)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 invalidateSelf()416 public void invalidateSelf() { 417 super.invalidateSelf(); 418 mInvalidated = true; 419 } 420 421 @Override isStateful()422 public boolean isStateful() { 423 return mFrameColor != null && mFrameColor.isStateful(); 424 } 425 426 @Override getOpacity()427 public int getOpacity() { 428 return PixelFormat.TRANSLUCENT; 429 } 430 431 @Override getIntrinsicWidth()432 public int getIntrinsicWidth() { 433 return (mSize <= 0 ? (int) mIntrinsicRadius * 2 : mSize); 434 } 435 436 @Override getIntrinsicHeight()437 public int getIntrinsicHeight() { 438 return getIntrinsicWidth(); 439 } 440 441 @Override invalidateDrawable(@onNull Drawable who)442 public void invalidateDrawable(@NonNull Drawable who) { 443 invalidateSelf(); 444 } 445 446 @Override scheduleDrawable(@onNull Drawable who, @NonNull Runnable what, long when)447 public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) { 448 scheduleSelf(what, when); 449 } 450 451 @Override unscheduleDrawable(@onNull Drawable who, @NonNull Runnable what)452 public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) { 453 unscheduleSelf(what); 454 } 455 } 456