blob: 7f469b5e74971a414a27bf766d00a47da495448b [file] [log] [blame]
/*
* 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.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 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
* @param context
* @return drawable containing just the badge
*/
public static Drawable getManagedUserBadgeDrawable(Context context) {
int displayDensity = context.getResources().getDisplayMetrics().densityDpi;
return context.getResources().getDrawableForDensity(
com.android.internal.R.drawable.ic_corp_user_badge,
displayDensity, 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;
boolean isManaged = context.getSystemService(DevicePolicyManager.class)
.getProfileOwnerAsUser(userId) != null;
if (isManaged) {
badge = getManagedUserBadgeDrawable(context);
}
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 (mPaint.getColorFilter() == null) {
mPaint.setColorFilter(new PorterDuffColorFilter(color, mTintMode));
} else {
((PorterDuffColorFilter) mPaint.getColorFilter()).setMode(mTintMode);
((PorterDuffColorFilter) mPaint.getColorFilter()).setColor(color);
}
}
canvas.drawBitmap(mBitmap, 0, 0, mPaint);
}
}
@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);
}
}