Files
Flexera-Launcher/src/com/android/launcher3/popup/PopupContainerWithArrow.java
T
Tony Wickham 2fe09f24eb Move update popup header out of BubbleTextView
Now we update from Launcher, which is both more efficient (only run it
when badges update, not whenever BubbleTextView applies an ItemInfo) and
more logical/cleaner.

Change-Id: Ib77433d9489c29e9781b3a48de37de0e07b5ba07
2017-04-26 08:46:39 -07:00

841 lines
35 KiB
Java

/*
* 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.launcher3.popup;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.CornerPathEffect;
import android.graphics.Paint;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.drawable.ShapeDrawable;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.accessibility.AccessibilityEvent;
import android.view.animation.DecelerateInterpolator;
import android.widget.FrameLayout;
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.DragSource;
import com.android.launcher3.DropTarget;
import com.android.launcher3.FastBitmapDrawable;
import com.android.launcher3.ItemInfo;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherAnimUtils;
import com.android.launcher3.LauncherModel;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.LogAccelerateInterpolator;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
import com.android.launcher3.accessibility.ShortcutMenuAccessibilityDelegate;
import com.android.launcher3.anim.PropertyListBuilder;
import com.android.launcher3.anim.PropertyResetListener;
import com.android.launcher3.badge.BadgeInfo;
import com.android.launcher3.dragndrop.DragController;
import com.android.launcher3.dragndrop.DragLayer;
import com.android.launcher3.dragndrop.DragOptions;
import com.android.launcher3.graphics.IconPalette;
import com.android.launcher3.graphics.TriangleShape;
import com.android.launcher3.notification.NotificationItemView;
import com.android.launcher3.notification.NotificationKeyData;
import com.android.launcher3.shortcuts.DeepShortcutManager;
import com.android.launcher3.shortcuts.DeepShortcutView;
import com.android.launcher3.shortcuts.ShortcutsItemView;
import com.android.launcher3.util.PackageUserKey;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
import static com.android.launcher3.userevent.nano.LauncherLogProto.ItemType;
import static com.android.launcher3.userevent.nano.LauncherLogProto.Target;
/**
* A container for shortcuts to deep links within apps.
*/
@TargetApi(Build.VERSION_CODES.N)
public class PopupContainerWithArrow extends AbstractFloatingView implements DragSource,
DragController.DragListener {
protected final Launcher mLauncher;
private final int mStartDragThreshold;
private LauncherAccessibilityDelegate mAccessibilityDelegate;
private final boolean mIsRtl;
public ShortcutsItemView mShortcutsItemView;
private NotificationItemView mNotificationItemView;
protected BubbleTextView mOriginalIcon;
private final Rect mTempRect = new Rect();
private PointF mInterceptTouchDown = new PointF();
private boolean mIsLeftAligned;
protected boolean mIsAboveIcon;
private View mArrow;
protected Animator mOpenCloseAnimator;
private boolean mDeferContainerRemoval;
public PopupContainerWithArrow(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mLauncher = Launcher.getLauncher(context);
mStartDragThreshold = getResources().getDimensionPixelSize(
R.dimen.deep_shortcuts_start_drag_threshold);
// TODO: make sure the delegate works for all items, not just shortcuts.
mAccessibilityDelegate = new ShortcutMenuAccessibilityDelegate(mLauncher);
mIsRtl = Utilities.isRtl(getResources());
}
public PopupContainerWithArrow(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public PopupContainerWithArrow(Context context) {
this(context, null, 0);
}
public LauncherAccessibilityDelegate getAccessibilityDelegate() {
return mAccessibilityDelegate;
}
/**
* Shows the notifications and deep shortcuts associated with {@param icon}.
* @return the container if shown or null.
*/
public static PopupContainerWithArrow showForIcon(BubbleTextView icon) {
Launcher launcher = Launcher.getLauncher(icon.getContext());
if (getOpen(launcher) != null) {
// There is already an items container open, so don't open this one.
icon.clearFocus();
return null;
}
ItemInfo itemInfo = (ItemInfo) icon.getTag();
if (!DeepShortcutManager.supportsShortcuts(itemInfo)) {
return null;
}
PopupDataProvider popupDataProvider = launcher.getPopupDataProvider();
List<String> shortcutIds = popupDataProvider.getShortcutIdsForItem(itemInfo);
List<NotificationKeyData> notificationKeys = popupDataProvider
.getNotificationKeysForItem(itemInfo);
List<SystemShortcut> systemShortcuts = popupDataProvider
.getEnabledSystemShortcutsForItem(itemInfo);
final PopupContainerWithArrow container =
(PopupContainerWithArrow) launcher.getLayoutInflater().inflate(
R.layout.popup_container, launcher.getDragLayer(), false);
container.setVisibility(View.INVISIBLE);
launcher.getDragLayer().addView(container);
container.populateAndShow(icon, shortcutIds, notificationKeys, systemShortcuts);
return container;
}
public void populateAndShow(final BubbleTextView originalIcon, final List<String> shortcutIds,
final List<NotificationKeyData> notificationKeys, List<SystemShortcut> systemShortcuts) {
final Resources resources = getResources();
final int arrowWidth = resources.getDimensionPixelSize(R.dimen.popup_arrow_width);
final int arrowHeight = resources.getDimensionPixelSize(R.dimen.popup_arrow_height);
final int arrowHorizontalOffset = resources.getDimensionPixelSize(
R.dimen.popup_arrow_horizontal_offset);
final int arrowVerticalOffset = resources.getDimensionPixelSize(
R.dimen.popup_arrow_vertical_offset);
mOriginalIcon = originalIcon;
// Add dummy views first, and populate with real info when ready.
PopupPopulator.Item[] itemsToPopulate = PopupPopulator
.getItemsToPopulate(shortcutIds, notificationKeys, systemShortcuts);
addDummyViews(originalIcon, itemsToPopulate, notificationKeys.size() > 1);
measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
orientAboutIcon(originalIcon, arrowHeight + arrowVerticalOffset);
boolean reverseOrder = mIsAboveIcon;
if (reverseOrder) {
removeAllViews();
mNotificationItemView = null;
mShortcutsItemView = null;
itemsToPopulate = PopupPopulator.reverseItems(itemsToPopulate);
addDummyViews(originalIcon, itemsToPopulate, notificationKeys.size() > 1);
measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
orientAboutIcon(originalIcon, arrowHeight + arrowVerticalOffset);
}
ItemInfo originalItemInfo = (ItemInfo) originalIcon.getTag();
List<DeepShortcutView> shortcutViews = mShortcutsItemView == null
? Collections.EMPTY_LIST
: mShortcutsItemView.getDeepShortcutViews(reverseOrder);
List<View> systemShortcutViews = mShortcutsItemView == null
? Collections.EMPTY_LIST
: mShortcutsItemView.getSystemShortcutViews(reverseOrder);
if (mNotificationItemView != null) {
updateNotificationHeader();
}
// Add the arrow.
mArrow = addArrowView(arrowHorizontalOffset, arrowVerticalOffset, arrowWidth, arrowHeight);
mArrow.setPivotX(arrowWidth / 2);
mArrow.setPivotY(mIsAboveIcon ? 0 : arrowHeight);
animateOpen();
mLauncher.getDragController().addDragListener(this);
mOriginalIcon.forceHideBadge(true);
// Load the shortcuts on a background thread and update the container as it animates.
final Looper workerLooper = LauncherModel.getWorkerLooper();
new Handler(workerLooper).postAtFrontOfQueue(PopupPopulator.createUpdateRunnable(
mLauncher, originalItemInfo, new Handler(Looper.getMainLooper()),
this, shortcutIds, shortcutViews, notificationKeys, mNotificationItemView,
systemShortcuts, systemShortcutViews));
}
private void addDummyViews(BubbleTextView originalIcon,
PopupPopulator.Item[] itemTypesToPopulate, boolean notificationFooterHasIcons) {
final Resources res = getResources();
final int spacing = res.getDimensionPixelSize(R.dimen.popup_items_spacing);
final LayoutInflater inflater = mLauncher.getLayoutInflater();
int numItems = itemTypesToPopulate.length;
for (int i = 0; i < numItems; i++) {
PopupPopulator.Item itemTypeToPopulate = itemTypesToPopulate[i];
PopupPopulator.Item nextItemTypeToPopulate =
i < numItems - 1 ? itemTypesToPopulate[i + 1] : null;
final View item = inflater.inflate(itemTypeToPopulate.layoutId, this, false);
if (itemTypeToPopulate == PopupPopulator.Item.NOTIFICATION) {
mNotificationItemView = (NotificationItemView) item;
int footerHeight = notificationFooterHasIcons ?
res.getDimensionPixelSize(R.dimen.notification_footer_height) : 0;
item.findViewById(R.id.footer).getLayoutParams().height = footerHeight;
}
boolean shouldAddBottomMargin = nextItemTypeToPopulate != null
&& itemTypeToPopulate.isShortcut ^ nextItemTypeToPopulate.isShortcut;
item.setAccessibilityDelegate(mAccessibilityDelegate);
if (itemTypeToPopulate.isShortcut) {
if (mShortcutsItemView == null) {
mShortcutsItemView = (ShortcutsItemView) inflater.inflate(
R.layout.shortcuts_item, this, false);
addView(mShortcutsItemView);
}
mShortcutsItemView.addShortcutView(item, itemTypeToPopulate);
if (shouldAddBottomMargin) {
((LayoutParams) mShortcutsItemView.getLayoutParams()).bottomMargin = spacing;
}
} else {
addView(item);
if (shouldAddBottomMargin) {
((LayoutParams) item.getLayoutParams()).bottomMargin = spacing;
}
}
}
// TODO: update this, since not all items are shortcuts
setContentDescription(getContext().getString(R.string.shortcuts_menu_description,
numItems, originalIcon.getContentDescription().toString()));
}
protected PopupItemView getItemViewAt(int index) {
if (!mIsAboveIcon) {
// Opening down, so arrow is the first view.
index++;
}
return (PopupItemView) getChildAt(index);
}
protected int getItemCount() {
// All children except the arrow are items.
return getChildCount() - 1;
}
private void animateOpen() {
setVisibility(View.VISIBLE);
mIsOpen = true;
final AnimatorSet shortcutAnims = LauncherAnimUtils.createAnimatorSet();
final int itemCount = getItemCount();
final long duration = getResources().getInteger(
R.integer.config_deepShortcutOpenDuration);
final long arrowScaleDuration = getResources().getInteger(
R.integer.config_deepShortcutArrowOpenDuration);
final long arrowScaleDelay = duration - arrowScaleDuration;
final long stagger = getResources().getInteger(
R.integer.config_deepShortcutOpenStagger);
final TimeInterpolator fadeInterpolator = new LogAccelerateInterpolator(100, 0);
// Animate shortcuts
DecelerateInterpolator interpolator = new DecelerateInterpolator();
for (int i = 0; i < itemCount; i++) {
final PopupItemView popupItemView = getItemViewAt(i);
popupItemView.setVisibility(INVISIBLE);
popupItemView.setAlpha(0);
Animator anim = popupItemView.createOpenAnimation(mIsAboveIcon, mIsLeftAligned);
anim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
popupItemView.setVisibility(VISIBLE);
}
});
anim.setDuration(duration);
int animationIndex = mIsAboveIcon ? itemCount - i - 1 : i;
anim.setStartDelay(stagger * animationIndex);
anim.setInterpolator(interpolator);
shortcutAnims.play(anim);
Animator fadeAnim = ObjectAnimator.ofFloat(popupItemView, View.ALPHA, 1);
fadeAnim.setInterpolator(fadeInterpolator);
// We want the shortcut to be fully opaque before the arrow starts animating.
fadeAnim.setDuration(arrowScaleDelay);
shortcutAnims.play(fadeAnim);
}
shortcutAnims.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mOpenCloseAnimator = null;
Utilities.sendCustomAccessibilityEvent(
PopupContainerWithArrow.this,
AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED,
getContext().getString(R.string.action_deep_shortcut));
}
});
// Animate the arrow
mArrow.setScaleX(0);
mArrow.setScaleY(0);
Animator arrowScale = createArrowScaleAnim(1).setDuration(arrowScaleDuration);
arrowScale.setStartDelay(arrowScaleDelay);
shortcutAnims.play(arrowScale);
mOpenCloseAnimator = shortcutAnims;
shortcutAnims.start();
}
/**
* Orients this container above or below the given icon, aligning with the left or right.
*
* These are the preferred orientations, in order (RTL prefers right-aligned over left):
* - Above and left-aligned
* - Above and right-aligned
* - Below and left-aligned
* - Below and right-aligned
*
* So we always align left if there is enough horizontal space
* and align above if there is enough vertical space.
*/
private void orientAboutIcon(BubbleTextView icon, int arrowHeight) {
int width = getMeasuredWidth();
int height = getMeasuredHeight() + arrowHeight;
DragLayer dragLayer = mLauncher.getDragLayer();
dragLayer.getDescendantRectRelativeToSelf(icon, mTempRect);
Rect insets = dragLayer.getInsets();
// Align left (right in RTL) if there is room.
int leftAlignedX = mTempRect.left + icon.getPaddingLeft();
int rightAlignedX = mTempRect.right - width - icon.getPaddingRight();
int x = leftAlignedX;
boolean canBeLeftAligned = leftAlignedX + width + insets.left
< dragLayer.getRight() - insets.right;
boolean canBeRightAligned = rightAlignedX > dragLayer.getLeft() + insets.left;
if (!canBeLeftAligned || (mIsRtl && canBeRightAligned)) {
x = rightAlignedX;
}
mIsLeftAligned = x == leftAlignedX;
if (mIsRtl) {
x -= dragLayer.getWidth() - width;
}
// Offset x so that the arrow and shortcut icons are center-aligned with the original icon.
int iconWidth = icon.getWidth() - icon.getTotalPaddingLeft() - icon.getTotalPaddingRight();
iconWidth *= icon.getScaleX();
Resources resources = getResources();
int xOffset;
if (isAlignedWithStart()) {
// Aligning with the shortcut icon.
int shortcutIconWidth = resources.getDimensionPixelSize(R.dimen.deep_shortcut_icon_size);
int shortcutPaddingStart = resources.getDimensionPixelSize(
R.dimen.popup_padding_start);
xOffset = iconWidth / 2 - shortcutIconWidth / 2 - shortcutPaddingStart;
} else {
// Aligning with the drag handle.
int shortcutDragHandleWidth = resources.getDimensionPixelSize(
R.dimen.deep_shortcut_drag_handle_size);
int shortcutPaddingEnd = resources.getDimensionPixelSize(
R.dimen.popup_padding_end);
xOffset = iconWidth / 2 - shortcutDragHandleWidth / 2 - shortcutPaddingEnd;
}
x += mIsLeftAligned ? xOffset : -xOffset;
// Open above icon if there is room.
int iconHeight = icon.getIcon().getBounds().height();
int y = mTempRect.top + icon.getPaddingTop() - height;
mIsAboveIcon = y > dragLayer.getTop() + insets.top;
if (!mIsAboveIcon) {
y = mTempRect.top + icon.getPaddingTop() + iconHeight;
}
// Insets are added later, so subtract them now.
if (mIsRtl) {
x += insets.right;
} else {
x -= insets.left;
}
y -= insets.top;
if (y < dragLayer.getTop() || y + height > dragLayer.getBottom()) {
// The container is opening off the screen, so just center it in the drag layer instead.
((FrameLayout.LayoutParams) getLayoutParams()).gravity = Gravity.CENTER_VERTICAL;
// Put the container next to the icon, preferring the right side in ltr (left in rtl).
int rightSide = leftAlignedX + iconWidth - insets.left;
int leftSide = rightAlignedX - iconWidth - insets.left;
if (!mIsRtl) {
if (rightSide + width < dragLayer.getRight()) {
x = rightSide;
mIsLeftAligned = true;
} else {
x = leftSide;
mIsLeftAligned = false;
}
} else {
if (leftSide > dragLayer.getLeft()) {
x = leftSide;
mIsLeftAligned = false;
} else {
x = rightSide;
mIsLeftAligned = true;
}
}
mIsAboveIcon = true;
}
if (x < dragLayer.getLeft() || x + width > dragLayer.getRight()) {
// If we are still off screen, center horizontally too.
((FrameLayout.LayoutParams) getLayoutParams()).gravity |= Gravity.CENTER_HORIZONTAL;
}
int gravity = ((FrameLayout.LayoutParams) getLayoutParams()).gravity;
if (!Gravity.isHorizontal(gravity)) {
setX(x);
}
if (!Gravity.isVertical(gravity)) {
setY(y);
}
}
private boolean isAlignedWithStart() {
return mIsLeftAligned && !mIsRtl || !mIsLeftAligned && mIsRtl;
}
/**
* Adds an arrow view pointing at the original icon.
* @param horizontalOffset the horizontal offset of the arrow, so that it
* points at the center of the original icon
*/
private View addArrowView(int horizontalOffset, int verticalOffset, int width, int height) {
LayoutParams layoutParams = new LayoutParams(width, height);
if (mIsLeftAligned) {
layoutParams.gravity = Gravity.LEFT;
layoutParams.leftMargin = horizontalOffset;
} else {
layoutParams.gravity = Gravity.RIGHT;
layoutParams.rightMargin = horizontalOffset;
}
if (mIsAboveIcon) {
layoutParams.topMargin = verticalOffset;
} else {
layoutParams.bottomMargin = verticalOffset;
}
View arrowView = new View(getContext());
if (Gravity.isVertical(((FrameLayout.LayoutParams) getLayoutParams()).gravity)) {
// This is only true if there wasn't room for the container next to the icon,
// so we centered it instead. In that case we don't want to show the arrow.
arrowView.setVisibility(INVISIBLE);
} else {
ShapeDrawable arrowDrawable = new ShapeDrawable(TriangleShape.create(
width, height, !mIsAboveIcon));
Paint arrowPaint = arrowDrawable.getPaint();
arrowPaint.setColor(Color.WHITE);
// The corner path effect won't be reflected in the shadow, but shouldn't be noticeable.
int radius = getResources().getDimensionPixelSize(R.dimen.popup_arrow_corner_radius);
arrowPaint.setPathEffect(new CornerPathEffect(radius));
arrowView.setBackground(arrowDrawable);
arrowView.setElevation(getElevation());
}
addView(arrowView, mIsAboveIcon ? getChildCount() : 0, layoutParams);
return arrowView;
}
@Override
public View getExtendedTouchView() {
return mOriginalIcon;
}
/**
* Determines when the deferred drag should be started.
*
* Current behavior:
* - Start the drag if the touch passes a certain distance from the original touch down.
*/
public DragOptions.PreDragCondition createPreDragCondition() {
return new DragOptions.PreDragCondition() {
@Override
public boolean shouldStartDrag(double distanceDragged) {
return distanceDragged > mStartDragThreshold;
}
@Override
public void onPreDragStart(DropTarget.DragObject dragObject) {
mOriginalIcon.setVisibility(INVISIBLE);
}
@Override
public void onPreDragEnd(DropTarget.DragObject dragObject, boolean dragStarted) {
if (!dragStarted) {
mOriginalIcon.setVisibility(VISIBLE);
mLauncher.getUserEventDispatcher().logDeepShortcutsOpen(mOriginalIcon);
if (!mIsAboveIcon) {
mOriginalIcon.setTextVisibility(false);
}
}
}
};
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
mInterceptTouchDown.set(ev.getX(), ev.getY());
return false;
}
// Stop sending touch events to deep shortcut views if user moved beyond touch slop.
return Math.hypot(mInterceptTouchDown.x - ev.getX(), mInterceptTouchDown.y - ev.getY())
> ViewConfiguration.get(getContext()).getScaledTouchSlop();
}
/**
* Updates the notification header if the original icon's badge updated.
*/
public void updateNotificationHeader(Set<PackageUserKey> updatedBadges) {
ItemInfo itemInfo = (ItemInfo) mOriginalIcon.getTag();
PackageUserKey packageUser = PackageUserKey.fromItemInfo(itemInfo);
if (updatedBadges.contains(packageUser)) {
updateNotificationHeader();
}
}
private void updateNotificationHeader() {
ItemInfo itemInfo = (ItemInfo) mOriginalIcon.getTag();
BadgeInfo badgeInfo = mLauncher.getPopupDataProvider().getBadgeInfoForItem(itemInfo);
if (mNotificationItemView != null && badgeInfo != null) {
IconPalette palette = mOriginalIcon.getIcon() instanceof FastBitmapDrawable
? ((FastBitmapDrawable) mOriginalIcon.getIcon()).getIconPalette()
: null;
mNotificationItemView.updateHeader(badgeInfo.getNotificationCount(), palette);
}
}
public void trimNotifications(Map<PackageUserKey, BadgeInfo> updatedBadges) {
if (mNotificationItemView == null) {
return;
}
ItemInfo originalInfo = (ItemInfo) mOriginalIcon.getTag();
BadgeInfo badgeInfo = updatedBadges.get(PackageUserKey.fromItemInfo(originalInfo));
if (badgeInfo == null || badgeInfo.getNotificationCount() == 0) {
AnimatorSet removeNotification = LauncherAnimUtils.createAnimatorSet();
final int duration = getResources().getInteger(
R.integer.config_removeNotificationViewDuration);
final int spacing = getResources().getDimensionPixelSize(R.dimen.popup_items_spacing);
removeNotification.play(reduceNotificationViewHeight(
mNotificationItemView.getHeight() + spacing, duration));
final View removeMarginView = mIsAboveIcon ? getItemViewAt(getItemCount() - 2)
: mNotificationItemView;
if (removeMarginView != null) {
ValueAnimator removeMargin = ValueAnimator.ofFloat(1, 0).setDuration(duration);
removeMargin.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
((MarginLayoutParams) removeMarginView.getLayoutParams()).bottomMargin
= (int) (spacing * (float) valueAnimator.getAnimatedValue());
}
});
removeNotification.play(removeMargin);
}
Animator fade = ObjectAnimator.ofFloat(mNotificationItemView, ALPHA, 0)
.setDuration(duration);
fade.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
removeView(mNotificationItemView);
mNotificationItemView = null;
if (getItemCount() == 0) {
close(false);
return;
}
}
});
removeNotification.play(fade);
final long arrowScaleDuration = getResources().getInteger(
R.integer.config_deepShortcutArrowOpenDuration);
Animator hideArrow = createArrowScaleAnim(0).setDuration(arrowScaleDuration);
hideArrow.setStartDelay(0);
Animator showArrow = createArrowScaleAnim(1).setDuration(arrowScaleDuration);
showArrow.setStartDelay((long) (duration - arrowScaleDuration * 1.5));
removeNotification.playSequentially(hideArrow, showArrow);
removeNotification.start();
return;
}
mNotificationItemView.trimNotifications(NotificationKeyData.extractKeysOnly(
badgeInfo.getNotificationKeys()));
}
@Override
protected void onWidgetsBound() {
if (mShortcutsItemView != null) {
mShortcutsItemView.enableWidgetsIfExist(mOriginalIcon);
}
}
private ObjectAnimator createArrowScaleAnim(float scale) {
return LauncherAnimUtils.ofPropertyValuesHolder(
mArrow, new PropertyListBuilder().scale(scale).build());
}
/**
* Animates the height of the notification item and the translationY of other items accordingly.
*/
public Animator reduceNotificationViewHeight(int heightToRemove, int duration) {
final int translateYBy = mIsAboveIcon ? heightToRemove : -heightToRemove;
AnimatorSet animatorSet = LauncherAnimUtils.createAnimatorSet();
animatorSet.play(mNotificationItemView.animateHeightRemoval(heightToRemove));
PropertyResetListener<View, Float> resetTranslationYListener
= new PropertyResetListener<>(TRANSLATION_Y, 0f);
for (int i = 0; i < getItemCount(); i++) {
final PopupItemView itemView = getItemViewAt(i);
if (!mIsAboveIcon && itemView == mNotificationItemView) {
// The notification view is already in the right place when container is below icon.
continue;
}
ValueAnimator translateItem = ObjectAnimator.ofFloat(itemView, TRANSLATION_Y,
itemView.getTranslationY() + translateYBy).setDuration(duration);
translateItem.addListener(resetTranslationYListener);
animatorSet.play(translateItem);
}
if (mIsAboveIcon) {
// All the items, including the notification item, translated down, but the
// container itself did not. This means the items would jump back to their
// original translation unless we update the container's translationY here.
animatorSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
setTranslationY(getTranslationY() + translateYBy);
}
});
}
return animatorSet;
}
@Override
public boolean supportsAppInfoDropTarget() {
return true;
}
@Override
public boolean supportsDeleteDropTarget() {
return false;
}
@Override
public float getIntrinsicIconScaleFactor() {
return 1f;
}
@Override
public void onDropCompleted(View target, DropTarget.DragObject d, boolean isFlingToDelete,
boolean success) {
if (!success) {
d.dragView.remove();
mLauncher.showWorkspace(true);
mLauncher.getDropTargetBar().onDragEnd();
}
}
@Override
public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
// Either the original icon or one of the shortcuts was dragged.
// Hide the container, but don't remove it yet because that interferes with touch events.
mDeferContainerRemoval = true;
animateClose();
}
@Override
public void onDragEnd() {
if (!mIsOpen) {
if (mOpenCloseAnimator != null) {
// Close animation is running.
mDeferContainerRemoval = false;
} else {
// Close animation is not running.
if (mDeferContainerRemoval) {
closeComplete();
}
}
}
}
@Override
public void fillInLogContainerData(View v, ItemInfo info, Target target, Target targetParent) {
target.itemType = ItemType.DEEPSHORTCUT;
targetParent.containerType = ContainerType.DEEPSHORTCUTS;
}
@Override
protected void handleClose(boolean animate) {
if (animate) {
animateClose();
} else {
closeComplete();
}
}
protected void animateClose() {
if (!mIsOpen) {
return;
}
if (mOpenCloseAnimator != null) {
mOpenCloseAnimator.cancel();
}
mIsOpen = false;
final AnimatorSet shortcutAnims = LauncherAnimUtils.createAnimatorSet();
final int itemCount = getItemCount();
int numOpenShortcuts = 0;
for (int i = 0; i < itemCount; i++) {
if (getItemViewAt(i).isOpenOrOpening()) {
numOpenShortcuts++;
}
}
final long duration = getResources().getInteger(
R.integer.config_deepShortcutCloseDuration);
final long arrowScaleDuration = getResources().getInteger(
R.integer.config_deepShortcutArrowOpenDuration);
final long stagger = getResources().getInteger(
R.integer.config_deepShortcutCloseStagger);
final TimeInterpolator fadeInterpolator = new LogAccelerateInterpolator(100, 0);
int firstOpenItemIndex = mIsAboveIcon ? itemCount - numOpenShortcuts : 0;
for (int i = firstOpenItemIndex; i < firstOpenItemIndex + numOpenShortcuts; i++) {
final PopupItemView view = getItemViewAt(i);
Animator anim;
anim = view.createCloseAnimation(mIsAboveIcon, mIsLeftAligned, duration);
int animationIndex = mIsAboveIcon ? i - firstOpenItemIndex
: numOpenShortcuts - i - 1;
anim.setStartDelay(stagger * animationIndex);
Animator fadeAnim = ObjectAnimator.ofFloat(view, View.ALPHA, 0);
// Don't start fading until the arrow is gone.
fadeAnim.setStartDelay(stagger * animationIndex + arrowScaleDuration);
fadeAnim.setDuration(duration - arrowScaleDuration);
fadeAnim.setInterpolator(fadeInterpolator);
shortcutAnims.play(fadeAnim);
anim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
view.setVisibility(INVISIBLE);
}
});
shortcutAnims.play(anim);
}
Animator arrowAnim = createArrowScaleAnim(0).setDuration(arrowScaleDuration);
arrowAnim.setStartDelay(0);
shortcutAnims.play(arrowAnim);
shortcutAnims.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mOpenCloseAnimator = null;
if (mDeferContainerRemoval) {
setVisibility(INVISIBLE);
} else {
closeComplete();
}
}
});
mOpenCloseAnimator = shortcutAnims;
shortcutAnims.start();
mOriginalIcon.forceHideBadge(false);
}
/**
* Closes the folder without animation.
*/
protected void closeComplete() {
if (mOpenCloseAnimator != null) {
mOpenCloseAnimator.cancel();
mOpenCloseAnimator = null;
}
mIsOpen = false;
mDeferContainerRemoval = false;
boolean isInHotseat = ((ItemInfo) mOriginalIcon.getTag()).container
== LauncherSettings.Favorites.CONTAINER_HOTSEAT;
mOriginalIcon.setTextVisibility(!isInHotseat);
mOriginalIcon.forceHideBadge(false);
mLauncher.getDragController().removeDragListener(this);
mLauncher.getDragLayer().removeView(this);
}
@Override
protected boolean isOfType(int type) {
return (type & TYPE_POPUP_CONTAINER_WITH_ARROW) != 0;
}
/**
* Returns a DeepShortcutsContainer which is already open or null
*/
public static PopupContainerWithArrow getOpen(Launcher launcher) {
return getOpenView(launcher, TYPE_POPUP_CONTAINER_WITH_ARROW);
}
@Override
public int getLogContainerType() {
return ContainerType.DEEPSHORTCUTS;
}
}