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 17package com.android.settingslib.drawable; 18 19import android.annotation.DrawableRes; 20import android.annotation.NonNull; 21import android.app.admin.DevicePolicyManager; 22import android.content.Context; 23import android.content.res.ColorStateList; 24import android.graphics.Bitmap; 25import android.graphics.BitmapShader; 26import android.graphics.Canvas; 27import android.graphics.Color; 28import android.graphics.ColorFilter; 29import android.graphics.Matrix; 30import android.graphics.Paint; 31import android.graphics.PixelFormat; 32import android.graphics.PorterDuff; 33import android.graphics.PorterDuffColorFilter; 34import android.graphics.PorterDuffXfermode; 35import android.graphics.Rect; 36import android.graphics.RectF; 37import android.graphics.Shader; 38import android.graphics.drawable.BitmapDrawable; 39import android.graphics.drawable.Drawable; 40import android.os.UserHandle; 41 42import com.android.settingslib.R; 43 44/** 45 * Converts the user avatar icon to a circularly clipped one with an optional badge and frame 46 */ 47public 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 */ 81 public static Drawable getManagedUserDrawable(Context context) { 82 return getDrawableForDisplayDensity 83 (context, com.android.internal.R.drawable.ic_corp_user_badge); 84 } 85 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 */ 98 public static int getSizeForList(Context context) { 99 return (int) context.getResources().getDimension(R.dimen.circle_avatar_size); 100 } 101 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 */ 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 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 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 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 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 183 public void setBadgeRadius(float radius) { 184 mBadgeRadius = radius; 185 onBoundsChange(getBounds()); 186 } 187 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 */ 197 public void setPadding(float padding) { 198 mPadding = padding; 199 onBoundsChange(getBounds()); 200 } 201 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 210 public void setFrameWidth(float width) { 211 initFramePaint(); 212 mFrameWidth = width; 213 mFramePaint.setStrokeWidth(width); 214 onBoundsChange(getBounds()); 215 } 216 217 public void setFramePadding(float padding) { 218 initFramePaint(); 219 mFramePadding = padding; 220 onBoundsChange(getBounds()); 221 } 222 223 public void setFrameColor(int color) { 224 initFramePaint(); 225 mFramePaint.setColor(color); 226 invalidateSelf(); 227 } 228 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 */ 240 public void setIntrinsicSize(int size) { 241 mSize = size; 242 } 243 244 @Override 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 267 public void setAlpha(int alpha) { 268 mPaint.setAlpha(alpha); 269 super.invalidateSelf(); 270 } 271 272 @Override 273 public void setColorFilter(ColorFilter colorFilter) { 274 } 275 276 @Override 277 public void setTintList(ColorStateList tintList) { 278 mTintColor = tintList; 279 super.invalidateSelf(); 280 } 281 282 @Override 283 public void setTintMode(@NonNull PorterDuff.Mode mode) { 284 mTintMode = mode; 285 super.invalidateSelf(); 286 } 287 288 @Override 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 */ 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 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 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 404 public void invalidateSelf() { 405 super.invalidateSelf(); 406 mInvalidated = true; 407 } 408 409 @Override 410 public boolean isStateful() { 411 return mFrameColor != null && mFrameColor.isStateful(); 412 } 413 414 @Override 415 public int getOpacity() { 416 return PixelFormat.TRANSLUCENT; 417 } 418 419 @Override 420 public int getIntrinsicWidth() { 421 return (mSize <= 0 ? (int) mIntrinsicRadius * 2 : mSize); 422 } 423 424 @Override 425 public int getIntrinsicHeight() { 426 return getIntrinsicWidth(); 427 } 428 429 @Override 430 public void invalidateDrawable(@NonNull Drawable who) { 431 invalidateSelf(); 432 } 433 434 @Override 435 public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) { 436 scheduleSelf(what, when); 437 } 438 439 @Override 440 public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) { 441 unscheduleSelf(what); 442 } 443} 444