Updating the UI of the options popup to make it look similar to icon popup

Bug: 77327164
Change-Id: I3580df8bf8a43cb44123f203ffed9a85fa33aea7
diff --git a/res/layout/longpress_options_menu.xml b/res/layout/longpress_options_menu.xml
index 71d117a..168dbc3 100644
--- a/res/layout/longpress_options_menu.xml
+++ b/res/layout/longpress_options_menu.xml
@@ -15,83 +15,11 @@
 -->
 <com.android.launcher3.views.OptionsPopupView
     xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:launcher="http://schemas.android.com/apk/res-auto"
-    android:layout_width="match_parent"
-    android:layout_height="96dp"
+    android:id="@+id/deep_shortcuts_container"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
     android:background="?attr/popupColorPrimary"
+    android:clipToPadding="false"
+    android:clipChildren="false"
     android:elevation="@dimen/deep_shortcuts_elevation"
-    android:orientation="horizontal"
-    launcher:layout_ignoreInsets="true">
-
-    <FrameLayout
-        android:id="@+id/wallpaper_button"
-        android:layout_width="0dp"
-        android:layout_height="match_parent"
-        android:layout_weight="1"
-        android:background="?android:attr/selectableItemBackground">
-
-        <TextView
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_gravity="center"
-            android:drawablePadding="4dp"
-            android:drawableTint="?android:attr/textColorPrimary"
-            android:drawableTop="@drawable/ic_wallpaper"
-            android:fontFamily="sans-serif-condensed"
-            android:gravity="center"
-            android:paddingLeft="16dp"
-            android:paddingRight="16dp"
-            android:text="@string/wallpaper_button_text"
-            android:textColor="?android:attr/textColorPrimary"
-            android:textSize="12sp"/>
-    </FrameLayout>
-
-    <FrameLayout
-        android:id="@+id/widget_button"
-        android:layout_width="0dp"
-        android:layout_height="match_parent"
-        android:layout_weight="1"
-        android:background="?android:attr/selectableItemBackground">
-
-        <TextView
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_gravity="center"
-            android:drawablePadding="4dp"
-            android:drawableTint="?android:attr/textColorPrimary"
-            android:drawableTop="@drawable/ic_widget"
-            android:fontFamily="sans-serif-condensed"
-            android:gravity="center"
-            android:paddingLeft="16dp"
-            android:paddingRight="16dp"
-            android:text="@string/widget_button_text"
-            android:textColor="?android:attr/textColorPrimary"
-            android:textSize="12sp"/>
-
-    </FrameLayout>
-
-    <FrameLayout
-        android:id="@+id/settings_button"
-        android:layout_width="0dp"
-        android:layout_height="match_parent"
-        android:layout_weight="1"
-        android:background="?android:attr/selectableItemBackground">
-
-        <TextView
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_gravity="center"
-            android:drawablePadding="4dp"
-            android:drawableTint="?android:attr/textColorPrimary"
-            android:drawableTop="@drawable/ic_setting"
-            android:fontFamily="sans-serif-condensed"
-            android:gravity="center"
-            android:paddingLeft="16dp"
-            android:paddingRight="16dp"
-            android:text="@string/settings_button_text"
-            android:textColor="?android:attr/textColorPrimary"
-            android:textSize="12sp"/>
-
-    </FrameLayout>
-
-</com.android.launcher3.views.OptionsPopupView>
\ No newline at end of file
+    android:orientation="vertical" />
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index a2f7286..b1ad11e 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -222,4 +222,5 @@
 
 <!-- Overview -->
     <dimen name="options_menu_icon_size">24dp</dimen>
+    <dimen name="options_menu_thumb_size">32dp</dimen>
 </resources>
diff --git a/src/com/android/launcher3/BaseDraggingActivity.java b/src/com/android/launcher3/BaseDraggingActivity.java
index 4c11fe6..bde9ad3 100644
--- a/src/com/android/launcher3/BaseDraggingActivity.java
+++ b/src/com/android/launcher3/BaseDraggingActivity.java
@@ -47,7 +47,7 @@
     private static final String TAG = "BaseDraggingActivity";
 
     // The Intent extra that defines whether to ignore the launch animation
-    protected static final String INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION =
+    public static final String INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION =
             "com.android.launcher3.intent.extra.shortcut.INGORE_LAUNCH_ANIMATION";
 
     // When starting an action mode, setting this tag will cause the action mode to be cancelled
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index e2f7488..ccc774a 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -1634,30 +1634,6 @@
         }
     }
 
-    /**
-     * Event handler for the wallpaper picker button that appears after a long press
-     * on the home screen.
-     */
-    public void onClickWallpaperPicker(View v) {
-        if (!Utilities.isWallpaperAllowed(this)) {
-            Toast.makeText(this, R.string.msg_disabled_by_admin, Toast.LENGTH_SHORT).show();
-            return;
-        }
-        int pageScroll = mWorkspace.getScrollForPage(mWorkspace.getPageNearestToCenterOfScreen());
-        float offset = mWorkspace.mWallpaperOffset.wallpaperOffsetForScroll(pageScroll);
-        Intent intent = new Intent(Intent.ACTION_SET_WALLPAPER)
-                .putExtra(Utilities.EXTRA_WALLPAPER_OFFSET, offset);
-
-        String pickerPackage = getString(R.string.wallpaper_picker_package);
-        if (!TextUtils.isEmpty(pickerPackage)) {
-            intent.setPackage(pickerPackage);
-        } else {
-            // If there is no target package, use the default intent chooser animation
-            intent.putExtra(INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION, true);
-        }
-        startActivitySafely(v, intent, null);
-    }
-
     @TargetApi(Build.VERSION_CODES.M)
     @Override
     public ActivityOptions getActivityLaunchOptions(View v, boolean useDefaultLaunchOptions) {
@@ -2415,7 +2391,7 @@
 
                 // Setting the touch point to (-1, -1) will show the options popup in the center of
                 // the screen.
-                OptionsPopupView.show(this, -1, -1);
+                OptionsPopupView.showDefaultOptions(this, -1, -1);
             }
             return true;
         }
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 34ae8ea..a208c7a 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -349,6 +349,11 @@
         }
     }
 
+    public float getWallpaperOffsetForCenterPage() {
+        int pageScroll = getScrollForPage(getPageNearestToCenterOfScreen());
+        return mWallpaperOffset.wallpaperOffsetForScroll(pageScroll);
+    }
+
     public Rect estimateItemPosition(CellLayout cl, int hCell, int vCell, int hSpan, int vSpan) {
         Rect r = new Rect();
         cl.cellToRect(hCell, vCell, hSpan, vSpan, r);
diff --git a/src/com/android/launcher3/notification/NotificationItemView.java b/src/com/android/launcher3/notification/NotificationItemView.java
index 2fefa85..32410a6 100644
--- a/src/com/android/launcher3/notification/NotificationItemView.java
+++ b/src/com/android/launcher3/notification/NotificationItemView.java
@@ -20,7 +20,6 @@
 import android.content.Context;
 import android.graphics.Color;
 import android.graphics.Rect;
-import android.support.annotation.Nullable;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup.MarginLayoutParams;
@@ -83,7 +82,7 @@
 
     public void addGutter() {
         if (mGutter == null) {
-            mGutter = mContainer.inflateAndAdd(R.layout.notification_gutter);
+            mGutter = mContainer.inflateAndAdd(R.layout.notification_gutter, mContainer);
         }
     }
 
diff --git a/src/com/android/launcher3/popup/ArrowPopup.java b/src/com/android/launcher3/popup/ArrowPopup.java
new file mode 100644
index 0000000..bd08aaa
--- /dev/null
+++ b/src/com/android/launcher3/popup/ArrowPopup.java
@@ -0,0 +1,470 @@
+/*
+ * Copyright (C) 2018 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.content.Context;
+import android.content.res.Resources;
+import android.graphics.CornerPathEffect;
+import android.graphics.Outline;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.drawable.ShapeDrawable;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewOutlineProvider;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.animation.AccelerateDecelerateInterpolator;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAnimUtils;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.RevealOutlineAnimation;
+import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
+import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.graphics.TriangleShape;
+import com.android.launcher3.util.Themes;
+
+import java.util.ArrayList;
+import java.util.Collections;
+
+import static com.android.launcher3.compat.AccessibilityManagerCompat.sendCustomAccessibilityEvent;
+
+/**
+ * A container for shortcuts to deep links and notifications associated with an app.
+ */
+public abstract class ArrowPopup extends AbstractFloatingView {
+
+    private final Rect mTempRect = new Rect();
+
+    protected final LayoutInflater mInflater;
+    private final float mOutlineRadius;
+    protected final Launcher mLauncher;
+    protected final boolean mIsRtl;
+
+    private final int mArrayOffset;
+    private final View mArrow;
+
+    protected boolean mIsLeftAligned;
+    protected boolean mIsAboveIcon;
+    private int mGravity;
+
+    protected Animator mOpenCloseAnimator;
+    protected boolean mDeferContainerRemoval;
+    private final Rect mStartRect = new Rect();
+    private final Rect mEndRect = new Rect();
+
+    public ArrowPopup(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        mInflater = LayoutInflater.from(context);
+        mOutlineRadius = getResources().getDimension(R.dimen.bg_round_rect_radius);
+        mLauncher = Launcher.getLauncher(context);
+        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 ArrowPopup(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public ArrowPopup(Context context) {
+        this(context, null, 0);
+    }
+
+    @Override
+    protected void handleClose(boolean animate) {
+        if (animate) {
+            animateClose();
+        } else {
+            closeComplete();
+        }
+    }
+
+    public <T extends View> T inflateAndAdd(int resId, ViewGroup container) {
+        View view = mInflater.inflate(resId, container, false);
+        container.addView(view);
+        return (T) view;
+    }
+
+    /**
+     * Called when all view inflation and reordering in complete.
+     */
+    protected void onInflationComplete(boolean isReversed) { }
+
+    /**
+     * Shows the popup at the desired location, optionally reversing the children.
+     * @param viewsToFlip number of views from the top to to flip in case of reverse order
+     */
+    protected void reorderAndShow(int viewsToFlip) {
+        setVisibility(View.INVISIBLE);
+        mIsOpen = true;
+        mLauncher.getDragLayer().addView(this);
+        orientAboutObject();
+
+        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));
+            }
+
+            orientAboutObject();
+        }
+        onInflationComplete(reverseOrder);
+
+        // Add the arrow.
+        final Resources res = getResources();
+        final int arrowCenterOffset = res.getDimensionPixelSize(isAlignedWithStart()
+                ? R.dimen.popup_arrow_horizontal_center_start
+                : R.dimen.popup_arrow_horizontal_center_end);
+        final int halfArrowWidth = res.getDimensionPixelSize(R.dimen.popup_arrow_width) / 2;
+        mLauncher.getDragLayer().addView(mArrow);
+        DragLayer.LayoutParams arrowLp = (DragLayer.LayoutParams) mArrow.getLayoutParams();
+        if (mIsLeftAligned) {
+            mArrow.setX(getX() + arrowCenterOffset - halfArrowWidth);
+        } else {
+            mArrow.setX(getX() + getMeasuredWidth() - arrowCenterOffset - halfArrowWidth);
+        }
+
+        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 showDefaultOptions 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();
+    }
+
+    protected boolean isAlignedWithStart() {
+        return mIsLeftAligned && !mIsRtl || !mIsLeftAligned && mIsRtl;
+    }
+
+    /**
+     * Provide the location of the target object relative to the dragLayer.
+     */
+    protected abstract void getTargetObjectLocation(Rect outPos);
+
+    /**
+     * 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 orientAboutObject() {
+        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;
+
+        getTargetObjectLocation(mTempRect);
+        DragLayer dragLayer = mLauncher.getDragLayer();
+        Rect insets = dragLayer.getInsets();
+
+        // Align left (right in RTL) if there is room.
+        int leftAlignedX = mTempRect.left;
+        int rightAlignedX = mTempRect.right - width;
+        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;
+
+        // Offset x so that the arrow and shortcut icons are center-aligned with the original icon.
+        int iconWidth = mTempRect.width();
+        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 = mTempRect.height();
+        int y = mTempRect.top - height;
+        mIsAboveIcon = y > dragLayer.getTop() + insets.top;
+        if (!mIsAboveIcon) {
+            y = mTempRect.top + 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 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;
+        }
+
+        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 animateOpen() {
+        setVisibility(View.VISIBLE);
+
+        final AnimatorSet openAnim = LauncherAnimUtils.createAnimatorSet();
+        final Resources res = getResources();
+        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;
+                sendCustomAccessibilityEvent(
+                        ArrowPopup.this,
+                        AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED,
+                        getContext().getString(R.string.action_deep_shortcut));
+            }
+        });
+
+        mOpenCloseAnimator = openAnim;
+        openAnim.playSequentially(revealAnim, arrowScale);
+        openAnim.start();
+    }
+
+    protected void animateClose() {
+        if (!mIsOpen) {
+            return;
+        }
+        mEndRect.setEmpty();
+        if (getOutlineProvider() instanceof RevealOutlineAnimation) {
+            ((RevealOutlineAnimation) getOutlineProvider()).getOutline(mEndRect);
+        }
+        if (mOpenCloseAnimator != null) {
+            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));
+
+        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);
+
+        onCreateCloseAnimation(closeAnim);
+        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();
+    }
+
+    /**
+     * Called when creating the close transition allowing subclass can add additional animations.
+     */
+    protected void onCreateCloseAnimation(AnimatorSet anim) { }
+
+    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.
+     */
+    protected void closeComplete() {
+        if (mOpenCloseAnimator != null) {
+            mOpenCloseAnimator.cancel();
+            mOpenCloseAnimator = null;
+        }
+        mIsOpen = false;
+        mDeferContainerRemoval = false;
+        mLauncher.getDragLayer().removeView(this);
+        mLauncher.getDragLayer().removeView(mArrow);
+    }
+}
diff --git a/src/com/android/launcher3/popup/PopupContainerWithArrow.java b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
index 033fdf8..422a4ec 100644
--- a/src/com/android/launcher3/popup/PopupContainerWithArrow.java
+++ b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
@@ -16,36 +16,28 @@
 
 package com.android.launcher3.popup;
 
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
+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;
+
 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;
@@ -56,20 +48,15 @@
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.ItemInfoWithIcon;
 import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAnimUtils;
 import com.android.launcher3.LauncherModel;
 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.RevealOutlineAnimation;
-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.TriangleShape;
 import com.android.launcher3.logging.LoggerUtils;
 import com.android.launcher3.notification.NotificationInfo;
 import com.android.launcher3.notification.NotificationItemView;
@@ -79,85 +66,38 @@
 import com.android.launcher3.shortcuts.ShortcutDragPreviewProvider;
 import com.android.launcher3.touch.ItemLongClickListener;
 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.compat.AccessibilityManagerCompat.sendCustomAccessibilityEvent;
-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 AbstractFloatingView implements DragSource,
+public class PopupContainerWithArrow extends ArrowPopup 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 int mNumNotifications;
 
     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) {
@@ -222,21 +162,6 @@
         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.
@@ -267,13 +192,30 @@
         return container;
     }
 
+    @Override
+    protected void onInflationComplete(boolean isReversed) {
+        if (isReversed && mNotificationItemView != null) {
+            mNotificationItemView.inverseGutterMargin();
+        }
+
+        // Update dividers
+        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);
+                }
+                lastView = (DeepShortcutView) view;
+                lastView.setDividerVisibility(INVISIBLE);
+            }
+        }
+    }
+
     private void populateAndShow(final BubbleTextView originalIcon, final List<String> shortcutIds,
             final List<NotificationKeyData> notificationKeys, List<SystemShortcut> systemShortcuts) {
         mNumNotifications = notificationKeys.size();
-
-        setVisibility(View.INVISIBLE);
-        mLauncher.getDragLayer().addView(this);
-
         mOriginalIcon = originalIcon;
 
         // Add views
@@ -295,17 +237,15 @@
             }
 
             for (int i = shortcutIds.size(); i > 0; i--) {
-                mShortcuts.add(inflateAndAdd(R.layout.deep_shortcut));
+                mShortcuts.add(inflateAndAdd(R.layout.deep_shortcut, this));
             }
             updateHiddenShortcuts();
 
             if (!systemShortcuts.isEmpty()) {
-                mSystemShortcutContainer = inflateAndAdd(R.layout.system_shortcut_icons);
+                mSystemShortcutContainer = inflateAndAdd(R.layout.system_shortcut_icons, this);
                 for (SystemShortcut shortcut : systemShortcuts) {
-                    View view = mInflater.inflate(R.layout.system_shortcut_icon_only,
-                            mSystemShortcutContainer, false);
-                    mSystemShortcutContainer.addView(view);
-                    initializeSystemShortcut(view, shortcut);
+                    initializeSystemShortcut(
+                            R.layout.system_shortcut_icon_only, mSystemShortcutContainer, shortcut);
                 }
             }
         } else if (!systemShortcuts.isEmpty()) {
@@ -314,68 +254,11 @@
             }
 
             for (SystemShortcut shortcut : systemShortcuts) {
-                initializeSystemShortcut(inflateAndAdd(R.layout.system_shortcut), shortcut);
+                initializeSystemShortcut(R.layout.system_shortcut, this, 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 Resources res = getResources();
-        final int arrowCenterOffset = res.getDimensionPixelSize(isAlignedWithStart()
-                ? R.dimen.popup_arrow_horizontal_center_start
-                : R.dimen.popup_arrow_horizontal_center_end);
-        final int halfArrowWidth = res.getDimensionPixelSize(R.dimen.popup_arrow_width) / 2;
-        mLauncher.getDragLayer().addView(mArrow);
-        DragLayer.LayoutParams arrowLp = (DragLayer.LayoutParams) mArrow.getLayoutParams();
-        if (mIsLeftAligned) {
-            mArrow.setX(getX() + arrowCenterOffset - halfArrowWidth);
-        } else {
-            mArrow.setX(getX() + getMeasuredWidth() - arrowCenterOffset - halfArrowWidth);
-        }
-
-        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();
+        reorderAndShow(viewsToFlip);
 
         ItemInfo originalItemInfo = (ItemInfo) originalIcon.getTag();
         int numShortcuts = mShortcuts.size() + systemShortcuts.size();
@@ -401,189 +284,15 @@
                 this, shortcutIds, mShortcuts, notificationKeys));
     }
 
-    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;
-
-        // 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 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;
-        }
-
-        if (Gravity.isHorizontal(mGravity)) {
-            setX(dragLayer.getWidth() / 2 - getMeasuredWidth() / 2);
-            mArrow.setVisibility(INVISIBLE);
-        }
-        if (Gravity.isVertical(mGravity)) {
-            setY(dragLayer.getHeight() / 2 - getMeasuredHeight() / 2);
-        }
-    }
-
-    protected void animateOpen() {
-        setVisibility(View.VISIBLE);
-        mIsOpen = true;
-
-        final AnimatorSet openAnim = LauncherAnimUtils.createAnimatorSet();
-        final Resources res = getResources();
-        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;
-                sendCustomAccessibilityEvent(
-                        PopupContainerWithArrow.this,
-                        AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED,
-                        getContext().getString(R.string.action_deep_shortcut));
-            }
-        });
-
-        mOpenCloseAnimator = openAnim;
-        openAnim.playSequentially(revealAnim, arrowScale);
-        openAnim.start();
+    protected void getTargetObjectLocation(Rect outPos) {
+        mLauncher.getDragLayer().getDescendantRectRelativeToSelf(mOriginalIcon, outPos);
+        outPos.top += mOriginalIcon.getPaddingTop();
+        outPos.left += mOriginalIcon.getPaddingLeft();
+        outPos.right -= mOriginalIcon.getPaddingRight();
+        outPos.bottom = outPos.top + (mOriginalIcon.getIcon() != null
+                ? mOriginalIcon.getIcon().getBounds().height()
+                : mOriginalIcon.getHeight());
     }
 
     public void applyNotificationInfos(List<NotificationInfo> notificationInfos) {
@@ -642,10 +351,8 @@
         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);
+                initializeSystemShortcut(
+                        R.layout.system_shortcut_icon_only, mSystemShortcutContainer, 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
@@ -665,7 +372,8 @@
         }
     }
 
-    private void initializeSystemShortcut(View view, SystemShortcut info) {
+    private void initializeSystemShortcut(int resId, ViewGroup container, SystemShortcut info) {
+        View view = inflateAndAdd(resId, container);
         if (view instanceof DeepShortcutView) {
             // Expanded system shortcut, with both icon and text shown on white background.
             final DeepShortcutView shortcutView = (DeepShortcutView) view;
@@ -682,12 +390,6 @@
                 (ItemInfo) mOriginalIcon.getTag()));
     }
 
-    protected int getIconHeightForPopupPlacement() {
-        return mOriginalIcon.getIcon() != null
-                ? mOriginalIcon.getIcon().getBounds().height()
-                : mOriginalIcon.getHeight();
-    }
-
     /**
      * Determines when the deferred drag should be started.
      *
@@ -807,91 +509,11 @@
         targetParent.containerType = ContainerType.DEEPSHORTCUTS;
     }
 
-    protected void animateClose() {
-        if (!mIsOpen) {
-            return;
-        }
-        mEndRect.setEmpty();
-        if (getOutlineProvider() instanceof RevealOutlineAnimation) {
-            ((RevealOutlineAnimation) getOutlineProvider()).getOutline(mEndRect);
-        }
-        if (mOpenCloseAnimator != null) {
-            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));
-
+    @Override
+    protected void onCreateCloseAnimation(AnimatorSet anim) {
         // Animate original icon's text back in.
-        closeAnim.play(mOriginalIcon.createTextAlphaAnimator(true /* fadeIn */));
+        anim.play(mOriginalIcon.createTextAlphaAnimator(true /* fadeIn */));
         mOriginalIcon.forceHideBadge(false);
-
-        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();
-    }
-
-    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);
-        if (mOpenCloseAnimator != null) {
-            mOpenCloseAnimator.cancel();
-            mOpenCloseAnimator = null;
-        }
-        mIsOpen = false;
-        mDeferContainerRemoval = false;
-        mLauncher.getDragLayer().removeView(this);
-        mLauncher.getDragLayer().removeView(mArrow);
     }
 
     @Override
diff --git a/src/com/android/launcher3/touch/WorkspaceTouchListener.java b/src/com/android/launcher3/touch/WorkspaceTouchListener.java
index 2f9cf3a..23f55aa 100644
--- a/src/com/android/launcher3/touch/WorkspaceTouchListener.java
+++ b/src/com/android/launcher3/touch/WorkspaceTouchListener.java
@@ -144,7 +144,7 @@
             mLauncher.getUserEventDispatcher().logActionOnContainer(Action.Touch.LONGPRESS,
                     Action.Direction.NONE, ContainerType.WORKSPACE,
                     mWorkspace.getCurrentPage());
-            OptionsPopupView.show(mLauncher, mTouchDownPoint.x, mTouchDownPoint.y);
+            OptionsPopupView.showDefaultOptions(mLauncher, mTouchDownPoint.x, mTouchDownPoint.y);
         }
     }
 }
diff --git a/src/com/android/launcher3/views/LauncherDragIndicator.java b/src/com/android/launcher3/views/LauncherDragIndicator.java
index f15129c..986e4be 100644
--- a/src/com/android/launcher3/views/LauncherDragIndicator.java
+++ b/src/com/android/launcher3/views/LauncherDragIndicator.java
@@ -108,15 +108,12 @@
 
     @Override
     public boolean performAccessibilityAction(int action, Bundle arguments) {
-        Launcher launcher = Launcher.getLauncher(getContext());
         if (action == WALLPAPERS) {
-            launcher.onClickWallpaperPicker(this);
-            return true;
+            return OptionsPopupView.startWallpaperPicker(this);
         } else if (action == WIDGETS) {
-            return OptionsPopupView.onWidgetsClicked(launcher);
+            return OptionsPopupView.onWidgetsClicked(this);
         } else if (action == SETTINGS) {
-            OptionsPopupView.startSettings(launcher);
-            return true;
+            return OptionsPopupView.startSettings(this);
         }
         return super.performAccessibilityAction(action, arguments);
     }
diff --git a/src/com/android/launcher3/views/OptionsPopupView.java b/src/com/android/launcher3/views/OptionsPopupView.java
index 709a7e5..56b92c7 100644
--- a/src/com/android/launcher3/views/OptionsPopupView.java
+++ b/src/com/android/launcher3/views/OptionsPopupView.java
@@ -15,52 +15,42 @@
  */
 package com.android.launcher3.views;
 
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.animation.ValueAnimator;
+import static com.android.launcher3.BaseDraggingActivity.INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION;
+import static com.android.launcher3.Utilities.EXTRA_WALLPAPER_OFFSET;
+
 import android.content.Context;
 import android.content.Intent;
-import android.graphics.Outline;
-import android.graphics.PointF;
 import android.graphics.Rect;
+import android.graphics.RectF;
+import android.text.TextUtils;
+import android.util.ArrayMap;
 import android.util.AttributeSet;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.view.View.OnLongClickListener;
-import android.view.ViewGroup;
-import android.view.ViewOutlineProvider;
 import android.widget.Toast;
 
-import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAnimUtils;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.anim.Interpolators;
-import com.android.launcher3.anim.RevealOutlineAnimation;
-import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
-import com.android.launcher3.dragndrop.DragLayer;
-import com.android.launcher3.graphics.ColorScrim;
+import com.android.launcher3.popup.ArrowPopup;
+import com.android.launcher3.shortcuts.DeepShortcutView;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ControlType;
 import com.android.launcher3.widget.WidgetsFullSheet;
 
+import java.util.ArrayList;
+import java.util.List;
+
 /**
  * Popup shown on long pressing an empty space in launcher
  */
-public class OptionsPopupView extends AbstractFloatingView
+public class OptionsPopupView extends ArrowPopup
         implements OnClickListener, OnLongClickListener {
 
-    private final float mOutlineRadius;
-    private final Launcher mLauncher;
-    private final PointF mTouchPoint = new PointF();
-
-    private final ColorScrim mScrim;
-
-    protected Animator mOpenCloseAnimator;
+    private final ArrayMap<View, OptionItem> mItemMap = new ArrayMap<>();
+    private RectF mTargetRect;
 
     public OptionsPopupView(Context context, AttributeSet attrs) {
         this(context, attrs, 0);
@@ -68,31 +58,6 @@
 
     public OptionsPopupView(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
-
-        mOutlineRadius = getResources().getDimension(R.dimen.bg_round_rect_radius);
-        setClipToOutline(true);
-        setOutlineProvider(new ViewOutlineProvider() {
-            @Override
-            public void getOutline(View view, Outline outline) {
-                outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mOutlineRadius);
-            }
-        });
-
-        mLauncher = Launcher.getLauncher(context);
-        mScrim = ColorScrim.createExtractedColorScrim(this);
-    }
-
-    @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-        attachListeners(findViewById(R.id.wallpaper_button));
-        attachListeners(findViewById(R.id.widget_button));
-        attachListeners(findViewById(R.id.settings_button));
-    }
-
-    private void attachListeners(View view) {
-        view.setOnClickListener(this);
-        view.setOnLongClickListener(this);
     }
 
     @Override
@@ -106,20 +71,14 @@
     }
 
     private boolean handleViewClick(View view, int action) {
-        if (view.getId() == R.id.wallpaper_button) {
-            mLauncher.onClickWallpaperPicker(view);
-            logTap(action, ControlType.WALLPAPER_BUTTON);
-            close(true);
-            return true;
-        } else if (view.getId() == R.id.widget_button) {
-            logTap(action, ControlType.WIDGETS_BUTTON);
-            if (onWidgetsClicked(mLauncher)) {
-                close(true);
-                return true;
-            }
-        } else if (view.getId() == R.id.settings_button) {
-            startSettings(mLauncher);
-            logTap(action, ControlType.SETTINGS_BUTTON);
+        OptionItem item = mItemMap.get(view);
+        if (item == null) {
+            return false;
+        }
+        if (item.mControlTypeForLog > 0) {
+            logTap(action, item.mControlTypeForLog);
+        }
+        if (item.mClickListener.onLongClick(view)) {
             close(true);
             return true;
         }
@@ -143,63 +102,6 @@
     }
 
     @Override
-    protected void handleClose(boolean animate) {
-        if (animate) {
-            animateClose();
-        } else {
-            closeComplete();
-        }
-    }
-
-    protected void animateClose() {
-        if (!mIsOpen) {
-            return;
-        }
-        mIsOpen = false;
-
-        final AnimatorSet closeAnim = LauncherAnimUtils.createAnimatorSet();
-        closeAnim.setDuration(getResources().getInteger(R.integer.config_popupOpenCloseDuration));
-
-        // Rectangular reveal (reversed).
-        final ValueAnimator revealAnim = createOpenCloseOutlineProvider()
-                .createRevealAnimator(this, true);
-        closeAnim.play(revealAnim);
-
-        Animator fadeOut = ObjectAnimator.ofFloat(this, ALPHA, 0);
-        fadeOut.setInterpolator(Interpolators.DEACCEL);
-        closeAnim.play(fadeOut);
-
-        Animator gradientAlpha = ObjectAnimator.ofFloat(mScrim, ColorScrim.PROGRESS, 0);
-        gradientAlpha.setInterpolator(Interpolators.DEACCEL);
-        closeAnim.play(gradientAlpha);
-
-        closeAnim.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                mOpenCloseAnimator = null;
-                closeComplete();
-            }
-        });
-        if (mOpenCloseAnimator != null) {
-            mOpenCloseAnimator.cancel();
-        }
-        mOpenCloseAnimator = closeAnim;
-        closeAnim.start();
-    }
-
-    /**
-     * Closes the popup without animation.
-     */
-    private void closeComplete() {
-        if (mOpenCloseAnimator != null) {
-            mOpenCloseAnimator.cancel();
-            mOpenCloseAnimator = null;
-        }
-        mIsOpen = false;
-        mLauncher.getDragLayer().removeView(this);
-    }
-
-    @Override
     public void logActionCommand(int command) {
         // TODO:
     }
@@ -209,90 +111,49 @@
         return (type & TYPE_OPTIONS_POPUP) != 0;
     }
 
-    private RoundedRectRevealOutlineProvider createOpenCloseOutlineProvider() {
-        DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams();
-        Rect startRect = new Rect();
-        startRect.offset((int) (mTouchPoint.x - lp.x), (int) (mTouchPoint.y - lp.y));
-
-        Rect endRect = new Rect(0, 0, lp.width, lp.height);
-        if (getOutlineProvider() instanceof RevealOutlineAnimation) {
-            ((RevealOutlineAnimation) getOutlineProvider()).getOutline(endRect);
-        }
-
-        return new RoundedRectRevealOutlineProvider
-                (mOutlineRadius, mOutlineRadius, startRect, endRect);
+    @Override
+    protected void getTargetObjectLocation(Rect outPos) {
+        mTargetRect.roundOut(outPos);
     }
 
-    private void animateOpen() {
-        mIsOpen = true;
-        final AnimatorSet openAnim = LauncherAnimUtils.createAnimatorSet();
-        openAnim.setDuration(getResources().getInteger(R.integer.config_popupOpenCloseDuration));
+    public static void show(Launcher launcher, RectF targetRect, List<OptionItem> items) {
+        OptionsPopupView popup = (OptionsPopupView) launcher.getLayoutInflater()
+                .inflate(R.layout.longpress_options_menu, launcher.getDragLayer(), false);
+        popup.mTargetRect = targetRect;
 
-        final ValueAnimator revealAnim = createOpenCloseOutlineProvider()
-                .createRevealAnimator(this, false);
-        openAnim.play(revealAnim);
-
-        Animator gradientAlpha = ObjectAnimator.ofFloat(mScrim, ColorScrim.PROGRESS, 1);
-        gradientAlpha.setInterpolator(Interpolators.ACCEL);
-        openAnim.play(gradientAlpha);
-
-        mOpenCloseAnimator = openAnim;
-
-        openAnim.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                mOpenCloseAnimator = null;
-            }
-        });
-        openAnim.start();
+        for (OptionItem item : items) {
+            DeepShortcutView view = popup.inflateAndAdd(R.layout.system_shortcut, popup);
+            view.getIconView().setBackgroundResource(item.mIconRes);
+            view.getBubbleText().setText(item.mLabelRes);
+            view.setDividerVisibility(View.INVISIBLE);
+            view.setOnClickListener(popup);
+            view.setOnLongClickListener(popup);
+            popup.mItemMap.put(view, item);
+        }
+        popup.reorderAndShow(popup.getChildCount());
     }
 
-    public static void show(Launcher launcher, float x, float y) {
-        DragLayer dl = launcher.getDragLayer();
-        OptionsPopupView view = (OptionsPopupView) launcher.getLayoutInflater()
-                .inflate(R.layout.longpress_options_menu, dl, false);
-        DragLayer.LayoutParams lp = (DragLayer.LayoutParams) view.getLayoutParams();
-
-        int maxWidth = dl.getWidth();
-        int maxHeight = dl.getHeight();
-        if (x <= 0 || y <= 0 || x >= maxWidth || y >= maxHeight) {
-            x = maxWidth / 2;
-            y = maxHeight / 2;
+    public static void showDefaultOptions(Launcher launcher, float x, float y) {
+        float halfSize = launcher.getResources().getDimension(R.dimen.options_menu_thumb_size) / 2;
+        if (x < 0 || y < 0) {
+            x = launcher.getDragLayer().getWidth() / 2;
+            y = launcher.getDragLayer().getHeight() / 2;
         }
-        view.mTouchPoint.set(x, y);
+        RectF target = new RectF(x - halfSize, y - halfSize, x + halfSize, y + halfSize);
 
-        int height = lp.height;
+        ArrayList<OptionItem> options = new ArrayList<>();
+        options.add(new OptionItem(R.string.wallpaper_button_text, R.drawable.ic_wallpaper,
+                ControlType.WALLPAPER_BUTTON, OptionsPopupView::startWallpaperPicker));
+        options.add(new OptionItem(R.string.widget_button_text, R.drawable.ic_widget,
+                ControlType.WIDGETS_BUTTON, OptionsPopupView::onWidgetsClicked));
+        options.add(new OptionItem(R.string.settings_button_text, R.drawable.ic_setting,
+                ControlType.SETTINGS_BUTTON, OptionsPopupView::startSettings));
 
-        // Find a good width;
-        int childCount = view.getChildCount();
-        int heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST);
-        int widthSpec = MeasureSpec.makeMeasureSpec(maxWidth / childCount, MeasureSpec.AT_MOST);
-        int maxChildWidth = 0;
-
-        for (int i = 0; i < childCount; i ++) {
-            View child = ((ViewGroup) view.getChildAt(i)).getChildAt(0);
-            child.measure(widthSpec, heightSpec);
-            maxChildWidth = Math.max(maxChildWidth, child.getMeasuredWidth());
-        }
-        Rect insets = dl.getInsets();
-        int margin = (int) (2 * view.getElevation());
-
-        int width = Math.min(maxWidth - insets.left - insets.right - 2 * margin,
-                maxChildWidth * childCount);
-        lp.width = width;
-
-        // Position is towards the finger
-        lp.customPosition = true;
-        lp.x = Utilities.boundToRange((int) (x - width / 2), insets.left + margin,
-                maxWidth - insets.right - width - margin);
-        lp.y = Utilities.boundToRange((int) (y - height / 2), insets.top + margin,
-                maxHeight - insets.bottom - height - margin);
-
-        view.animateOpen();
-        launcher.getDragLayer().addView(view);
+        show(launcher, target, options);
     }
 
-    public static boolean onWidgetsClicked(Launcher launcher) {
+    public static boolean onWidgetsClicked(View view) {
+        Launcher launcher = Launcher.getLauncher(view.getContext());
         if (launcher.getPackageManager().isSafeMode()) {
             Toast.makeText(launcher, R.string.safemode_widget_error, Toast.LENGTH_SHORT).show();
             return false;
@@ -302,9 +163,51 @@
         }
     }
 
-    public static void startSettings(Launcher launcher) {
+    public static boolean startSettings(View view) {
+        Launcher launcher = Launcher.getLauncher(view.getContext());
         launcher.startActivity(new Intent(Intent.ACTION_APPLICATION_PREFERENCES)
                 .setPackage(launcher.getPackageName())
                 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
+        return true;
+    }
+
+    /**
+     * Event handler for the wallpaper picker button that appears after a long press
+     * on the home screen.
+     */
+    public static boolean startWallpaperPicker(View v) {
+        Launcher launcher = Launcher.getLauncher(v.getContext());
+        if (!Utilities.isWallpaperAllowed(launcher)) {
+            Toast.makeText(launcher, R.string.msg_disabled_by_admin, Toast.LENGTH_SHORT).show();
+            return false;
+        }
+        Intent intent = new Intent(Intent.ACTION_SET_WALLPAPER)
+                .putExtra(EXTRA_WALLPAPER_OFFSET,
+                        launcher.getWorkspace().getWallpaperOffsetForCenterPage());
+
+        String pickerPackage = launcher.getString(R.string.wallpaper_picker_package);
+        if (!TextUtils.isEmpty(pickerPackage)) {
+            intent.setPackage(pickerPackage);
+        } else {
+            // If there is no target package, use the default intent chooser animation
+            intent.putExtra(INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION, true);
+        }
+        return launcher.startActivitySafely(v, intent, null);
+    }
+
+    public static class OptionItem {
+
+        private final int mLabelRes;
+        private final int mIconRes;
+        private final int mControlTypeForLog;
+        private final OnLongClickListener mClickListener;
+
+        public OptionItem(int labelRes, int iconRes, int controlTypeForLog,
+                OnLongClickListener clickListener) {
+            mLabelRes = labelRes;
+            mIconRes = iconRes;
+            mControlTypeForLog = controlTypeForLog;
+            mClickListener = clickListener;
+        }
     }
 }