Refactored the layout of a notifications

Notifications now consist of ExpandableViews instead of SizeAdaptiveLayouts
to avoid layout passes during the resizing. The StackScrollAlgorithm and its
States are also refactored in order to support the new behaviour. In addition,
the generation of the outline is moved to the notification views instead of
the container which contains them.

Change-Id: I1ac1292a6520f5951610039bfa204c204be9d640
diff --git a/packages/SystemUI/res/layout/status_bar_notification_keyguard_overflow.xml b/packages/SystemUI/res/layout/status_bar_notification_keyguard_overflow.xml
index d81e525..2e08bff 100644
--- a/packages/SystemUI/res/layout/status_bar_notification_keyguard_overflow.xml
+++ b/packages/SystemUI/res/layout/status_bar_notification_keyguard_overflow.xml
@@ -18,10 +18,9 @@
 <com.android.systemui.statusbar.NotificationOverflowContainer
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
-    android:layout_height="wrap_content"
+    android:layout_height="32dp"
     android:focusable="true"
     android:clickable="true"
-    android:background="@*android:drawable/notification_quantum_bg_dim"
     >
     <TextView
         android:id="@+id/more_text"
diff --git a/packages/SystemUI/res/layout/status_bar_notification_row.xml b/packages/SystemUI/res/layout/status_bar_notification_row.xml
index 41e7dac..8959a5b 100644
--- a/packages/SystemUI/res/layout/status_bar_notification_row.xml
+++ b/packages/SystemUI/res/layout/status_bar_notification_row.xml
@@ -4,14 +4,13 @@
     android:layout_height="wrap_content"
     android:focusable="true"
     android:clickable="true"
-    android:background="@*android:drawable/notification_quantum_bg"
     >
 
-    <com.android.internal.widget.SizeAdaptiveLayout android:id="@+id/expanded"
+    <com.android.systemui.statusbar.NotificationContentView android:id="@+id/expanded"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
 
-    <com.android.internal.widget.SizeAdaptiveLayout android:id="@+id/expandedPublic"
+    <com.android.systemui.statusbar.NotificationContentView android:id="@+id/expandedPublic"
         android:layout_width="match_parent"
         android:layout_height="wrap_content" />
 
diff --git a/packages/SystemUI/src/com/android/systemui/ExpandHelper.java b/packages/SystemUI/src/com/android/systemui/ExpandHelper.java
index c585a5b..61c268e 100644
--- a/packages/SystemUI/src/com/android/systemui/ExpandHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/ExpandHelper.java
@@ -31,8 +31,9 @@
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.view.ViewConfiguration;
-import android.view.ViewGroup;
 
+import com.android.systemui.statusbar.ExpandableView;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
 import com.android.systemui.statusbar.policy.ScrollAdapter;
 
 public class ExpandHelper implements Gefingerpoken, OnClickListener {
@@ -115,9 +116,7 @@
             float focusY = detector.getFocusY();
 
             final View underFocus = findView(focusX, focusY);
-            if (underFocus != null) {
-                startExpanding(underFocus, STRETCH);
-            }
+            startExpanding(underFocus, STRETCH);
             return mExpanding;
         }
 
@@ -133,41 +132,21 @@
     };
 
     private class ViewScaler {
-        View mView;
+        ExpandableView mView;
 
         public ViewScaler() {}
-        public void setView(View v) {
+        public void setView(ExpandableView v) {
             mView = v;
         }
         public void setHeight(float h) {
             if (DEBUG_SCALE) Log.v(TAG, "SetHeight: setting to " + h);
-            ViewGroup.LayoutParams lp = mView.getLayoutParams();
-            lp.height = (int)h;
-            mView.setLayoutParams(lp);
-            mView.requestLayout();
+            mView.setActualHeight((int) h);
         }
         public float getHeight() {
-            int height = mView.getLayoutParams().height;
-            if (height < 0) {
-                height = mView.getMeasuredHeight();
-            }
-            return height;
+            return mView.getActualHeight();
         }
         public int getNaturalHeight(int maximum) {
-            ViewGroup.LayoutParams lp = mView.getLayoutParams();
-            if (DEBUG_SCALE) Log.v(TAG, "Inspecting a child of type: " +
-                    mView.getClass().getName());
-            int oldHeight = lp.height;
-            lp.height = ViewGroup.LayoutParams.WRAP_CONTENT;
-            mView.setLayoutParams(lp);
-            mView.measure(
-                    View.MeasureSpec.makeMeasureSpec(mView.getMeasuredWidth(),
-                                                     View.MeasureSpec.EXACTLY),
-                    View.MeasureSpec.makeMeasureSpec(maximum,
-                                                     View.MeasureSpec.AT_MOST));
-            lp.height = oldHeight;
-            mView.setLayoutParams(lp);
-            return mView.getMeasuredHeight();
+            return Math.min(maximum, mView.getMaxHeight());
         }
     }
 
@@ -189,12 +168,6 @@
         mGravity = Gravity.TOP;
         mScaleAnimation = ObjectAnimator.ofFloat(mScaler, "height", 0f);
         mScaleAnimation.setDuration(EXPAND_DURATION);
-        mScaleAnimation.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                mCallback.setUserLockedChild(mCurrView, false);
-            }
-        });
         mPopLimit = mContext.getResources().getDimension(R.dimen.blinds_pop_threshold);
         mPopDuration = mContext.getResources().getInteger(R.integer.blinds_pop_duration_ms);
         mPullGestureMinXSpan = mContext.getResources().getDimension(R.dimen.pull_span_min);
@@ -341,9 +314,7 @@
                 if (DEBUG_SCALE) Log.v(TAG, "got pull gesture (xspan=" + xspan + "px)");
 
                 final View underFocus = findView(x, y);
-                if (underFocus != null) {
-                    startExpanding(underFocus, PULL);
-                }
+                startExpanding(underFocus, PULL);
                 return true;
             }
             if (mScrollAdapter != null && !mScrollAdapter.isScrolledToTop()) {
@@ -358,8 +329,7 @@
                         if (DEBUG) Log.v(TAG, "got venetian gesture (dy=" + yDiff + "px)");
                         mLastMotionY = y;
                         final View underFocus = findView(x, y);
-                        if (underFocus != null) {
-                            startExpanding(underFocus, BLINDS);
+                        if (startExpanding(underFocus, BLINDS)) {
                             mInitialTouchY = mLastMotionY;
                             mHasPopped = false;
                         }
@@ -459,16 +429,22 @@
         return true;
     }
 
-    private void startExpanding(View v, int expandType) {
+    /**
+     * @return True if the view is expandable, false otherwise.
+     */
+    private boolean startExpanding(View v, int expandType) {
+        if (!(v instanceof ExpandableNotificationRow)) {
+            return false;
+        }
         mExpansionStyle = expandType;
-        if (mExpanding &&  v == mCurrView) {
-            return;
+        if (mExpanding && v == mCurrView) {
+            return true;
         }
         mExpanding = true;
         if (DEBUG) Log.d(TAG, "scale type " + expandType + " beginning on view: " + v);
         mCallback.setUserLockedChild(v, true);
         setView(v);
-        mScaler.setView(v);
+        mScaler.setView((ExpandableView) v);
         mOldHeight = mScaler.getHeight();
         if (mCallback.canChildBeExpanded(v)) {
             if (DEBUG) Log.d(TAG, "working on an expandable child");
@@ -480,6 +456,7 @@
         if (DEBUG) Log.d(TAG, "got mOldHeight: " + mOldHeight +
                     " mNaturalHeight: " + mNaturalHeight);
         v.getParent().requestDisallowInterceptTouchEvent(true);
+        return true;
     }
 
     private void finishExpanding(boolean force) {
@@ -499,10 +476,18 @@
         if (mScaleAnimation.isRunning()) {
             mScaleAnimation.cancel();
         }
-        mCallback.setUserExpandedChild(mCurrView, h == mNaturalHeight);
+        mCallback.setUserExpandedChild(mCurrView, targetHeight == mNaturalHeight);
         if (targetHeight != currentHeight) {
             mScaleAnimation.setFloatValues(targetHeight);
             mScaleAnimation.setupStartValues();
+            final View scaledView = mCurrView;
+            mScaleAnimation.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    mCallback.setUserLockedChild(scaledView, false);
+                    mScaleAnimation.removeListener(this);
+                }
+            });
             mScaleAnimation.start();
         } else {
             mCallback.setUserLockedChild(mCurrView, false);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java
index d647dfa..0f32dc0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java
@@ -29,7 +29,7 @@
  * Base class for both {@link ExpandableNotificationRow} and {@link NotificationOverflowContainer}
  * to implement dimming/activating on Keyguard for the double-tap gesture
  */
-public class ActivatableNotificationView extends FrameLayout {
+public abstract class ActivatableNotificationView extends ExpandableOutlineView {
 
     private static final long DOUBLETAP_TIMEOUT_MS = 1000;
 
@@ -54,6 +54,7 @@
     public ActivatableNotificationView(Context context, AttributeSet attrs) {
         super(context, attrs);
         mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+        updateBackgroundResource();
     }
 
 
@@ -84,6 +85,9 @@
             case MotionEvent.ACTION_DOWN:
                 mDownX = event.getX();
                 mDownY = event.getY();
+                if (mDownY > getActualHeight()) {
+                    return false;
+                }
 
                 // Call the listener tentatively directly, even if we don't know whether the user
                 // will stay within the touch slop, as the listener is implemented as a scale
@@ -122,7 +126,7 @@
     }
 
     private void makeActive(float x, float y) {
-        getBackground().setHotspot(0, x, y);
+        mCustomBackground.setHotspot(0, x, y);
         mActivated = true;
     }
 
@@ -132,8 +136,8 @@
     private void makeInactive() {
         if (mActivated) {
             // Make sure that we clear the hotspot from the center.
-            getBackground().setHotspot(0, getWidth() / 2, getHeight() / 2);
-            getBackground().removeHotspot(0);
+            mCustomBackground.setHotspot(0, getWidth() / 2, getActualHeight() / 2);
+            mCustomBackground.removeHotspot(0);
             mActivated = false;
         }
         if (mOnActivatedListener != null) {
@@ -178,7 +182,19 @@
     }
 
     private void updateBackgroundResource() {
-        setBackgroundResource(mDimmed ? mDimmedBgResId : mBgResId);
+        setCustomBackgroundResource(mDimmed ? mDimmedBgResId : mBgResId);
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        setPivotX(getWidth()/2);
+    }
+
+    @Override
+    public void setActualHeight(int actualHeight) {
+        super.setActualHeight(actualHeight);
+        setPivotY(actualHeight/2);
     }
 
     public void setOnActivatedListener(OnActivatedListener onActivatedListener) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
index 3e21640..90b63f4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
@@ -760,9 +760,10 @@
         // NB: the large icon is now handled entirely by the template
 
         // bind the click event to the content area
-        SizeAdaptiveLayout expanded = (SizeAdaptiveLayout)row.findViewById(R.id.expanded);
-        SizeAdaptiveLayout expandedPublic
-                = (SizeAdaptiveLayout)row.findViewById(R.id.expandedPublic);
+        NotificationContentView expanded =
+                (NotificationContentView) row.findViewById(R.id.expanded);
+        NotificationContentView expandedPublic =
+                (NotificationContentView) row.findViewById(R.id.expandedPublic);
 
         row.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
 
@@ -794,19 +795,11 @@
 
         if (contentViewLocal != null) {
             contentViewLocal.setIsRootNamespace(true);
-            SizeAdaptiveLayout.LayoutParams params =
-                    new SizeAdaptiveLayout.LayoutParams(contentViewLocal.getLayoutParams());
-            params.minHeight = minHeight;
-            params.maxHeight = minHeight;
-            expanded.addView(contentViewLocal, params);
+            expanded.setContractedChild(contentViewLocal);
         }
         if (bigContentViewLocal != null) {
             bigContentViewLocal.setIsRootNamespace(true);
-            SizeAdaptiveLayout.LayoutParams params =
-                    new SizeAdaptiveLayout.LayoutParams(bigContentViewLocal.getLayoutParams());
-            params.minHeight = minHeight+1;
-            params.maxHeight = maxHeight;
-            expanded.addView(bigContentViewLocal, params);
+            expanded.setExpandedChild(bigContentViewLocal);
         }
 
         PackageManager pm = mContext.getPackageManager();
@@ -820,11 +813,7 @@
 
                 if (publicViewLocal != null) {
                     publicViewLocal.setIsRootNamespace(true);
-                    SizeAdaptiveLayout.LayoutParams params =
-                            new SizeAdaptiveLayout.LayoutParams(publicViewLocal.getLayoutParams());
-                    params.minHeight = minHeight;
-                    params.maxHeight = minHeight;
-                    expandedPublic.addView(publicViewLocal, params);
+                    expandedPublic.setContractedChild(publicViewLocal);
                 }
             }
             catch (RuntimeException e) {
@@ -1352,6 +1341,7 @@
         } else {
             entry.row.setOnClickListener(null);
         }
+        entry.row.notifyContentUpdated();
     }
 
     protected void notifyHeadsUpScreenOn(boolean screenOn) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java
index 35c02eb..b813e65 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java
@@ -17,12 +17,12 @@
 package com.android.systemui.statusbar;
 
 import android.content.Context;
+import android.graphics.Canvas;
 import android.util.AttributeSet;
+import android.util.Log;
 import android.view.View;
-import android.view.ViewGroup;
 import android.view.accessibility.AccessibilityEvent;
 
-import com.android.internal.widget.SizeAdaptiveLayout;
 import com.android.systemui.R;
 
 public class ExpandableNotificationRow extends ActivatableNotificationView {
@@ -45,12 +45,10 @@
      * user expansion.
      */
     private boolean mIsSystemExpanded;
-    private SizeAdaptiveLayout mPublicLayout;
-    private SizeAdaptiveLayout mPrivateLayout;
+    private NotificationContentView mPublicLayout;
+    private NotificationContentView mPrivateLayout;
     private int mMaxExpandHeight;
-    private boolean mMaxHeightNeedsUpdate;
     private NotificationActivator mActivator;
-    private boolean mSelfInitiatedLayout;
 
     public ExpandableNotificationRow(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -59,8 +57,8 @@
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
-        mPublicLayout = (SizeAdaptiveLayout) findViewById(R.id.expandedPublic);
-        mPrivateLayout = (SizeAdaptiveLayout) findViewById(R.id.expanded);
+        mPublicLayout = (NotificationContentView) findViewById(R.id.expandedPublic);
+        mPrivateLayout = (NotificationContentView) findViewById(R.id.expanded);
 
         mActivator = new NotificationActivator(this);
     }
@@ -82,7 +80,6 @@
     public void setHeightRange(int rowMinHeight, int rowMaxHeight) {
         mRowMinHeight = rowMinHeight;
         mRowMaxHeight = rowMaxHeight;
-        mMaxHeightNeedsUpdate = true;
     }
 
     public boolean isExpandable() {
@@ -145,13 +142,11 @@
      * @param expand should the layout be in the expanded state
      */
     public void applyExpansionToLayout(boolean expand) {
-        ViewGroup.LayoutParams lp = getLayoutParams();
         if (expand && mExpandable) {
-            lp.height = ViewGroup.LayoutParams.WRAP_CONTENT;
+            setActualHeight(mMaxExpandHeight);
         } else {
-            lp.height = mRowMinHeight;
+            setActualHeight(mRowMinHeight);
         }
-        setLayoutParams(lp);
     }
 
     /**
@@ -161,6 +156,9 @@
      * @return the maximum allowed expansion height of this view.
      */
     public int getMaximumAllowedExpandHeight() {
+        if (isUserLocked()) {
+            return getActualHeight();
+        }
         boolean inExpansionState = isExpanded();
         if (!inExpansionState) {
             // not expanded, so we return the collapsed size
@@ -170,31 +168,6 @@
         return mShowingPublic ? mRowMinHeight : getMaxExpandHeight();
     }
 
-    private void updateMaxExpandHeight() {
-
-        // We don't want this method to trigger a layout of the whole view hierarchy,
-        // as the layout parameters in the end are the same which they were in the beginning.
-        // Otherwise a loop may occur if this method is called on the layout of a parent.
-        mSelfInitiatedLayout = true;
-        ViewGroup.LayoutParams lp = getLayoutParams();
-        int oldHeight = lp.height;
-        lp.height = ViewGroup.LayoutParams.WRAP_CONTENT;
-        setLayoutParams(lp);
-        measure(View.MeasureSpec.makeMeasureSpec(getWidth(), View.MeasureSpec.EXACTLY),
-                View.MeasureSpec.makeMeasureSpec(mRowMaxHeight, View.MeasureSpec.AT_MOST));
-        lp.height = oldHeight;
-        setLayoutParams(lp);
-        mMaxExpandHeight = getMeasuredHeight();
-        mSelfInitiatedLayout = false;
-    }
-
-    @Override
-    public void requestLayout() {
-        if (!mSelfInitiatedLayout) {
-            super.requestLayout();
-        }
-    }
-
     /**
      * Check whether the view state is currently expanded. This is given by the system in {@link
      * #setSystemExpanded(boolean)} and can be overridden by user expansion or
@@ -210,7 +183,11 @@
     @Override
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
         super.onLayout(changed, left, top, right, bottom);
-        mMaxHeightNeedsUpdate = true;
+        boolean updateExpandHeight = mMaxExpandHeight == 0;
+        mMaxExpandHeight = mPrivateLayout.getMaxHeight();
+        if (updateExpandHeight) {
+            applyExpansionToLayout(isExpanded());
+        }
     }
 
     public void setShowingPublic(boolean show) {
@@ -233,10 +210,6 @@
     }
 
     public int getMaxExpandHeight() {
-        if (mMaxHeightNeedsUpdate) {
-            updateMaxExpandHeight();
-            mMaxHeightNeedsUpdate = false;
-        }
         return mMaxExpandHeight;
     }
 
@@ -248,6 +221,28 @@
      * @return the potential height this view could expand in addition.
      */
     public int getExpandPotential() {
-        return getMaximumAllowedExpandHeight() - getHeight();
+        return getMaximumAllowedExpandHeight() - getActualHeight();
+    }
+
+    @Override
+    public void setActualHeight(int height) {
+        mPrivateLayout.setActualHeight(height);
+        invalidate();
+        super.setActualHeight(height);
+    }
+
+    @Override
+    public int getMaxHeight() {
+        return mPrivateLayout.getMaxHeight();
+    }
+
+    @Override
+    public void setClipTopAmount(int clipTopAmount) {
+        super.setClipTopAmount(clipTopAmount);
+        mPrivateLayout.setClipTopAmount(clipTopAmount);
+    }
+
+    public void notifyContentUpdated() {
+        mPrivateLayout.notifyContentUpdated();
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableOutlineView.java b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableOutlineView.java
new file mode 100644
index 0000000..43eb5b5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableOutlineView.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2014 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.systemui.statusbar;
+
+import android.content.Context;
+import android.graphics.Outline;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+
+/**
+ * Like {@link ExpandableView}, but setting an outline for the height and clipping.
+ */
+public abstract class ExpandableOutlineView extends ExpandableView {
+
+    private final Outline mOutline = new Outline();
+
+    public ExpandableOutlineView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    public void setActualHeight(int actualHeight) {
+        super.setActualHeight(actualHeight);
+        updateOutline();
+    }
+
+    @Override
+    public void setClipTopAmount(int clipTopAmount) {
+        super.setClipTopAmount(clipTopAmount);
+        updateOutline();
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        updateOutline();
+    }
+
+    private void updateOutline() {
+        mOutline.setRect(0,
+                mClipTopAmount,
+                getWidth(),
+                Math.max(mActualHeight, mClipTopAmount));
+        setOutline(mOutline);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableView.java b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableView.java
new file mode 100644
index 0000000..35913fa
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableView.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2014 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.systemui.statusbar;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Outline;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.InsetDrawable;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.FrameLayout;
+
+/**
+ * An abstract view for expandable views.
+ */
+public abstract class ExpandableView extends FrameLayout {
+
+    private OnHeightChangedListener mOnHeightChangedListener;
+    protected int mActualHeight;
+    protected int mClipTopAmount;
+    protected Drawable mCustomBackground;
+    private boolean mActualHeightInitialized;
+
+    public ExpandableView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        if (mCustomBackground != null) {
+            mCustomBackground.setBounds(0, mClipTopAmount, getWidth(), mActualHeight);
+            mCustomBackground.draw(canvas);
+        }
+    }
+
+    @Override
+    protected boolean verifyDrawable(Drawable who) {
+        return super.verifyDrawable(who) || who == mCustomBackground;
+    }
+
+    @Override
+    protected void drawableStateChanged() {
+        final Drawable d = mCustomBackground;
+        if (d != null && d.isStateful()) {
+            d.setState(getDrawableState());
+        }
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        if (!mActualHeightInitialized && mActualHeight == 0) {
+            mActualHeight = getHeight();
+        }
+        mActualHeightInitialized = true;
+    }
+
+    /**
+     * Sets the actual height of this notification. This is different than the laid out
+     * {@link View#getHeight()}, as we want to avoid layouting during scrolling and expanding.
+     */
+    public void setActualHeight(int actualHeight) {
+        mActualHeight = actualHeight;
+        invalidate();
+        if (mOnHeightChangedListener != null) {
+            mOnHeightChangedListener.onHeightChanged(this);
+        }
+    }
+
+    /**
+     * See {@link #setActualHeight}.
+     *
+     * @return The actual height of this notification.
+     */
+    public int getActualHeight() {
+        return mActualHeight;
+    }
+
+    /**
+     * @return The maximum height of this notification.
+     */
+    public abstract int getMaxHeight();
+
+    /**
+     * Sets the amount this view should be clipped from the top. This is used when an expanded
+     * notification is scrolling in the top or bottom stack.
+     *
+     * @param clipTopAmount The amount of pixels this view should be clipped from top.
+     */
+    public void setClipTopAmount(int clipTopAmount) {
+        mClipTopAmount = clipTopAmount;
+        invalidate();
+    }
+
+    public void setOnHeightChangedListener(OnHeightChangedListener listener) {
+        mOnHeightChangedListener = listener;
+    }
+
+    /**
+     * Sets a custom background drawable. As we need to change our bounds independently of layout,
+     * we need the notition of a custom background.
+     */
+    public void setCustomBackground(Drawable customBackground) {
+        if (mCustomBackground != null) {
+            mCustomBackground.setCallback(null);
+            unscheduleDrawable(mCustomBackground);
+        }
+        mCustomBackground = customBackground;
+        mCustomBackground.setCallback(this);
+        setWillNotDraw(customBackground == null);
+        invalidate();
+    }
+
+    public void setCustomBackgroundResource(int drawableResId) {
+        setCustomBackground(getResources().getDrawable(drawableResId));
+    }
+
+    /**
+     * A listener notifying when {@link #getActualHeight} changes.
+     */
+    public interface OnHeightChangedListener {
+        void onHeightChanged(ExpandableView view);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java
new file mode 100644
index 0000000..fd0cb08
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2014 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.systemui.statusbar;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+
+import com.android.systemui.R;
+
+/**
+ * A frame layout containing the actual payload of the notification, including the contracted and
+ * expanded layout. This class is responsible for clipping the content and and switching between the
+ * expanded and contracted view depending on its clipped size.
+ */
+public class NotificationContentView extends ExpandableView {
+
+    private final Rect mClipBounds = new Rect();
+
+    private View mContractedChild;
+    private View mExpandedChild;
+
+    private int mSmallHeight;
+
+    public NotificationContentView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mSmallHeight = getResources().getDimensionPixelSize(R.dimen.notification_min_height);
+        mActualHeight = mSmallHeight;
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        updateClipping();
+    }
+
+    public void setContractedChild(View child) {
+        if (mContractedChild != null) {
+            removeView(mContractedChild);
+        }
+        sanitizeContractedLayoutParams(child);
+        addView(child);
+        mContractedChild = child;
+        selectLayout();
+    }
+
+    public void setExpandedChild(View child) {
+        if (mExpandedChild != null) {
+            removeView(mExpandedChild);
+        }
+        addView(child);
+        mExpandedChild = child;
+        selectLayout();
+    }
+
+    @Override
+    public void setActualHeight(int actualHeight) {
+        super.setActualHeight(actualHeight);
+        selectLayout();
+        updateClipping();
+    }
+
+    @Override
+    public int getMaxHeight() {
+
+        // The maximum height is just the laid out height.
+        return getHeight();
+    }
+
+    @Override
+    public void setClipTopAmount(int clipTopAmount) {
+        super.setClipTopAmount(clipTopAmount);
+        updateClipping();
+    }
+
+    public int getClipTopAmount() {
+        return mClipTopAmount;
+    }
+
+    private void updateClipping() {
+        mClipBounds.set(0, mClipTopAmount, getWidth(), mActualHeight);
+        setClipBounds(mClipBounds);
+    }
+
+    private void sanitizeContractedLayoutParams(View contractedChild) {
+        LayoutParams lp = (LayoutParams) contractedChild.getLayoutParams();
+        lp.height = mSmallHeight;
+        contractedChild.setLayoutParams(lp);
+    }
+
+    private void selectLayout() {
+        if (mActualHeight <= mSmallHeight || mExpandedChild == null) {
+            if (mContractedChild.getVisibility() != View.VISIBLE) {
+                mContractedChild.setVisibility(View.VISIBLE);
+            }
+            if (mExpandedChild != null && mExpandedChild.getVisibility() != View.INVISIBLE) {
+                mExpandedChild.setVisibility(View.INVISIBLE);
+            }
+        } else {
+            if (mExpandedChild.getVisibility() != View.VISIBLE) {
+                mExpandedChild.setVisibility(View.VISIBLE);
+            }
+            if (mContractedChild.getVisibility() != View.INVISIBLE) {
+                mContractedChild.setVisibility(View.INVISIBLE);
+            }
+        }
+    }
+
+    public void notifyContentUpdated() {
+        selectLayout();
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationOverflowContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationOverflowContainer.java
index af91314..8ebd50d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationOverflowContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationOverflowContainer.java
@@ -35,6 +35,26 @@
     }
 
     @Override
+    public void setActualHeight(int currentHeight) {
+        // noop
+    }
+
+    @Override
+    public int getActualHeight() {
+        return getHeight();
+    }
+
+    @Override
+    public int getMaxHeight() {
+        return getHeight();
+    }
+
+    @Override
+    public void setClipTopAmount(int clipTopAmount) {
+        // noop
+    }
+
+    @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
         mIconsView = (NotificationOverflowIconsView) findViewById(R.id.overflow_icons_view);
@@ -43,6 +63,7 @@
         mActivator = new NotificationActivator(this);
         mActivator.setDimmed(true);
         setLocked(true);
+        setDimmed(true);
     }
 
     public NotificationOverflowIconsView getIconsView() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
index 5d75801..82fbb16 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
@@ -23,10 +23,12 @@
 import android.view.accessibility.AccessibilityEvent;
 
 import com.android.systemui.R;
+import com.android.systemui.statusbar.ExpandableView;
 import com.android.systemui.statusbar.GestureRecorder;
 import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
 
-public class NotificationPanelView extends PanelView {
+public class NotificationPanelView extends PanelView implements
+        ExpandableView.OnHeightChangedListener {
     public static final boolean DEBUG_GESTURES = true;
 
     PhoneStatusBar mStatusBar;
@@ -67,6 +69,7 @@
         mKeyguardStatusView = findViewById(R.id.keyguard_status_view);
         mNotificationStackScroller = (NotificationStackScrollLayout)
                 findViewById(R.id.notification_stack_scroller);
+        mNotificationStackScroller.setOnHeightChangedListener(this);
         mNotificationParent = findViewById(R.id.notification_container_parent);
     }
 
@@ -218,4 +221,9 @@
         super.onExpandingFinished();
         mNotificationStackScroller.onExpansionStopped();
     }
+
+    @Override
+    public void onHeightChanged(ExpandableView view) {
+        requestPanelHeightUpdate();
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
index 948ef90..d6d90a6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
@@ -37,6 +37,7 @@
 import com.android.systemui.R;
 import com.android.systemui.SwipeHelper;
 import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.ExpandableView;
 import com.android.systemui.statusbar.stack.StackScrollState.ViewState;
 import com.android.systemui.statusbar.policy.ScrollAdapter;
 
@@ -44,7 +45,8 @@
  * A layout which handles a dynamic amount of notifications and presents them in a scrollable stack.
  */
 public class NotificationStackScrollLayout extends ViewGroup
-        implements SwipeHelper.Callback, ExpandHelper.Callback, ScrollAdapter {
+        implements SwipeHelper.Callback, ExpandHelper.Callback, ScrollAdapter,
+        ExpandableView.OnHeightChangedListener {
 
     private static final String TAG = "NotificationStackScrollLayout";
     private static final boolean DEBUG = false;
@@ -78,6 +80,7 @@
     private int mBottomStackPeekSize;
     private int mEmptyMarginBottom;
     private int mPaddingBetweenElements;
+    private boolean mListenForHeightChanges = true;
 
     /**
      * The algorithm which calculates the properties for our children
@@ -90,6 +93,7 @@
     private final StackScrollState mCurrentStackScrollState = new StackScrollState(this);
 
     private OnChildLocationsChangedListener mListener;
+    private ExpandableView.OnHeightChangedListener mOnHeightChangedListener;
 
     public NotificationStackScrollLayout(Context context) {
         this(context, null);
@@ -227,7 +231,9 @@
         if (!isCurrentlyAnimating()) {
             mCurrentStackScrollState.setScrollY(mOwnScrollY);
             mStackScrollAlgorithm.getStackScrollState(mCurrentStackScrollState);
+            mListenForHeightChanges = false;
             mCurrentStackScrollState.apply();
+            mListenForHeightChanges = true;
             if (mListener != null) {
                 mListener.onChildLocationsChanged(this);
             }
@@ -306,12 +312,12 @@
         // find the view under the pointer, accounting for GONE views
         final int count = getChildCount();
         for (int childIdx = 0; childIdx < count; childIdx++) {
-            View slidingChild = getChildAt(childIdx);
+            ExpandableView slidingChild = (ExpandableView) getChildAt(childIdx);
             if (slidingChild.getVisibility() == GONE) {
                 continue;
             }
             float top = slidingChild.getTranslationY();
-            float bottom = top + slidingChild.getHeight();
+            float bottom = top + slidingChild.getActualHeight();
             int left = slidingChild.getLeft();
             int right = slidingChild.getRight();
 
@@ -615,16 +621,11 @@
 
     private int getScrollRange() {
         int scrollRange = 0;
-        View firstChild = getFirstChildNotGone();
+        ExpandableView firstChild = (ExpandableView) getFirstChildNotGone();
         if (firstChild != null) {
             int contentHeight = getContentHeight();
             int firstChildMaxExpandHeight = getMaxExpandHeight(firstChild);
-            int firstChildExpandPotential = firstChildMaxExpandHeight - firstChild.getHeight();
 
-            // If we already scrolled in, the first child is layouted smaller than it actually
-            // could be when expanded. We have to compensate for this loss of the contentHeight
-            // by adding the expand potential again.
-            contentHeight += firstChildExpandPotential;
             scrollRange = Math.max(0, contentHeight - mMaxLayoutHeight + mBottomStackPeekSize);
             if (scrollRange > 0 && getChildCount() > 0) {
                 // We want to at least be able collapse the first item and not ending in a weird
@@ -666,10 +667,17 @@
         for (int i = 0; i < getChildCount(); i++) {
             View child = getChildAt(i);
             if (child.getVisibility() != View.GONE) {
-                height += child.getHeight();
-                if (i < getChildCount()-1) {
+                if (height != 0) {
+                    // add the padding before this element
                     height += mPaddingBetweenElements;
                 }
+                if (child instanceof ExpandableNotificationRow) {
+                    ExpandableNotificationRow row = (ExpandableNotificationRow) child;
+                    height += row.getMaximumAllowedExpandHeight();
+                } else if (child instanceof ExpandableView) {
+                    ExpandableView expandableView = (ExpandableView) child;
+                    height += expandableView.getActualHeight();
+                }
             }
         }
         mContentHeight = height;
@@ -723,6 +731,7 @@
     @Override
     protected void onViewRemoved(View child) {
         super.onViewRemoved(child);
+        ((ExpandableView) child).setOnHeightChangedListener(null);
         mCurrentStackScrollState.removeViewStateForView(child);
         mStackScrollAlgorithm.notifyChildrenChanged(this);
     }
@@ -731,6 +740,7 @@
     protected void onViewAdded(View child) {
         super.onViewAdded(child);
         mStackScrollAlgorithm.notifyChildrenChanged(this);
+        ((ExpandableView) child).setOnHeightChangedListener(this);
     }
 
     private boolean onInterceptTouchEventScroll(MotionEvent ev) {
@@ -891,6 +901,23 @@
         }
     }
 
+    @Override
+    public void onHeightChanged(ExpandableView view) {
+        if (mListenForHeightChanges) {
+            updateContentHeight();
+            updateScrollPositionIfNecessary();
+            if (mOnHeightChangedListener != null) {
+                mOnHeightChangedListener.onHeightChanged(view);
+            }
+            updateChildren();
+        }
+    }
+
+    public void setOnHeightChangedListener(
+            ExpandableView.OnHeightChangedListener mOnHeightChangedListener) {
+        this.mOnHeightChangedListener = mOnHeightChangedListener;
+    }
+
     /**
      * A listener that is notified when some child locations might have changed.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java
index 2a6e4ae..acd1c6a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java
@@ -23,6 +23,7 @@
 
 import com.android.systemui.R;
 import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.ExpandableView;
 
 import java.util.ArrayList;
 
@@ -53,7 +54,7 @@
     private boolean mIsExpansionChanging;
     private int mFirstChildMaxHeight;
     private boolean mIsExpanded;
-    private View mFirstChildWhileExpanding;
+    private ExpandableView mFirstChildWhileExpanding;
     private boolean mExpandedOnStart;
     private int mTopStackTotalSize;
 
@@ -82,7 +83,7 @@
         mBottomStackIndentationFunctor = new PiecewiseLinearIndentationFunctor(
                 MAX_ITEMS_IN_BOTTOM_STACK,
                 mBottomStackPeekSize,
-                mCollapsedSize + mPaddingBetweenElements + mBottomStackPeekSize,
+                mCollapsedSize + mBottomStackPeekSize + mPaddingBetweenElements,
                 0.5f);
     }
 
@@ -101,11 +102,10 @@
         algorithmState.scrolledPixelsTop = 0;
         algorithmState.itemsInBottomStack = 0.0f;
         algorithmState.partialInBottom = 0.0f;
+        algorithmState.scrollY = resultState.getScrollY() + mCollapsedSize;
 
         updateVisibleChildren(resultState, algorithmState);
 
-        algorithmState.scrollY = getAlgorithmScrollPosition(resultState, algorithmState);
-
         // Phase 1:
         findNumberOfItemsInTopStackAndUpdateState(resultState, algorithmState);
 
@@ -117,42 +117,6 @@
     }
 
     /**
-     * Calculates the scroll offset of the algorithm, based on the resultState.
-     *
-     * @param resultState the state to base the calculation on
-     * @param algorithmState The state in which the current pass of the algorithm is currently in
-     * @return the scroll offset used for the algorithm
-     */
-    private int getAlgorithmScrollPosition(StackScrollState resultState,
-            StackScrollAlgorithmState algorithmState) {
-
-        int resultScroll = resultState.getScrollY() + mCollapsedSize;
-
-        // If the first child was collapsed in an earlier pass, we have to decrease the scroll
-        // position to get into the same state again.
-        if (algorithmState.visibleChildren.size() > 0) {
-            View firstView = algorithmState.visibleChildren.get(0);
-            if (firstView instanceof ExpandableNotificationRow) {
-                ExpandableNotificationRow firstRow = (ExpandableNotificationRow) firstView;
-                if (firstRow.isUserLocked()) {
-                    // User is currently modifying this height.
-                    return resultScroll;
-                }
-                int scrolledInAmount = 0;
-                // If the child size was not decreased due to scrolling, we don't substract it,
-                if (!mIsExpansionChanging) {
-                    scrolledInAmount = firstRow.getExpandPotential();
-                } else if (mExpandedOnStart && mFirstChildWhileExpanding == firstView) {
-                    scrolledInAmount = firstRow.getMaximumAllowedExpandHeight() -
-                            mFirstChildMaxHeight;
-                }
-                resultScroll -= scrolledInAmount;
-            }
-        }
-        return resultScroll;
-    }
-
-    /**
      * Update the visible children on the state.
      */
     private void updateVisibleChildren(StackScrollState resultState,
@@ -162,7 +126,7 @@
         state.visibleChildren.clear();
         state.visibleChildren.ensureCapacity(childCount);
         for (int i = 0; i < childCount; i++) {
-            View v = hostView.getChildAt(i);
+            ExpandableView v = (ExpandableView) hostView.getChildAt(i);
             if (v.getVisibility() != View.GONE) {
                 state.visibleChildren.add(v);
             }
@@ -194,10 +158,10 @@
         int childCount = algorithmState.visibleChildren.size();
         int numberOfElementsCompletelyIn = (int) algorithmState.itemsInTopStack;
         for (int i = 0; i < childCount; i++) {
-            View child = algorithmState.visibleChildren.get(i);
+            ExpandableView child = algorithmState.visibleChildren.get(i);
             StackScrollState.ViewState childViewState = resultState.getViewStateForView(child);
             childViewState.location = StackScrollState.ViewState.LOCATION_UNKNOWN;
-            int childHeight = child.getHeight();
+            int childHeight = getMaxAllowedChildHeight(child);
             float yPositionInScrollViewAfterElement = yPositionInScrollView
                     + childHeight
                     + mPaddingBetweenElements;
@@ -291,7 +255,7 @@
 
     /**
      * Clamp the yTranslation of the child up such that its end is at lest on the end of the top
-     * stack.
+     * stack.get
      *
      * @param childViewState the view state of the child
      * @param childHeight the height of this child
@@ -306,8 +270,11 @@
         if (child instanceof ExpandableNotificationRow) {
             ExpandableNotificationRow row = (ExpandableNotificationRow) child;
             return row.getMaximumAllowedExpandHeight();
+        } else if (child instanceof ExpandableView) {
+            ExpandableView expandableView = (ExpandableView) child;
+            return expandableView.getActualHeight();
         }
-        return child.getHeight();
+        return child == null? mCollapsedSize : child.getHeight();
     }
 
     private void updateStateForChildTransitioningInBottom(StackScrollAlgorithmState algorithmState,
@@ -419,9 +386,9 @@
 
         // find the number of elements in the top stack.
         for (int i = 0; i < childCount; i++) {
-            View child = algorithmState.visibleChildren.get(i);
+            ExpandableView child = algorithmState.visibleChildren.get(i);
             StackScrollState.ViewState childViewState = resultState.getViewStateForView(child);
-            int childHeight = child.getHeight();
+            int childHeight = getMaxAllowedChildHeight(child);
             float yPositionInScrollViewAfterElement = yPositionInScrollView
                     + childHeight
                     + mPaddingBetweenElements;
@@ -524,13 +491,13 @@
     }
 
     private void updateFirstChildHeightWhileExpanding(ViewGroup hostView) {
-        mFirstChildWhileExpanding = findFirstVisibleChild(hostView);
+        mFirstChildWhileExpanding = (ExpandableView) findFirstVisibleChild(hostView);
         if (mFirstChildWhileExpanding != null) {
             if (mExpandedOnStart) {
 
                 // We are collapsing the shade, so the first child can get as most as high as the
                 // current height.
-                mFirstChildMaxHeight = mFirstChildWhileExpanding.getHeight();
+                mFirstChildMaxHeight = mFirstChildWhileExpanding.getActualHeight();
             } else {
 
                 // We are expanding the shade, expand it to its full height.
@@ -627,7 +594,7 @@
         /**
          * The children from the host view which are not gone.
          */
-        public final ArrayList<View> visibleChildren = new ArrayList<View>();
+        public final ArrayList<ExpandableView> visibleChildren = new ArrayList<ExpandableView>();
     }
 
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollState.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollState.java
index 6e2e87e..9215110 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollState.java
@@ -22,7 +22,7 @@
 import android.view.View;
 import android.view.ViewGroup;
 
-import com.android.systemui.R;
+import com.android.systemui.statusbar.ExpandableView;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -36,12 +36,11 @@
     private static final String CHILD_NOT_FOUND_TAG = "StackScrollStateNoSuchChild";
 
     private final ViewGroup mHostView;
-    private Map<View, ViewState> mStateMap;
+    private Map<ExpandableView, ViewState> mStateMap;
     private int mScrollY;
     private final Rect mClipRect = new Rect();
     private int mBackgroundRoundedRectCornerRadius;
     private final Outline mChildOutline = new Outline();
-    private final int mChildDividerHeight;
 
     public int getScrollY() {
         return mScrollY;
@@ -53,11 +52,9 @@
 
     public StackScrollState(ViewGroup hostView) {
         mHostView = hostView;
-        mStateMap = new HashMap<View, ViewState>();
+        mStateMap = new HashMap<ExpandableView, ViewState>();
         mBackgroundRoundedRectCornerRadius = hostView.getResources().getDimensionPixelSize(
                 com.android.internal.R.dimen.notification_quantum_rounded_rect_radius);
-        mChildDividerHeight = hostView.getResources().getDimensionPixelSize(R.dimen
-                .notification_divider_height);
     }
 
     public ViewGroup getHostView() {
@@ -67,14 +64,14 @@
     public void resetViewStates() {
         int numChildren = mHostView.getChildCount();
         for (int i = 0; i < numChildren; i++) {
-            View child = mHostView.getChildAt(i);
+            ExpandableView child = (ExpandableView) mHostView.getChildAt(i);
             ViewState viewState = mStateMap.get(child);
             if (viewState == null) {
                 viewState = new ViewState();
                 mStateMap.put(child, viewState);
             }
             // initialize with the default values of the view
-            viewState.height = child.getHeight();
+            viewState.height = child.getActualHeight();
             viewState.alpha = 1;
             viewState.gone = child.getVisibility() == View.GONE;
         }
@@ -98,7 +95,7 @@
         float previousNotificationEnd = 0;
         float previousNotificationStart = 0;
         for (int i = 0; i < numChildren; i++) {
-            View child = mHostView.getChildAt(i);
+            ExpandableView child = (ExpandableView) mHostView.getChildAt(i);
             ViewState state = mStateMap.get(child);
             if (state == null) {
                 Log.wtf(CHILD_NOT_FOUND_TAG, "No child state was found when applying this state " +
@@ -109,7 +106,7 @@
                 float alpha = child.getAlpha();
                 float yTranslation = child.getTranslationY();
                 float zTranslation = child.getTranslationZ();
-                int height = child.getHeight();
+                int height = child.getActualHeight();
                 float newAlpha = state.alpha;
                 float newYTranslation = state.yTranslation;
                 float newZTranslation = state.zTranslation;
@@ -152,14 +149,14 @@
 
                 // apply height
                 if (height != newHeight) {
-                    applyNewHeight(child, newHeight);
+                    child.setActualHeight(newHeight);
                 }
 
                 // apply clipping and shadow
                 float newNotificationEnd = newYTranslation + newHeight;
-                updateChildClippingAndShadow(child, newHeight,
-                        newNotificationEnd - (previousNotificationEnd - mChildDividerHeight),
-                        newHeight - (previousNotificationStart - newYTranslation));
+                updateChildClippingAndBackground(child, newHeight,
+                        newNotificationEnd - (previousNotificationEnd),
+                        (int) (newHeight - (previousNotificationStart - newYTranslation)));
 
                 previousNotificationStart = newYTranslation;
                 previousNotificationEnd = newNotificationEnd;
@@ -173,20 +170,21 @@
      * @param child the view to update
      * @param realHeight the currently applied height of the view
      * @param clipHeight the desired clip height, the rest of the view will be clipped from the top
-     * @param shadowHeight the desired height of the shadow, the shadow ends on the bottom
+     * @param backgroundHeight the desired background height. The shadows of the view will be
+     *                         based on this height and the content will be clipped from the top
      */
-    private void updateChildClippingAndShadow(View child, int realHeight, float clipHeight,
-            float shadowHeight) {
-        if (realHeight > shadowHeight) {
-            updateChildOutline(child, realHeight, shadowHeight);
-        } else {
-            updateChildOutline(child, realHeight, realHeight);
-        }
+    private void updateChildClippingAndBackground(ExpandableView child, int realHeight,
+            float clipHeight, int backgroundHeight) {
         if (realHeight > clipHeight) {
             updateChildClip(child, realHeight, clipHeight);
         } else {
             child.setClipBounds(null);
         }
+        if (realHeight > backgroundHeight) {
+            child.setClipTopAmount(realHeight - backgroundHeight);
+        } else {
+            child.setClipTopAmount(0);
+        }
     }
 
     /**
@@ -205,37 +203,6 @@
         child.setClipBounds(mClipRect);
     }
 
-    /**
-     * Updates the outline of a view
-     *
-     * @param child the view to update
-     * @param height the currently applied height of the view
-     * @param outlineHeight the desired height of the outline, the outline ends on the bottom
-     */
-    private void updateChildOutline(View child, int height,
-        float outlineHeight) {
-        int shadowInset = (int) (height - outlineHeight);
-        getOutlineForSize(child.getLeft(),
-                child.getTop() + shadowInset,
-                child.getWidth(),
-                child.getHeight() - shadowInset,
-                mChildOutline);
-        child.setOutline(mChildOutline);
-    }
-
-    private void getOutlineForSize(int leftInset, int topInset, int width, int height,
-            Outline result) {
-        result.setRoundRect(leftInset, topInset, leftInset + width, topInset + height,
-                mBackgroundRoundedRectCornerRadius);
-    }
-
-    private void applyNewHeight(View child, int newHeight) {
-        ViewGroup.LayoutParams lp = child.getLayoutParams();
-        lp.height = newHeight;
-        child.setLayoutParams(lp);
-    }
-
-
     public static class ViewState {
 
         // These are flags such that we can create masks for filtering.