Files
lawnchair/src/com/android/launcher3/BubbleTextView.java
Federico Baron 4eb502ae10 Add finish icon scale animation for downloading apps
We add an animation when we finish downloading an app where the scale of the icon increases to from scale 0.867 to scale 1

Bug: 254858049
Test: Download an app and visualize finish download animation
Change-Id: I67912f86b0e35091a5fe1b39c3463a1ff203bd3b
2023-01-13 15:35:14 -08:00

1079 lines
38 KiB
Java

/*
* 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.launcher3;
import static com.android.launcher3.config.FeatureFlags.ENABLE_DOWNLOAD_APP_UX_V2;
import static com.android.launcher3.config.FeatureFlags.ENABLE_ICON_LABEL_AUTO_SCALING;
import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon;
import static com.android.launcher3.icons.BitmapInfo.FLAG_NO_BADGE;
import static com.android.launcher3.icons.BitmapInfo.FLAG_THEMED;
import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_INCREMENTAL_DOWNLOAD_ACTIVE;
import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.icu.text.MessageFormat;
import android.text.TextPaint;
import android.text.TextUtils;
import android.text.TextUtils.TruncateAt;
import android.util.AttributeSet;
import android.util.Property;
import android.util.TypedValue;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewDebug;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import com.android.launcher3.accessibility.BaseAccessibilityDelegate;
import com.android.launcher3.dot.DotInfo;
import com.android.launcher3.dragndrop.DragOptions.PreDragCondition;
import com.android.launcher3.dragndrop.DraggableView;
import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.graphics.IconShape;
import com.android.launcher3.graphics.PreloadIconDrawable;
import com.android.launcher3.icons.DotRenderer;
import com.android.launcher3.icons.FastBitmapDrawable;
import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver;
import com.android.launcher3.icons.PlaceHolderIconDrawable;
import com.android.launcher3.icons.cache.HandlerRunnable;
import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.ItemInfoWithIcon;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.popup.PopupContainerWithArrow;
import com.android.launcher3.util.SafeCloseable;
import com.android.launcher3.util.ShortcutUtil;
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.views.IconLabelDotView;
import java.text.NumberFormat;
import java.util.HashMap;
import java.util.Locale;
/**
* TextView that draws a bubble behind the text. We cannot use a LineBackgroundSpan
* because we want to make the bubble taller than the text and TextView's clip is
* too aggressive.
*/
public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
IconLabelDotView, DraggableView, Reorderable {
private static final int DISPLAY_WORKSPACE = 0;
private static final int DISPLAY_ALL_APPS = 1;
private static final int DISPLAY_FOLDER = 2;
protected static final int DISPLAY_TASKBAR = 5;
private static final int DISPLAY_SEARCH_RESULT = 6;
private static final int DISPLAY_SEARCH_RESULT_SMALL = 7;
private static final float MIN_LETTER_SPACING = -0.05f;
private static final int MAX_SEARCH_LOOP_COUNT = 20;
private static final int[] STATE_PRESSED = new int[]{android.R.attr.state_pressed};
private final PointF mTranslationForReorderBounce = new PointF(0, 0);
private final PointF mTranslationForReorderPreview = new PointF(0, 0);
private float mTranslationXForTaskbarAlignmentAnimation = 0f;
private final PointF mTranslationForMoveFromCenterAnimation = new PointF(0, 0);
private float mScaleForReorderBounce = 1f;
private static final Property<BubbleTextView, Float> DOT_SCALE_PROPERTY
= new Property<BubbleTextView, Float>(Float.TYPE, "dotScale") {
@Override
public Float get(BubbleTextView bubbleTextView) {
return bubbleTextView.mDotParams.scale;
}
@Override
public void set(BubbleTextView bubbleTextView, Float value) {
bubbleTextView.mDotParams.scale = value;
bubbleTextView.invalidate();
}
};
public static final Property<BubbleTextView, Float> TEXT_ALPHA_PROPERTY
= new Property<BubbleTextView, Float>(Float.class, "textAlpha") {
@Override
public Float get(BubbleTextView bubbleTextView) {
return bubbleTextView.mTextAlpha;
}
@Override
public void set(BubbleTextView bubbleTextView, Float alpha) {
bubbleTextView.setTextAlpha(alpha);
}
};
private final ActivityContext mActivity;
private FastBitmapDrawable mIcon;
private boolean mCenterVertically;
protected final int mDisplay;
private final CheckLongPressHelper mLongPressHelper;
private final boolean mLayoutHorizontal;
private final boolean mIsRtl;
private final int mIconSize;
@ViewDebug.ExportedProperty(category = "launcher")
private boolean mHideBadge = false;
@ViewDebug.ExportedProperty(category = "launcher")
private boolean mIsIconVisible = true;
@ViewDebug.ExportedProperty(category = "launcher")
private int mTextColor;
@ViewDebug.ExportedProperty(category = "launcher")
private ColorStateList mTextColorStateList;
@ViewDebug.ExportedProperty(category = "launcher")
private float mTextAlpha = 1;
@ViewDebug.ExportedProperty(category = "launcher")
private DotInfo mDotInfo;
private DotRenderer mDotRenderer;
@ViewDebug.ExportedProperty(category = "launcher", deepExport = true)
protected DotRenderer.DrawParams mDotParams;
private Animator mDotScaleAnim;
private boolean mForceHideDot;
@ViewDebug.ExportedProperty(category = "launcher")
private boolean mStayPressed;
@ViewDebug.ExportedProperty(category = "launcher")
private boolean mIgnorePressedStateChange;
@ViewDebug.ExportedProperty(category = "launcher")
private boolean mDisableRelayout = false;
private HandlerRunnable mIconLoadRequest;
private boolean mEnableIconUpdateAnimation = false;
public BubbleTextView(Context context) {
this(context, null, 0);
}
public BubbleTextView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public BubbleTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mActivity = ActivityContext.lookupContext(context);
TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.BubbleTextView, defStyle, 0);
mLayoutHorizontal = a.getBoolean(R.styleable.BubbleTextView_layoutHorizontal, false);
mIsRtl = (getResources().getConfiguration().getLayoutDirection()
== View.LAYOUT_DIRECTION_RTL);
DeviceProfile grid = mActivity.getDeviceProfile();
mDisplay = a.getInteger(R.styleable.BubbleTextView_iconDisplay, DISPLAY_WORKSPACE);
final int defaultIconSize;
if (mDisplay == DISPLAY_WORKSPACE) {
setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.iconTextSizePx);
setCompoundDrawablePadding(grid.iconDrawablePaddingPx);
defaultIconSize = grid.iconSizePx;
setCenterVertically(grid.isScalableGrid);
} else if (mDisplay == DISPLAY_ALL_APPS) {
setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.allAppsIconTextSizePx);
setCompoundDrawablePadding(grid.allAppsIconDrawablePaddingPx);
defaultIconSize = grid.allAppsIconSizePx;
} else if (mDisplay == DISPLAY_FOLDER) {
setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.folderChildTextSizePx);
setCompoundDrawablePadding(grid.folderChildDrawablePaddingPx);
defaultIconSize = grid.folderChildIconSizePx;
} else if (mDisplay == DISPLAY_SEARCH_RESULT) {
defaultIconSize = getResources().getDimensionPixelSize(R.dimen.search_row_icon_size);
} else if (mDisplay == DISPLAY_SEARCH_RESULT_SMALL) {
defaultIconSize = getResources().getDimensionPixelSize(
R.dimen.search_row_small_icon_size);
} else if (mDisplay == DISPLAY_TASKBAR) {
defaultIconSize = grid.iconSizePx;
} else {
// widget_selection or shortcut_popup
defaultIconSize = grid.iconSizePx;
}
mCenterVertically = a.getBoolean(R.styleable.BubbleTextView_centerVertically, false);
mIconSize = a.getDimensionPixelSize(R.styleable.BubbleTextView_iconSizeOverride,
defaultIconSize);
a.recycle();
mLongPressHelper = new CheckLongPressHelper(this);
mDotParams = new DotRenderer.DrawParams();
setEllipsize(TruncateAt.END);
setAccessibilityDelegate(mActivity.getAccessibilityDelegate());
setTextAlpha(1f);
}
@Override
protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
// Disable marques when not focused to that, so that updating text does not cause relayout.
setEllipsize(focused ? TruncateAt.MARQUEE : TruncateAt.END);
super.onFocusChanged(focused, direction, previouslyFocusedRect);
}
public void setHideBadge(boolean hideBadge) {
mHideBadge = hideBadge;
}
/**
* Resets the view so it can be recycled.
*/
public void reset() {
mDotInfo = null;
mDotParams.dotColor = Color.TRANSPARENT;
mDotParams.appColor = Color.TRANSPARENT;
cancelDotScaleAnim();
mDotParams.scale = 0f;
mForceHideDot = false;
setBackground(null);
setTag(null);
if (mIconLoadRequest != null) {
mIconLoadRequest.cancel();
mIconLoadRequest = null;
}
}
private void cancelDotScaleAnim() {
if (mDotScaleAnim != null) {
mDotScaleAnim.cancel();
}
}
private void animateDotScale(float... dotScales) {
cancelDotScaleAnim();
mDotScaleAnim = ObjectAnimator.ofFloat(this, DOT_SCALE_PROPERTY, dotScales);
mDotScaleAnim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mDotScaleAnim = null;
}
});
mDotScaleAnim.start();
}
@UiThread
public void applyFromWorkspaceItem(WorkspaceItemInfo info) {
applyFromWorkspaceItem(info, /* animate = */ false, /* staggerIndex = */ 0);
}
@UiThread
public void applyFromWorkspaceItem(WorkspaceItemInfo info, boolean animate, int staggerIndex) {
applyFromWorkspaceItem(info, null);
}
/**
* Returns whether the newInfo differs from the current getTag().
*/
public boolean shouldAnimateIconChange(WorkspaceItemInfo newInfo) {
WorkspaceItemInfo oldInfo = getTag() instanceof WorkspaceItemInfo
? (WorkspaceItemInfo) getTag()
: null;
boolean changedIcons = oldInfo != null && oldInfo.getTargetComponent() != null
&& newInfo.getTargetComponent() != null
&& !oldInfo.getTargetComponent().equals(newInfo.getTargetComponent());
return changedIcons && isShown();
}
@Override
public void setAccessibilityDelegate(AccessibilityDelegate delegate) {
if (delegate instanceof BaseAccessibilityDelegate) {
super.setAccessibilityDelegate(delegate);
} else {
// NO-OP
// Workaround for b/129745295 where RecyclerView is setting our Accessibility
// delegate incorrectly. There are no cases when we shouldn't be using the
// LauncherAccessibilityDelegate for BubbleTextView.
}
}
@UiThread
public void applyFromWorkspaceItem(WorkspaceItemInfo info, PreloadIconDrawable icon) {
applyIconAndLabel(info);
setItemInfo(info);
applyLoadingState(icon);
applyDotState(info, false /* animate */);
setDownloadStateContentDescription(info, info.getProgressLevel());
}
@UiThread
public void applyFromApplicationInfo(AppInfo info) {
applyIconAndLabel(info);
// We don't need to check the info since it's not a WorkspaceItemInfo
setItemInfo(info);
// Verify high res immediately
verifyHighRes();
if ((info.runtimeStatusFlags & ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK) != 0) {
applyProgressLevel();
}
applyDotState(info, false /* animate */);
setDownloadStateContentDescription(info, info.getProgressLevel());
}
/**
* Apply label and tag using a generic {@link ItemInfoWithIcon}
*/
@UiThread
public void applyFromItemInfoWithIcon(ItemInfoWithIcon info) {
applyIconAndLabel(info);
// We don't need to check the info since it's not a WorkspaceItemInfo
setItemInfo(info);
// Verify high res immediately
verifyHighRes();
setDownloadStateContentDescription(info, info.getProgressLevel());
}
protected void setItemInfo(ItemInfoWithIcon itemInfo) {
setTag(itemInfo);
}
@UiThread
protected void applyIconAndLabel(ItemInfoWithIcon info) {
boolean useTheme = mDisplay == DISPLAY_WORKSPACE || mDisplay == DISPLAY_FOLDER
|| mDisplay == DISPLAY_TASKBAR;
int flags = useTheme ? FLAG_THEMED : 0;
if (mHideBadge) {
flags |= FLAG_NO_BADGE;
}
FastBitmapDrawable iconDrawable = info.newIcon(getContext(), flags);
mDotParams.appColor = iconDrawable.getIconColor();
mDotParams.dotColor = getContext().getResources()
.getColor(android.R.color.system_accent3_200, getContext().getTheme());
setIcon(iconDrawable);
applyLabel(info);
}
@UiThread
private void applyLabel(ItemInfoWithIcon info) {
setText(info.title);
if (info.contentDescription != null) {
setContentDescription(info.isDisabled()
? getContext().getString(R.string.disabled_app_label, info.contentDescription)
: info.contentDescription);
}
}
/**
* Overrides the default long press timeout.
*/
public void setLongPressTimeoutFactor(float longPressTimeoutFactor) {
mLongPressHelper.setLongPressTimeoutFactor(longPressTimeoutFactor);
}
@Override
public void refreshDrawableState() {
if (!mIgnorePressedStateChange) {
super.refreshDrawableState();
}
}
@Override
protected int[] onCreateDrawableState(int extraSpace) {
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
if (mStayPressed) {
mergeDrawableStates(drawableState, STATE_PRESSED);
}
return drawableState;
}
/** Returns the icon for this view. */
public FastBitmapDrawable getIcon() {
return mIcon;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// ignore events if they happen in padding area
if (event.getAction() == MotionEvent.ACTION_DOWN
&& shouldIgnoreTouchDown(event.getX(), event.getY())) {
return false;
}
if (isLongClickable()) {
super.onTouchEvent(event);
mLongPressHelper.onTouchEvent(event);
// Keep receiving the rest of the events
return true;
} else {
return super.onTouchEvent(event);
}
}
/**
* Returns true if the touch down at the provided position be ignored
*/
protected boolean shouldIgnoreTouchDown(float x, float y) {
if (mDisplay == DISPLAY_TASKBAR) {
// Allow touching within padding on taskbar, given icon sizes are smaller.
return false;
}
return y < getPaddingTop()
|| x < getPaddingLeft()
|| y > getHeight() - getPaddingBottom()
|| x > getWidth() - getPaddingRight();
}
void setStayPressed(boolean stayPressed) {
mStayPressed = stayPressed;
refreshDrawableState();
}
@Override
public void onVisibilityAggregated(boolean isVisible) {
super.onVisibilityAggregated(isVisible);
if (mIcon != null) {
mIcon.setVisible(isVisible, false);
}
}
public void clearPressedBackground() {
setPressed(false);
setStayPressed(false);
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
// Unlike touch events, keypress event propagate pressed state change immediately,
// without waiting for onClickHandler to execute. Disable pressed state changes here
// to avoid flickering.
mIgnorePressedStateChange = true;
boolean result = super.onKeyUp(keyCode, event);
mIgnorePressedStateChange = false;
refreshDrawableState();
return result;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
checkForEllipsis();
}
@Override
protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
super.onTextChanged(text, start, lengthBefore, lengthAfter);
checkForEllipsis();
}
private void checkForEllipsis() {
if (!ENABLE_ICON_LABEL_AUTO_SCALING.get()) {
return;
}
float width = getWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight();
if (width <= 0) {
return;
}
setLetterSpacing(0);
String text = getText().toString();
TextPaint paint = getPaint();
if (paint.measureText(text) < width) {
return;
}
float spacing = findBestSpacingValue(paint, text, width, MIN_LETTER_SPACING);
// Reset the paint value so that the call to TextView does appropriate diff.
paint.setLetterSpacing(0);
setLetterSpacing(spacing);
}
/**
* Find the appropriate text spacing to display the provided text
* @param paint the paint used by the text view
* @param text the text to display
* @param allowedWidthPx available space to render the text
* @param minSpacingEm minimum spacing allowed between characters
* @return the final textSpacing value
*
* @see #setLetterSpacing(float)
*/
private float findBestSpacingValue(TextPaint paint, String text, float allowedWidthPx,
float minSpacingEm) {
paint.setLetterSpacing(minSpacingEm);
if (paint.measureText(text) > allowedWidthPx) {
// If there is no result at high limit, we can do anything more
return minSpacingEm;
}
float lowLimit = 0;
float highLimit = minSpacingEm;
for (int i = 0; i < MAX_SEARCH_LOOP_COUNT; i++) {
float value = (lowLimit + highLimit) / 2;
paint.setLetterSpacing(value);
if (paint.measureText(text) < allowedWidthPx) {
highLimit = value;
} else {
lowLimit = value;
}
}
// At the end error on the higher side
return highLimit;
}
@SuppressWarnings("wrongcall")
protected void drawWithoutDot(Canvas canvas) {
super.onDraw(canvas);
}
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawDotIfNecessary(canvas);
}
/**
* Draws the notification dot in the top right corner of the icon bounds.
*
* @param canvas The canvas to draw to.
*/
protected void drawDotIfNecessary(Canvas canvas) {
if (!mForceHideDot && (hasDot() || mDotParams.scale > 0)) {
getIconBounds(mDotParams.iconBounds);
Utilities.scaleRectAboutCenter(mDotParams.iconBounds,
IconShape.getNormalizationScale());
final int scrollX = getScrollX();
final int scrollY = getScrollY();
canvas.translate(scrollX, scrollY);
mDotRenderer.draw(canvas, mDotParams);
canvas.translate(-scrollX, -scrollY);
}
}
@Override
public void setForceHideDot(boolean forceHideDot) {
if (mForceHideDot == forceHideDot) {
return;
}
mForceHideDot = forceHideDot;
if (forceHideDot) {
invalidate();
} else if (hasDot()) {
animateDotScale(0, 1);
}
}
private boolean hasDot() {
return mDotInfo != null;
}
/**
* Get the icon bounds on the view depending on the layout type.
*/
public void getIconBounds(Rect outBounds) {
getIconBounds(mIconSize, outBounds);
}
/**
* Get the icon bounds on the view depending on the layout type.
*/
public void getIconBounds(int iconSize, Rect outBounds) {
outBounds.set(0, 0, iconSize, iconSize);
if (mLayoutHorizontal) {
int top = (getHeight() - iconSize) / 2;
if (mIsRtl) {
outBounds.offsetTo(getWidth() - iconSize - getPaddingRight(), top);
} else {
outBounds.offsetTo(getPaddingLeft(), top);
}
} else {
outBounds.offset((getWidth() - iconSize) / 2, getPaddingTop());
}
}
/**
* Sets whether to vertically center the content.
*/
public void setCenterVertically(boolean centerVertically) {
mCenterVertically = centerVertically;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mCenterVertically) {
Paint.FontMetrics fm = getPaint().getFontMetrics();
int cellHeightPx = mIconSize + getCompoundDrawablePadding() +
(int) Math.ceil(fm.bottom - fm.top);
int height = MeasureSpec.getSize(heightMeasureSpec);
setPadding(getPaddingLeft(), (height - cellHeightPx) / 2, getPaddingRight(),
getPaddingBottom());
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
public void setTextColor(int color) {
mTextColor = color;
mTextColorStateList = null;
super.setTextColor(getModifiedColor());
}
@Override
public void setTextColor(ColorStateList colors) {
mTextColor = colors.getDefaultColor();
mTextColorStateList = colors;
if (Float.compare(mTextAlpha, 1) == 0) {
super.setTextColor(colors);
} else {
super.setTextColor(getModifiedColor());
}
}
public boolean shouldTextBeVisible() {
// Text should be visible everywhere but the hotseat.
Object tag = getParent() instanceof FolderIcon ? ((View) getParent()).getTag() : getTag();
ItemInfo info = tag instanceof ItemInfo ? (ItemInfo) tag : null;
return info == null || (info.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT
&& info.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION);
}
public void setTextVisibility(boolean visible) {
setTextAlpha(visible ? 1 : 0);
}
private void setTextAlpha(float alpha) {
mTextAlpha = alpha;
if (mTextColorStateList != null) {
setTextColor(mTextColorStateList);
} else {
super.setTextColor(getModifiedColor());
}
}
private int getModifiedColor() {
if (mTextAlpha == 0) {
// Special case to prevent text shadows in high contrast mode
return Color.TRANSPARENT;
}
return setColorAlphaBound(mTextColor, Math.round(Color.alpha(mTextColor) * mTextAlpha));
}
/**
* Creates an animator to fade the text in or out.
*
* @param fadeIn Whether the text should fade in or fade out.
*/
public ObjectAnimator createTextAlphaAnimator(boolean fadeIn) {
float toAlpha = shouldTextBeVisible() && fadeIn ? 1 : 0;
return ObjectAnimator.ofFloat(this, TEXT_ALPHA_PROPERTY, toAlpha);
}
@Override
public void cancelLongPress() {
super.cancelLongPress();
mLongPressHelper.cancelLongPress();
}
/**
* Applies the loading progress value to the progress bar.
*
* If this app is installing, the progress bar will be updated with the installation progress.
* If this app is installed and downloading incrementally, the progress bar will be updated
* with the total download progress.
*/
public void applyLoadingState(PreloadIconDrawable icon) {
if (getTag() instanceof ItemInfoWithIcon) {
WorkspaceItemInfo info = (WorkspaceItemInfo) getTag();
if ((info.runtimeStatusFlags & FLAG_INCREMENTAL_DOWNLOAD_ACTIVE) != 0
|| info.hasPromiseIconUi()
|| (info.runtimeStatusFlags & FLAG_INSTALL_SESSION_ACTIVE) != 0
|| (ENABLE_DOWNLOAD_APP_UX_V2.get() && icon != null)) {
updateProgressBarUi(icon);
}
}
}
private void updateProgressBarUi(PreloadIconDrawable oldIcon) {
FastBitmapDrawable originalIcon = mIcon;
PreloadIconDrawable preloadDrawable = applyProgressLevel();
if (preloadDrawable != null && oldIcon != null) {
preloadDrawable.maybePerformFinishedAnimation(oldIcon, () -> setIcon(originalIcon));
}
}
/** Applies the given progress level to the this icon's progress bar. */
@Nullable
public PreloadIconDrawable applyProgressLevel() {
if (!(getTag() instanceof ItemInfoWithIcon)) {
return null;
}
ItemInfoWithIcon info = (ItemInfoWithIcon) getTag();
int progressLevel = info.getProgressLevel();
if (progressLevel >= 100) {
setContentDescription(info.contentDescription != null
? info.contentDescription : "");
} else if (progressLevel > 0) {
setDownloadStateContentDescription(info, progressLevel);
} else {
setContentDescription(getContext()
.getString(R.string.app_waiting_download_title, info.title));
}
if (mIcon != null) {
PreloadIconDrawable preloadIconDrawable;
if (mIcon instanceof PreloadIconDrawable) {
preloadIconDrawable = (PreloadIconDrawable) mIcon;
preloadIconDrawable.setLevel(progressLevel);
preloadIconDrawable.setIsDisabled(!info.isAppStartable());
} else {
preloadIconDrawable = makePreloadIcon();
setIcon(preloadIconDrawable);
}
return preloadIconDrawable;
}
return null;
}
/**
* Creates a PreloadIconDrawable with the appropriate progress level without mutating this
* object.
*/
@Nullable
public PreloadIconDrawable makePreloadIcon() {
if (!(getTag() instanceof ItemInfoWithIcon)) {
return null;
}
ItemInfoWithIcon info = (ItemInfoWithIcon) getTag();
int progressLevel = info.getProgressLevel();
final PreloadIconDrawable preloadDrawable = newPendingIcon(getContext(), info);
preloadDrawable.setLevel(progressLevel);
preloadDrawable.setIsDisabled(!info.isAppStartable());
return preloadDrawable;
}
public void applyDotState(ItemInfo itemInfo, boolean animate) {
if (mIcon instanceof FastBitmapDrawable) {
boolean wasDotted = mDotInfo != null;
mDotInfo = mActivity.getDotInfoForItem(itemInfo);
boolean isDotted = mDotInfo != null;
float newDotScale = isDotted ? 1f : 0;
if (mDisplay == DISPLAY_ALL_APPS) {
mDotRenderer = mActivity.getDeviceProfile().mDotRendererAllApps;
} else {
mDotRenderer = mActivity.getDeviceProfile().mDotRendererWorkSpace;
}
if (wasDotted || isDotted) {
// Animate when a dot is first added or when it is removed.
if (animate && (wasDotted ^ isDotted) && isShown()) {
animateDotScale(newDotScale);
} else {
cancelDotScaleAnim();
mDotParams.scale = newDotScale;
invalidate();
}
}
if (!TextUtils.isEmpty(itemInfo.contentDescription)) {
if (itemInfo.isDisabled()) {
setContentDescription(getContext().getString(R.string.disabled_app_label,
itemInfo.contentDescription));
} else if (hasDot()) {
int count = mDotInfo.getNotificationCount();
setContentDescription(
getAppLabelPluralString(itemInfo.contentDescription.toString(), count));
} else {
setContentDescription(itemInfo.contentDescription);
}
}
}
}
private void setDownloadStateContentDescription(ItemInfoWithIcon info, int progressLevel) {
if ((info.runtimeStatusFlags & ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK)
!= 0) {
String percentageString = NumberFormat.getPercentInstance()
.format(progressLevel * 0.01);
if ((info.runtimeStatusFlags & FLAG_INSTALL_SESSION_ACTIVE) != 0) {
setContentDescription(getContext()
.getString(
R.string.app_installing_title, info.title, percentageString));
} else if ((info.runtimeStatusFlags
& FLAG_INCREMENTAL_DOWNLOAD_ACTIVE) != 0) {
setContentDescription(getContext()
.getString(
R.string.app_downloading_title, info.title, percentageString));
}
}
}
/**
* Sets the icon for this view based on the layout direction.
*/
protected void setIcon(FastBitmapDrawable icon) {
if (mIsIconVisible) {
applyCompoundDrawables(icon);
}
mIcon = icon;
if (mIcon != null) {
mIcon.setVisible(getWindowVisibility() == VISIBLE && isShown(), false);
}
}
@Override
public void setIconVisible(boolean visible) {
mIsIconVisible = visible;
if (!mIsIconVisible) {
resetIconScale();
}
Drawable icon = visible ? mIcon : new ColorDrawable(Color.TRANSPARENT);
applyCompoundDrawables(icon);
}
protected boolean iconUpdateAnimationEnabled() {
return mEnableIconUpdateAnimation;
}
protected void applyCompoundDrawables(Drawable icon) {
if (icon == null) {
// Icon can be null when we use the BubbleTextView for text only.
return;
}
// If we had already set an icon before, disable relayout as the icon size is the
// same as before.
mDisableRelayout = mIcon != null;
icon.setBounds(0, 0, mIconSize, mIconSize);
updateIcon(icon);
// If the current icon is a placeholder color, animate its update.
if (mIcon != null
&& mIcon instanceof PlaceHolderIconDrawable
&& iconUpdateAnimationEnabled()) {
((PlaceHolderIconDrawable) mIcon).animateIconUpdate(icon);
}
mDisableRelayout = false;
}
@Override
public void requestLayout() {
if (!mDisableRelayout) {
super.requestLayout();
}
}
/**
* Applies the item info if it is same as what the view is pointing to currently.
*/
@Override
public void reapplyItemInfo(ItemInfoWithIcon info) {
if (getTag() == info) {
mIconLoadRequest = null;
mDisableRelayout = true;
mEnableIconUpdateAnimation = true;
// Optimization: Starting in N, pre-uploads the bitmap to RenderThread.
info.bitmap.icon.prepareToDraw();
if (info instanceof AppInfo) {
applyFromApplicationInfo((AppInfo) info);
} else if (info instanceof WorkspaceItemInfo) {
applyFromWorkspaceItem((WorkspaceItemInfo) info);
mActivity.invalidateParent(info);
} else if (info != null) {
applyFromItemInfoWithIcon(info);
}
mDisableRelayout = false;
mEnableIconUpdateAnimation = false;
}
}
/**
* Verifies that the current icon is high-res otherwise posts a request to load the icon.
*/
public void verifyHighRes() {
if (mIconLoadRequest != null) {
mIconLoadRequest.cancel();
mIconLoadRequest = null;
}
if (getTag() instanceof ItemInfoWithIcon) {
ItemInfoWithIcon info = (ItemInfoWithIcon) getTag();
if (info.usingLowResIcon()) {
mIconLoadRequest = LauncherAppState.getInstance(getContext()).getIconCache()
.updateIconInBackground(BubbleTextView.this, info);
}
}
}
public int getIconSize() {
return mIconSize;
}
public boolean isDisplaySearchResult() {
return mDisplay == DISPLAY_SEARCH_RESULT ||
mDisplay == DISPLAY_SEARCH_RESULT_SMALL;
}
private void updateTranslation() {
super.setTranslationX(mTranslationForReorderBounce.x + mTranslationForReorderPreview.x
+ mTranslationForMoveFromCenterAnimation.x
+ mTranslationXForTaskbarAlignmentAnimation);
super.setTranslationY(mTranslationForReorderBounce.y + mTranslationForReorderPreview.y
+ mTranslationForMoveFromCenterAnimation.y);
}
public void setReorderBounceOffset(float x, float y) {
mTranslationForReorderBounce.set(x, y);
updateTranslation();
}
public void getReorderBounceOffset(PointF offset) {
offset.set(mTranslationForReorderBounce);
}
@Override
public void setReorderPreviewOffset(float x, float y) {
mTranslationForReorderPreview.set(x, y);
updateTranslation();
}
@Override
public void getReorderPreviewOffset(PointF offset) {
offset.set(mTranslationForReorderPreview);
}
public void setReorderBounceScale(float scale) {
mScaleForReorderBounce = scale;
super.setScaleX(scale);
super.setScaleY(scale);
}
public float getReorderBounceScale() {
return mScaleForReorderBounce;
}
/**
* Sets translation values for move from center animation
*/
public void setTranslationForMoveFromCenterAnimation(float x, float y) {
mTranslationForMoveFromCenterAnimation.set(x, y);
updateTranslation();
}
/**
* Sets translationX for taskbar to launcher alignment animation
*/
public void setTranslationXForTaskbarAlignmentAnimation(float translationX) {
mTranslationXForTaskbarAlignmentAnimation = translationX;
updateTranslation();
}
/**
* Returns translationX value for taskbar to launcher alignment animation
*/
public float getTranslationXForTaskbarAlignmentAnimation() {
return mTranslationXForTaskbarAlignmentAnimation;
}
public View getView() {
return this;
}
@Override
public int getViewType() {
return DRAGGABLE_ICON;
}
@Override
public void getWorkspaceVisualDragBounds(Rect bounds) {
getIconBounds(mIconSize, bounds);
}
public void getSourceVisualDragBounds(Rect bounds) {
getIconBounds(mIconSize, bounds);
}
@Override
public SafeCloseable prepareDrawDragView() {
resetIconScale();
setForceHideDot(true);
return () -> { };
}
private void resetIconScale() {
if (mIcon != null) {
mIcon.resetScale();
}
}
private void updateIcon(Drawable newIcon) {
if (mLayoutHorizontal) {
setCompoundDrawablesRelative(newIcon, null, null, null);
} else {
setCompoundDrawables(null, newIcon, null, null);
}
}
private String getAppLabelPluralString(String appName, int notificationCount) {
MessageFormat icuCountFormat = new MessageFormat(
getResources().getString(R.string.dotted_app_label),
Locale.getDefault());
HashMap<String, Object> args = new HashMap();
args.put("app_name", appName);
args.put("count", notificationCount);
return icuCountFormat.format(args);
}
/**
* Starts a long press action and returns the corresponding pre-drag condition
*/
public PreDragCondition startLongPressAction() {
PopupContainerWithArrow popup = PopupContainerWithArrow.showForIcon(this);
return popup != null ? popup.createPreDragCondition(true) : null;
}
/**
* Returns true if the view can show long-press popup
*/
public boolean canShowLongPressPopup() {
return getTag() instanceof ItemInfo && ShortcutUtil.supportsShortcuts((ItemInfo) getTag());
}
}