| /* |
| * 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; |
| } |
| } |