| /* |
| * Copyright (C) 2016 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.settingslib.drawable; |
| |
| import android.annotation.ColorInt; |
| import android.annotation.DrawableRes; |
| import android.annotation.NonNull; |
| import android.app.admin.DevicePolicyManager; |
| import android.content.Context; |
| import android.content.res.ColorStateList; |
| import android.graphics.Bitmap; |
| import android.graphics.BitmapShader; |
| import android.graphics.Canvas; |
| import android.graphics.Color; |
| import android.graphics.ColorFilter; |
| import android.graphics.Matrix; |
| import android.graphics.Paint; |
| import android.graphics.PixelFormat; |
| import android.graphics.PorterDuff; |
| import android.graphics.PorterDuffColorFilter; |
| import android.graphics.PorterDuffXfermode; |
| import android.graphics.Rect; |
| import android.graphics.RectF; |
| import android.graphics.Shader; |
| import android.graphics.drawable.BitmapDrawable; |
| import android.graphics.drawable.Drawable; |
| import android.os.UserHandle; |
| |
| import com.android.settingslib.R; |
| |
| /** |
| * Converts the user avatar icon to a circularly clipped one with an optional badge and frame |
| */ |
| public class UserIconDrawable extends Drawable implements Drawable.Callback { |
| |
| private Drawable mUserDrawable; |
| private Bitmap mUserIcon; |
| private Bitmap mBitmap; // baked representation. Required for transparent border around badge |
| private final Paint mIconPaint = new Paint(); |
| private final Paint mPaint = new Paint(); |
| private final Matrix mIconMatrix = new Matrix(); |
| private float mIntrinsicRadius; |
| private float mDisplayRadius; |
| private float mPadding = 0; |
| private int mSize = 0; // custom "intrinsic" size for this drawable if non-zero |
| private boolean mInvalidated = true; |
| private ColorStateList mTintColor = null; |
| private PorterDuff.Mode mTintMode = PorterDuff.Mode.SRC_ATOP; |
| |
| private float mFrameWidth; |
| private float mFramePadding; |
| private ColorStateList mFrameColor = null; |
| private Paint mFramePaint; |
| |
| private Drawable mBadge; |
| private Paint mClearPaint; |
| private float mBadgeRadius; |
| private float mBadgeMargin; |
| |
| /** |
| * Gets the system default managed-user badge as a drawable. This drawable is tint-able. |
| * For badging purpose, consider |
| * {@link android.content.pm.PackageManager#getUserBadgedDrawableForDensity(Drawable, UserHandle, Rect, int)}. |
| * |
| * @param context |
| * @return drawable containing just the badge |
| */ |
| public static Drawable getManagedUserDrawable(Context context) { |
| return getDrawableForDisplayDensity |
| (context, com.android.internal.R.drawable.ic_corp_user_badge); |
| } |
| |
| private static Drawable getDrawableForDisplayDensity( |
| Context context, @DrawableRes int drawable) { |
| int density = context.getResources().getDisplayMetrics().densityDpi; |
| return context.getResources().getDrawableForDensity( |
| drawable, density, context.getTheme()); |
| } |
| |
| /** |
| * Gets the preferred list-item size of this drawable. |
| * @param context |
| * @return size in pixels |
| */ |
| public static int getSizeForList(Context context) { |
| return (int) context.getResources().getDimension(R.dimen.circle_avatar_size); |
| } |
| |
| public UserIconDrawable() { |
| this(0); |
| } |
| |
| /** |
| * Use this constructor if the drawable is intended to be placed in listviews |
| * @param intrinsicSize if 0, the intrinsic size will come from the icon itself |
| */ |
| public UserIconDrawable(int intrinsicSize) { |
| super(); |
| mIconPaint.setAntiAlias(true); |
| mIconPaint.setFilterBitmap(true); |
| mPaint.setFilterBitmap(true); |
| mPaint.setAntiAlias(true); |
| if (intrinsicSize > 0) { |
| setBounds(0, 0, intrinsicSize, intrinsicSize); |
| setIntrinsicSize(intrinsicSize); |
| } |
| setIcon(null); |
| } |
| |
| public UserIconDrawable setIcon(Bitmap icon) { |
| if (mUserDrawable != null) { |
| mUserDrawable.setCallback(null); |
| mUserDrawable = null; |
| } |
| mUserIcon = icon; |
| if (mUserIcon == null) { |
| mIconPaint.setShader(null); |
| mBitmap = null; |
| } else { |
| mIconPaint.setShader(new BitmapShader(icon, Shader.TileMode.CLAMP, |
| Shader.TileMode.CLAMP)); |
| } |
| onBoundsChange(getBounds()); |
| return this; |
| } |
| |
| public UserIconDrawable setIconDrawable(Drawable icon) { |
| if (mUserDrawable != null) { |
| mUserDrawable.setCallback(null); |
| } |
| mUserIcon = null; |
| mUserDrawable = icon; |
| if (mUserDrawable == null) { |
| mBitmap = null; |
| } else { |
| mUserDrawable.setCallback(this); |
| } |
| onBoundsChange(getBounds()); |
| return this; |
| } |
| |
| public UserIconDrawable setBadge(Drawable badge) { |
| mBadge = badge; |
| if (mBadge != null) { |
| if (mClearPaint == null) { |
| mClearPaint = new Paint(); |
| mClearPaint.setAntiAlias(true); |
| mClearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); |
| mClearPaint.setStyle(Paint.Style.FILL); |
| } |
| // update metrics |
| onBoundsChange(getBounds()); |
| } else { |
| invalidateSelf(); |
| } |
| return this; |
| } |
| |
| public UserIconDrawable setBadgeIfManagedUser(Context context, int userId) { |
| Drawable badge = null; |
| if (userId != UserHandle.USER_NULL) { |
| boolean isManaged = context.getSystemService(DevicePolicyManager.class) |
| .getProfileOwnerAsUser(userId) != null; |
| if (isManaged) { |
| badge = getDrawableForDisplayDensity( |
| context, com.android.internal.R.drawable.ic_corp_badge_case); |
| } |
| } |
| return setBadge(badge); |
| } |
| |
| public void setBadgeRadius(float radius) { |
| mBadgeRadius = radius; |
| onBoundsChange(getBounds()); |
| } |
| |
| public void setBadgeMargin(float margin) { |
| mBadgeMargin = margin; |
| onBoundsChange(getBounds()); |
| } |
| |
| /** |
| * Sets global padding of icon/frame. Doesn't effect the badge. |
| * @param padding |
| */ |
| public void setPadding(float padding) { |
| mPadding = padding; |
| onBoundsChange(getBounds()); |
| } |
| |
| private void initFramePaint() { |
| if (mFramePaint == null) { |
| mFramePaint = new Paint(); |
| mFramePaint.setStyle(Paint.Style.STROKE); |
| mFramePaint.setAntiAlias(true); |
| } |
| } |
| |
| public void setFrameWidth(float width) { |
| initFramePaint(); |
| mFrameWidth = width; |
| mFramePaint.setStrokeWidth(width); |
| onBoundsChange(getBounds()); |
| } |
| |
| public void setFramePadding(float padding) { |
| initFramePaint(); |
| mFramePadding = padding; |
| onBoundsChange(getBounds()); |
| } |
| |
| public void setFrameColor(int color) { |
| initFramePaint(); |
| mFramePaint.setColor(color); |
| invalidateSelf(); |
| } |
| |
| public void setFrameColor(ColorStateList colorList) { |
| initFramePaint(); |
| mFrameColor = colorList; |
| invalidateSelf(); |
| } |
| |
| /** |
| * This sets the "intrinsic" size of this drawable. Useful for views which use the drawable's |
| * intrinsic size for layout. It is independent of the bounds. |
| * @param size if 0, the intrinsic size will be set to the displayed icon's size |
| */ |
| public void setIntrinsicSize(int size) { |
| mSize = size; |
| } |
| |
| @Override |
| public void draw(Canvas canvas) { |
| if (mInvalidated) { |
| rebake(); |
| } |
| if (mBitmap != null) { |
| if (mTintColor == null) { |
| mPaint.setColorFilter(null); |
| } else { |
| int color = mTintColor.getColorForState(getState(), mTintColor.getDefaultColor()); |
| if (shouldUpdateColorFilter(color, mTintMode)) { |
| mPaint.setColorFilter(new PorterDuffColorFilter(color, mTintMode)); |
| } |
| } |
| |
| canvas.drawBitmap(mBitmap, 0, 0, mPaint); |
| } |
| } |
| |
| private boolean shouldUpdateColorFilter(@ColorInt int color, PorterDuff.Mode mode) { |
| ColorFilter colorFilter = mPaint.getColorFilter(); |
| if (colorFilter instanceof PorterDuffColorFilter) { |
| PorterDuffColorFilter porterDuffColorFilter = (PorterDuffColorFilter) colorFilter; |
| int currentColor = porterDuffColorFilter.getColor(); |
| PorterDuff.Mode currentMode = porterDuffColorFilter.getMode(); |
| return currentColor != color || currentMode != mode; |
| } else { |
| return true; |
| } |
| } |
| |
| @Override |
| public void setAlpha(int alpha) { |
| mPaint.setAlpha(alpha); |
| super.invalidateSelf(); |
| } |
| |
| @Override |
| public void setColorFilter(ColorFilter colorFilter) { |
| } |
| |
| @Override |
| public void setTintList(ColorStateList tintList) { |
| mTintColor = tintList; |
| super.invalidateSelf(); |
| } |
| |
| @Override |
| public void setTintMode(@NonNull PorterDuff.Mode mode) { |
| mTintMode = mode; |
| super.invalidateSelf(); |
| } |
| |
| @Override |
| public ConstantState getConstantState() { |
| return new BitmapDrawable(mBitmap).getConstantState(); |
| } |
| |
| /** |
| * This 'bakes' the current state of this icon into a bitmap and removes/recycles the source |
| * bitmap/drawable. Use this when no more changes will be made and an intrinsic size is set. |
| * This effectively turns this into a static drawable. |
| */ |
| public UserIconDrawable bake() { |
| if (mSize <= 0) { |
| throw new IllegalStateException("Baking requires an explicit intrinsic size"); |
| } |
| onBoundsChange(new Rect(0, 0, mSize, mSize)); |
| rebake(); |
| mFrameColor = null; |
| mFramePaint = null; |
| mClearPaint = null; |
| if (mUserDrawable != null) { |
| mUserDrawable.setCallback(null); |
| mUserDrawable = null; |
| } else if (mUserIcon != null) { |
| mUserIcon.recycle(); |
| mUserIcon = null; |
| } |
| return this; |
| } |
| |
| private void rebake() { |
| mInvalidated = false; |
| |
| if (mBitmap == null || (mUserDrawable == null && mUserIcon == null)) { |
| return; |
| } |
| |
| final Canvas canvas = new Canvas(mBitmap); |
| canvas.drawColor(0, PorterDuff.Mode.CLEAR); |
| |
| if(mUserDrawable != null) { |
| mUserDrawable.draw(canvas); |
| } else if (mUserIcon != null) { |
| int saveId = canvas.save(); |
| canvas.concat(mIconMatrix); |
| canvas.drawCircle(mUserIcon.getWidth() * 0.5f, mUserIcon.getHeight() * 0.5f, |
| mIntrinsicRadius, mIconPaint); |
| canvas.restoreToCount(saveId); |
| } |
| if (mFrameColor != null) { |
| mFramePaint.setColor(mFrameColor.getColorForState(getState(), Color.TRANSPARENT)); |
| } |
| if ((mFrameWidth + mFramePadding) > 0.001f) { |
| float radius = mDisplayRadius - mPadding - mFrameWidth * 0.5f; |
| canvas.drawCircle(getBounds().exactCenterX(), getBounds().exactCenterY(), |
| radius, mFramePaint); |
| } |
| |
| if ((mBadge != null) && (mBadgeRadius > 0.001f)) { |
| final float badgeDiameter = mBadgeRadius * 2f; |
| final float badgeTop = mBitmap.getHeight() - badgeDiameter; |
| float badgeLeft = mBitmap.getWidth() - badgeDiameter; |
| |
| mBadge.setBounds((int) badgeLeft, (int) badgeTop, |
| (int) (badgeLeft + badgeDiameter), (int) (badgeTop + badgeDiameter)); |
| |
| final float borderRadius = mBadge.getBounds().width() * 0.5f + mBadgeMargin; |
| canvas.drawCircle(badgeLeft + mBadgeRadius, badgeTop + mBadgeRadius, |
| borderRadius, mClearPaint); |
| mBadge.draw(canvas); |
| } |
| } |
| |
| @Override |
| protected void onBoundsChange(Rect bounds) { |
| if (bounds.isEmpty() || (mUserIcon == null && mUserDrawable == null)) { |
| return; |
| } |
| |
| // re-create bitmap if applicable |
| float newDisplayRadius = Math.min(bounds.width(), bounds.height()) * 0.5f; |
| int size = (int) (newDisplayRadius * 2); |
| if (mBitmap == null || size != ((int) (mDisplayRadius * 2))) { |
| mDisplayRadius = newDisplayRadius; |
| if (mBitmap != null) { |
| mBitmap.recycle(); |
| } |
| mBitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); |
| } |
| |
| // update metrics |
| mDisplayRadius = Math.min(bounds.width(), bounds.height()) * 0.5f; |
| final float iconRadius = mDisplayRadius - mFrameWidth - mFramePadding - mPadding; |
| RectF dstRect = new RectF(bounds.exactCenterX() - iconRadius, |
| bounds.exactCenterY() - iconRadius, |
| bounds.exactCenterX() + iconRadius, |
| bounds.exactCenterY() + iconRadius); |
| if (mUserDrawable != null) { |
| Rect rounded = new Rect(); |
| dstRect.round(rounded); |
| mIntrinsicRadius = Math.min(mUserDrawable.getIntrinsicWidth(), |
| mUserDrawable.getIntrinsicHeight()) * 0.5f; |
| mUserDrawable.setBounds(rounded); |
| } else if (mUserIcon != null) { |
| // Build square-to-square transformation matrix |
| final float iconCX = mUserIcon.getWidth() * 0.5f; |
| final float iconCY = mUserIcon.getHeight() * 0.5f; |
| mIntrinsicRadius = Math.min(iconCX, iconCY); |
| RectF srcRect = new RectF(iconCX - mIntrinsicRadius, iconCY - mIntrinsicRadius, |
| iconCX + mIntrinsicRadius, iconCY + mIntrinsicRadius); |
| mIconMatrix.setRectToRect(srcRect, dstRect, Matrix.ScaleToFit.FILL); |
| } |
| |
| invalidateSelf(); |
| } |
| |
| @Override |
| public void invalidateSelf() { |
| super.invalidateSelf(); |
| mInvalidated = true; |
| } |
| |
| @Override |
| public boolean isStateful() { |
| return mFrameColor != null && mFrameColor.isStateful(); |
| } |
| |
| @Override |
| public int getOpacity() { |
| return PixelFormat.TRANSLUCENT; |
| } |
| |
| @Override |
| public int getIntrinsicWidth() { |
| return (mSize <= 0 ? (int) mIntrinsicRadius * 2 : mSize); |
| } |
| |
| @Override |
| public int getIntrinsicHeight() { |
| return getIntrinsicWidth(); |
| } |
| |
| @Override |
| public void invalidateDrawable(@NonNull Drawable who) { |
| invalidateSelf(); |
| } |
| |
| @Override |
| public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) { |
| scheduleSelf(what, when); |
| } |
| |
| @Override |
| public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) { |
| unscheduleSelf(what); |
| } |
| } |