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