diff options
author | Sunny Goyal <sunnygoyal@google.com> | 2017-11-09 15:24:06 -0800 |
---|---|---|
committer | Sunny Goyal <sunnygoyal@google.com> | 2017-11-15 16:43:20 -0800 |
commit | 00ac9202417cb5023b9990b2f6f33d321378ff26 (patch) | |
tree | 28cd25a215dbe9bb568aa1a11394a5ae38e3a8a6 /src/com/android/launcher3/popup/PopupContainerWithArrow.java | |
parent | ea529083bd45bae8edcb86d0be056ff90921d0c1 (diff) | |
download | android_packages_apps_Trebuchet-00ac9202417cb5023b9990b2f6f33d321378ff26.tar.gz android_packages_apps_Trebuchet-00ac9202417cb5023b9990b2f6f33d321378ff26.tar.bz2 android_packages_apps_Trebuchet-00ac9202417cb5023b9990b2f6f33d321378ff26.zip |
Simplifying app icon popup
> Using a single linearLayout instead of multiple nested views
> Using clipToOutline for rounded corners instead of using canvas.saveLayer
> Removing nested view elevations and overdraw
> Using LayoutTransition for animating layout changes, instead of manually creating animators
Change-Id: I8e57092f52ca5a032a2756594fdd39788acc5a0d
Diffstat (limited to 'src/com/android/launcher3/popup/PopupContainerWithArrow.java')
-rw-r--r-- | src/com/android/launcher3/popup/PopupContainerWithArrow.java | 925 |
1 files changed, 664 insertions, 261 deletions
diff --git a/src/com/android/launcher3/popup/PopupContainerWithArrow.java b/src/com/android/launcher3/popup/PopupContainerWithArrow.java index 4435afb8f..3dc58a1f2 100644 --- a/src/com/android/launcher3/popup/PopupContainerWithArrow.java +++ b/src/com/android/launcher3/popup/PopupContainerWithArrow.java @@ -16,26 +16,39 @@ package com.android.launcher3.popup; -import static com.android.launcher3.popup.PopupPopulator.MAX_SHORTCUTS_IF_NOTIFICATIONS; -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; - import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; +import android.animation.LayoutTransition; 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.CornerPathEffect; +import android.graphics.Outline; +import android.graphics.Paint; +import android.graphics.Point; +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.ViewGroup; +import android.view.ViewOutlineProvider; +import android.view.accessibility.AccessibilityEvent; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.widget.ImageView; + +import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.BubbleTextView; import com.android.launcher3.DragSource; import com.android.launcher3.DropTarget; @@ -45,42 +58,103 @@ import com.android.launcher3.Launcher; import com.android.launcher3.LauncherAnimUtils; import com.android.launcher3.LauncherModel; import com.android.launcher3.R; -import com.android.launcher3.anim.PropertyResetListener; +import com.android.launcher3.Utilities; +import com.android.launcher3.accessibility.LauncherAccessibilityDelegate; +import com.android.launcher3.accessibility.ShortcutMenuAccessibilityDelegate; +import com.android.launcher3.anim.RoundedRectRevealOutlineProvider; 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.dragndrop.DragView; import com.android.launcher3.graphics.IconPalette; +import com.android.launcher3.graphics.TriangleShape; +import com.android.launcher3.logging.LoggerUtils; +import com.android.launcher3.notification.NotificationInfo; import com.android.launcher3.notification.NotificationItemView; import com.android.launcher3.notification.NotificationKeyData; -import com.android.launcher3.popup.PopupPopulator.Item; import com.android.launcher3.shortcuts.DeepShortcutManager; import com.android.launcher3.shortcuts.DeepShortcutView; -import com.android.launcher3.shortcuts.ShortcutsItemView; +import com.android.launcher3.shortcuts.ShortcutDragPreviewProvider; import com.android.launcher3.util.PackageUserKey; import com.android.launcher3.util.Themes; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; +import static com.android.launcher3.notification.NotificationMainView.NOTIFICATION_ITEM_INFO; +import static com.android.launcher3.popup.PopupPopulator.MAX_SHORTCUTS; +import static com.android.launcher3.popup.PopupPopulator.MAX_SHORTCUTS_IF_NOTIFICATIONS; +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 and notifications associated with an app. */ @TargetApi(Build.VERSION_CODES.N) -public class PopupContainerWithArrow extends BaseActionPopup<BubbleTextView> implements DragSource, - DragController.DragListener { +public class PopupContainerWithArrow extends AbstractFloatingView implements DragSource, + DragController.DragListener, View.OnLongClickListener, + View.OnTouchListener { + + private final List<DeepShortcutView> mShortcuts = new ArrayList<>(); + private final PointF mInterceptTouchDown = new PointF(); + private final Rect mTempRect = new Rect(); + private final Point mIconLastTouchPos = new Point(); private final int mStartDragThreshold; + private final LayoutInflater mInflater; + private final float mOutlineRadius; + private final Launcher mLauncher; + private final LauncherAccessibilityDelegate mAccessibilityDelegate; + private final boolean mIsRtl; + + private final int mArrayOffset; + private final View mArrow; + private BubbleTextView mOriginalIcon; private NotificationItemView mNotificationItemView; - private AnimatorSet mReduceHeightAnimatorSet; + + private ViewGroup mSystemShortcutContainer; + + private boolean mIsLeftAligned; + protected boolean mIsAboveIcon; private int mNumNotifications; + private int mGravity; + + protected Animator mOpenCloseAnimator; + protected boolean mDeferContainerRemoval; + private final Rect mStartRect = new Rect(); + private final Rect mEndRect = new Rect(); public PopupContainerWithArrow(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mStartDragThreshold = getResources().getDimensionPixelSize( R.dimen.deep_shortcuts_start_drag_threshold); + mInflater = LayoutInflater.from(context); + mOutlineRadius = getResources().getDimension(R.dimen.bg_round_rect_radius); + mLauncher = Launcher.getLauncher(context); + mAccessibilityDelegate = new ShortcutMenuAccessibilityDelegate(mLauncher); + mIsRtl = Utilities.isRtl(getResources()); + + setClipToOutline(true); + setOutlineProvider(new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mOutlineRadius); + } + }); + + // Initialize arrow view + final Resources resources = getResources(); + final int arrowWidth = resources.getDimensionPixelSize(R.dimen.popup_arrow_width); + final int arrowHeight = resources.getDimensionPixelSize(R.dimen.popup_arrow_height); + mArrow = new View(context); + mArrow.setLayoutParams(new DragLayer.LayoutParams(arrowWidth, arrowHeight)); + mArrayOffset = resources.getDimensionPixelSize(R.dimen.popup_arrow_vertical_offset); } public PopupContainerWithArrow(Context context, AttributeSet attrs) { @@ -91,6 +165,75 @@ public class PopupContainerWithArrow extends BaseActionPopup<BubbleTextView> imp this(context, null, 0); } + public LauncherAccessibilityDelegate getAccessibilityDelegate() { + return mAccessibilityDelegate; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + mInterceptTouchDown.set(ev.getX(), ev.getY()); + } + if (mNotificationItemView != null + && mNotificationItemView.onInterceptTouchEvent(ev)) { + return true; + } + // 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(); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (mNotificationItemView != null) { + return mNotificationItemView.onTouchEvent(ev); + } + return super.onTouchEvent(ev); + } + + @Override + protected boolean isOfType(int type) { + return (type & TYPE_ACTION_POPUP) != 0; + } + + @Override + public void logActionCommand(int command) { + mLauncher.getUserEventDispatcher().logActionCommand( + command, mOriginalIcon, ContainerType.DEEPSHORTCUTS); + } + + @Override + public boolean onControllerInterceptTouchEvent(MotionEvent ev) { + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + DragLayer dl = mLauncher.getDragLayer(); + if (!dl.isEventOverView(this, ev)) { + mLauncher.getUserEventDispatcher().logActionTapOutside( + LoggerUtils.newContainerTarget(ContainerType.DEEPSHORTCUTS)); + close(true); + + // We let touches on the original icon go through so that users can launch + // the app with one tap if they don't find a shortcut they want. + return mOriginalIcon == null || !dl.isEventOverView(mOriginalIcon, ev); + } + } + return false; + } + + @Override + protected void handleClose(boolean animate) { + if (animate) { + animateClose(); + } else { + closeComplete(); + } + } + + public <T extends View> T inflateAndAdd(int resId) { + View view = mInflater.inflate(resId, this, false); + addView(view); + return (T) view; + } + /** * Shows the notifications and deep shortcuts associated with {@param icon}. * @return the container if shown or null. @@ -124,188 +267,420 @@ public class PopupContainerWithArrow extends BaseActionPopup<BubbleTextView> imp private void populateAndShow(final BubbleTextView originalIcon, final List<String> shortcutIds, final List<NotificationKeyData> notificationKeys, List<SystemShortcut> systemShortcuts) { mNumNotifications = notificationKeys.size(); - PopupPopulator.Item[] itemsToPopulate = PopupPopulator - .getItemsToPopulate(shortcutIds, notificationKeys, systemShortcuts); - populateAndShow(originalIcon, itemsToPopulate); - ItemInfo originalItemInfo = (ItemInfo) originalIcon.getTag(); - List<DeepShortcutView> shortcutViews = mShortcutsItemView == null - ? Collections.EMPTY_LIST - : mShortcutsItemView.getDeepShortcutViews(mIsAboveIcon); - List<View> systemShortcutViews = mShortcutsItemView == null - ? Collections.EMPTY_LIST - : mShortcutsItemView.getSystemShortcutViews(mIsAboveIcon); - if (mNotificationItemView != null) { + setVisibility(View.INVISIBLE); + mLauncher.getDragLayer().addView(this); + + mOriginalIcon = originalIcon; + + // Add views + if (mNumNotifications > 0) { + // Add notification entries + View.inflate(getContext(), R.layout.notification_content, this); + mNotificationItemView = new NotificationItemView(this); + if (mNumNotifications == 1) { + mNotificationItemView.removeFooter(); + } updateNotificationHeader(); } + int viewsToFlip = getChildCount(); + mSystemShortcutContainer = this; + + if (!shortcutIds.isEmpty()) { + if (mNotificationItemView != null) { + mNotificationItemView.addGutter(); + } + + for (int i = shortcutIds.size(); i > 0; i--) { + mShortcuts.add(inflateAndAdd(R.layout.deep_shortcut)); + } + updateHiddenShortcuts(); + + if (!systemShortcuts.isEmpty()) { + mSystemShortcutContainer = inflateAndAdd(R.layout.system_shortcut_icons); + for (SystemShortcut shortcut : systemShortcuts) { + View view = mInflater.inflate(R.layout.system_shortcut_icon_only, + mSystemShortcutContainer, false); + mSystemShortcutContainer.addView(view); + initializeSystemShortcut(view, shortcut); + } + } + } else if (!systemShortcuts.isEmpty()) { + if (mNotificationItemView != null) { + mNotificationItemView.addGutter(); + } - int numShortcuts = shortcutViews.size() + systemShortcutViews.size(); - int numNotifications = notificationKeys.size(); - if (numNotifications == 0) { + for (SystemShortcut shortcut : systemShortcuts) { + initializeSystemShortcut(inflateAndAdd(R.layout.system_shortcut), shortcut); + } + } + orientAboutIcon(); + + boolean reverseOrder = mIsAboveIcon; + if (reverseOrder) { + int count = getChildCount(); + ArrayList<View> allViews = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + if (i == viewsToFlip) { + Collections.reverse(allViews); + } + allViews.add(getChildAt(i)); + } + Collections.reverse(allViews); + removeAllViews(); + for (int i = 0; i < count; i++) { + addView(allViews.get(i)); + } + if (mNotificationItemView != null) { + mNotificationItemView.inverseGutterMargin(); + } + + orientAboutIcon(); + } + updateDividers(); + + // Add the arrow. + final int arrowHorizontalOffset = getResources().getDimensionPixelSize(isAlignedWithStart() + ? R.dimen.popup_arrow_horizontal_offset_start + : R.dimen.popup_arrow_horizontal_offset_end); + mLauncher.getDragLayer().addView(mArrow); + DragLayer.LayoutParams arrowLp = (DragLayer.LayoutParams) mArrow.getLayoutParams(); + if (mIsLeftAligned) { + mArrow.setX(getX() + arrowHorizontalOffset); + } else { + mArrow.setX(getX() + getMeasuredWidth() - arrowHorizontalOffset); + } + + if (Gravity.isVertical(mGravity)) { + // 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. + mArrow.setVisibility(INVISIBLE); + } else { + ShapeDrawable arrowDrawable = new ShapeDrawable(TriangleShape.create( + arrowLp.width, arrowLp.height, !mIsAboveIcon)); + Paint arrowPaint = arrowDrawable.getPaint(); + arrowPaint.setColor(Themes.getAttrColor(mLauncher, R.attr.popupColorPrimary)); + // 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)); + mArrow.setBackground(arrowDrawable); + mArrow.setElevation(getElevation()); + } + + mArrow.setPivotX(arrowLp.width / 2); + mArrow.setPivotY(mIsAboveIcon ? 0 : arrowLp.height); + + animateOpen(); + + ItemInfo originalItemInfo = (ItemInfo) originalIcon.getTag(); + int numShortcuts = mShortcuts.size() + systemShortcuts.size(); + if (mNumNotifications == 0) { setContentDescription(getContext().getString(R.string.shortcuts_menu_description, numShortcuts, originalIcon.getContentDescription().toString())); } else { setContentDescription(getContext().getString( R.string.shortcuts_menu_with_notifications_description, numShortcuts, - numNotifications, originalIcon.getContentDescription().toString())); + mNumNotifications, originalIcon.getContentDescription().toString())); } mLauncher.getDragController().addDragListener(this); mOriginalIcon.forceHideBadge(true); + // All views are added. Animate layout from now on. + setLayoutTransition(new LayoutTransition()); + // 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)); + this, shortcutIds, mShortcuts, notificationKeys)); } - @Override - protected void addDummyViews(Item[] itemTypesToPopulate) { - mNotificationItemView = null; - super.addDummyViews(itemTypesToPopulate); - if (mNumNotifications > 0) { - mShortcutsItemView.hideShortcuts(mIsAboveIcon, MAX_SHORTCUTS_IF_NOTIFICATIONS); + protected boolean isAlignedWithStart() { + return mIsLeftAligned && !mIsRtl || !mIsLeftAligned && mIsRtl; + } + + /** + * 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. + */ + protected void orientAboutIcon() { + measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + int width = getMeasuredWidth(); + int extraVerticalSpace = mArrow.getLayoutParams().height + mArrayOffset + + getResources().getDimensionPixelSize(R.dimen.popup_vertical_padding); + int height = getMeasuredHeight() + extraVerticalSpace; + + DragLayer dragLayer = mLauncher.getDragLayer(); + dragLayer.getDescendantRectRelativeToSelf(mOriginalIcon, mTempRect); + Rect insets = dragLayer.getInsets(); + + // Align left (right in RTL) if there is room. + int leftAlignedX = mTempRect.left + mOriginalIcon.getPaddingLeft(); + int rightAlignedX = mTempRect.right - width - mOriginalIcon.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 = mOriginalIcon.getWidth() + - mOriginalIcon.getTotalPaddingLeft() - mOriginalIcon.getTotalPaddingRight(); + iconWidth *= mOriginalIcon.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 = getIconHeightForPopupPlacement(); + int y = mTempRect.top + mOriginalIcon.getPaddingTop() - height; + mIsAboveIcon = y > dragLayer.getTop() + insets.top; + if (!mIsAboveIcon) { + y = mTempRect.top + mOriginalIcon.getPaddingTop() + iconHeight + extraVerticalSpace; + } + + // Insets are added later, so subtract them now. + if (mIsRtl) { + x += insets.right; + } else { + x -= insets.left; + } + y -= insets.top; + + mGravity = 0; + if (y + height > dragLayer.getBottom() - insets.bottom) { + // The container is opening off the screen, so just center it in the drag layer instead. + mGravity = 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; + } + + setX(x); + if (Gravity.isVertical(mGravity)) { + return; + } + + DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams(); + DragLayer.LayoutParams arrowLp = (DragLayer.LayoutParams) mArrow.getLayoutParams(); + if (mIsAboveIcon) { + arrowLp.gravity = lp.gravity = Gravity.BOTTOM; + lp.bottomMargin = + mLauncher.getDragLayer().getHeight() - y - getMeasuredHeight() - insets.top; + arrowLp.bottomMargin = lp.bottomMargin - arrowLp.height - mArrayOffset - insets.bottom; + } else { + arrowLp.gravity = lp.gravity = Gravity.TOP; + lp.topMargin = y + insets.top; + arrowLp.topMargin = lp.topMargin - insets.top - arrowLp.height - mArrayOffset; } } @Override - protected void onViewInflated(View view, Item itemType, - boolean shouldUnroundTopCorners, boolean shouldUnroundBottomCorners) { - if (itemType == PopupPopulator.Item.NOTIFICATION) { - mNotificationItemView = (NotificationItemView) view; - boolean notificationFooterHasIcons = mNumNotifications > 1; - int footerHeight = getResources().getDimensionPixelSize( - notificationFooterHasIcons ? R.dimen.notification_footer_height - : R.dimen.notification_empty_footer_height); - view.findViewById(R.id.footer).getLayoutParams().height = footerHeight; - if (notificationFooterHasIcons) { - mNotificationItemView.findViewById(R.id.divider).setVisibility(VISIBLE); - } + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + + // enforce contained is within screen + DragLayer dragLayer = mLauncher.getDragLayer(); + if (getTranslationX() + l < 0 || + getTranslationX() + r > dragLayer.getWidth()) { + // If we are still off screen, center horizontally too. + mGravity |= Gravity.CENTER_HORIZONTAL; + } - int roundedCorners = ROUNDED_TOP_CORNERS | ROUNDED_BOTTOM_CORNERS; - if (shouldUnroundTopCorners) { - roundedCorners &= ~ROUNDED_TOP_CORNERS; - mNotificationItemView.findViewById(R.id.gutter_top).setVisibility(VISIBLE); - } - if (shouldUnroundBottomCorners) { - roundedCorners &= ~ROUNDED_BOTTOM_CORNERS; - mNotificationItemView.findViewById(R.id.gutter_bottom).setVisibility(VISIBLE); - } - int backgroundColor = Themes.getAttrColor(mLauncher, R.attr.popupColorTertiary); - mNotificationItemView.setBackgroundWithCorners(backgroundColor, roundedCorners); - - mNotificationItemView.getMainView().setAccessibilityDelegate(mAccessibilityDelegate); - } else if (itemType == PopupPopulator.Item.SHORTCUT) { - view.setAccessibilityDelegate(mAccessibilityDelegate); - } - - if (itemType != PopupPopulator.Item.SYSTEM_SHORTCUT_ICON && itemType.isShortcut - && mNumNotifications > 0) { - int prevHeight = view.getLayoutParams().height; - // Condense shortcuts height when there are notifications. - view.getLayoutParams().height = getResources().getDimensionPixelSize( - R.dimen.bg_popup_item_condensed_height); - if (view instanceof DeepShortcutView) { - float iconScale = (float) view.getLayoutParams().height / prevHeight; - ((DeepShortcutView) view).getIconView().setScaleX(iconScale); - ((DeepShortcutView) view).getIconView().setScaleY(iconScale); - } + if (Gravity.isHorizontal(mGravity)) { + setX(dragLayer.getWidth() / 2 - getMeasuredWidth() / 2); + mArrow.setVisibility(INVISIBLE); + } + if (Gravity.isVertical(mGravity)) { + setY(dragLayer.getHeight() / 2 - getMeasuredHeight() / 2); } } - private void addDummyViews(PopupPopulator.Item[] itemTypesToPopulate, int numNotifications) { + protected void animateOpen() { + setVisibility(View.VISIBLE); + mIsOpen = true; + + final AnimatorSet openAnim = LauncherAnimUtils.createAnimatorSet(); final Resources res = getResources(); - final LayoutInflater inflater = mLauncher.getLayoutInflater(); - - int shortcutsItemRoundedCorners = ROUNDED_TOP_CORNERS | ROUNDED_BOTTOM_CORNERS; - int numItems = itemTypesToPopulate.length; - for (int i = 0; i < numItems; i++) { - PopupPopulator.Item itemTypeToPopulate = itemTypesToPopulate[i]; - PopupPopulator.Item prevItemTypeToPopulate = - i > 0 ? itemTypesToPopulate[i - 1] : null; - PopupPopulator.Item nextItemTypeToPopulate = - i < numItems - 1 ? itemTypesToPopulate[i + 1] : null; - final View item = inflater.inflate(itemTypeToPopulate.layoutId, this, false); - - boolean shouldUnroundTopCorners = prevItemTypeToPopulate != null - && itemTypeToPopulate.isShortcut ^ prevItemTypeToPopulate.isShortcut; - boolean shouldUnroundBottomCorners = nextItemTypeToPopulate != null - && itemTypeToPopulate.isShortcut ^ nextItemTypeToPopulate.isShortcut; - - if (itemTypeToPopulate == PopupPopulator.Item.NOTIFICATION) { - mNotificationItemView = (NotificationItemView) item; - boolean notificationFooterHasIcons = numNotifications > 1; - int footerHeight = res.getDimensionPixelSize( - notificationFooterHasIcons ? R.dimen.notification_footer_height - : R.dimen.notification_empty_footer_height); - item.findViewById(R.id.footer).getLayoutParams().height = footerHeight; - if (notificationFooterHasIcons) { - mNotificationItemView.findViewById(R.id.divider).setVisibility(VISIBLE); - } + final long revealDuration = (long) res.getInteger(R.integer.config_popupOpenCloseDuration); + final TimeInterpolator revealInterpolator = new AccelerateDecelerateInterpolator(); + + // Rectangular reveal. + final ValueAnimator revealAnim = createOpenCloseOutlineProvider() + .createRevealAnimator(this, false); + revealAnim.setDuration(revealDuration); + revealAnim.setInterpolator(revealInterpolator); + + Animator fadeIn = ObjectAnimator.ofFloat(this, ALPHA, 0, 1); + fadeIn.setDuration(revealDuration); + fadeIn.setInterpolator(revealInterpolator); + openAnim.play(fadeIn); + + // Animate the arrow. + mArrow.setScaleX(0); + mArrow.setScaleY(0); + Animator arrowScale = ObjectAnimator.ofFloat(mArrow, LauncherAnimUtils.SCALE_PROPERTY, 1) + .setDuration(res.getInteger(R.integer.config_popupArrowOpenDuration)); + + openAnim.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)); + } + }); - int roundedCorners = ROUNDED_TOP_CORNERS | ROUNDED_BOTTOM_CORNERS; - if (shouldUnroundTopCorners) { - roundedCorners &= ~ROUNDED_TOP_CORNERS; - mNotificationItemView.findViewById(R.id.gutter_top).setVisibility(VISIBLE); - } - if (shouldUnroundBottomCorners) { - roundedCorners &= ~ROUNDED_BOTTOM_CORNERS; - mNotificationItemView.findViewById(R.id.gutter_bottom).setVisibility(VISIBLE); - } - int backgroundColor = Themes.getAttrColor(mLauncher, R.attr.popupColorTertiary); - mNotificationItemView.setBackgroundWithCorners(backgroundColor, roundedCorners); + mOpenCloseAnimator = openAnim; + openAnim.playSequentially(revealAnim, arrowScale); + openAnim.start(); + } - mNotificationItemView.getMainView().setAccessibilityDelegate(mAccessibilityDelegate); - } else if (itemTypeToPopulate == PopupPopulator.Item.SHORTCUT) { - item.setAccessibilityDelegate(mAccessibilityDelegate); - } + public void applyNotificationInfos(List<NotificationInfo> notificationInfos) { + mNotificationItemView.applyNotificationInfos(notificationInfos); + } - if (itemTypeToPopulate.isShortcut) { - if (mShortcutsItemView == null) { - mShortcutsItemView = (ShortcutsItemView) inflater.inflate( - R.layout.shortcuts_item, this, false); - addView(mShortcutsItemView); - if (shouldUnroundTopCorners) { - shortcutsItemRoundedCorners &= ~ROUNDED_TOP_CORNERS; - } - } - if (itemTypeToPopulate != PopupPopulator.Item.SYSTEM_SHORTCUT_ICON - && numNotifications > 0) { - int prevHeight = item.getLayoutParams().height; - // Condense shortcuts height when there are notifications. - item.getLayoutParams().height = res.getDimensionPixelSize( - R.dimen.bg_popup_item_condensed_height); - if (item instanceof DeepShortcutView) { - float iconScale = (float) item.getLayoutParams().height / prevHeight; - ((DeepShortcutView) item).getIconView().setScaleX(iconScale); - ((DeepShortcutView) item).getIconView().setScaleY(iconScale); - } - } - mShortcutsItemView.addShortcutView(item, itemTypeToPopulate); - if (shouldUnroundBottomCorners) { - shortcutsItemRoundedCorners &= ~ROUNDED_BOTTOM_CORNERS; + private void updateHiddenShortcuts() { + int allowedCount = mNotificationItemView != null + ? MAX_SHORTCUTS_IF_NOTIFICATIONS : MAX_SHORTCUTS; + int originalHeight = getResources().getDimensionPixelSize(R.dimen.bg_popup_item_height); + int itemHeight = mNotificationItemView != null ? + getResources().getDimensionPixelSize(R.dimen.bg_popup_item_condensed_height) + : originalHeight; + float iconScale = ((float) itemHeight) / originalHeight; + + int total = mShortcuts.size(); + for (int i = 0; i < total; i++) { + DeepShortcutView view = mShortcuts.get(i); + view.setVisibility(i >= allowedCount ? GONE : VISIBLE); + view.getLayoutParams().height = itemHeight; + view.getIconView().setScaleX(iconScale); + view.getIconView().setScaleY(iconScale); + } + } + + private void updateDividers() { + int count = getChildCount(); + DeepShortcutView lastView = null; + for (int i = 0; i < count; i++) { + View view = getChildAt(i); + if (view.getVisibility() == VISIBLE && view instanceof DeepShortcutView) { + if (lastView != null) { + lastView.setDividerVisibility(VISIBLE); } - } else { - addView(item); + lastView = (DeepShortcutView) view; + lastView.setDividerVisibility(INVISIBLE); } } - int backgroundColor = Themes.getAttrColor(mLauncher, R.attr.popupColorPrimary); - mShortcutsItemView.setBackgroundWithCorners(backgroundColor, shortcutsItemRoundedCorners); - if (numNotifications > 0) { - mShortcutsItemView.hideShortcuts(mIsAboveIcon, MAX_SHORTCUTS_IF_NOTIFICATIONS); - } } @Override protected void onWidgetsBound() { - if (mShortcutsItemView != null) { - mShortcutsItemView.enableWidgetsIfExist(mOriginalIcon); + ItemInfo itemInfo = (ItemInfo) mOriginalIcon.getTag(); + SystemShortcut widgetInfo = new SystemShortcut.Widgets(); + View.OnClickListener onClickListener = widgetInfo.getOnClickListener(mLauncher, itemInfo); + View widgetsView = null; + int count = mSystemShortcutContainer.getChildCount(); + for (int i = 0; i < count; i++) { + View systemShortcutView = mSystemShortcutContainer.getChildAt(i); + if (systemShortcutView.getTag() instanceof SystemShortcut.Widgets) { + widgetsView = systemShortcutView; + break; + } + } + + if (onClickListener != null && widgetsView == null) { + // We didn't have any widgets cached but now there are some, so enable the shortcut. + if (mSystemShortcutContainer != this) { + View view = mInflater.inflate(R.layout.system_shortcut_icon_only, + mSystemShortcutContainer, false); + mSystemShortcutContainer.addView(view); + initializeSystemShortcut(view, widgetInfo); + } else { + // If using the expanded system shortcut (as opposed to just the icon), we need to + // reopen the container to ensure measurements etc. all work out. While this could + // be quite janky, in practice the user would typically see a small flicker as the + // animation restarts partway through, and this is a very rare edge case anyway. + ((PopupContainerWithArrow) getParent()).close(false); + PopupContainerWithArrow.showForIcon(mOriginalIcon); + } + } else if (onClickListener == null && widgetsView != null) { + // No widgets exist, but we previously added the shortcut so remove it. + if (mSystemShortcutContainer != this) { + mSystemShortcutContainer.removeView(widgetsView); + } else { + ((PopupContainerWithArrow) getParent()).close(false); + PopupContainerWithArrow.showForIcon(mOriginalIcon); + } } } - @Override + private void initializeSystemShortcut(View view, SystemShortcut info) { + if (view instanceof DeepShortcutView) { + // Expanded system shortcut, with both icon and text shown on white background. + final DeepShortcutView shortcutView = (DeepShortcutView) view; + shortcutView.getIconView().setBackgroundResource(info.iconResId); + shortcutView.getBubbleText().setText(info.labelResId); + } else if (view instanceof ImageView) { + // Only the system shortcut icon shows on a gray background header. + final ImageView shortcutIcon = (ImageView) view; + shortcutIcon.setImageResource(info.iconResId); + shortcutIcon.setContentDescription(getContext().getText(info.labelResId)); + } + view.setTag(info); + view.setOnClickListener(info.getOnClickListener(mLauncher, + (ItemInfo) mOriginalIcon.getTag())); + } + protected int getIconHeightForPopupPlacement() { return mOriginalIcon.getIcon() != null ? mOriginalIcon.getIcon().getBounds().height() @@ -383,108 +758,15 @@ public class PopupContainerWithArrow extends BaseActionPopup<BubbleTextView> imp ItemInfo originalInfo = (ItemInfo) mOriginalIcon.getTag(); BadgeInfo badgeInfo = updatedBadges.get(PackageUserKey.fromItemInfo(originalInfo)); if (badgeInfo == null || badgeInfo.getNotificationKeys().size() == 0) { - // There are no more notifications, so create an animation to remove - // the notifications view and expand the shortcuts view (if possible). - AnimatorSet removeNotification = LauncherAnimUtils.createAnimatorSet(); - int hiddenShortcutsHeight = 0; - if (mShortcutsItemView != null) { - hiddenShortcutsHeight = mShortcutsItemView.getHiddenShortcutsHeight(); - int backgroundColor = Themes.getAttrColor(mLauncher, R.attr.popupColorPrimary); - // With notifications gone, all corners of shortcuts item should be rounded. - mShortcutsItemView.setBackgroundWithCorners(backgroundColor, - ROUNDED_TOP_CORNERS | ROUNDED_BOTTOM_CORNERS); - removeNotification.play(mShortcutsItemView.showAllShortcuts(mIsAboveIcon)); - } - final int duration = getResources().getInteger( - R.integer.config_removeNotificationViewDuration); - removeNotification.play(adjustItemHeights(mNotificationItemView.getHeightMinusFooter(), - hiddenShortcutsHeight, duration)); - 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); - } - } - }); - removeNotification.play(fade); - final long arrowScaleDuration = getResources().getInteger( - R.integer.config_popupArrowOpenDuration); - 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())); - } - - public Animator reduceNotificationViewHeight(int heightToRemove, int duration) { - return adjustItemHeights(heightToRemove, 0, duration); - } - - /** - * Animates the height of the notification item and the translationY of other items accordingly. - */ - public Animator adjustItemHeights(int notificationHeightToRemove, int shortcutHeightToAdd, - int duration) { - if (mReduceHeightAnimatorSet != null) { - mReduceHeightAnimatorSet.cancel(); - } - final int translateYBy = mIsAboveIcon ? notificationHeightToRemove - shortcutHeightToAdd - : -notificationHeightToRemove; - mReduceHeightAnimatorSet = LauncherAnimUtils.createAnimatorSet(); - boolean removingNotification = - notificationHeightToRemove == mNotificationItemView.getHeightMinusFooter(); - boolean shouldRemoveNotificationHeightFromTop = mIsAboveIcon && removingNotification; - mReduceHeightAnimatorSet.play(mNotificationItemView.animateHeightRemoval( - notificationHeightToRemove, shouldRemoveNotificationHeightFromTop)); - PropertyResetListener<View, Float> resetTranslationYListener - = new PropertyResetListener<>(TRANSLATION_Y, 0f); - boolean itemIsAfterShortcuts = false; - for (int i = 0; i < getItemCount(); i++) { - final PopupItemView itemView = getItemViewAt(i); - if (itemIsAfterShortcuts) { - // Every item after the shortcuts item needs to adjust for the new height. - itemView.setTranslationY(itemView.getTranslationY() - shortcutHeightToAdd); - } - if (itemView == mNotificationItemView && (!mIsAboveIcon || removingNotification)) { - // The notification view is already in the right place. - continue; - } - ValueAnimator translateItem = ObjectAnimator.ofFloat(itemView, TRANSLATION_Y, - itemView.getTranslationY() + translateYBy).setDuration(duration); - translateItem.addListener(resetTranslationYListener); - mReduceHeightAnimatorSet.play(translateItem); - if (itemView == mShortcutsItemView) { - itemIsAfterShortcuts = true; - } - } - if (mIsAboveIcon) { - // We also need to adjust the arrow position to account for the new shortcuts height. - mArrow.setTranslationY(mArrow.getTranslationY() - shortcutHeightToAdd); + // No more notifications, remove the notification views and expand all shortcuts. + mNotificationItemView.removeAllViews(); + mNotificationItemView = null; + updateHiddenShortcuts(); + updateDividers(); + } else { + mNotificationItemView.trimNotifications( + NotificationKeyData.extractKeysOnly(badgeInfo.getNotificationKeys())); } - mReduceHeightAnimatorSet.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - 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. - setTranslationY(getTranslationY() + translateYBy); - mArrow.setTranslationY(0); - } - mReduceHeightAnimatorSet = null; - } - }); - return mReduceHeightAnimatorSet; } @Override @@ -515,25 +797,146 @@ public class PopupContainerWithArrow extends BaseActionPopup<BubbleTextView> imp @Override public void fillInLogContainerData(View v, ItemInfo info, Target target, Target targetParent) { - target.itemType = ItemType.DEEPSHORTCUT; + if (info == NOTIFICATION_ITEM_INFO) { + target.itemType = ItemType.NOTIFICATION; + } else { + target.itemType = ItemType.DEEPSHORTCUT; + target.rank = info.rank; + } targetParent.containerType = ContainerType.DEEPSHORTCUTS; } - @Override - protected void prepareCloseAnimator(AnimatorSet closeAnim) { + protected void animateClose() { + if (!mIsOpen) { + return; + } + mEndRect.setEmpty(); + if (mOpenCloseAnimator != null) { + Outline outline = new Outline(); + getOutlineProvider().getOutline(this, outline); + outline.getRect(mEndRect); + mOpenCloseAnimator.cancel(); + } + mIsOpen = false; + + final AnimatorSet closeAnim = LauncherAnimUtils.createAnimatorSet(); + // Hide the arrow + closeAnim.play(ObjectAnimator.ofFloat(mArrow, LauncherAnimUtils.SCALE_PROPERTY, 0)); + closeAnim.play(ObjectAnimator.ofFloat(mArrow, ALPHA, 0)); + // Animate original icon's text back in. closeAnim.play(mOriginalIcon.createTextAlphaAnimator(true /* fadeIn */)); - mOriginalIcon.forceHideBadge(false); - super.prepareCloseAnimator(closeAnim); + + final Resources res = getResources(); + final TimeInterpolator revealInterpolator = new AccelerateDecelerateInterpolator(); + + // Rectangular reveal (reversed). + final ValueAnimator revealAnim = createOpenCloseOutlineProvider() + .createRevealAnimator(this, true); + revealAnim.setInterpolator(revealInterpolator); + closeAnim.play(revealAnim); + + Animator fadeOut = ObjectAnimator.ofFloat(this, ALPHA, 0); + fadeOut.setInterpolator(revealInterpolator); + closeAnim.play(fadeOut); + closeAnim.setDuration((long) res.getInteger(R.integer.config_popupOpenCloseDuration)); + + closeAnim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mOpenCloseAnimator = null; + if (mDeferContainerRemoval) { + setVisibility(INVISIBLE); + } else { + closeComplete(); + } + } + }); + mOpenCloseAnimator = closeAnim; + closeAnim.start(); } - @Override - protected void closeComplete() { + private RoundedRectRevealOutlineProvider createOpenCloseOutlineProvider() { + int arrowCenterX = getResources().getDimensionPixelSize(mIsLeftAligned ^ mIsRtl ? + R.dimen.popup_arrow_horizontal_center_start: + R.dimen.popup_arrow_horizontal_center_end); + if (!mIsLeftAligned) { + arrowCenterX = getMeasuredWidth() - arrowCenterX; + } + int arrowCenterY = mIsAboveIcon ? getMeasuredHeight() : 0; + + mStartRect.set(arrowCenterX, arrowCenterY, arrowCenterX, arrowCenterY); + if (mEndRect.isEmpty()) { + mEndRect.set(0, 0, getMeasuredWidth(), getMeasuredHeight()); + } + + return new RoundedRectRevealOutlineProvider + (mOutlineRadius, mOutlineRadius, mStartRect, mEndRect); + } + + /** + * Closes the popup without animation. + */ + private void closeComplete() { mOriginalIcon.setTextVisibility(mOriginalIcon.shouldTextBeVisible()); mOriginalIcon.forceHideBadge(false); mLauncher.getDragController().removeDragListener(this); - super.closeComplete(); + if (mOpenCloseAnimator != null) { + mOpenCloseAnimator.cancel(); + mOpenCloseAnimator = null; + } + mIsOpen = false; + mDeferContainerRemoval = false; + mLauncher.getDragLayer().removeView(this); + mLauncher.getDragLayer().removeView(mArrow); + } + + @Override + public boolean onTouch(View v, MotionEvent ev) { + // Touched a shortcut, update where it was touched so we can drag from there on long click. + switch (ev.getAction()) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_MOVE: + mIconLastTouchPos.set((int) ev.getX(), (int) ev.getY()); + break; + } + return false; + } + + @Override + public boolean onLongClick(View v) { + // Return early if not the correct view + if (!(v.getParent() instanceof DeepShortcutView)) return false; + // Return early if global dragging is not enabled + if (!mLauncher.isDraggingEnabled()) return false; + // Return early if an item is already being dragged (e.g. when long-pressing two shortcuts) + if (mLauncher.getDragController().isDragging()) return false; + + // Long clicked on a shortcut. + DeepShortcutView sv = (DeepShortcutView) v.getParent(); + sv.setWillDrawIcon(false); + + // Move the icon to align with the center-top of the touch point + Point iconShift = new Point(); + iconShift.x = mIconLastTouchPos.x - sv.getIconCenter().x; + iconShift.y = mIconLastTouchPos.y - mLauncher.getDeviceProfile().iconSizePx; + + DragView dv = mLauncher.getWorkspace().beginDragShared(sv.getIconView(), + this, sv.getFinalInfo(), + new ShortcutDragPreviewProvider(sv.getIconView(), iconShift), new DragOptions()); + dv.animateShift(-iconShift.x, -iconShift.y); + + // TODO: support dragging from within folder without having to close it + AbstractFloatingView.closeOpenContainer(mLauncher, AbstractFloatingView.TYPE_FOLDER); + return false; + } + + /** + * Returns a PopupContainerWithArrow which is already open or null + */ + public static PopupContainerWithArrow getOpen(Launcher launcher) { + return getOpenView(launcher, TYPE_ACTION_POPUP); } } |