Allow users to dismiss notifications in popup view.

Bug: 193014051
Test: - dismiss notifications (left/right)
      - open popup, dismiss notification from shade
        and ensure popup gets updates
      - open popup, trigger new notification
        and ensure popup gets updated

Change-Id: Iea4d458218cbf5cb22f5f89aa0a4cc1bee18cc73
diff --git a/res/layout/notification_content.xml b/res/layout/notification_content.xml
index 84822a6..91897e9 100644
--- a/res/layout/notification_content.xml
+++ b/res/layout/notification_content.xml
@@ -14,10 +14,11 @@
      limitations under the License.
 -->
 
-<merge
+<com.android.launcher3.notification.NotificationMainView
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
-    android:layout_height="wrap_content">
+    android:layout_height="wrap_content"
+    android:orientation="vertical">
 
     <!-- header -->
     <FrameLayout
@@ -49,7 +50,7 @@
     </FrameLayout>
 
     <!-- Main view -->
-    <com.android.launcher3.notification.NotificationMainView
+    <FrameLayout
         android:id="@+id/main_view"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
@@ -59,7 +60,6 @@
             android:id="@+id/text_and_background"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
-            android:background="?attr/popupColorPrimary"
             android:gravity="center_vertical"
             android:orientation="vertical"
             android:paddingTop="@dimen/notification_padding"
@@ -95,5 +95,5 @@
             android:layout_marginTop="@dimen/notification_padding"
             android:layout_marginStart="@dimen/notification_icon_padding" />
 
-    </com.android.launcher3.notification.NotificationMainView>
-</merge>
\ No newline at end of file
+    </FrameLayout>
+</com.android.launcher3.notification.NotificationMainView>
\ No newline at end of file
diff --git a/res/layout/popup_container.xml b/res/layout/popup_container.xml
index 18014bb..9327287 100644
--- a/res/layout/popup_container.xml
+++ b/res/layout/popup_container.xml
@@ -31,12 +31,9 @@
         android:elevation="@dimen/deep_shortcuts_elevation"
         android:orientation="vertical"/>
 
-    <LinearLayout
+    <com.android.launcher3.notification.NotificationContainer
         android:id="@+id/notification_container"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:visibility="gone"
-        android:background="?attr/popupColorPrimary"
-        android:elevation="@dimen/deep_shortcuts_elevation"
-        android:orientation="vertical"/>
+        android:visibility="gone"/>
 </com.android.launcher3.popup.PopupContainerWithArrow>
\ No newline at end of file
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index f434644..7d8012f 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -271,6 +271,8 @@
 
 <!-- Notifications -->
     <dimen name="bg_round_rect_radius">8dp</dimen>
+    <dimen name="notification_max_trans">8dp</dimen>
+    <dimen name="notification_space">8dp</dimen>
     <dimen name="notification_padding">16dp</dimen>
     <dimen name="notification_padding_top">18dp</dimen>
     <dimen name="notification_header_text_size">14sp</dimen>
diff --git a/src/com/android/launcher3/notification/NotificationContainer.java b/src/com/android/launcher3/notification/NotificationContainer.java
new file mode 100644
index 0000000..9eb05cd
--- /dev/null
+++ b/src/com/android/launcher3/notification/NotificationContainer.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright (C) 2021 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.notification;
+
+import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
+import static com.android.launcher3.touch.SingleAxisSwipeDetector.HORIZONTAL;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.util.FloatProperty;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import com.android.launcher3.R;
+import com.android.launcher3.anim.AnimationSuccessListener;
+import com.android.launcher3.popup.PopupContainerWithArrow;
+import com.android.launcher3.touch.BaseSwipeDetector;
+import com.android.launcher3.touch.OverScroll;
+import com.android.launcher3.touch.SingleAxisSwipeDetector;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Class to manage the notification UI in a {@link PopupContainerWithArrow}.
+ *
+ * - Has two {@link NotificationMainView} that represent the top two notifications
+ * - Handles dismissing a notification
+ */
+public class NotificationContainer extends FrameLayout implements SingleAxisSwipeDetector.Listener {
+
+    private static final FloatProperty<NotificationContainer> DRAG_TRANSLATION_X =
+            new FloatProperty<NotificationContainer>("notificationProgress") {
+                @Override
+                public void setValue(NotificationContainer view, float transX) {
+                    view.setDragTranslationX(transX);
+                }
+
+                @Override
+                public Float get(NotificationContainer view) {
+                    return view.mDragTranslationX;
+                }
+            };
+
+    private static final Rect sTempRect = new Rect();
+
+    private final SingleAxisSwipeDetector mSwipeDetector;
+    private final List<NotificationInfo> mNotificationInfos = new ArrayList<>();
+    private boolean mIgnoreTouch = false;
+
+    private final ObjectAnimator mContentTranslateAnimator;
+    private float mDragTranslationX = 0;
+
+    private final NotificationMainView mPrimaryView;
+    private final NotificationMainView mSecondaryView;
+    private PopupContainerWithArrow mPopupContainer;
+
+    public NotificationContainer(Context context) {
+        this(context, null, 0);
+    }
+
+    public NotificationContainer(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public NotificationContainer(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        mSwipeDetector = new SingleAxisSwipeDetector(getContext(), this, HORIZONTAL);
+        mSwipeDetector.setDetectableScrollConditions(SingleAxisSwipeDetector.DIRECTION_BOTH, false);
+        mContentTranslateAnimator = ObjectAnimator.ofFloat(this, DRAG_TRANSLATION_X, 0);
+
+        mPrimaryView = (NotificationMainView) View.inflate(getContext(),
+                R.layout.notification_content, null);
+        mSecondaryView = (NotificationMainView) View.inflate(getContext(),
+                R.layout.notification_content, null);
+        mSecondaryView.setAlpha(0);
+
+        addView(mSecondaryView);
+        addView(mPrimaryView);
+
+    }
+
+    public void setPopupView(PopupContainerWithArrow popupView) {
+        mPopupContainer = popupView;
+    }
+
+    /**
+     * Returns true if we should intercept the swipe.
+     */
+    public boolean onInterceptSwipeEvent(MotionEvent ev) {
+        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+            sTempRect.set(getLeft(), getTop(), getRight(), getBottom());
+            mIgnoreTouch = !sTempRect.contains((int) ev.getX(), (int) ev.getY());
+            if (!mIgnoreTouch) {
+                mPopupContainer.getParent().requestDisallowInterceptTouchEvent(true);
+            }
+        }
+        if (mIgnoreTouch) {
+            return false;
+        }
+        if (mPrimaryView.getNotificationInfo() == null) {
+            // The notification hasn't been populated yet.
+            return false;
+        }
+
+        mSwipeDetector.onTouchEvent(ev);
+        return mSwipeDetector.isDraggingOrSettling();
+    }
+
+    /**
+     * Returns true when we should handle the swipe.
+     */
+    public boolean onSwipeEvent(MotionEvent ev) {
+        if (mIgnoreTouch) {
+            return false;
+        }
+        if (mPrimaryView.getNotificationInfo() == null) {
+            // The notification hasn't been populated yet.
+            return false;
+        }
+        return mSwipeDetector.onTouchEvent(ev);
+    }
+
+    /**
+     * Applies the list of @param notificationInfos to this container.
+     */
+    public void applyNotificationInfos(final List<NotificationInfo> notificationInfos) {
+        mNotificationInfos.clear();
+        if (notificationInfos.isEmpty()) {
+            mPrimaryView.applyNotificationInfo(null);
+            mSecondaryView.applyNotificationInfo(null);
+            return;
+        }
+        mNotificationInfos.addAll(notificationInfos);
+
+        NotificationInfo mainNotification = notificationInfos.get(0);
+        mPrimaryView.applyNotificationInfo(mainNotification);
+        mSecondaryView.applyNotificationInfo(notificationInfos.size() > 1
+                ? notificationInfos.get(1)
+                : null);
+    }
+
+    /**
+     * Trims the notifications.
+     * @param notificationKeys List of all valid notification keys.
+     */
+    public void trimNotifications(final List<String> notificationKeys) {
+        Iterator<NotificationInfo> iterator = mNotificationInfos.iterator();
+        while (iterator.hasNext()) {
+            if (!notificationKeys.contains(iterator.next().notificationKey)) {
+                iterator.remove();
+            }
+        }
+
+        NotificationInfo primaryInfo = mNotificationInfos.size() > 0
+                ? mNotificationInfos.get(0)
+                : null;
+        NotificationInfo secondaryInfo = mNotificationInfos.size() > 1
+                ? mNotificationInfos.get(1)
+                : null;
+
+        mPrimaryView.applyNotificationInfo(primaryInfo);
+        mSecondaryView.applyNotificationInfo(secondaryInfo);
+
+        mPrimaryView.onPrimaryDrag(0);
+        mSecondaryView.onSecondaryDrag(0);
+    }
+
+    private void setDragTranslationX(float translationX) {
+        mDragTranslationX = translationX;
+
+        float progress = translationX / getWidth();
+        mPrimaryView.onPrimaryDrag(progress);
+        if (mSecondaryView.getNotificationInfo() == null) {
+            mSecondaryView.setAlpha(0f);
+        } else {
+            mSecondaryView.onSecondaryDrag(progress);
+        }
+    }
+
+    // SingleAxisSwipeDetector.Listener's
+    @Override
+    public void onDragStart(boolean start, float startDisplacement) {
+        mPopupContainer.showArrow(false);
+    }
+
+    @Override
+    public boolean onDrag(float displacement) {
+        if (!mPrimaryView.canChildBeDismissed()) {
+            displacement = OverScroll.dampedScroll(displacement, getWidth());
+        }
+
+        float progress = displacement / getWidth();
+        mPrimaryView.onPrimaryDrag(progress);
+        if (mSecondaryView.getNotificationInfo() == null) {
+            mSecondaryView.setAlpha(0f);
+        } else {
+            mSecondaryView.onSecondaryDrag(progress);
+        }
+        mContentTranslateAnimator.cancel();
+        return true;
+    }
+
+    @Override
+    public void onDragEnd(float velocity) {
+        final boolean willExit;
+        final float endTranslation;
+        final float startTranslation = mPrimaryView.getTranslationX();
+        final float width = getWidth();
+
+        if (!mPrimaryView.canChildBeDismissed()) {
+            willExit = false;
+            endTranslation = 0;
+        } else if (mSwipeDetector.isFling(velocity)) {
+            willExit = true;
+            endTranslation = velocity < 0 ? -width : width;
+        } else if (Math.abs(startTranslation) > width / 2f) {
+            willExit = true;
+            endTranslation = (startTranslation < 0 ? -width : width);
+        } else {
+            willExit = false;
+            endTranslation = 0;
+        }
+
+        long duration = BaseSwipeDetector.calculateDuration(velocity,
+                (endTranslation - startTranslation) / width);
+
+        mContentTranslateAnimator.removeAllListeners();
+        mContentTranslateAnimator.setDuration(duration)
+                .setInterpolator(scrollInterpolatorForVelocity(velocity));
+        mContentTranslateAnimator.setFloatValues(startTranslation, endTranslation);
+
+        NotificationMainView current = mPrimaryView;
+        mContentTranslateAnimator.addListener(new AnimationSuccessListener() {
+            @Override
+            public void onAnimationSuccess(Animator animator) {
+                mSwipeDetector.finishedScrolling();
+                if (willExit) {
+                    current.onChildDismissed();
+                }
+                mPopupContainer.showArrow(true);
+            }
+        });
+        mContentTranslateAnimator.start();
+    }
+
+    /**
+     * Animates the background color to a new color.
+     * @param color The color to change to.
+     * @param animatorSetOut The AnimatorSet where we add the color animator to.
+     */
+    public void updateBackgroundColor(int color, AnimatorSet animatorSetOut) {
+        mPrimaryView.updateBackgroundColor(color, animatorSetOut);
+        mSecondaryView.updateBackgroundColor(color, animatorSetOut);
+    }
+
+    /**
+     * Updates the header with a new @param notificationCount.
+     */
+    public void updateHeader(int notificationCount) {
+        mPrimaryView.updateHeader(notificationCount);
+        mSecondaryView.updateHeader(notificationCount - 1);
+    }
+}
diff --git a/src/com/android/launcher3/notification/NotificationItemView.java b/src/com/android/launcher3/notification/NotificationItemView.java
deleted file mode 100644
index af943a6..0000000
--- a/src/com/android/launcher3/notification/NotificationItemView.java
+++ /dev/null
@@ -1,179 +0,0 @@
-/*
- * Copyright (C) 2017 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.notification;
-
-import android.animation.AnimatorSet;
-import android.content.Context;
-import android.graphics.Outline;
-import android.graphics.Rect;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewGroup.MarginLayoutParams;
-import android.view.ViewOutlineProvider;
-import android.widget.TextView;
-
-import com.android.launcher3.R;
-import com.android.launcher3.popup.PopupContainerWithArrow;
-import com.android.launcher3.util.Themes;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Utility class to manage notification UI
- */
-public class NotificationItemView {
-
-    private static final Rect sTempRect = new Rect();
-
-    private final Context mContext;
-    private final PopupContainerWithArrow mPopupContainer;
-    private final ViewGroup mRootView;
-
-    private final TextView mHeaderCount;
-    private final NotificationMainView mMainView;
-
-    private final View mHeader;
-
-    private View mGutter;
-
-    private boolean mIgnoreTouch = false;
-    private List<NotificationInfo> mNotificationInfos = new ArrayList<>();
-
-    public NotificationItemView(PopupContainerWithArrow container, ViewGroup rootView) {
-        mPopupContainer = container;
-        mRootView = rootView;
-        mContext = container.getContext();
-
-        mHeaderCount = container.findViewById(R.id.notification_count);
-        mMainView = container.findViewById(R.id.main_view);
-
-        mHeader = container.findViewById(R.id.header);
-
-        float radius = Themes.getDialogCornerRadius(mContext);
-        rootView.setClipToOutline(true);
-        rootView.setOutlineProvider(new ViewOutlineProvider() {
-            @Override
-            public void getOutline(View view, Outline outline) {
-                outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), radius);
-            }
-        });
-    }
-
-    /**
-     * Animates the background color to a new color.
-     * @param color The color to change to.
-     * @param animatorSetOut The AnimatorSet where we add the color animator to.
-     */
-    public void updateBackgroundColor(int color, AnimatorSet animatorSetOut) {
-        mMainView.updateBackgroundColor(color, animatorSetOut);
-    }
-
-    public void addGutter() {
-        if (mGutter == null) {
-            mGutter = mPopupContainer.inflateAndAdd(R.layout.notification_gutter, mRootView);
-        }
-    }
-
-    public void inverseGutterMargin() {
-        MarginLayoutParams lp = (MarginLayoutParams) mGutter.getLayoutParams();
-        int top = lp.topMargin;
-        lp.topMargin = lp.bottomMargin;
-        lp.bottomMargin = top;
-    }
-
-    public void removeAllViews() {
-        mRootView.removeView(mMainView);
-        mRootView.removeView(mHeader);
-        if (mGutter != null) {
-            mRootView.removeView(mGutter);
-        }
-    }
-
-    /**
-     * Updates the header text.
-     * @param notificationCount The number of notifications.
-     */
-    public void updateHeader(int notificationCount) {
-        final String text;
-        final int visibility;
-        if (notificationCount <= 1) {
-            text = "";
-            visibility = View.INVISIBLE;
-        } else {
-            text = String.valueOf(notificationCount);
-            visibility = View.VISIBLE;
-
-        }
-        mHeaderCount.setText(text);
-        mHeaderCount.setVisibility(visibility);
-    }
-
-    public boolean onInterceptTouchEvent(MotionEvent ev) {
-        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
-            sTempRect.set(mRootView.getLeft(), mRootView.getTop(),
-                    mRootView.getRight(), mRootView.getBottom());
-            mIgnoreTouch = !sTempRect.contains((int) ev.getX(), (int) ev.getY());
-            if (!mIgnoreTouch) {
-                mPopupContainer.getParent().requestDisallowInterceptTouchEvent(true);
-            }
-        }
-        if (mIgnoreTouch) {
-            return false;
-        }
-        if (mMainView.getNotificationInfo() == null) {
-            // The notification hasn't been populated yet.
-            return false;
-        }
-
-        return false;
-    }
-
-    public void applyNotificationInfos(final List<NotificationInfo> notificationInfos) {
-        mNotificationInfos.clear();
-        if (notificationInfos.isEmpty()) {
-            return;
-        }
-        mNotificationInfos.addAll(notificationInfos);
-
-        NotificationInfo mainNotification = notificationInfos.get(0);
-        mMainView.applyNotificationInfo(mainNotification, false);
-    }
-
-    public void trimNotifications(final List<String> notificationKeys) {
-        NotificationInfo currentMainNotificationInfo = mMainView.getNotificationInfo();
-        boolean shouldUpdateMainNotification = !notificationKeys.contains(
-                currentMainNotificationInfo.notificationKey);
-
-        if (shouldUpdateMainNotification) {
-            int size = notificationKeys.size();
-            NotificationInfo nextNotification = null;
-            // We get the latest notification by finding the notification after the one that was
-            // just dismissed.
-            for (int i = 0; i < size; ++i) {
-                if (currentMainNotificationInfo == mNotificationInfos.get(i) && i + 1 < size) {
-                    nextNotification = mNotificationInfos.get(i + 1);
-                    break;
-                }
-            }
-            if (nextNotification != null) {
-                mMainView.applyNotificationInfo(nextNotification, true);
-            }
-        }
-    }
-}
diff --git a/src/com/android/launcher3/notification/NotificationMainView.java b/src/com/android/launcher3/notification/NotificationMainView.java
index b8aa824..f9ff8a6 100644
--- a/src/com/android/launcher3/notification/NotificationMainView.java
+++ b/src/com/android/launcher3/notification/NotificationMainView.java
@@ -16,62 +16,70 @@
 
 package com.android.launcher3.notification;
 
+import static com.android.launcher3.Utilities.mapToRange;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NOTIFICATION_DISMISSED;
 
 import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
 import android.animation.ValueAnimator;
 import android.annotation.TargetApi;
 import android.content.Context;
-import android.graphics.Color;
-import android.graphics.drawable.ColorDrawable;
+import android.graphics.Outline;
+import android.graphics.Rect;
+import android.graphics.drawable.GradientDrawable;
 import android.os.Build;
 import android.text.TextUtils;
 import android.util.AttributeSet;
-import android.util.FloatProperty;
 import android.view.View;
 import android.view.ViewGroup;
-import android.widget.FrameLayout;
+import android.view.ViewOutlineProvider;
+import android.widget.LinearLayout;
 import android.widget.TextView;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
 import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.touch.SingleAxisSwipeDetector;
+import com.android.launcher3.util.Themes;
 
 /**
  * A {@link android.widget.FrameLayout} that contains a single notification,
  * e.g. icon + title + text.
  */
 @TargetApi(Build.VERSION_CODES.N)
-public class NotificationMainView extends FrameLayout {
-
-    private static final FloatProperty<NotificationMainView> CONTENT_TRANSLATION =
-            new FloatProperty<NotificationMainView>("contentTranslation") {
-        @Override
-        public void setValue(NotificationMainView view, float v) {
-            view.setContentTranslation(v);
-        }
-
-        @Override
-        public Float get(NotificationMainView view) {
-            return view.mTextAndBackground.getTranslationX();
-        }
-    };
+public class NotificationMainView extends LinearLayout {
 
     // This is used only to track the notification view, so that it can be properly logged.
     public static final ItemInfo NOTIFICATION_ITEM_INFO = new ItemInfo();
 
+    // Value when the primary notification main view will be gone (zero alpha).
+    private static final float PRIMARY_GONE_PROGRESS = 0.7f;
+    private static final float PRIMARY_MIN_PROGRESS = 0.40f;
+    private static final float PRIMARY_MAX_PROGRESS = 0.60f;
+    private static final float SECONDARY_MIN_PROGRESS = 0.30f;
+    private static final float SECONDARY_MAX_PROGRESS = 0.50f;
+    private static final float SECONDARY_CONTENT_MAX_PROGRESS = 0.6f;
+
     private NotificationInfo mNotificationInfo;
-    private ViewGroup mTextAndBackground;
     private int mBackgroundColor;
     private TextView mTitleView;
     private TextView mTextView;
     private View mIconView;
 
-    private SingleAxisSwipeDetector mSwipeDetector;
+    private View mHeader;
+    private View mMainView;
 
-    private final ColorDrawable mColorDrawable;
+    private TextView mHeaderCount;
+    private final Rect mOutline = new Rect();
+
+    // Space between notifications during swipe
+    private final int mNotificationSpace;
+    private final int mMaxTransX;
+    private final int mMaxElevation;
+
+    private final GradientDrawable mBackground;
 
     public NotificationMainView(Context context) {
         this(context, null, 0);
@@ -82,28 +90,77 @@
     }
 
     public NotificationMainView(Context context, AttributeSet attrs, int defStyle) {
-        super(context, attrs, defStyle);
+        this(context, attrs, defStyle, 0);
+    }
 
-        mColorDrawable = new ColorDrawable(Color.TRANSPARENT);
+    public NotificationMainView(Context context, AttributeSet attrs, int defStyle, int defStylRes) {
+        super(context, attrs, defStyle, defStylRes);
+
+        float outlineRadius = Themes.getDialogCornerRadius(context);
+
+        mBackground = new GradientDrawable();
+        mBackground.setColor(Themes.getAttrColor(context, R.attr.popupColorPrimary));
+        mBackground.setCornerRadius(outlineRadius);
+        setBackground(mBackground);
+
+        mMaxElevation = getResources().getDimensionPixelSize(R.dimen.deep_shortcuts_elevation);
+        setElevation(mMaxElevation);
+
+        mMaxTransX = getResources().getDimensionPixelSize(R.dimen.notification_max_trans);
+        mNotificationSpace = getResources().getDimensionPixelSize(R.dimen.notification_space);
+
+        setClipToOutline(true);
+        setOutlineProvider(new ViewOutlineProvider() {
+            @Override
+            public void getOutline(View view, Outline outline) {
+                outline.setRoundRect(mOutline, outlineRadius);
+            }
+        });
+    }
+
+    /**
+     * Updates the header text.
+     * @param notificationCount The number of notifications.
+     */
+    public void updateHeader(int notificationCount) {
+        final String text;
+        final int visibility;
+        if (notificationCount <= 1) {
+            text = "";
+            visibility = View.INVISIBLE;
+        } else {
+            text = String.valueOf(notificationCount);
+            visibility = View.VISIBLE;
+
+        }
+        mHeaderCount.setText(text);
+        mHeaderCount.setVisibility(visibility);
     }
 
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
 
-        mTextAndBackground = findViewById(R.id.text_and_background);
-        mTitleView = mTextAndBackground.findViewById(R.id.title);
-        mTextView = mTextAndBackground.findViewById(R.id.text);
+        ViewGroup textAndBackground = findViewById(R.id.text_and_background);
+        mTitleView = textAndBackground.findViewById(R.id.title);
+        mTextView = textAndBackground.findViewById(R.id.text);
         mIconView = findViewById(R.id.popup_item_icon);
+        mHeaderCount = findViewById(R.id.notification_count);
 
-        ColorDrawable colorBackground = (ColorDrawable) mTextAndBackground.getBackground();
-        updateBackgroundColor(colorBackground.getColor());
+        mHeader = findViewById(R.id.header);
+        mMainView = findViewById(R.id.main_view);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        mOutline.set(0, 0, getWidth(), getHeight());
+        invalidateOutline();
     }
 
     private void updateBackgroundColor(int color) {
         mBackgroundColor = color;
-        mColorDrawable.setColor(color);
-        mTextAndBackground.setBackground(mColorDrawable);
+        mBackground.setColor(color);
         if (mNotificationInfo != null) {
             mIconView.setBackground(mNotificationInfo.getIconForBackground(getContext(),
                     mBackgroundColor));
@@ -128,8 +185,11 @@
     /**
      * Sets the content of this view, animating it after a new icon shifts up if necessary.
      */
-    public void applyNotificationInfo(NotificationInfo mainNotification, boolean animate) {
-        mNotificationInfo = mainNotification;
+    public void applyNotificationInfo(NotificationInfo notificationInfo) {
+        mNotificationInfo = notificationInfo;
+        if (notificationInfo == null) {
+            return;
+        }
         NotificationListener listener = NotificationListener.getInstanceIfConnected();
         if (listener != null) {
             listener.setNotificationsShown(new String[] {mNotificationInfo.notificationKey});
@@ -149,25 +209,112 @@
         if (mNotificationInfo.intent != null) {
             setOnClickListener(mNotificationInfo);
         }
-        setContentTranslation(0);
+
         // Add a stub ItemInfo so that logging populates the correct container and item types
         // instead of DEFAULT_CONTAINERTYPE and DEFAULT_ITEMTYPE, respectively.
         setTag(NOTIFICATION_ITEM_INFO);
-        if (animate) {
-            ObjectAnimator.ofFloat(mTextAndBackground, ALPHA, 0, 1).setDuration(150).start();
+    }
+
+    /**
+     * Sets the alpha of only the child views.
+     */
+    public void setContentAlpha(float alpha) {
+        mHeader.setAlpha(alpha);
+        mMainView.setAlpha(alpha);
+    }
+
+    /**
+     * Sets the translation of only the child views.
+     */
+    public void setContentTranslationX(float transX) {
+        mHeader.setTranslationX(transX);
+        mMainView.setTranslationX(transX);
+    }
+
+    /**
+     * Updates the alpha, content alpha, and elevation of this view.
+     *
+     * @param progress Range from [0, 1] or [-1, 0]
+     *                 When 0: Full alpha
+     *                 When 1/-1: zero alpha
+     */
+    public void onPrimaryDrag(float progress) {
+        float absProgress = Math.abs(progress);
+        final int width = getWidth();
+
+        float min = PRIMARY_MIN_PROGRESS;
+        float max = PRIMARY_MAX_PROGRESS;
+
+        if (absProgress < min) {
+            setAlpha(1f);
+            setContentAlpha(1);
+            setElevation(mMaxElevation);
+        } else if (absProgress < max) {
+            setAlpha(1f);
+            setContentAlpha(mapToRange(absProgress, min, max, 1f, 0f, LINEAR));
+            setElevation(Utilities.mapToRange(absProgress, min, max, mMaxElevation, 0, LINEAR));
+        } else {
+            setAlpha(mapToRange(absProgress, max, PRIMARY_GONE_PROGRESS, 1f, 0f, LINEAR));
+            setContentAlpha(0f);
+            setElevation(0f);
         }
+
+        setTranslationX(width * progress);
     }
 
-    public void setContentTranslation(float translation) {
-        mTextAndBackground.setTranslationX(translation);
-        mIconView.setTranslationX(translation);
+    /**
+     * Updates the alpha, content alpha, elevation, and clipping of this view.
+     * @param progress Range from [0, 1] or [-1, 0]
+      *                 When 0: Smallest clipping, zero alpha
+      *                 When 1/-1: Full clip, full alpha
+     */
+    public void onSecondaryDrag(float progress) {
+        final float absProgress = Math.abs(progress);
+
+        float min = SECONDARY_MIN_PROGRESS;
+        float max = SECONDARY_MAX_PROGRESS;
+        float contentMax = SECONDARY_CONTENT_MAX_PROGRESS;
+
+        if (absProgress < min) {
+            setAlpha(0f);
+            setContentAlpha(0);
+            setElevation(0f);
+        } else if (absProgress < max) {
+            setAlpha(mapToRange(absProgress, min, max, 0, 1f, LINEAR));
+            setContentAlpha(0f);
+            setElevation(0f);
+        } else {
+            setAlpha(1f);
+            setContentAlpha(absProgress > contentMax
+                    ? 1f
+                    : mapToRange(absProgress, max, contentMax, 0, 1f, LINEAR));
+            setElevation(Utilities.mapToRange(absProgress, max, 1, 0, mMaxElevation, LINEAR));
+        }
+
+        final int width = getWidth();
+        int crop = (int) (width * absProgress);
+        int space = (int) (absProgress > PRIMARY_GONE_PROGRESS
+                ? mapToRange(absProgress, PRIMARY_GONE_PROGRESS, 1f, mNotificationSpace, 0, LINEAR)
+                : mNotificationSpace);
+        if (progress < 0) {
+            mOutline.left = Math.max(0, getWidth() - crop + space);
+            mOutline.right = getWidth();
+        } else {
+            mOutline.right = Math.min(getWidth(), crop - space);
+            mOutline.left = 0;
+        }
+
+        float contentTransX = mMaxTransX * (1f - absProgress);
+        setContentTranslationX(progress < 0
+                ? contentTransX
+                : -contentTransX);
+        invalidateOutline();
     }
 
-    public NotificationInfo getNotificationInfo() {
+    public @Nullable NotificationInfo getNotificationInfo() {
         return mNotificationInfo;
     }
 
-
     public boolean canChildBeDismissed() {
         return mNotificationInfo != null && mNotificationInfo.dismissable;
     }
diff --git a/src/com/android/launcher3/popup/ArrowPopup.java b/src/com/android/launcher3/popup/ArrowPopup.java
index a534ee3..5d3ba75 100644
--- a/src/com/android/launcher3/popup/ArrowPopup.java
+++ b/src/com/android/launcher3/popup/ArrowPopup.java
@@ -487,6 +487,13 @@
         return getMeasuredWidth() - mArrowOffsetHorizontal - mArrowWidth;
     }
 
+    /**
+     * @param show If true, shows arrow (when applicable), otherwise hides arrow.
+     */
+    public void showArrow(boolean show) {
+        mArrow.setVisibility(show && shouldAddArrow() ? VISIBLE : INVISIBLE);
+    }
+
     private void addArrow() {
         getPopupContainer().addView(mArrow);
         mArrow.setX(getX() + getArrowLeft());
diff --git a/src/com/android/launcher3/popup/PopupContainerWithArrow.java b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
index 18f263a..bc3419a 100644
--- a/src/com/android/launcher3/popup/PopupContainerWithArrow.java
+++ b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
@@ -58,8 +58,8 @@
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.ItemInfoWithIcon;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.notification.NotificationContainer;
 import com.android.launcher3.notification.NotificationInfo;
-import com.android.launcher3.notification.NotificationItemView;
 import com.android.launcher3.notification.NotificationKeyData;
 import com.android.launcher3.popup.PopupDataProvider.PopupDataChangeListener;
 import com.android.launcher3.shortcuts.DeepShortcutView;
@@ -92,9 +92,8 @@
     private final int mStartDragThreshold;
 
     private BubbleTextView mOriginalIcon;
-    private NotificationItemView mNotificationItemView;
     private int mNumNotifications;
-    private ViewGroup mNotificationContainer;
+    private NotificationContainer mNotificationContainer;
 
     private ViewGroup mWidgetContainer;
 
@@ -128,8 +127,8 @@
         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
             mInterceptTouchDown.set(ev.getX(), ev.getY());
         }
-        if (mNotificationItemView != null
-                && mNotificationItemView.onInterceptTouchEvent(ev)) {
+        if (mNotificationContainer != null
+                && mNotificationContainer.onInterceptSwipeEvent(ev)) {
             return true;
         }
         // Stop sending touch events to deep shortcut views if user moved beyond touch slop.
@@ -138,6 +137,14 @@
     }
 
     @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        if (mNotificationContainer != null) {
+            return mNotificationContainer.onSwipeEvent(ev) || super.onTouchEvent(ev);
+        }
+        return super.onTouchEvent(ev);
+    }
+
+    @Override
     protected boolean isOfType(int type) {
         return (type & TYPE_ACTION_POPUP) != 0;
     }
@@ -172,8 +179,8 @@
     @Override
     protected void setChildColor(View view, int color, AnimatorSet animatorSetOut) {
         super.setChildColor(view, color, animatorSetOut);
-        if (view.getId() == R.id.notification_container && mNotificationItemView != null) {
-            mNotificationItemView.updateBackgroundColor(color, animatorSetOut);
+        if (view.getId() == R.id.notification_container && mNotificationContainer != null) {
+            mNotificationContainer.updateBackgroundColor(color, animatorSetOut);
         }
     }
 
@@ -232,13 +239,6 @@
                 mNotificationContainer);
     }
 
-    @Override
-    protected void onInflationComplete(boolean isReversed) {
-        if (isReversed && mNotificationItemView != null) {
-            mNotificationItemView.inverseGutterMargin();
-        }
-    }
-
     @TargetApi(Build.VERSION_CODES.P)
     public void populateAndShow(final BubbleTextView originalIcon, int shortcutCount,
             final List<NotificationKeyData> notificationKeys, List<SystemShortcut> systemShortcuts) {
@@ -261,9 +261,10 @@
             if (mNotificationContainer == null) {
                 mNotificationContainer = findViewById(R.id.notification_container);
                 mNotificationContainer.setVisibility(VISIBLE);
+                mNotificationContainer.setPopupView(this);
+            } else {
+                mNotificationContainer.setVisibility(GONE);
             }
-            View.inflate(getContext(), R.layout.notification_content, mNotificationContainer);
-            mNotificationItemView = new NotificationItemView(this, mNotificationContainer);
             updateNotificationHeader();
         }
         int viewsToFlip = getChildCount();
@@ -274,10 +275,6 @@
         if (hasDeepShortcuts) {
             mDeepShortcutContainer.setVisibility(View.VISIBLE);
 
-            if (mNotificationItemView != null) {
-                mNotificationItemView.addGutter();
-            }
-
             for (int i = shortcutCount; i > 0; i--) {
                 DeepShortcutView v = inflateAndAdd(R.layout.deep_shortcut, mDeepShortcutContainer);
                 v.getLayoutParams().width = containerWidth;
@@ -309,10 +306,6 @@
         } else {
             mDeepShortcutContainer.setVisibility(View.GONE);
             if (!systemShortcuts.isEmpty()) {
-                if (mNotificationItemView != null) {
-                    mNotificationItemView.addGutter();
-                }
-
                 for (SystemShortcut shortcut : systemShortcuts) {
                     initializeSystemShortcut(R.layout.system_shortcut, this, shortcut);
                 }
@@ -355,13 +348,13 @@
     }
 
     public void applyNotificationInfos(List<NotificationInfo> notificationInfos) {
-        if (mNotificationItemView != null) {
-            mNotificationItemView.applyNotificationInfos(notificationInfos);
+        if (mNotificationContainer != null) {
+            mNotificationContainer.applyNotificationInfos(notificationInfos);
         }
     }
 
     private void updateHiddenShortcuts() {
-        int allowedCount = mNotificationItemView != null
+        int allowedCount = mNotificationContainer != null
                 ? MAX_SHORTCUTS_IF_NOTIFICATIONS : MAX_SHORTCUTS;
 
         int total = mShortcuts.size();
@@ -447,8 +440,8 @@
     private void updateNotificationHeader() {
         ItemInfoWithIcon itemInfo = (ItemInfoWithIcon) mOriginalIcon.getTag();
         DotInfo dotInfo = mLauncher.getDotInfoForItem(itemInfo);
-        if (mNotificationItemView != null && dotInfo != null) {
-            mNotificationItemView.updateHeader(dotInfo.getNotificationCount());
+        if (mNotificationContainer != null && dotInfo != null) {
+            mNotificationContainer.updateHeader(dotInfo.getNotificationCount());
         }
     }
 
@@ -590,20 +583,18 @@
 
         @Override
         public void trimNotifications(Map<PackageUserKey, DotInfo> updatedDots) {
-            if (mNotificationItemView == null) {
+            if (mNotificationContainer == null) {
                 return;
             }
             ItemInfo originalInfo = (ItemInfo) mOriginalIcon.getTag();
             DotInfo dotInfo = updatedDots.get(PackageUserKey.fromItemInfo(originalInfo));
             if (dotInfo == null || dotInfo.getNotificationKeys().size() == 0) {
                 // No more notifications, remove the notification views and expand all shortcuts.
-                mNotificationItemView.removeAllViews();
-                mNotificationItemView = null;
                 mNotificationContainer.setVisibility(GONE);
                 updateHiddenShortcuts();
                 assignMarginsAndBackgrounds(PopupContainerWithArrow.this);
             } else {
-                mNotificationItemView.trimNotifications(
+                mNotificationContainer.trimNotifications(
                         NotificationKeyData.extractKeysOnly(dotInfo.getNotificationKeys()));
             }
         }