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