blob: 25ae5c2dadef14fc59ecfc9ab919ff1a3055edfe [file] [log] [blame]
/*
* Copyright (C) 2008 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.systemui.statusbar;
import static com.android.systemui.plugins.DarkIconDispatcher.getTint;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.app.Notification;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.res.ColorStateList;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.os.Parcelable;
import android.os.UserHandle;
import android.service.notification.StatusBarNotification;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.FloatProperty;
import android.util.Log;
import android.util.Property;
import android.util.TypedValue;
import android.view.ViewDebug;
import android.view.accessibility.AccessibilityEvent;
import android.view.animation.Interpolator;
import androidx.core.graphics.ColorUtils;
import com.android.internal.statusbar.StatusBarIcon;
import com.android.internal.util.ContrastColorUtil;
import com.android.systemui.Interpolators;
import com.android.systemui.R;
import com.android.systemui.statusbar.notification.NotificationIconDozeHelper;
import com.android.systemui.statusbar.notification.NotificationUtils;
import java.text.NumberFormat;
import java.util.Arrays;
public class StatusBarIconView extends AnimatedImageView implements StatusIconDisplayable {
public static final int NO_COLOR = 0;
/**
* Multiply alpha values with (1+DARK_ALPHA_BOOST) when dozing. The chosen value boosts
* everything above 30% to 50%, making it appear on 1bit color depths.
*/
private static final float DARK_ALPHA_BOOST = 0.67f;
/**
* Status icons are currently drawn with the intention of being 17dp tall, but we
* want to scale them (in a way that doesn't require an asset dump) down 2dp. So
* 17dp * (15 / 17) = 15dp, the new height. After the first call to {@link #reloadDimens} all
* values will be in px.
*/
private float mSystemIconDesiredHeight = 15f;
private float mSystemIconIntrinsicHeight = 17f;
private float mSystemIconDefaultScale = mSystemIconDesiredHeight / mSystemIconIntrinsicHeight;
private final int ANIMATION_DURATION_FAST = 100;
public static final int STATE_ICON = 0;
public static final int STATE_DOT = 1;
public static final int STATE_HIDDEN = 2;
/** Maximum allowed width or height for an icon drawable */
private static final int MAX_IMAGE_SIZE = 500;
private static final String TAG = "StatusBarIconView";
private static final Property<StatusBarIconView, Float> ICON_APPEAR_AMOUNT
= new FloatProperty<StatusBarIconView>("iconAppearAmount") {
@Override
public void setValue(StatusBarIconView object, float value) {
object.setIconAppearAmount(value);
}
@Override
public Float get(StatusBarIconView object) {
return object.getIconAppearAmount();
}
};
private static final Property<StatusBarIconView, Float> DOT_APPEAR_AMOUNT
= new FloatProperty<StatusBarIconView>("dot_appear_amount") {
@Override
public void setValue(StatusBarIconView object, float value) {
object.setDotAppearAmount(value);
}
@Override
public Float get(StatusBarIconView object) {
return object.getDotAppearAmount();
}
};
private boolean mAlwaysScaleIcon;
private int mStatusBarIconDrawingSizeIncreased = 1;
private int mStatusBarIconDrawingSize = 1;
private int mStatusBarIconSize = 1;
private StatusBarIcon mIcon;
@ViewDebug.ExportedProperty private String mSlot;
private Drawable mNumberBackground;
private Paint mNumberPain;
private int mNumberX;
private int mNumberY;
private String mNumberText;
private StatusBarNotification mNotification;
private final boolean mBlocked;
private int mDensity;
private boolean mNightMode;
private float mIconScale = 1.0f;
private final Paint mDotPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private float mDotRadius;
private int mStaticDotRadius;
private int mVisibleState = STATE_ICON;
private float mIconAppearAmount = 1.0f;
private ObjectAnimator mIconAppearAnimator;
private ObjectAnimator mDotAnimator;
private float mDotAppearAmount;
private OnVisibilityChangedListener mOnVisibilityChangedListener;
private int mDrawableColor;
private int mIconColor;
private int mDecorColor;
private float mDozeAmount;
private ValueAnimator mColorAnimator;
private int mCurrentSetColor = NO_COLOR;
private int mAnimationStartColor = NO_COLOR;
private final ValueAnimator.AnimatorUpdateListener mColorUpdater
= animation -> {
int newColor = NotificationUtils.interpolateColors(mAnimationStartColor, mIconColor,
animation.getAnimatedFraction());
setColorInternal(newColor);
};
private final NotificationIconDozeHelper mDozer;
private int mContrastedDrawableColor;
private int mCachedContrastBackgroundColor = NO_COLOR;
private float[] mMatrix;
private ColorMatrixColorFilter mMatrixColorFilter;
private boolean mIsInShelf;
private Runnable mLayoutRunnable;
private boolean mDismissed;
private Runnable mOnDismissListener;
private boolean mIncreasedSize;
private boolean mShowsConversation;
public StatusBarIconView(Context context, String slot, StatusBarNotification sbn) {
this(context, slot, sbn, false);
}
public StatusBarIconView(Context context, String slot, StatusBarNotification sbn,
boolean blocked) {
super(context);
mDozer = new NotificationIconDozeHelper(context);
mBlocked = blocked;
mSlot = slot;
mNumberPain = new Paint();
mNumberPain.setTextAlign(Paint.Align.CENTER);
mNumberPain.setColor(context.getColor(R.drawable.notification_number_text_color));
mNumberPain.setAntiAlias(true);
setNotification(sbn);
setScaleType(ScaleType.CENTER);
mDensity = context.getResources().getDisplayMetrics().densityDpi;
Configuration configuration = context.getResources().getConfiguration();
mNightMode = (configuration.uiMode & Configuration.UI_MODE_NIGHT_MASK)
== Configuration.UI_MODE_NIGHT_YES;
initializeDecorColor();
reloadDimens();
maybeUpdateIconScaleDimens();
}
public StatusBarIconView(Context context, AttributeSet attrs) {
super(context, attrs);
mDozer = new NotificationIconDozeHelper(context);
mBlocked = false;
mAlwaysScaleIcon = true;
reloadDimens();
maybeUpdateIconScaleDimens();
mDensity = context.getResources().getDisplayMetrics().densityDpi;
}
/** Should always be preceded by {@link #reloadDimens()} */
private void maybeUpdateIconScaleDimens() {
// We do not resize and scale system icons (on the right), only notification icons (on the
// left).
if (mNotification != null || mAlwaysScaleIcon) {
updateIconScaleForNotifications();
} else {
updateIconScaleForSystemIcons();
}
}
private void updateIconScaleForNotifications() {
final float imageBounds = mIncreasedSize ?
mStatusBarIconDrawingSizeIncreased : mStatusBarIconDrawingSize;
final int outerBounds = mStatusBarIconSize;
mIconScale = imageBounds / (float)outerBounds;
updatePivot();
}
// Makes sure that all icons are scaled to the same height (15dp). If we cannot get a height
// for the icon, it uses the default SCALE (15f / 17f) which is the old behavior
private void updateIconScaleForSystemIcons() {
float iconHeight = getIconHeight();
if (iconHeight != 0) {
mIconScale = mSystemIconDesiredHeight / iconHeight;
} else {
mIconScale = mSystemIconDefaultScale;
}
}
private float getIconHeight() {
Drawable d = getDrawable();
if (d != null) {
return (float) getDrawable().getIntrinsicHeight();
} else {
return mSystemIconIntrinsicHeight;
}
}
public float getIconScaleIncreased() {
return (float) mStatusBarIconDrawingSizeIncreased / mStatusBarIconDrawingSize;
}
public float getIconScale() {
return mIconScale;
}
@Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
int density = newConfig.densityDpi;
if (density != mDensity) {
mDensity = density;
reloadDimens();
updateDrawable();
maybeUpdateIconScaleDimens();
}
boolean nightMode = (newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK)
== Configuration.UI_MODE_NIGHT_YES;
if (nightMode != mNightMode) {
mNightMode = nightMode;
initializeDecorColor();
}
}
private void reloadDimens() {
boolean applyRadius = mDotRadius == mStaticDotRadius;
Resources res = getResources();
mStaticDotRadius = res.getDimensionPixelSize(R.dimen.overflow_dot_radius);
mStatusBarIconSize = res.getDimensionPixelSize(R.dimen.status_bar_icon_size);
mStatusBarIconDrawingSizeIncreased =
res.getDimensionPixelSize(R.dimen.status_bar_icon_drawing_size_dark);
mStatusBarIconDrawingSize =
res.getDimensionPixelSize(R.dimen.status_bar_icon_drawing_size);
if (applyRadius) {
mDotRadius = mStaticDotRadius;
}
mSystemIconDesiredHeight = res.getDimension(
com.android.internal.R.dimen.status_bar_system_icon_size);
mSystemIconIntrinsicHeight = res.getDimension(
com.android.internal.R.dimen.status_bar_system_icon_intrinsic_size);
mSystemIconDefaultScale = mSystemIconDesiredHeight / mSystemIconIntrinsicHeight;
}
public void setNotification(StatusBarNotification notification) {
mNotification = notification;
if (notification != null) {
setContentDescription(notification.getNotification());
}
maybeUpdateIconScaleDimens();
}
private static boolean streq(String a, String b) {
if (a == b) {
return true;
}
if (a == null && b != null) {
return false;
}
if (a != null && b == null) {
return false;
}
return a.equals(b);
}
public boolean equalIcons(Icon a, Icon b) {
if (a == b) return true;
if (a.getType() != b.getType()) return false;
switch (a.getType()) {
case Icon.TYPE_RESOURCE:
return a.getResPackage().equals(b.getResPackage()) && a.getResId() == b.getResId();
case Icon.TYPE_URI:
case Icon.TYPE_URI_ADAPTIVE_BITMAP:
return a.getUriString().equals(b.getUriString());
default:
return false;
}
}
/**
* Returns whether the set succeeded.
*/
public boolean set(StatusBarIcon icon) {
final boolean iconEquals = mIcon != null && equalIcons(mIcon.icon, icon.icon);
final boolean levelEquals = iconEquals
&& mIcon.iconLevel == icon.iconLevel;
final boolean visibilityEquals = mIcon != null
&& mIcon.visible == icon.visible;
final boolean numberEquals = mIcon != null
&& mIcon.number == icon.number;
mIcon = icon.clone();
setContentDescription(icon.contentDescription);
if (!iconEquals) {
if (!updateDrawable(false /* no clear */)) return false;
// we have to clear the grayscale tag since it may have changed
setTag(R.id.icon_is_grayscale, null);
// Maybe set scale based on icon height
maybeUpdateIconScaleDimens();
}
if (!levelEquals) {
setImageLevel(icon.iconLevel);
}
if (!numberEquals) {
if (icon.number > 0 && getContext().getResources().getBoolean(
R.bool.config_statusBarShowNumber)) {
if (mNumberBackground == null) {
mNumberBackground = getContext().getResources().getDrawable(
R.drawable.ic_notification_overlay);
}
placeNumber();
} else {
mNumberBackground = null;
mNumberText = null;
}
invalidate();
}
if (!visibilityEquals) {
setVisibility(icon.visible && !mBlocked ? VISIBLE : GONE);
}
return true;
}
public void updateDrawable() {
updateDrawable(true /* with clear */);
}
private boolean updateDrawable(boolean withClear) {
if (mIcon == null) {
return false;
}
Drawable drawable;
try {
drawable = getIcon(mIcon);
} catch (OutOfMemoryError e) {
Log.w(TAG, "OOM while inflating " + mIcon.icon + " for slot " + mSlot);
return false;
}
if (drawable == null) {
Log.w(TAG, "No icon for slot " + mSlot + "; " + mIcon.icon);
return false;
}
if (drawable.getIntrinsicWidth() > MAX_IMAGE_SIZE
|| drawable.getIntrinsicHeight() > MAX_IMAGE_SIZE) {
Log.w(TAG, "Drawable is too large " + mIcon);
return false;
}
if (withClear) {
setImageDrawable(null);
}
setImageDrawable(drawable);
return true;
}
public Icon getSourceIcon() {
return mIcon.icon;
}
private Drawable getIcon(StatusBarIcon icon) {
return getIcon(getContext(), icon);
}
/**
* Returns the right icon to use for this item
*
* @param context Context to use to get resources
* @return Drawable for this item, or null if the package or item could not
* be found
*/
public static Drawable getIcon(Context context, StatusBarIcon statusBarIcon) {
int userId = statusBarIcon.user.getIdentifier();
if (userId == UserHandle.USER_ALL) {
userId = UserHandle.USER_SYSTEM;
}
Drawable icon = statusBarIcon.icon.loadDrawableAsUser(context, userId);
TypedValue typedValue = new TypedValue();
context.getResources().getValue(R.dimen.status_bar_icon_scale_factor, typedValue, true);
float scaleFactor = typedValue.getFloat();
// No need to scale the icon, so return it as is.
if (scaleFactor == 1.f) {
return icon;
}
return new ScalingDrawableWrapper(icon, scaleFactor);
}
public StatusBarIcon getStatusBarIcon() {
return mIcon;
}
@Override
public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
super.onInitializeAccessibilityEvent(event);
if (mNotification != null) {
event.setParcelableData(mNotification.getNotification());
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (mNumberBackground != null) {
placeNumber();
}
}
@Override
public void onRtlPropertiesChanged(int layoutDirection) {
super.onRtlPropertiesChanged(layoutDirection);
updateDrawable();
}
@Override
protected void onDraw(Canvas canvas) {
if (mIconAppearAmount > 0.0f) {
canvas.save();
canvas.scale(mIconScale * mIconAppearAmount, mIconScale * mIconAppearAmount,
getWidth() / 2, getHeight() / 2);
super.onDraw(canvas);
canvas.restore();
}
if (mNumberBackground != null) {
mNumberBackground.draw(canvas);
canvas.drawText(mNumberText, mNumberX, mNumberY, mNumberPain);
}
if (mDotAppearAmount != 0.0f) {
float radius;
float alpha = Color.alpha(mDecorColor) / 255.f;
if (mDotAppearAmount <= 1.0f) {
radius = mDotRadius * mDotAppearAmount;
} else {
float fadeOutAmount = mDotAppearAmount - 1.0f;
alpha = alpha * (1.0f - fadeOutAmount);
radius = NotificationUtils.interpolate(mDotRadius, getWidth() / 4, fadeOutAmount);
}
mDotPaint.setAlpha((int) (alpha * 255));
canvas.drawCircle(mStatusBarIconSize / 2, getHeight() / 2, radius, mDotPaint);
}
}
@Override
protected void debug(int depth) {
super.debug(depth);
Log.d("View", debugIndent(depth) + "slot=" + mSlot);
Log.d("View", debugIndent(depth) + "icon=" + mIcon);
}
void placeNumber() {
final String str;
final int tooBig = getContext().getResources().getInteger(
android.R.integer.status_bar_notification_info_maxnum);
if (mIcon.number > tooBig) {
str = getContext().getResources().getString(
android.R.string.status_bar_notification_info_overflow);
} else {
NumberFormat f = NumberFormat.getIntegerInstance();
str = f.format(mIcon.number);
}
mNumberText = str;
final int w = getWidth();
final int h = getHeight();
final Rect r = new Rect();
mNumberPain.getTextBounds(str, 0, str.length(), r);
final int tw = r.right - r.left;
final int th = r.bottom - r.top;
mNumberBackground.getPadding(r);
int dw = r.left + tw + r.right;
if (dw < mNumberBackground.getMinimumWidth()) {
dw = mNumberBackground.getMinimumWidth();
}
mNumberX = w-r.right-((dw-r.right-r.left)/2);
int dh = r.top + th + r.bottom;
if (dh < mNumberBackground.getMinimumWidth()) {
dh = mNumberBackground.getMinimumWidth();
}
mNumberY = h-r.bottom-((dh-r.top-th-r.bottom)/2);
mNumberBackground.setBounds(w-dw, h-dh, w, h);
}
private void setContentDescription(Notification notification) {
if (notification != null) {
String d = contentDescForNotification(mContext, notification);
if (!TextUtils.isEmpty(d)) {
setContentDescription(d);
}
}
}
public String toString() {
return "StatusBarIconView(slot=" + mSlot + " icon=" + mIcon
+ " notification=" + mNotification + ")";
}
public StatusBarNotification getNotification() {
return mNotification;
}
public String getSlot() {
return mSlot;
}
public static String contentDescForNotification(Context c, Notification n) {
String appName = "";
try {
Notification.Builder builder = Notification.Builder.recoverBuilder(c, n);
appName = builder.loadHeaderAppName();
} catch (RuntimeException e) {
Log.e(TAG, "Unable to recover builder", e);
// Trying to get the app name from the app info instead.
Parcelable appInfo = n.extras.getParcelable(
Notification.EXTRA_BUILDER_APPLICATION_INFO);
if (appInfo instanceof ApplicationInfo) {
appName = String.valueOf(((ApplicationInfo) appInfo).loadLabel(
c.getPackageManager()));
}
}
CharSequence title = n.extras.getCharSequence(Notification.EXTRA_TITLE);
CharSequence text = n.extras.getCharSequence(Notification.EXTRA_TEXT);
CharSequence ticker = n.tickerText;
// Some apps just put the app name into the title
CharSequence titleOrText = TextUtils.equals(title, appName) ? text : title;
CharSequence desc = !TextUtils.isEmpty(titleOrText) ? titleOrText
: !TextUtils.isEmpty(ticker) ? ticker : "";
return c.getString(R.string.accessibility_desc_notification_icon, appName, desc);
}
/**
* Set the color that is used to draw decoration like the overflow dot. This will not be applied
* to the drawable.
*/
public void setDecorColor(int iconTint) {
mDecorColor = iconTint;
updateDecorColor();
}
private void initializeDecorColor() {
if (mNotification != null) {
setDecorColor(getContext().getColor(mNightMode
? com.android.internal.R.color.notification_default_color_dark
: com.android.internal.R.color.notification_default_color_light));
}
}
private void updateDecorColor() {
int color = NotificationUtils.interpolateColors(mDecorColor, Color.WHITE, mDozeAmount);
if (mDotPaint.getColor() != color) {
mDotPaint.setColor(color);
if (mDotAppearAmount != 0) {
invalidate();
}
}
}
/**
* Set the static color that should be used for the drawable of this icon if it's not
* transitioning this also immediately sets the color.
*/
public void setStaticDrawableColor(int color) {
mDrawableColor = color;
setColorInternal(color);
updateContrastedStaticColor();
mIconColor = color;
mDozer.setColor(color);
}
private void setColorInternal(int color) {
mCurrentSetColor = color;
updateIconColor();
}
private void updateIconColor() {
if (mShowsConversation) {
setColorFilter(null);
return;
}
if (mCurrentSetColor != NO_COLOR) {
if (mMatrixColorFilter == null) {
mMatrix = new float[4 * 5];
mMatrixColorFilter = new ColorMatrixColorFilter(mMatrix);
}
int color = NotificationUtils.interpolateColors(
mCurrentSetColor, Color.WHITE, mDozeAmount);
updateTintMatrix(mMatrix, color, DARK_ALPHA_BOOST * mDozeAmount);
mMatrixColorFilter.setColorMatrixArray(mMatrix);
setColorFilter(null); // setColorFilter only invalidates if the instance changed.
setColorFilter(mMatrixColorFilter);
} else {
mDozer.updateGrayscale(this, mDozeAmount);
}
}
/**
* Updates {@param array} such that it represents a matrix that changes RGB to {@param color}
* and multiplies the alpha channel with the color's alpha+{@param alphaBoost}.
*/
private static void updateTintMatrix(float[] array, int color, float alphaBoost) {
Arrays.fill(array, 0);
array[4] = Color.red(color);
array[9] = Color.green(color);
array[14] = Color.blue(color);
array[18] = Color.alpha(color) / 255f + alphaBoost;
}
public void setIconColor(int iconColor, boolean animate) {
if (mIconColor != iconColor) {
mIconColor = iconColor;
if (mColorAnimator != null) {
mColorAnimator.cancel();
}
if (mCurrentSetColor == iconColor) {
return;
}
if (animate && mCurrentSetColor != NO_COLOR) {
mAnimationStartColor = mCurrentSetColor;
mColorAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
mColorAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
mColorAnimator.setDuration(ANIMATION_DURATION_FAST);
mColorAnimator.addUpdateListener(mColorUpdater);
mColorAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mColorAnimator = null;
mAnimationStartColor = NO_COLOR;
}
});
mColorAnimator.start();
} else {
setColorInternal(iconColor);
}
}
}
public int getStaticDrawableColor() {
return mDrawableColor;
}
/**
* A drawable color that passes GAR on a specific background.
* This value is cached.
*
* @param backgroundColor Background to test against.
* @return GAR safe version of {@link StatusBarIconView#getStaticDrawableColor()}.
*/
int getContrastedStaticDrawableColor(int backgroundColor) {
if (mCachedContrastBackgroundColor != backgroundColor) {
mCachedContrastBackgroundColor = backgroundColor;
updateContrastedStaticColor();
}
return mContrastedDrawableColor;
}
private void updateContrastedStaticColor() {
if (Color.alpha(mCachedContrastBackgroundColor) != 255) {
mContrastedDrawableColor = mDrawableColor;
return;
}
// We'll modify the color if it doesn't pass GAR
int contrastedColor = mDrawableColor;
if (!ContrastColorUtil.satisfiesTextContrast(mCachedContrastBackgroundColor,
contrastedColor)) {
float[] hsl = new float[3];
ColorUtils.colorToHSL(mDrawableColor, hsl);
// This is basically a light grey, pushing the color will only distort it.
// Best thing to do in here is to fallback to the default color.
if (hsl[1] < 0.2f) {
contrastedColor = Notification.COLOR_DEFAULT;
}
boolean isDark = !ContrastColorUtil.isColorLight(mCachedContrastBackgroundColor);
contrastedColor = ContrastColorUtil.resolveContrastColor(mContext,
contrastedColor, mCachedContrastBackgroundColor, isDark);
}
mContrastedDrawableColor = contrastedColor;
}
@Override
public void setVisibleState(int state) {
setVisibleState(state, true /* animate */, null /* endRunnable */);
}
public void setVisibleState(int state, boolean animate) {
setVisibleState(state, animate, null);
}
@Override
public boolean hasOverlappingRendering() {
return false;
}
public void setVisibleState(int visibleState, boolean animate, Runnable endRunnable) {
setVisibleState(visibleState, animate, endRunnable, 0);
}
/**
* Set the visibleState of this view.
*
* @param visibleState The new state.
* @param animate Should we animate?
* @param endRunnable The runnable to run at the end.
* @param duration The duration of an animation or 0 if the default should be taken.
*/
public void setVisibleState(int visibleState, boolean animate, Runnable endRunnable,
long duration) {
boolean runnableAdded = false;
if (visibleState != mVisibleState) {
mVisibleState = visibleState;
if (mIconAppearAnimator != null) {
mIconAppearAnimator.cancel();
}
if (mDotAnimator != null) {
mDotAnimator.cancel();
}
if (animate) {
float targetAmount = 0.0f;
Interpolator interpolator = Interpolators.FAST_OUT_LINEAR_IN;
if (visibleState == STATE_ICON) {
targetAmount = 1.0f;
interpolator = Interpolators.LINEAR_OUT_SLOW_IN;
}
float currentAmount = getIconAppearAmount();
if (targetAmount != currentAmount) {
mIconAppearAnimator = ObjectAnimator.ofFloat(this, ICON_APPEAR_AMOUNT,
currentAmount, targetAmount);
mIconAppearAnimator.setInterpolator(interpolator);
mIconAppearAnimator.setDuration(duration == 0 ? ANIMATION_DURATION_FAST
: duration);
mIconAppearAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mIconAppearAnimator = null;
runRunnable(endRunnable);
}
});
mIconAppearAnimator.start();
runnableAdded = true;
}
targetAmount = visibleState == STATE_ICON ? 2.0f : 0.0f;
interpolator = Interpolators.FAST_OUT_LINEAR_IN;
if (visibleState == STATE_DOT) {
targetAmount = 1.0f;
interpolator = Interpolators.LINEAR_OUT_SLOW_IN;
}
currentAmount = getDotAppearAmount();
if (targetAmount != currentAmount) {
mDotAnimator = ObjectAnimator.ofFloat(this, DOT_APPEAR_AMOUNT,
currentAmount, targetAmount);
mDotAnimator.setInterpolator(interpolator);;
mDotAnimator.setDuration(duration == 0 ? ANIMATION_DURATION_FAST
: duration);
final boolean runRunnable = !runnableAdded;
mDotAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mDotAnimator = null;
if (runRunnable) {
runRunnable(endRunnable);
}
}
});
mDotAnimator.start();
runnableAdded = true;
}
} else {
setIconAppearAmount(visibleState == STATE_ICON ? 1.0f : 0.0f);
setDotAppearAmount(visibleState == STATE_DOT ? 1.0f
: visibleState == STATE_ICON ? 2.0f
: 0.0f);
}
}
if (!runnableAdded) {
runRunnable(endRunnable);
}
}
private void runRunnable(Runnable runnable) {
if (runnable != null) {
runnable.run();
}
}
public void setIconAppearAmount(float iconAppearAmount) {
if (mIconAppearAmount != iconAppearAmount) {
mIconAppearAmount = iconAppearAmount;
invalidate();
}
}
public float getIconAppearAmount() {
return mIconAppearAmount;
}
public int getVisibleState() {
return mVisibleState;
}
public void setDotAppearAmount(float dotAppearAmount) {
if (mDotAppearAmount != dotAppearAmount) {
mDotAppearAmount = dotAppearAmount;
invalidate();
}
}
@Override
public void setVisibility(int visibility) {
super.setVisibility(visibility);
if (mOnVisibilityChangedListener != null) {
mOnVisibilityChangedListener.onVisibilityChanged(visibility);
}
}
public float getDotAppearAmount() {
return mDotAppearAmount;
}
public void setOnVisibilityChangedListener(OnVisibilityChangedListener listener) {
mOnVisibilityChangedListener = listener;
}
public void setDozing(boolean dozing, boolean fade, long delay) {
mDozer.setDozing(f -> {
mDozeAmount = f;
updateDecorColor();
updateIconColor();
updateAllowAnimation();
}, dozing, fade, delay, this);
}
private void updateAllowAnimation() {
if (mDozeAmount == 0 || mDozeAmount == 1) {
setAllowAnimation(mDozeAmount == 0);
}
}
/**
* This method returns the drawing rect for the view which is different from the regular
* drawing rect, since we layout all children at position 0 and usually the translation is
* neglected. The standard implementation doesn't account for translation.
*
* @param outRect The (scrolled) drawing bounds of the view.
*/
@Override
public void getDrawingRect(Rect outRect) {
super.getDrawingRect(outRect);
float translationX = getTranslationX();
float translationY = getTranslationY();
outRect.left += translationX;
outRect.right += translationX;
outRect.top += translationY;
outRect.bottom += translationY;
}
public void setIsInShelf(boolean isInShelf) {
mIsInShelf = isInShelf;
}
public boolean isInShelf() {
return mIsInShelf;
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (mLayoutRunnable != null) {
mLayoutRunnable.run();
mLayoutRunnable = null;
}
updatePivot();
}
private void updatePivot() {
if (isLayoutRtl()) {
setPivotX((1 + mIconScale) / 2.0f * getWidth());
} else {
setPivotX((1 - mIconScale) / 2.0f * getWidth());
}
setPivotY((getHeight() - mIconScale * getWidth()) / 2.0f);
}
public void executeOnLayout(Runnable runnable) {
mLayoutRunnable = runnable;
}
public void setDismissed() {
mDismissed = true;
if (mOnDismissListener != null) {
mOnDismissListener.run();
}
}
public boolean isDismissed() {
return mDismissed;
}
public void setOnDismissListener(Runnable onDismissListener) {
mOnDismissListener = onDismissListener;
}
@Override
public void onDarkChanged(Rect area, float darkIntensity, int tint) {
int areaTint = getTint(area, this, tint);
ColorStateList color = ColorStateList.valueOf(areaTint);
setImageTintList(color);
setDecorColor(areaTint);
}
@Override
public boolean isIconVisible() {
return mIcon != null && mIcon.visible;
}
@Override
public boolean isIconBlocked() {
return mBlocked;
}
public void setIncreasedSize(boolean increasedSize) {
mIncreasedSize = increasedSize;
maybeUpdateIconScaleDimens();
}
/**
* Sets whether this icon shows a person and should be tinted.
* If the state differs from the supplied setting, this
* will update the icon colors.
*
* @param showsConversation Whether the icon shows a person
*/
public void setShowsConversation(boolean showsConversation) {
if (mShowsConversation != showsConversation) {
mShowsConversation = showsConversation;
updateIconColor();
}
}
/**
* @return if this icon shows a conversation
*/
public boolean showsConversation() {
return mShowsConversation;
}
public interface OnVisibilityChangedListener {
void onVisibilityChanged(int newVisibility);
}
}