OSDN Git Service

Refactored the layout of a notifications
authorJorim Jaggi <jjaggi@google.com>
Mon, 28 Apr 2014 15:51:23 +0000 (17:51 +0200)
committerSelim Cinek <cinek@google.com>
Tue, 29 Apr 2014 20:42:55 +0000 (22:42 +0200)
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

14 files changed:
packages/SystemUI/res/layout/status_bar_notification_keyguard_overflow.xml
packages/SystemUI/res/layout/status_bar_notification_row.xml
packages/SystemUI/src/com/android/systemui/ExpandHelper.java
packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java
packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java
packages/SystemUI/src/com/android/systemui/statusbar/ExpandableOutlineView.java [new file with mode: 0644]
packages/SystemUI/src/com/android/systemui/statusbar/ExpandableView.java [new file with mode: 0644]
packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java [new file with mode: 0644]
packages/SystemUI/src/com/android/systemui/statusbar/NotificationOverflowContainer.java
packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java
packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollState.java

index d81e525..2e08bff 100644 (file)
 <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"
index 41e7dac..8959a5b 100644 (file)
@@ -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" />
 
index c585a5b..61c268e 100644 (file)
@@ -31,8 +31,9 @@ import android.view.ScaleGestureDetector.OnScaleGestureListener;
 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 @@ public class ExpandHelper implements Gefingerpoken, OnClickListener {
             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 @@ public class ExpandHelper implements Gefingerpoken, OnClickListener {
     };
 
     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 @@ public class ExpandHelper implements Gefingerpoken, OnClickListener {
         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 @@ public class ExpandHelper implements Gefingerpoken, OnClickListener {
                 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 @@ public class ExpandHelper implements Gefingerpoken, OnClickListener {
                         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 @@ public class ExpandHelper implements Gefingerpoken, OnClickListener {
         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 @@ public class ExpandHelper implements Gefingerpoken, OnClickListener {
         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 @@ public class ExpandHelper implements Gefingerpoken, OnClickListener {
         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);
index d647dfa..0f32dc0 100644 (file)
@@ -29,7 +29,7 @@ import com.android.internal.R;
  * 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 class ActivatableNotificationView extends FrameLayout {
     public ActivatableNotificationView(Context context, AttributeSet attrs) {
         super(context, attrs);
         mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+        updateBackgroundResource();
     }
 
 
@@ -84,6 +85,9 @@ public class ActivatableNotificationView extends FrameLayout {
             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 @@ public class ActivatableNotificationView extends FrameLayout {
     }
 
     private void makeActive(float x, float y) {
-        getBackground().setHotspot(0, x, y);
+        mCustomBackground.setHotspot(0, x, y);
         mActivated = true;
     }
 
@@ -132,8 +136,8 @@ public class ActivatableNotificationView extends FrameLayout {
     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 @@ public class ActivatableNotificationView extends FrameLayout {
     }
 
     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) {
index 3e21640..90b63f4 100644 (file)
@@ -760,9 +760,10 @@ public abstract class BaseStatusBar extends SystemUI implements
         // 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 @@ public abstract class BaseStatusBar extends SystemUI implements
 
         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 @@ public abstract class BaseStatusBar extends SystemUI implements
 
                 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 @@ public abstract class BaseStatusBar extends SystemUI implements
         } else {
             entry.row.setOnClickListener(null);
         }
+        entry.row.notifyContentUpdated();
     }
 
     protected void notifyHeadsUpScreenOn(boolean screenOn) {
index 35c02eb..b813e65 100644 (file)
 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 @@ public class ExpandableNotificationRow extends ActivatableNotificationView {
      * 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 @@ public class ExpandableNotificationRow extends ActivatableNotificationView {
     @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 class ExpandableNotificationRow extends ActivatableNotificationView {
     public void setHeightRange(int rowMinHeight, int rowMaxHeight) {
         mRowMinHeight = rowMinHeight;
         mRowMaxHeight = rowMaxHeight;
-        mMaxHeightNeedsUpdate = true;
     }
 
     public boolean isExpandable() {
@@ -145,13 +142,11 @@ public class ExpandableNotificationRow extends ActivatableNotificationView {
      * @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 @@ public class ExpandableNotificationRow extends ActivatableNotificationView {
      * @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 @@ public class ExpandableNotificationRow extends ActivatableNotificationView {
         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 @@ public class ExpandableNotificationRow extends ActivatableNotificationView {
     @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 class ExpandableNotificationRow extends ActivatableNotificationView {
     }
 
     public int getMaxExpandHeight() {
-        if (mMaxHeightNeedsUpdate) {
-            updateMaxExpandHeight();
-            mMaxHeightNeedsUpdate = false;
-        }
         return mMaxExpandHeight;
     }
 
@@ -248,6 +221,28 @@ public class ExpandableNotificationRow extends ActivatableNotificationView {
      * @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 (file)
index 0000000..43eb5b5
--- /dev/null
@@ -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 (file)
index 0000000..35913fa
--- /dev/null
@@ -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 (file)
index 0000000..fd0cb08
--- /dev/null
@@ -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();
+    }
+}
index af91314..8ebd50d 100644 (file)
@@ -35,6 +35,26 @@ public class NotificationOverflowContainer extends ActivatableNotificationView {
     }
 
     @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 @@ public class NotificationOverflowContainer extends ActivatableNotificationView {
         mActivator = new NotificationActivator(this);
         mActivator.setDimmed(true);
         setLocked(true);
+        setDimmed(true);
     }
 
     public NotificationOverflowIconsView getIconsView() {
index 5d75801..82fbb16 100644 (file)
@@ -23,10 +23,12 @@ import android.view.View;
 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 @@ public class NotificationPanelView extends PanelView {
         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 @@ public class NotificationPanelView extends PanelView {
         super.onExpandingFinished();
         mNotificationStackScroller.onExpansionStopped();
     }
+
+    @Override
+    public void onHeightChanged(ExpandableView view) {
+        requestPanelHeightUpdate();
+    }
 }
index 948ef90..d6d90a6 100644 (file)
@@ -37,6 +37,7 @@ import com.android.systemui.ExpandHelper;
 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 @@ import com.android.systemui.statusbar.policy.ScrollAdapter;
  * 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 @@ public class NotificationStackScrollLayout extends ViewGroup
     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 @@ public class NotificationStackScrollLayout extends ViewGroup
     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 @@ public class NotificationStackScrollLayout extends ViewGroup
         if (!isCurrentlyAnimating()) {
             mCurrentStackScrollState.setScrollY(mOwnScrollY);
             mStackScrollAlgorithm.getStackScrollState(mCurrentStackScrollState);
+            mListenForHeightChanges = false;
             mCurrentStackScrollState.apply();
+            mListenForHeightChanges = true;
             if (mListener != null) {
                 mListener.onChildLocationsChanged(this);
             }
@@ -306,12 +312,12 @@ public class NotificationStackScrollLayout extends ViewGroup
         // 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 @@ public class NotificationStackScrollLayout extends ViewGroup
 
     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 @@ public class NotificationStackScrollLayout extends ViewGroup
         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 @@ public class NotificationStackScrollLayout extends ViewGroup
     @Override
     protected void onViewRemoved(View child) {
         super.onViewRemoved(child);
+        ((ExpandableView) child).setOnHeightChangedListener(null);
         mCurrentStackScrollState.removeViewStateForView(child);
         mStackScrollAlgorithm.notifyChildrenChanged(this);
     }
@@ -731,6 +740,7 @@ public class NotificationStackScrollLayout extends ViewGroup
     protected void onViewAdded(View child) {
         super.onViewAdded(child);
         mStackScrollAlgorithm.notifyChildrenChanged(this);
+        ((ExpandableView) child).setOnHeightChangedListener(this);
     }
 
     private boolean onInterceptTouchEventScroll(MotionEvent ev) {
@@ -891,6 +901,23 @@ public class NotificationStackScrollLayout extends ViewGroup
         }
     }
 
+    @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.
      */
index 2a6e4ae..acd1c6a 100644 (file)
@@ -23,6 +23,7 @@ import android.view.ViewGroup;
 
 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 @@ public class StackScrollAlgorithm {
     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 @@ public class StackScrollAlgorithm {
         mBottomStackIndentationFunctor = new PiecewiseLinearIndentationFunctor(
                 MAX_ITEMS_IN_BOTTOM_STACK,
                 mBottomStackPeekSize,
-                mCollapsedSize + mPaddingBetweenElements + mBottomStackPeekSize,
+                mCollapsedSize + mBottomStackPeekSize + mPaddingBetweenElements,
                 0.5f);
     }
 
@@ -101,11 +102,10 @@ public class StackScrollAlgorithm {
         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 @@ public class StackScrollAlgorithm {
     }
 
     /**
-     * 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 @@ public class StackScrollAlgorithm {
         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 @@ public class StackScrollAlgorithm {
         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 @@ public class StackScrollAlgorithm {
 
     /**
      * 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 @@ public class StackScrollAlgorithm {
         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 @@ public class StackScrollAlgorithm {
 
         // 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 @@ public class StackScrollAlgorithm {
     }
 
     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 @@ public class StackScrollAlgorithm {
         /**
          * 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>();
     }
 
 }
index 6e2e87e..9215110 100644 (file)
@@ -22,7 +22,7 @@ import android.util.Log;
 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 @@ public class StackScrollState {
     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 class StackScrollState {
 
     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 class StackScrollState {
     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 @@ public class StackScrollState {
         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 @@ public class StackScrollState {
                 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 @@ public class StackScrollState {
 
                 // 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 @@ public class StackScrollState {
      * @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 @@ public class StackScrollState {
         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.