Implemented a nicer transition when the icons overflow

The overflowing icons are now represented as dots and
animate in and out nicer.
The shelf also animates much nicer from the regular statusbar
size if there are a lot of notifications.

Test: Add a lot of notifications, observe them nicely overflowing into dots
Bug: 32437839
Change-Id: I5906c076bbf5d48cbabdbacfd21234bed55c6caa
diff --git a/packages/SystemUI/res/layout/status_bar_notification_shelf.xml b/packages/SystemUI/res/layout/status_bar_notification_shelf.xml
index fde61eb..088deba8 100644
--- a/packages/SystemUI/res/layout/status_bar_notification_shelf.xml
+++ b/packages/SystemUI/res/layout/status_bar_notification_shelf.xml
@@ -35,8 +35,8 @@
         android:id="@+id/content"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
-        android:layout_marginStart="13dp"
-        android:layout_marginEnd="13dp"
+        android:paddingStart="13dp"
+        android:paddingEnd="13dp"
         android:gravity="center_vertical" />
 
     <com.android.systemui.statusbar.notification.FakeShadowView
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 496fb9c..11d42b3 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -112,6 +112,12 @@
     <!-- the padding on the start of the statusbar -->
     <dimen name="status_bar_padding_start">6dp</dimen>
 
+    <!-- the radius of the overflow dot in the status bar -->
+    <dimen name="overflow_dot_radius">1dp</dimen>
+
+    <!-- the padding between dots in the icon overflow -->
+    <dimen name="overflow_icon_dot_padding">3dp</dimen>
+
     <!-- The padding on the global screenshot background image -->
     <dimen name="global_screenshot_bg_padding">20dp</dimen>
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
index 9a1e064..de88e66 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
@@ -48,7 +48,7 @@
 
     private ViewInvertHelper mViewInvertHelper;
     private boolean mDark;
-    private NotificationIconContainer mNotificationIconContainer;
+    private NotificationIconContainer mShelfIcons;
     private ArrayList<StatusBarIconView> mIcons = new ArrayList<>();
     private ShelfState mShelfState;
     private int[] mTmp = new int[2];
@@ -62,6 +62,7 @@
     private int mPaddingBetweenElements;
     private int mNotGoneIndex;
     private boolean mHasItemsInStableShelf;
+    private NotificationIconContainer mCollapsedIcons;
 
     public NotificationShelf(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -70,14 +71,15 @@
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
-        mNotificationIconContainer = (NotificationIconContainer) findViewById(R.id.content);
-        mNotificationIconContainer.setClipChildren(false);
-        mNotificationIconContainer.setClipToPadding(false);
+        mShelfIcons = (NotificationIconContainer) findViewById(R.id.content);
+        mShelfIcons.setClipChildren(false);
+        mShelfIcons.setClipToPadding(false);
+
         setClipToActualHeight(false);
         setClipChildren(false);
         setClipToPadding(false);
-        mNotificationIconContainer.setShowAllIcons(false);
-        mViewInvertHelper = new ViewInvertHelper(mNotificationIconContainer,
+        mShelfIcons.setShowAllIcons(false);
+        mViewInvertHelper = new ViewInvertHelper(mShelfIcons,
                 NotificationPanelView.DOZE_ANIMATION_DURATION);
         mShelfState = new ShelfState();
         initDimens();
@@ -118,11 +120,11 @@
 
     @Override
     protected View getContentView() {
-        return mNotificationIconContainer;
+        return mShelfIcons;
     }
 
-    public NotificationIconContainer getNotificationIconContainer() {
-        return mNotificationIconContainer;
+    public NotificationIconContainer getShelfIcons() {
+        return mShelfIcons;
     }
 
     @Override
@@ -142,13 +144,13 @@
             float viewEnd = lastViewState.yTranslation + lastViewState.height;
             mShelfState.copyFrom(lastViewState);
             mShelfState.height = getIntrinsicHeight();
-            mShelfState.yTranslation = Math.min(viewEnd, maxShelfEnd) - mShelfState.height;
+            mShelfState.yTranslation = Math.max(Math.min(viewEnd, maxShelfEnd) - mShelfState.height,
+                    getFullyClosedTranslation());
             mShelfState.zTranslation = ambientState.getBaseZHeight();
             float openedAmount = (mShelfState.yTranslation - getFullyClosedTranslation())
                     / (getIntrinsicHeight() * 2);
             openedAmount = Math.min(1.0f, openedAmount);
-            mShelfState.iconContainerTranslation = (1.0f - openedAmount)
-                    * (mStatusBarPaddingStart - mNotificationIconContainer.getLeft());
+            mShelfState.openedAmount = openedAmount;
             mShelfState.clipTopAmount = 0;
             mShelfState.alpha = 1.0f;
             mShelfState.belowShelf = false;
@@ -172,10 +174,16 @@
      */
     public void updateAppearance() {
         WeakHashMap<View, IconState> iconStates =
-                mNotificationIconContainer.resetViewStates();
+                mShelfIcons.resetViewStates();
         float numIconsInShelf = 0.0f;
         int shelfIndex = mAmbientState.getShelfIndex();
         mNotGoneIndex = -1;
+        float interpolationStart = mMaxLayoutHeight - getIntrinsicHeight() * 2;
+        float expandAmount = 0.0f;
+        if (getTranslationY() >= interpolationStart) {
+            expandAmount = (getTranslationY() - interpolationStart) / getIntrinsicHeight();
+            expandAmount = Math.min(1.0f, expandAmount);
+        }
         //  find the first view that doesn't overlap with the shelf
         int notificationIndex = 0;
         int notGoneNotifications = 0;
@@ -205,7 +213,7 @@
                 }
             }
             updateNotificationClipHeight(row, notificationClipEnd);
-            updateIconAppearance(row, iconState, icon);
+            updateIconAppearance(row, iconState, icon, expandAmount);
             numIconsInShelf += iconState.iconAppearAmount;
             if (row.getTranslationY() >= getTranslationY() && mNotGoneIndex == -1) {
                 mNotGoneIndex = notGoneNotifications;
@@ -228,8 +236,8 @@
             }
             notificationIndex++;
         }
-        mNotificationIconContainer.calculateIconTranslations();
-        mNotificationIconContainer.applyIconStates();
+        mShelfIcons.calculateIconTranslations();
+        mShelfIcons.applyIconStates();
         setVisibility(numIconsInShelf == 0.0f || !mAmbientState.isShadeExpanded() ? INVISIBLE
                 : VISIBLE);
         setHideBackground(numIconsInShelf < 1.0f);
@@ -247,8 +255,8 @@
         }
     }
 
-    private void updateIconAppearance(ExpandableNotificationRow row,
-            IconState iconState, StatusBarIconView icon) {
+    private void updateIconAppearance(ExpandableNotificationRow row, IconState iconState,
+            StatusBarIconView icon, float expandAmount) {
         // Let calculate how much the view is in the shelf
         float viewStart = row.getTranslationY();
         int transformHeight = row.getActualHeight() + mPaddingBetweenElements;
@@ -259,12 +267,6 @@
                 float linearAmount = (getTranslationY() - viewStart) / transformHeight;
                 float interpolatedAmount =  Interpolators.ACCELERATE_DECELERATE.getInterpolation(
                         linearAmount);
-                float interpolationStart = mMaxLayoutHeight - getIntrinsicHeight() * 2;
-                float expandAmount = 0.0f;
-                if (getTranslationY() >= interpolationStart) {
-                    expandAmount = (getTranslationY() - interpolationStart) / getIntrinsicHeight();
-                    expandAmount = Math.min(1.0f, expandAmount);
-                }
                 interpolatedAmount = NotificationUtils.interpolate(
                         interpolatedAmount, linearAmount, expandAmount);
                 iconState.iconAppearAmount = 1.0f - interpolatedAmount;
@@ -318,7 +320,7 @@
         // The notification size is different from the size in the shelf / statusbar
         float newSize = NotificationUtils.interpolate(notificationIconSize, shelfIconSize,
                 transitionAmount);
-        iconState.scaleX = newSize / icon.getHeight();
+        iconState.scaleX = newSize / icon.getHeight() / icon.getIconScale();
         iconState.scaleY = iconState.scaleX;
         iconState.hidden = transitionAmount == 0.0f;
         row.setIconTransformationAmount(transitionAmount);
@@ -326,8 +328,8 @@
         if (row.isInShelf() && !row.isTransformingIntoShelf()) {
             iconState.iconAppearAmount = 1.0f;
             iconState.alpha = 1.0f;
-            iconState.scaleX = shelfIconSize / icon.getHeight();
-            iconState.scaleY = iconState.scaleX;
+            iconState.scaleX = 1.0f;
+            iconState.scaleY = 1.0f;
             iconState.hidden = false;
         }
     }
@@ -378,8 +380,23 @@
         return super.shouldHideBackground() || mHideBackground;
     }
 
-    private void setIconContainerTranslation(float iconContainerTranslation) {
-        mNotificationIconContainer.setTranslationX(iconContainerTranslation);
+    private void setOpenedAmount(float openedAmount) {
+        mCollapsedIcons.getLocationOnScreen(mTmp);
+        int start = mTmp[0];
+        if (isLayoutRtl()) {
+            start = getWidth() - start - mCollapsedIcons.getWidth();
+        }
+        int width = (int) NotificationUtils.interpolate(start + mCollapsedIcons.getWidth(),
+                mShelfIcons.getWidth(),
+                openedAmount);
+        mShelfIcons.setActualLayoutWidth(width);
+        float padding = NotificationUtils.interpolate(mCollapsedIcons.getPaddingEnd(),
+                mShelfIcons.getPaddingEnd(),
+                openedAmount);
+        mShelfIcons.setActualPaddingEnd(padding);
+        float paddingStart = NotificationUtils.interpolate(start,
+                mShelfIcons.getPaddingStart(), openedAmount);
+        mShelfIcons.setActualPaddingStart(paddingStart);
     }
 
     public void setMaxLayoutHeight(int maxLayoutHeight) {
@@ -405,23 +422,27 @@
         return mHasItemsInStableShelf;
     }
 
+    public void setCollapsedIcons(NotificationIconContainer collapsedIcons) {
+        mCollapsedIcons = collapsedIcons;
+    }
+
     private class ShelfState extends ExpandableViewState {
-        private float iconContainerTranslation;
+        private float openedAmount;
         private boolean hasItemsInStableShelf;
 
         @Override
         public void applyToView(View view) {
             super.applyToView(view);
             updateAppearance();
-            setIconContainerTranslation(iconContainerTranslation);
+            setOpenedAmount(openedAmount);
             setHasItemsInStableShelf(hasItemsInStableShelf);
         }
 
         @Override
         public void animateTo(View child, AnimationProperties properties) {
             super.animateTo(child, properties);
+            setOpenedAmount(openedAmount);
             updateAppearance();
-            setIconContainerTranslation(iconContainerTranslation);
             setHasItemsInStableShelf(hasItemsInStableShelf);
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java
index 03e3662..543f899 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java
@@ -16,6 +16,10 @@
 
 package com.android.systemui.statusbar;
 
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
 import android.app.Notification;
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
@@ -30,20 +34,55 @@
 import android.os.UserHandle;
 import android.text.TextUtils;
 import android.util.AttributeSet;
+import android.util.FloatProperty;
 import android.util.Log;
+import android.util.Property;
 import android.util.TypedValue;
 import android.view.ViewDebug;
 import android.view.accessibility.AccessibilityEvent;
+import android.view.animation.Interpolator;
 
 import com.android.internal.statusbar.StatusBarIcon;
+import com.android.systemui.Interpolators;
 import com.android.systemui.R;
+import com.android.systemui.statusbar.notification.NotificationUtils;
 
 import java.text.NumberFormat;
 
 public class StatusBarIconView extends AnimatedImageView {
-    private static final String TAG = "StatusBarIconView";
-    private boolean mAlwaysScaleIcon;
+    public static final int STATE_ICON = 0;
+    public static final int STATE_DOT = 1;
+    public static final int STATE_HIDDEN = 2;
 
+    private static final String TAG = "StatusBarIconView";
+    private static final Property<StatusBarIconView, Float> ICON_APPEAR_AMOUNT
+            = new FloatProperty<StatusBarIconView>("iconAppearAmount") {
+
+        @Override
+        public void setValue(StatusBarIconView object, float value) {
+            object.setIconAppearAmount(value);
+        }
+
+        @Override
+        public Float get(StatusBarIconView object) {
+            return object.getIconAppearAmount();
+        }
+    };
+    private static final Property<StatusBarIconView, Float> DOT_APPEAR_AMOUNG
+            = new FloatProperty<StatusBarIconView>("dot_appear_amount") {
+
+        @Override
+        public void setValue(StatusBarIconView object, float value) {
+            object.setDotAppearAmount(value);
+        }
+
+        @Override
+        public Float get(StatusBarIconView object) {
+            return object.getDotAppearAmount();
+        }
+    };
+
+    private boolean mAlwaysScaleIcon;
     private StatusBarIcon mIcon;
     @ViewDebug.ExportedProperty private String mSlot;
     private Drawable mNumberBackground;
@@ -55,6 +94,15 @@
     private final boolean mBlocked;
     private int mDensity;
     private float mIconScale = 1.0f;
+    private final Paint mDotPaint = new Paint();
+    private boolean mDotVisible;
+    private float mDotRadius;
+    private int mStaticDotRadius;
+    private int mVisibleState = STATE_ICON;
+    private float mIconAppearAmount = 1.0f;
+    private ObjectAnimator mIconAppearAnimator;
+    private ObjectAnimator mDotAnimator;
+    private float mDotAppearAmount;
 
     public StatusBarIconView(Context context, String slot, Notification notification) {
         this(context, slot, notification, false);
@@ -73,6 +121,11 @@
         maybeUpdateIconScale();
         setScaleType(ScaleType.CENTER);
         mDensity = context.getResources().getDisplayMetrics().densityDpi;
+        if (mNotification != null) {
+            setIconTint(getContext().getColor(
+                    com.android.internal.R.color.notification_icon_default_color));
+        }
+        reloadDimens();
     }
 
     private void maybeUpdateIconScale() {
@@ -88,8 +141,6 @@
         final int outerBounds = res.getDimensionPixelSize(R.dimen.status_bar_icon_size);
         final int imageBounds = res.getDimensionPixelSize(R.dimen.status_bar_icon_drawing_size);
         mIconScale = (float)imageBounds / (float)outerBounds;
-        setScaleX(mIconScale);
-        setScaleY(mIconScale);
     }
 
     public float getIconScale() {
@@ -104,6 +155,15 @@
             mDensity = density;
             maybeUpdateIconScale();
             updateDrawable();
+            reloadDimens();
+        }
+    }
+
+    private void reloadDimens() {
+        boolean applyRadius = mDotRadius == mStaticDotRadius;
+        mStaticDotRadius = getResources().getDimensionPixelSize(R.dimen.overflow_dot_radius);
+        if (applyRadius) {
+            mDotRadius = mStaticDotRadius;
         }
     }
 
@@ -264,12 +324,32 @@
 
     @Override
     protected void onDraw(Canvas canvas) {
-        super.onDraw(canvas);
+        if (mIconAppearAmount > 0.0f) {
+            canvas.save();
+            canvas.scale(mIconScale * mIconAppearAmount, mIconScale * mIconAppearAmount,
+                    getWidth() / 2, getHeight() / 2);
+            super.onDraw(canvas);
+            canvas.restore();
+        }
 
         if (mNumberBackground != null) {
             mNumberBackground.draw(canvas);
             canvas.drawText(mNumberText, mNumberX, mNumberY, mNumberPain);
         }
+        if (mDotAppearAmount != 0.0f) {
+            float radius;
+            float alpha;
+            if (mDotAppearAmount <= 1.0f) {
+                radius = mDotRadius * mDotAppearAmount;
+                alpha = 1.0f;
+            } else {
+                float fadeOutAmount = mDotAppearAmount - 1.0f;
+                alpha = 1.0f - fadeOutAmount;
+                radius = NotificationUtils.interpolate(mDotRadius, getWidth() / 4, fadeOutAmount);
+            }
+            mDotPaint.setAlpha((int) (alpha * 255));
+            canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius, mDotPaint);
+        }
     }
 
     @Override
@@ -356,4 +436,76 @@
         return c.getString(R.string.accessibility_desc_notification_icon, appName, desc);
     }
 
+    public void setIconTint(int iconTint) {
+        mDotPaint.setColor(iconTint);
+    }
+
+    public void setVisibleState(int visibleState) {
+        if (visibleState != mVisibleState) {
+            mVisibleState = visibleState;
+            if (mIconAppearAnimator != null) {
+                mIconAppearAnimator.cancel();
+            }
+            float targetAmount = 0.0f;
+            Interpolator interpolator = Interpolators.FAST_OUT_LINEAR_IN;
+            if (visibleState == STATE_ICON) {
+                targetAmount = 1.0f;
+                interpolator = Interpolators.LINEAR_OUT_SLOW_IN;
+            }
+            mIconAppearAnimator = ObjectAnimator.ofFloat(this, ICON_APPEAR_AMOUNT,
+                    targetAmount);
+            mIconAppearAnimator.setInterpolator(interpolator);
+            mIconAppearAnimator.setDuration(100);
+            mIconAppearAnimator.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    mIconAppearAnimator = null;
+                }
+            });
+            mIconAppearAnimator.start();
+
+            if (mDotAnimator != null) {
+                mDotAnimator.cancel();
+            }
+            targetAmount = visibleState == STATE_ICON ? 2.0f : 0.0f;
+            interpolator = Interpolators.FAST_OUT_LINEAR_IN;
+            if (visibleState == STATE_DOT) {
+                targetAmount = 1.0f;
+                interpolator = Interpolators.LINEAR_OUT_SLOW_IN;
+            }
+            mDotAnimator = ObjectAnimator.ofFloat(this, DOT_APPEAR_AMOUNG,
+                    targetAmount);
+            mDotAnimator.setInterpolator(interpolator);
+            mDotAnimator.setDuration(100);
+            mDotAnimator.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    mDotAnimator = null;
+                }
+            });
+            mDotAnimator.start();
+        }
+    }
+
+    public void setIconAppearAmount(Float iconAppearAmount) {
+        mIconAppearAmount = iconAppearAmount;
+        invalidate();
+    }
+
+    public float getIconAppearAmount() {
+        return mIconAppearAmount;
+    }
+
+    public int getVisibleState() {
+        return mVisibleState;
+    }
+
+    public void setDotAppearAmount(float dotAppearAmount) {
+        mDotAppearAmount = dotAppearAmount;
+        invalidate();
+    }
+
+    public float getDotAppearAmount() {
+        return mDotAppearAmount;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java
index 72e84a4..7b67f31 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java
@@ -8,7 +8,6 @@
 import android.support.annotation.NonNull;
 import android.view.LayoutInflater;
 import android.view.View;
-import android.view.ViewGroup;
 import android.widget.LinearLayout;
 
 import com.android.internal.util.NotificationColorUtil;
@@ -36,7 +35,6 @@
 
     private PhoneStatusBar mPhoneStatusBar;
     protected View mNotificationIconArea;
-    private NotificationShelf mNotificationIconAreaScroller;
     private NotificationIconContainer mNotificationIcons;
     private NotificationIconContainer mNotificationIconsScroller;
     private final Rect mTintArea = new Rect();
@@ -66,8 +64,9 @@
         mNotificationIcons = (NotificationIconContainer) mNotificationIconArea.findViewById(
                 R.id.notificationIcons);
 
-        mNotificationIconAreaScroller = mPhoneStatusBar.getNotificationShelf();
-        mNotificationIconsScroller = mNotificationIconAreaScroller.getNotificationIconContainer();
+        NotificationShelf shelf = mPhoneStatusBar.getNotificationShelf();
+        mNotificationIconsScroller = shelf.getShelfIcons();
+        shelf.setCollapsedIcons(mNotificationIcons);
 
         mNotificationScrollLayout = mPhoneStatusBar.getNotificationScrollLayout();
     }
@@ -115,12 +114,10 @@
     }
 
     /**
-     * Sets the color that should be used to tint any icons in the notification area. If this
-     * method is not called, the default tint is {@link Color#WHITE}.
+     * Sets the color that should be used to tint any icons in the notification area.
      */
     public void setIconTint(int iconTint) {
         mIconTint = iconTint;
-        mNotificationIcons.setIconTint(mIconTint);
         applyNotificationIconsTint();
     }
 
@@ -178,7 +175,7 @@
      */
     private void updateIconsForLayout(NotificationData notificationData,
             Function<NotificationData.Entry, StatusBarIconView> function,
-            ViewGroup hostLayout) {
+            NotificationIconContainer hostLayout) {
         ArrayList<StatusBarIconView> toShow = new ArrayList<>(
                 mNotificationScrollLayout.getChildCount());
 
@@ -193,6 +190,7 @@
             }
         }
 
+
         ArrayList<View> toRemove = new ArrayList<>();
         for (int i = 0; i < hostLayout.getChildCount(); i++) {
             View child = hostLayout.getChildAt(i);
@@ -239,6 +237,7 @@
                 v.setImageTintList(ColorStateList.valueOf(
                         StatusBarIconController.getTint(mTintArea, v, mIconTint)));
             }
+            v.setIconTint(mIconTint);
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
index 11563d594..0f41e31 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
@@ -17,10 +17,18 @@
 package com.android.systemui.statusbar.phone;
 
 import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
 import android.util.AttributeSet;
 import android.view.View;
 
+import com.android.systemui.R;
 import com.android.systemui.statusbar.AlphaOptimizedFrameLayout;
+import com.android.systemui.statusbar.StatusBarIconView;
+import com.android.systemui.statusbar.stack.AnimationFilter;
+import com.android.systemui.statusbar.stack.AnimationProperties;
 import com.android.systemui.statusbar.stack.ViewState;
 
 import java.util.WeakHashMap;
@@ -31,15 +39,48 @@
  */
 public class NotificationIconContainer extends AlphaOptimizedFrameLayout {
     private static final String TAG = "NotificationIconContainer";
+    private static final boolean DEBUG = false;
+    private static final AnimationFilter DOT_ANIMATION_FILTER = new AnimationFilter().animateX();
+    private static final AnimationProperties DOT_ANIMATION_PROPERTIES = new AnimationProperties() {
+        @Override
+        public AnimationFilter getAnimationFilter() {
+            return DOT_ANIMATION_FILTER;
+        }
+    }.setDuration(200);
 
     private boolean mShowAllIcons = true;
-    private int mIconTint;
     private WeakHashMap<View, IconState> mIconStates = new WeakHashMap<>();
+    private int mDotPadding;
+    private int mStaticDotRadius;
+    private int mActualLayoutWidth = -1;
+    private float mActualPaddingEnd = -1;
+    private float mActualPaddingStart = -1;
 
     public NotificationIconContainer(Context context, AttributeSet attrs) {
         super(context, attrs);
+        initDimens();
+        setWillNotDraw(!DEBUG);
     }
 
+    private void initDimens() {
+        mDotPadding = getResources().getDimensionPixelSize(R.dimen.overflow_icon_dot_padding);
+        mStaticDotRadius = getResources().getDimensionPixelSize(R.dimen.overflow_dot_radius);
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+        Paint paint = new Paint();
+        paint.setColor(Color.RED);
+        paint.setStyle(Paint.Style.STROKE);
+        canvas.drawRect(getActualPaddingStart(), 0, getLayoutEnd(), getHeight(), paint);
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        initDimens();
+    }
     @Override
     protected void onLayout(boolean changed, int l, int t, int r, int b) {
         float centerY = getHeight() / 2.0f;
@@ -82,10 +123,6 @@
         mIconStates.remove(child);
     }
 
-    public void setIconTint(int iconTint) {
-        mIconTint = iconTint;
-    }
-
     public WeakHashMap<View, IconState> resetViewStates() {
         for (int i = 0; i < getChildCount(); i++) {
             View view = getChildAt(i);
@@ -100,17 +137,75 @@
      * are inserted into the notification container.
      * If this is not a whole number, the fraction means by how much the icon is appearing.
      */
-    public WeakHashMap<View, IconState> calculateIconTranslations() {
+    public void calculateIconTranslations() {
+        float translationX = getActualPaddingStart();
+        int overflowingIconIndex = -1;
+        int lastTwoIconWidth = 0;
         int childCount = getChildCount();
-        float visibleIconStart = childCount - numberOfVisibleIcons;
-        float translationX = 0.0f;
         for (int i = 0; i < childCount; i++) {
             View view = getChildAt(i);
             IconState iconState = mIconStates.get(view);
             iconState.xTranslation = translationX;
+            iconState.visibleState = StatusBarIconView.STATE_ICON;
             translationX += iconState.iconAppearAmount * view.getWidth();
+            if (translationX > getLayoutEnd()) {
+                // we are overflowing it with this icon
+                overflowingIconIndex = i - 1;
+                lastTwoIconWidth = view.getWidth();
+                break;
+            }
         }
-        return mIconStates;
+        if (overflowingIconIndex != -1) {
+            int numDots = 1;
+            View overflowIcon = getChildAt(overflowingIconIndex);
+            IconState overflowState = mIconStates.get(overflowIcon);
+            lastTwoIconWidth += overflowIcon.getWidth();
+            int dotWidth = mStaticDotRadius * 2 + mDotPadding;
+            int totalDotLength = mStaticDotRadius * 6 + 2 * mDotPadding;
+            translationX = (getLayoutEnd() - lastTwoIconWidth / 2 - totalDotLength / 2)
+                    - overflowIcon.getWidth() * 0.3f + mStaticDotRadius;
+            float overflowStart = getLayoutEnd() - lastTwoIconWidth;
+            float overlapAmount = (overflowState.xTranslation - overflowStart)
+                    / overflowIcon.getWidth();
+            translationX += overlapAmount * dotWidth;
+            for (int i = overflowingIconIndex; i < childCount; i++) {
+                View view = getChildAt(i);
+                IconState iconState = mIconStates.get(view);
+                iconState.xTranslation = translationX;
+                if (numDots <= 3) {
+                    iconState.visibleState = StatusBarIconView.STATE_DOT;
+                    translationX += numDots == 3 ? 3 * dotWidth : dotWidth;
+                } else {
+                    iconState.visibleState = StatusBarIconView.STATE_HIDDEN;
+                }
+                numDots++;
+            }
+        }
+        if (isLayoutRtl()) {
+            for (int i = 0; i < childCount; i++) {
+                View view = getChildAt(i);
+                IconState iconState = mIconStates.get(view);
+                iconState.xTranslation = getWidth() - iconState.xTranslation - view.getWidth();
+            }
+        }
+    }
+
+    private float getLayoutEnd() {
+        return getActualWidth() - getActualPaddingEnd();
+    }
+
+    private float getActualPaddingEnd() {
+        if (mActualPaddingEnd < 0) {
+            return getPaddingEnd();
+        }
+        return mActualPaddingEnd;
+    }
+
+    private float getActualPaddingStart() {
+        if (mActualPaddingStart < 0) {
+            return getPaddingStart();
+        }
+        return mActualPaddingStart;
     }
 
     /**
@@ -123,12 +218,49 @@
         mShowAllIcons = showAllIcons;
     }
 
+    public void setActualLayoutWidth(int actualLayoutWidth) {
+        mActualLayoutWidth = actualLayoutWidth;
+        if (DEBUG) {
+            invalidate();
+        }
+    }
+
+    public void setActualPaddingEnd(float paddingEnd) {
+        mActualPaddingEnd = paddingEnd;
+        if (DEBUG) {
+            invalidate();
+        }
+    }
+
+    public void setActualPaddingStart(float paddingStart) {
+        mActualPaddingStart = paddingStart;
+        if (DEBUG) {
+            invalidate();
+        }
+    }
+
+    public int getActualWidth() {
+        if (mActualLayoutWidth < 0) {
+            return getWidth();
+        }
+        return mActualLayoutWidth;
+    }
+
     public static class IconState extends ViewState {
         public float iconAppearAmount = 1.0f;
+        public int visibleState;
 
         @Override
         public void applyToView(View view) {
-            super.applyToView(view);
+            if (view instanceof StatusBarIconView) {
+                StatusBarIconView icon = (StatusBarIconView) view;
+                if (visibleState != icon.getVisibleState()) {
+                    icon.setVisibleState(visibleState);
+                    animateTo(icon, DOT_ANIMATION_PROPERTIES);
+                } else {
+                    super.applyToView(view);
+                }
+            }
         }
     }
 }