Merge "Initial implementation of NotificationStackScroller"
diff --git a/core/res/res/drawable/notification_quantum_background.xml b/core/res/res/drawable/notification_quantum_background.xml
index f33e2e3..7b508a9 100644
--- a/core/res/res/drawable/notification_quantum_background.xml
+++ b/core/res/res/drawable/notification_quantum_background.xml
@@ -17,5 +17,5 @@
 
 <shape xmlns:android="http://schemas.android.com/apk/res/android">
     <solid android:color="#ffffffff" />
-    <corners android:radius="2dp" />
+    <corners android:radius="@dimen/notification_quantum_rounded_rect_radius" />
 </shape>
\ No newline at end of file
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index 8ec2e6f..94123a2 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -366,4 +366,7 @@
     <!-- width of ImmersiveModeConfirmation (-1 for match_parent) -->
     <dimen name="immersive_mode_cling_width">-1px</dimen>
 
+    <!-- radius of the corners of the quantum rounded rect background -->
+    <dimen name="notification_quantum_rounded_rect_radius">2dp</dimen>
+
 </resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index db5f66e..fa84750 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -346,6 +346,7 @@
   <java-symbol type="dimen" name="notification_title_text_size" />
   <java-symbol type="dimen" name="notification_subtext_size" />
   <java-symbol type="dimen" name="immersive_mode_cling_width" />
+  <java-symbol type="dimen" name="notification_quantum_rounded_rect_radius" />
 
   <java-symbol type="string" name="add_account_button_label" />
   <java-symbol type="string" name="addToDictionary" />
diff --git a/packages/SystemUI/res/layout/status_bar_expanded.xml b/packages/SystemUI/res/layout/status_bar_expanded.xml
index 1693e01..1da66bb 100644
--- a/packages/SystemUI/res/layout/status_bar_expanded.xml
+++ b/packages/SystemUI/res/layout/status_bar_expanded.xml
@@ -92,6 +92,12 @@
                     systemui:rowHeight="@dimen/notification_row_min_height"
                     />
             </ScrollView>
+
+            <com.android.systemui.statusbar.stack.NotificationStackScrollLayout
+                    android:id="@+id/notification_stack_scroller"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    />
         </FrameLayout>
     </LinearLayout>
 </com.android.systemui.statusbar.phone.NotificationPanelView><!-- end of sliding panel -->
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index e67e8cf..a9b69854 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -233,6 +233,21 @@
     <!-- The size of the icon in the recents task view. -->
     <dimen name="recents_task_view_icon_size">60dp</dimen>
 
+    <!-- Space below the notification stack -->
+    <dimen name="notification_stack_margin_bottom">0dp</dimen>
+
+    <!-- Space reserved for the cards behind the top card in the top stack -->
+    <dimen name="top_stack_peek_amount">24dp</dimen>
+
+    <!-- Space reserved for the cards behind the top card in the bottom stack -->
+    <dimen name="bottom_stack_peek_amount">18dp</dimen>
+
+    <!-- The side padding of the notifications-->
+    <dimen name="notification_side_padding">8dp</dimen>
+
+    <!-- Z distance between notifications if they are in the stack -->
+    <dimen name="z_distance_between_notifications">2dp</dimen>
+
     <!-- Width of the zen mode interstitial dialog.  Defaults to MATCH_PARENT. -->
     <dimen name="zen_mode_dialog_width">-1px</dimen>
 </resources>
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
index e1a674a..bad5641 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
@@ -41,6 +41,7 @@
 import android.os.PowerManager;
 import android.os.RemoteException;
 import android.os.ServiceManager;
+import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.Settings;
@@ -77,7 +78,6 @@
 import com.android.systemui.SearchPanelView;
 import com.android.systemui.SystemUI;
 import com.android.systemui.statusbar.phone.KeyguardTouchDelegate;
-import com.android.systemui.statusbar.policy.NotificationRowLayout;
 
 import java.util.ArrayList;
 import java.util.Locale;
@@ -98,6 +98,8 @@
     protected static final int MSG_HIDE_HEADS_UP = 1027;
     protected static final int MSG_ESCALATE_HEADS_UP = 1028;
 
+    public static final boolean ENABLE_NOTIFICATION_STACK = SystemProperties
+            .getBoolean("persist.notifications.use_stack", false);
     protected static final boolean ENABLE_HEADS_UP = true;
     // scores above this threshold should be displayed in heads up mode.
     protected static final int INTERRUPTION_THRESHOLD = 10;
@@ -118,7 +120,7 @@
 
     // all notifications
     protected NotificationData mNotificationData = new NotificationData();
-    protected NotificationRowLayout mPile;
+    protected ViewGroup mPile;
 
     protected NotificationData.Entry mInterruptingNotificationEntry;
     protected long mInterruptingNotificationTime;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
index 650c557..160f2be 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
@@ -87,6 +87,7 @@
 import com.android.systemui.DemoMode;
 import com.android.systemui.EventLogTags;
 import com.android.systemui.R;
+import com.android.systemui.SwipeHelper;
 import com.android.systemui.statusbar.BaseStatusBar;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.GestureRecorder;
@@ -94,6 +95,7 @@
 import com.android.systemui.statusbar.NotificationData.Entry;
 import com.android.systemui.statusbar.SignalClusterView;
 import com.android.systemui.statusbar.StatusBarIconView;
+
 import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.statusbar.policy.BluetoothController;
 import com.android.systemui.statusbar.policy.DateView;
@@ -104,6 +106,8 @@
 import com.android.systemui.statusbar.policy.OnSizeChangedListener;
 import com.android.systemui.statusbar.policy.RotationLockController;
 
+import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
+
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -479,11 +483,25 @@
         mStatusBarContents = (LinearLayout)mStatusBarView.findViewById(R.id.status_bar_contents);
         mTickerView = mStatusBarView.findViewById(R.id.ticker);
 
-        mPile = (NotificationRowLayout)mStatusBarWindow.findViewById(R.id.latestItems);
-        mPile.setLayoutTransitionsEnabled(false);
-        mPile.setLongPressListener(getNotificationLongClicker());
+        NotificationRowLayout rowLayout
+                = (NotificationRowLayout) mStatusBarWindow.findViewById(R.id.latestItems);
+        NotificationStackScrollLayout notificationStack
+                = (NotificationStackScrollLayout) mStatusBarWindow
+                .findViewById(R.id.notification_stack_scroller);
+        if (ENABLE_NOTIFICATION_STACK) {
+            notificationStack.setLongPressListener(getNotificationLongClicker());
+            mPile = notificationStack;
+        } else {
+            rowLayout.setLayoutTransitionsEnabled(false);
+            rowLayout.setLongPressListener(getNotificationLongClicker());
+            mPile = rowLayout;
+            notificationStack.setVisibility(View.GONE);
+        }
+
         mExpandedContents = mPile; // was: expanded.findViewById(R.id.notificationLinearLayout);
 
+
+
         mNotificationPanelHeader = mStatusBarWindow.findViewById(R.id.header);
 
         mClearButton = mStatusBarWindow.findViewById(R.id.clear_all_button);
@@ -597,7 +615,8 @@
             }
 
             // set up the dynamic hide/show of the label
-            mPile.setOnSizeChangedListener(new OnSizeChangedListener() {
+            if(!ENABLE_NOTIFICATION_STACK)
+                ((NotificationRowLayout) mPile).setOnSizeChangedListener(new OnSizeChangedListener() {
                 @Override
                 public void onSizeChanged(View view, int w, int h, int oldw, int oldh) {
                     updateCarrierLabelVisibility(false);
@@ -1457,7 +1476,9 @@
         }
 
         mExpandedVisible = true;
-        mPile.setLayoutTransitionsEnabled(true);
+        if(!ENABLE_NOTIFICATION_STACK) {
+            ((NotificationRowLayout) mPile).setLayoutTransitionsEnabled(true);
+        }
         if (mNavigationBarView != null)
             mNavigationBarView.setSlippery(true);
 
@@ -1749,7 +1770,9 @@
         }
 
         mExpandedVisible = false;
-        mPile.setLayoutTransitionsEnabled(false);
+        if(!ENABLE_NOTIFICATION_STACK) {
+            ((NotificationRowLayout) mPile).setLayoutTransitionsEnabled(false);
+        }
         if (mNavigationBarView != null)
             mNavigationBarView.setSlippery(false);
         visibilityChanged(false);
@@ -2406,7 +2429,7 @@
                 final ArrayList<View> snapshot = new ArrayList<View>(numChildren);
                 for (int i=0; i<numChildren; i++) {
                     final View child = mPile.getChildAt(i);
-                    if (mPile.canChildBeDismissed(child) && child.getBottom() > scrollTop &&
+                    if (((SwipeHelper.Callback) mPile).canChildBeDismissed(child) && child.getBottom() > scrollTop &&
                             child.getTop() < scrollBottom) {
                         snapshot.add(child);
                     }
@@ -2424,10 +2447,13 @@
                         int currentDelay = 140;
                         int totalDelay = 0;
 
-                        // Set the shade-animating state to avoid doing other work during
-                        // all of these animations. In particular, avoid layout and
-                        // redrawing when collapsing the shade.
-                        mPile.setViewRemoval(false);
+
+                        if(!ENABLE_NOTIFICATION_STACK) {
+                            // Set the shade-animating state to avoid doing other work during
+                            // all of these animations. In particular, avoid layout and
+                            // redrawing when collapsing the shade.
+                            ((NotificationRowLayout) mPile).setViewRemoval(false);
+                        }
 
                         mPostCollapseCleanup = new Runnable() {
                             @Override
@@ -2436,7 +2462,9 @@
                                     Log.v(TAG, "running post-collapse cleanup");
                                 }
                                 try {
-                                    mPile.setViewRemoval(true);
+                                    if (!ENABLE_NOTIFICATION_STACK) {
+                                        ((NotificationRowLayout) mPile).setViewRemoval(true);
+                                    }
                                     mBarService.onClearAllNotifications(mCurrentUserId);
                                 } catch (Exception ex) { }
                             }
@@ -2450,7 +2478,13 @@
                             mHandler.postDelayed(new Runnable() {
                                 @Override
                                 public void run() {
-                                    mPile.dismissRowAnimated(_v, velocity);
+                                    if (!ENABLE_NOTIFICATION_STACK) {
+                                        ((NotificationRowLayout) mPile).dismissRowAnimated(
+                                                _v, velocity);
+                                    } else {
+                                        ((NotificationStackScrollLayout) mPile).dismissRowAnimated(
+                                                _v, velocity);
+                                    }
                                 }
                             }, totalDelay);
                             currentDelay = Math.max(50, currentDelay - ROW_DELAY_DECREMENT);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowView.java
index 7b03195..925e0d8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowView.java
@@ -24,6 +24,7 @@
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.View;
+import android.view.ViewGroup;
 import android.view.ViewRootImpl;
 import android.widget.FrameLayout;
 import android.widget.ScrollView;
@@ -31,7 +32,6 @@
 import com.android.systemui.ExpandHelper;
 import com.android.systemui.R;
 import com.android.systemui.statusbar.BaseStatusBar;
-import com.android.systemui.statusbar.policy.NotificationRowLayout;
 
 
 public class StatusBarWindowView extends FrameLayout
@@ -40,7 +40,7 @@
     public static final boolean DEBUG = BaseStatusBar.DEBUG;
 
     private ExpandHelper mExpandHelper;
-    private NotificationRowLayout latestItems;
+    private ViewGroup latestItems;
     private NotificationPanelView mNotificationPanel;
     private ScrollView mScrollView;
 
@@ -55,12 +55,18 @@
     @Override
     protected void onAttachedToWindow () {
         super.onAttachedToWindow();
-        latestItems = (NotificationRowLayout) findViewById(R.id.latestItems);
+
+        if (BaseStatusBar.ENABLE_NOTIFICATION_STACK) {
+            latestItems = (ViewGroup) findViewById(R.id.notification_stack_scroller);
+        } else {
+            latestItems = (ViewGroup) findViewById(R.id.latestItems);
+        }
         mScrollView = (ScrollView) findViewById(R.id.scroll);
         mNotificationPanel = (NotificationPanelView) findViewById(R.id.notification_panel);
         int minHeight = getResources().getDimensionPixelSize(R.dimen.notification_row_min_height);
         int maxHeight = getResources().getDimensionPixelSize(R.dimen.notification_row_max_height);
-        mExpandHelper = new ExpandHelper(getContext(), latestItems, minHeight, maxHeight);
+        mExpandHelper = new ExpandHelper(getContext(), (ExpandHelper.Callback) latestItems,
+                minHeight, maxHeight);
         mExpandHelper.setEventSource(this);
         mExpandHelper.setScrollView(mScrollView);
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
new file mode 100644
index 0000000..edac3a7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
@@ -0,0 +1,816 @@
+/*
+ * 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.stack;
+
+import android.content.Context;
+import android.content.res.Configuration;
+
+import android.graphics.Canvas;
+import android.graphics.Outline;
+import android.graphics.Paint;
+import android.util.AttributeSet;
+import android.util.Log;
+
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.widget.OverScroller;
+
+import com.android.systemui.ExpandHelper;
+import com.android.systemui.R;
+import com.android.systemui.SwipeHelper;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+
+/**
+ * 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 {
+
+    private static final String TAG = "NotificationStackScrollLayout";
+    private static final boolean DEBUG = false;
+
+    /**
+     * Sentinel value for no current active pointer. Used by {@link #mActivePointerId}.
+     */
+    private static final int INVALID_POINTER = -1;
+
+    private SwipeHelper mSwipeHelper;
+    private boolean mAllowScrolling = true;
+    private int mCurrentStackHeight = Integer.MAX_VALUE;
+    private int mOwnScrollY;
+    private int mMaxLayoutHeight;
+
+    private VelocityTracker mVelocityTracker;
+    private OverScroller mScroller;
+    private int mTouchSlop;
+    private int mMinimumVelocity;
+    private int mMaximumVelocity;
+    private int mOverscrollDistance;
+    private int mOverflingDistance;
+    private boolean mIsBeingDragged;
+    private int mLastMotionY;
+    private int mActivePointerId;
+
+    private int mSidePaddings;
+    private Paint mDebugPaint;
+    private int mBackgroundRoundedRectCornerRadius;
+    private int mContentHeight;
+    private int mCollapsedSize;
+    private int mBottomStackPeekSize;
+    private int mEmptyMarginBottom;
+    private int mPaddingBetweenElements;
+
+    /**
+     * The algorithm which calculates the properties for our children
+     */
+    private StackScrollAlgorithm mStackScrollAlgorithm;
+
+    /**
+     * The current State this Layout is in
+     */
+    private StackScrollState mCurrentStackScrollState;
+
+    public NotificationStackScrollLayout(Context context) {
+        this(context, null);
+    }
+
+    public NotificationStackScrollLayout(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public NotificationStackScrollLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public NotificationStackScrollLayout(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        initView(context);
+        if (DEBUG) {
+            setWillNotDraw(false);
+            mDebugPaint = new Paint();
+            mDebugPaint.setColor(0xffff0000);
+            mDebugPaint.setStrokeWidth(2);
+            mDebugPaint.setStyle(Paint.Style.STROKE);
+        }
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        if (DEBUG) {
+            int y = mCollapsedSize;
+            canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
+            y = (int) (getLayoutHeight() - mBottomStackPeekSize - mCollapsedSize);
+            canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
+            y = (int) getLayoutHeight();
+            canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
+        }
+    }
+
+    private void initView(Context context) {
+        mScroller = new OverScroller(getContext());
+        setFocusable(true);
+        setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
+        final ViewConfiguration configuration = ViewConfiguration.get(context);
+        mTouchSlop = configuration.getScaledTouchSlop();
+        mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
+        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
+        mOverscrollDistance = configuration.getScaledOverscrollDistance();
+        mOverflingDistance = configuration.getScaledOverflingDistance();
+        float densityScale = getResources().getDisplayMetrics().density;
+        float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop();
+        mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, densityScale, pagingTouchSlop);
+
+        mSidePaddings = context.getResources()
+                .getDimensionPixelSize(R.dimen.notification_side_padding);
+        mBackgroundRoundedRectCornerRadius = context.getResources()
+                .getDimensionPixelSize(
+                        com.android.internal.R.dimen.notification_quantum_rounded_rect_radius);
+        mCollapsedSize = context.getResources()
+                .getDimensionPixelSize(R.dimen.notification_row_min_height);
+        mBottomStackPeekSize = context.getResources()
+                .getDimensionPixelSize(R.dimen.bottom_stack_peek_amount);
+        mEmptyMarginBottom = context.getResources().getDimensionPixelSize(
+                R.dimen.notification_stack_margin_bottom);
+        // currently the padding is in the elements themself
+        mPaddingBetweenElements = 0;
+        mStackScrollAlgorithm = new StackScrollAlgorithm(context);
+        mCurrentStackScrollState = null;
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        int mode = MeasureSpec.getMode(widthMeasureSpec);
+        int size = MeasureSpec.getSize(widthMeasureSpec);
+        int childMeasureSpec = MeasureSpec.makeMeasureSpec(size - 2 * mSidePaddings, mode);
+        measureChildren(childMeasureSpec, heightMeasureSpec);
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+
+        // we layout all our children centered on the top
+        float centerX = getWidth() / 2.0f;
+        for (int i = 0; i < getChildCount(); i++) {
+            View child = getChildAt(i);
+            float width = child.getMeasuredWidth();
+            float height = child.getMeasuredHeight();
+            int oldWidth = child.getWidth();
+            int oldHeight = child.getHeight();
+            child.layout((int) (centerX - width / 2.0f),
+                    0,
+                    (int) (centerX + width / 2.0f),
+                    (int) height);
+            updateChildOutline(child, width, height, oldWidth, oldHeight);
+        }
+        setMaxLayoutHeight(getHeight() - mEmptyMarginBottom);
+        updateScrollPositionIfNecessary();
+        updateChildren();
+        updateContentHeight();
+    }
+
+    private void setMaxLayoutHeight(int maxLayoutHeight) {
+        mMaxLayoutHeight = maxLayoutHeight;
+        updateAlgorithmHeight();
+    }
+
+    private void updateAlgorithmHeight() {
+        mStackScrollAlgorithm.setLayoutHeight(getLayoutHeight());
+    }
+
+    /**
+     * Updates the children views according to the stack scroll algorithm. Call this whenever
+     * modifications to {@link #mOwnScrollY} are performed to reflect it in the view layout.
+     */
+    private void updateChildren() {
+        if (!isCurrentlyAnimating()) {
+            if (mCurrentStackScrollState == null) {
+                mCurrentStackScrollState = new StackScrollState(this);
+            }
+            mCurrentStackScrollState.setScrollY(mOwnScrollY);
+            mStackScrollAlgorithm.getStackScrollState(mCurrentStackScrollState);
+            mCurrentStackScrollState.apply();
+            mOwnScrollY = mCurrentStackScrollState.getScrollY();
+        } else {
+            // TODO: handle animation
+        }
+    }
+
+    private boolean isCurrentlyAnimating() {
+        return false;
+    }
+
+    private void updateChildOutline(View child,
+                                    float width,
+                                    float height,
+                                    int oldWidth,
+                                    int oldHeight) {
+        // The children currently have paddings inside themselfs because of the expansion
+        // visualization. In order for the shadows to work correctly we have to set the correct
+        // outline.
+        View container = child.findViewById(R.id.container);
+        if (container != null && (oldWidth != width || oldHeight != height)) {
+            Outline outline = getOutlineForSize(container.getLeft(),
+                    container.getTop(),
+                    container.getWidth(),
+                    container.getHeight());
+            child.setOutline(outline);
+        }
+    }
+
+    private Outline getOutlineForSize(int leftInset, int topInset, int width, int height) {
+        Outline result = new Outline();
+        result.setRoundRect(leftInset, topInset, leftInset + width, topInset + height,
+                mBackgroundRoundedRectCornerRadius);
+        return result;
+    }
+
+    private void updateScrollPositionIfNecessary() {
+        int scrollRange = getScrollRange();
+        if (scrollRange < mOwnScrollY) {
+            mOwnScrollY = scrollRange;
+        }
+    }
+
+    public void setCurrentStackHeight(int currentStackHeight) {
+        this.mCurrentStackHeight = currentStackHeight;
+        updateAlgorithmHeight();
+        updateChildren();
+    }
+
+    /**
+     * Get the current height of the view. This is at most the size of the view given by a the
+     * layout but it can also be made smaller by setting {@link #mCurrentStackHeight}
+     *
+     * @return either the layout height or the externally defined height, whichever is smaller
+     */
+    private float getLayoutHeight() {
+        return Math.min(mMaxLayoutHeight, mCurrentStackHeight);
+    }
+
+    public void setLongPressListener(View.OnLongClickListener listener) {
+        mSwipeHelper.setLongPressListener(listener);
+    }
+
+    public void onChildDismissed(View v) {
+        if (DEBUG) Log.v(TAG, "onChildDismissed: " + v);
+        final View veto = v.findViewById(R.id.veto);
+        if (veto != null && veto.getVisibility() != View.GONE) {
+            veto.performClick();
+        }
+        allowScrolling(true);
+    }
+
+    public void onBeginDrag(View v) {
+        allowScrolling(false);
+    }
+
+    public void onDragCancelled(View v) {
+        allowScrolling(true);
+    }
+
+    public View getChildAtPosition(MotionEvent ev) {
+        return getChildAtPosition(ev.getX(), ev.getY());
+    }
+
+    public View getChildAtRawPosition(float touchX, float touchY) {
+        int[] location = new int[2];
+        getLocationOnScreen(location);
+        return getChildAtPosition(touchX - location[0],touchY - location[1]);
+    }
+
+    public View getChildAtPosition(float touchX, float touchY) {
+        // 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);
+            if (slidingChild.getVisibility() == GONE) {
+                continue;
+            }
+            float top = slidingChild.getTranslationY();
+            float bottom = top + slidingChild.getMeasuredHeight();
+            int left = slidingChild.getLeft();
+            int right = slidingChild.getRight();
+
+            if (touchY >= top && touchY <= bottom && touchX >= left && touchX <= right) {
+                return slidingChild;
+            }
+        }
+        return null;
+    }
+
+    public boolean canChildBeExpanded(View v) {
+        return v instanceof ExpandableNotificationRow
+                && ((ExpandableNotificationRow) v).isExpandable();
+    }
+
+    public void setUserExpandedChild(View v, boolean userExpanded) {
+        if (v instanceof ExpandableNotificationRow) {
+            ((ExpandableNotificationRow) v).setUserExpanded(userExpanded);
+        }
+    }
+
+    public void setUserLockedChild(View v, boolean userLocked) {
+        if (v instanceof ExpandableNotificationRow) {
+            ((ExpandableNotificationRow) v).setUserLocked(userLocked);
+        }
+    }
+
+    public View getChildContentView(View v) {
+        return v;
+    }
+
+    public boolean canChildBeDismissed(View v) {
+        final View veto = v.findViewById(R.id.veto);
+        return (veto != null && veto.getVisibility() != View.GONE);
+    }
+
+    private void allowScrolling(boolean allow) {
+        mAllowScrolling = allow;
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        float densityScale = getResources().getDisplayMetrics().density;
+        mSwipeHelper.setDensityScale(densityScale);
+        float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop();
+        mSwipeHelper.setPagingTouchSlop(pagingTouchSlop);
+        initView(getContext());
+    }
+
+    public void dismissRowAnimated(View child, int vel) {
+        mSwipeHelper.dismissChild(child, vel);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        boolean scrollerWantsIt = false;
+        if (mAllowScrolling) {
+            scrollerWantsIt = onScrollTouch(ev);
+        }
+        boolean horizontalSwipeWantsIt = false;
+        if (!mIsBeingDragged) {
+            horizontalSwipeWantsIt = mSwipeHelper.onTouchEvent(ev);
+        }
+        return horizontalSwipeWantsIt || scrollerWantsIt || super.onTouchEvent(ev);
+    }
+
+    private boolean onScrollTouch(MotionEvent ev) {
+        initVelocityTrackerIfNotExists();
+        mVelocityTracker.addMovement(ev);
+
+        final int action = ev.getAction();
+
+        switch (action & MotionEvent.ACTION_MASK) {
+            case MotionEvent.ACTION_DOWN: {
+                if (getChildCount() == 0) {
+                    return false;
+                }
+                boolean isBeingDragged = !mScroller.isFinished();
+                setIsBeingDragged(isBeingDragged);
+                if (isBeingDragged) {
+                    final ViewParent parent = getParent();
+                    if (parent != null) {
+                        parent.requestDisallowInterceptTouchEvent(true);
+                    }
+                }
+
+                /*
+                 * If being flinged and user touches, stop the fling. isFinished
+                 * will be false if being flinged.
+                 */
+                if (!mScroller.isFinished()) {
+                    mScroller.abortAnimation();
+                }
+
+                // Remember where the motion event started
+                mLastMotionY = (int) ev.getY();
+                mActivePointerId = ev.getPointerId(0);
+                break;
+            }
+            case MotionEvent.ACTION_MOVE:
+                final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
+                if (activePointerIndex == -1) {
+                    Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
+                    break;
+                }
+
+                final int y = (int) ev.getY(activePointerIndex);
+                int deltaY = mLastMotionY - y;
+                if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
+                    final ViewParent parent = getParent();
+                    if (parent != null) {
+                        parent.requestDisallowInterceptTouchEvent(true);
+                    }
+                    setIsBeingDragged(true);
+                    if (deltaY > 0) {
+                        deltaY -= mTouchSlop;
+                    } else {
+                        deltaY += mTouchSlop;
+                    }
+                }
+                if (mIsBeingDragged) {
+                    // Scroll to follow the motion event
+                    mLastMotionY = y;
+
+                    final int oldX = mScrollX;
+                    final int oldY = mOwnScrollY;
+                    final int range = getScrollRange();
+                    final int overscrollMode = getOverScrollMode();
+                    final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
+                            (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
+
+                    // Calling overScrollBy will call onOverScrolled, which
+                    // calls onScrollChanged if applicable.
+                    if (overScrollBy(0, deltaY, 0, mOwnScrollY,
+                            0, range, 0, mOverscrollDistance, true)) {
+                        // Break our velocity if we hit a scroll barrier.
+                        mVelocityTracker.clear();
+                    }
+                    // TODO: Overscroll
+//                    if (canOverscroll) {
+//                        final int pulledToY = oldY + deltaY;
+//                        if (pulledToY < 0) {
+//                            mEdgeGlowTop.onPull((float) deltaY / getHeight());
+//                            if (!mEdgeGlowBottom.isFinished()) {
+//                                mEdgeGlowBottom.onRelease();
+//                            }
+//                        } else if (pulledToY > range) {
+//                            mEdgeGlowBottom.onPull((float) deltaY / getHeight());
+//                            if (!mEdgeGlowTop.isFinished()) {
+//                                mEdgeGlowTop.onRelease();
+//                            }
+//                        }
+//                        if (mEdgeGlowTop != null
+//                                && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())){
+//                            postInvalidateOnAnimation();
+//                        }
+//                    }
+                }
+                break;
+            case MotionEvent.ACTION_UP:
+                if (mIsBeingDragged) {
+                    final VelocityTracker velocityTracker = mVelocityTracker;
+                    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
+                    int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
+
+                    if (getChildCount() > 0) {
+                        if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
+                            fling(-initialVelocity);
+                        } else {
+                            if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0,
+                                    getScrollRange())) {
+                                postInvalidateOnAnimation();
+                            }
+                        }
+                    }
+
+                    mActivePointerId = INVALID_POINTER;
+                    endDrag();
+                }
+                break;
+            case MotionEvent.ACTION_CANCEL:
+                if (mIsBeingDragged && getChildCount() > 0) {
+                    if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) {
+                        postInvalidateOnAnimation();
+                    }
+                    mActivePointerId = INVALID_POINTER;
+                    endDrag();
+                }
+                break;
+            case MotionEvent.ACTION_POINTER_DOWN: {
+                final int index = ev.getActionIndex();
+                mLastMotionY = (int) ev.getY(index);
+                mActivePointerId = ev.getPointerId(index);
+                break;
+            }
+            case MotionEvent.ACTION_POINTER_UP:
+                onSecondaryPointerUp(ev);
+                mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId));
+                break;
+        }
+        return true;
+    }
+
+    private void onSecondaryPointerUp(MotionEvent ev) {
+        final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
+                MotionEvent.ACTION_POINTER_INDEX_SHIFT;
+        final int pointerId = ev.getPointerId(pointerIndex);
+        if (pointerId == mActivePointerId) {
+            // This was our active pointer going up. Choose a new
+            // active pointer and adjust accordingly.
+            // TODO: Make this decision more intelligent.
+            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
+            mLastMotionY = (int) ev.getY(newPointerIndex);
+            mActivePointerId = ev.getPointerId(newPointerIndex);
+            if (mVelocityTracker != null) {
+                mVelocityTracker.clear();
+            }
+        }
+    }
+
+    private void initVelocityTrackerIfNotExists() {
+        if (mVelocityTracker == null) {
+            mVelocityTracker = VelocityTracker.obtain();
+        }
+    }
+
+    private void recycleVelocityTracker() {
+        if (mVelocityTracker != null) {
+            mVelocityTracker.recycle();
+            mVelocityTracker = null;
+        }
+    }
+
+    private void initOrResetVelocityTracker() {
+        if (mVelocityTracker == null) {
+            mVelocityTracker = VelocityTracker.obtain();
+        } else {
+            mVelocityTracker.clear();
+        }
+    }
+
+    @Override
+    public void computeScroll() {
+        if (mScroller.computeScrollOffset()) {
+            // This is called at drawing time by ViewGroup.
+            int oldX = mScrollX;
+            int oldY = mOwnScrollY;
+            int x = mScroller.getCurrX();
+            int y = mScroller.getCurrY();
+
+            if (oldX != x || oldY != y) {
+                final int range = getScrollRange();
+                final int overscrollMode = getOverScrollMode();
+                final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
+                        (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
+
+                overScrollBy(x - oldX, y - oldY, oldX, oldY, 0, range,
+                        0, mOverflingDistance, false);
+                onScrollChanged(mScrollX, mOwnScrollY, oldX, oldY);
+
+                if (canOverscroll) {
+                    // TODO: Overscroll
+//                    if (y < 0 && oldY >= 0) {
+//                        mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity());
+//                    } else if (y > range && oldY <= range) {
+//                        mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity());
+//                    }
+                }
+                updateChildren();
+            }
+
+            // Keep on drawing until the animation has finished.
+            postInvalidateOnAnimation();
+        }
+    }
+
+    public void customScrollBy(int y) {
+        mOwnScrollY += y;
+        updateChildren();
+    }
+
+    public void customScrollTo(int y) {
+        mOwnScrollY = y;
+        updateChildren();
+    }
+
+    @Override
+    protected void onOverScrolled(int scrollX, int scrollY,
+                                  boolean clampedX, boolean clampedY) {
+        // Treat animating scrolls differently; see #computeScroll() for why.
+        if (!mScroller.isFinished()) {
+            final int oldX = mScrollX;
+            final int oldY = mOwnScrollY;
+            mScrollX = scrollX;
+            mOwnScrollY = scrollY;
+            invalidateParentIfNeeded();
+            onScrollChanged(mScrollX, mOwnScrollY, oldX, oldY);
+            if (clampedY) {
+                mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange());
+            }
+            updateChildren();
+        } else {
+            customScrollTo(scrollY);
+            scrollTo(scrollX, mScrollY);
+        }
+    }
+
+    private int getScrollRange() {
+        int scrollRange = 0;
+        if (getChildCount() > 0) {
+            int contentHeight = getContentHeight();
+            scrollRange = Math.max(0,
+                    contentHeight - mMaxLayoutHeight + mCollapsedSize);
+        }
+        return scrollRange;
+    }
+
+    private int getContentHeight() {
+        return mContentHeight;
+    }
+
+    private void updateContentHeight() {
+        int height = 0;
+        for (int i = 0; i < getChildCount(); i++) {
+            View child = getChildAt(i);
+            height += child.getHeight();
+            if (i < getChildCount()-1) {
+                height += mPaddingBetweenElements;
+            }
+        }
+        mContentHeight = height;
+    }
+
+    /**
+     * Fling the scroll view
+     *
+     * @param velocityY The initial velocity in the Y direction. Positive
+     *                  numbers mean that the finger/cursor is moving down the screen,
+     *                  which means we want to scroll towards the top.
+     */
+    private void fling(int velocityY) {
+        if (getChildCount() > 0) {
+            int height = (int) getLayoutHeight();
+            int bottom = getContentHeight();
+
+            mScroller.fling(mScrollX, mOwnScrollY, 0, velocityY, 0, 0, 0,
+                    Math.max(0, bottom - height), 0, height/2);
+
+            postInvalidateOnAnimation();
+        }
+    }
+
+    private void endDrag() {
+        setIsBeingDragged(false);
+
+        recycleVelocityTracker();
+
+        // TODO: Overscroll
+//        if (mEdgeGlowTop != null) {
+//            mEdgeGlowTop.onRelease();
+//            mEdgeGlowBottom.onRelease();
+//        }
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        boolean scrollWantsIt = false;
+        if (mAllowScrolling) {
+            scrollWantsIt = onInterceptTouchEventScroll(ev);
+        }
+        boolean swipeWantsIt = false;
+        if (!mIsBeingDragged) {
+            swipeWantsIt = mSwipeHelper.onInterceptTouchEvent(ev);
+        }
+        return swipeWantsIt || scrollWantsIt ||
+                super.onInterceptTouchEvent(ev);
+    }
+
+    private boolean onInterceptTouchEventScroll(MotionEvent ev) {
+        /*
+         * This method JUST determines whether we want to intercept the motion.
+         * If we return true, onMotionEvent will be called and we do the actual
+         * scrolling there.
+         */
+
+        /*
+        * Shortcut the most recurring case: the user is in the dragging
+        * state and he is moving his finger.  We want to intercept this
+        * motion.
+        */
+        final int action = ev.getAction();
+        if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
+            return true;
+        }
+
+        /*
+         * Don't try to intercept touch if we can't scroll anyway.
+         */
+        if (mOwnScrollY == 0 && getScrollRange() == 0) {
+            return false;
+        }
+
+        switch (action & MotionEvent.ACTION_MASK) {
+            case MotionEvent.ACTION_MOVE: {
+                /*
+                 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
+                 * whether the user has moved far enough from his original down touch.
+                 */
+
+                /*
+                * Locally do absolute value. mLastMotionY is set to the y value
+                * of the down event.
+                */
+                final int activePointerId = mActivePointerId;
+                if (activePointerId == INVALID_POINTER) {
+                    // If we don't have a valid id, the touch down wasn't on content.
+                    break;
+                }
+
+                final int pointerIndex = ev.findPointerIndex(activePointerId);
+                if (pointerIndex == -1) {
+                    Log.e(TAG, "Invalid pointerId=" + activePointerId
+                            + " in onInterceptTouchEvent");
+                    break;
+                }
+
+                final int y = (int) ev.getY(pointerIndex);
+                final int yDiff = Math.abs(y - mLastMotionY);
+                if (yDiff > mTouchSlop) {
+                    setIsBeingDragged(true);
+                    mLastMotionY = y;
+                    initVelocityTrackerIfNotExists();
+                    mVelocityTracker.addMovement(ev);
+                    final ViewParent parent = getParent();
+                    if (parent != null) {
+                        parent.requestDisallowInterceptTouchEvent(true);
+                    }
+                }
+                break;
+            }
+
+            case MotionEvent.ACTION_DOWN: {
+                final int y = (int) ev.getY();
+                if (getChildAtPosition(ev.getX(), y) == null) {
+                    setIsBeingDragged(false);
+                    recycleVelocityTracker();
+                    break;
+                }
+
+                /*
+                 * Remember location of down touch.
+                 * ACTION_DOWN always refers to pointer index 0.
+                 */
+                mLastMotionY = y;
+                mActivePointerId = ev.getPointerId(0);
+
+                initOrResetVelocityTracker();
+                mVelocityTracker.addMovement(ev);
+                /*
+                * If being flinged and user touches the screen, initiate drag;
+                * otherwise don't.  mScroller.isFinished should be false when
+                * being flinged.
+                */
+                boolean isBeingDragged = !mScroller.isFinished();
+                setIsBeingDragged(isBeingDragged);
+                break;
+            }
+
+            case MotionEvent.ACTION_CANCEL:
+            case MotionEvent.ACTION_UP:
+                /* Release the drag */
+                setIsBeingDragged(false);
+                mActivePointerId = INVALID_POINTER;
+                recycleVelocityTracker();
+                if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) {
+                    postInvalidateOnAnimation();
+                }
+                break;
+            case MotionEvent.ACTION_POINTER_UP:
+                onSecondaryPointerUp(ev);
+                break;
+        }
+
+        /*
+        * The only time we want to intercept motion events is if we are in the
+        * drag mode.
+        */
+        return mIsBeingDragged;
+    }
+
+    private void setIsBeingDragged(boolean isDragged) {
+        mIsBeingDragged = isDragged;
+        if (isDragged) {
+            mSwipeHelper.removeLongPressCallback();
+        }
+    }
+
+    @Override
+    public void onWindowFocusChanged(boolean hasWindowFocus) {
+        super.onWindowFocusChanged(hasWindowFocus);
+        if (!hasWindowFocus) {
+            mSwipeHelper.removeLongPressCallback();
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/PiecewiseLinearIndentationFunctor.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/PiecewiseLinearIndentationFunctor.java
new file mode 100644
index 0000000..38b544f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/PiecewiseLinearIndentationFunctor.java
@@ -0,0 +1,112 @@
+/*
+ * 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.stack;
+
+import java.util.ArrayList;
+
+/**
+ * A Functor which interpolates the stack distance linearly based on base values.
+ * The base values are based on an interpolation between a linear function and a
+ * quadratic function
+ */
+public class PiecewiseLinearIndentationFunctor extends StackIndentationFunctor {
+
+    private final ArrayList<Float> mBaseValues;
+    private final float mLinearPart;
+
+    /**
+     * @param maxItemsInStack The maximum number of items which should be visible at the same time,
+     *                        i.e the function returns totalTransitionDistance for the element with
+     *                        index maxItemsInStack
+     * @param peekSize The visual appearance of this is how far the cards in the stack peek
+     *                 out below the top card and it is measured in real pixels.
+     *                 Note that the visual appearance does not necessarily always correspond to
+     *                 the actual visual distance below the top card but is a maximum,
+     *                 achieved when the next card just starts transitioning into the stack and
+     *                 the stack is full.
+     *                 If totalTransitionDistance is equal to this, we directly start at the peek,
+     *                 otherwise the first element transitions between 0 and
+     *                 totalTransitionDistance - peekSize.
+     *                 Visualization:
+     *           ---------------------------------------------------   ---
+     *          |                                                   |   |
+     *          |                  FIRST ITEM                       |   | <- totalTransitionDistance
+     *          |                                                   |   |
+     *          |---------------------------------------------------|   |   ---
+     *          |__________________SECOND ITEM______________________|   |    |  <- peekSize
+     *          |===================================================|  _|_  _|_
+     *
+     * @param totalTransitionDistance The total transition distance an element has to go through
+     * @param linearPart The interpolation factor between the linear and the quadratic amount taken.
+     *                   This factor must be somewhere in [0 , 1]
+     */
+    PiecewiseLinearIndentationFunctor(int maxItemsInStack,
+                                      int peekSize,
+                                      int totalTransitionDistance,
+                                      float linearPart) {
+        super(maxItemsInStack, peekSize, totalTransitionDistance);
+        mBaseValues = new ArrayList<Float>(maxItemsInStack+1);
+        initBaseValues();
+        mLinearPart = linearPart;
+    }
+
+    private void initBaseValues() {
+        int sumOfSquares = getSumOfSquares(mMaxItemsInStack-1);
+        int totalWeight = 0;
+        mBaseValues.add(0.0f);
+        for (int i = 0; i < mMaxItemsInStack - 1; i++) {
+            totalWeight += (mMaxItemsInStack - i - 1) * (mMaxItemsInStack - i - 1);
+            mBaseValues.add((float) totalWeight / sumOfSquares);
+        }
+    }
+
+    /**
+     * Get the sum of squares up to and including n, i.e sum(i * i, 1, n)
+     *
+     * @param n the maximum square to include
+     * @return
+     */
+    private int getSumOfSquares(int n) {
+        return n * (n + 1) * (2 * n + 1) / 6;
+    }
+
+    @Override
+    public float getValue(float itemsBefore) {
+        if (mStackStartsAtPeek) {
+            // We directly start at the stack, so no initial interpolation.
+            itemsBefore++;
+        }
+        if (itemsBefore < 0) {
+            return 0;
+        } else if (itemsBefore >= mMaxItemsInStack) {
+            return mTotalTransitionDistance;
+        }
+        int below = (int) itemsBefore;
+        float partialIn = itemsBefore - below;
+
+        if (below == 0) {
+            return mDistanceToPeekStart * partialIn;
+        } else {
+            float result = mDistanceToPeekStart;
+            float progress = mBaseValues.get(below - 1) * (1 - partialIn)
+                    + mBaseValues.get(below) * partialIn;
+            result += (progress * (1 - mLinearPart)
+                    + (itemsBefore - 1) / (mMaxItemsInStack - 1)  * mLinearPart) * mPeekSize;
+            return result;
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackIndentationFunctor.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackIndentationFunctor.java
new file mode 100644
index 0000000..f72947a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackIndentationFunctor.java
@@ -0,0 +1,74 @@
+/*
+ * 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.stack;
+
+/**
+ * A functor which can be queried for offset given the number of items before it.
+ */
+public abstract class StackIndentationFunctor {
+
+    protected final int mTotalTransitionDistance;
+    protected final int mDistanceToPeekStart;
+    protected int mMaxItemsInStack;
+    protected int mPeekSize;
+    protected boolean mStackStartsAtPeek;
+
+    /**
+     * @param maxItemsInStack The maximum number of items which should be visible at the same time,
+     *                        i.e the function returns totalTransitionDistance for the element with
+     *                        index maxItemsInStack
+     * @param peekSize The visual appearance of this is how far the cards in the stack peek
+     *                 out below the top card and it is measured in real pixels.
+     *                 Note that the visual appearance does not necessarily always correspond to
+     *                 the actual visual distance below the top card but is a maximum,
+     *                 achieved when the next card just starts transitioning into the stack and
+     *                 the stack is full.
+     *                 If totalTransitionDistance is equal to this, we directly start at the peek,
+     *                 otherwise the first element transitions between 0 and
+     *                 totalTransitionDistance - peekSize.
+     *                 Visualization:
+     *           ---------------------------------------------------   ---
+     *          |                                                   |   |
+     *          |                  FIRST ITEM                       |   | <- totalTransitionDistance
+     *          |                                                   |   |
+     *          |---------------------------------------------------|   |   ---
+     *          |__________________SECOND ITEM______________________|   |    |  <- peekSize
+     *          |===================================================|  _|_  _|_
+     *
+     * @param totalTransitionDistance The total transition distance an element has to go through
+     */
+    StackIndentationFunctor(int maxItemsInStack, int peekSize, int totalTransitionDistance) {
+        mTotalTransitionDistance = totalTransitionDistance;
+        mDistanceToPeekStart = mTotalTransitionDistance - peekSize;
+        mStackStartsAtPeek = mDistanceToPeekStart == 0;
+        mMaxItemsInStack = maxItemsInStack;
+        mPeekSize = peekSize;
+
+    }
+
+    public void setPeekSize(int mPeekSize) {
+        this.mPeekSize = mPeekSize;
+    }
+
+    /**
+     * Gets the offset of this Functor given a the quantity of items before it
+     *
+     * @param itemsBefore how many items are already in the stack before this element
+     * @return the offset
+     */
+    public abstract float getValue(float itemsBefore);
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java
new file mode 100644
index 0000000..9db4e77
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java
@@ -0,0 +1,399 @@
+/*
+ * 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.stack;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import com.android.systemui.R;
+
+/**
+ * The Algorithm of the {@link com.android.systemui.statusbar.stack
+ * .NotificationStackScrollLayout} which can be queried for {@link com.android.systemui.statusbar
+ * .stack.StackScrollState}
+ */
+public class StackScrollAlgorithm {
+
+    private static final int MAX_ITEMS_IN_BOTTOM_STACK = 3;
+    private static final int MAX_ITEMS_IN_TOP_STACK = 3;
+
+    private int mPaddingBetweenElements;
+    private int mCollapsedSize;
+    private int mTopStackPeekSize;
+    private int mBottomStackPeekSize;
+    private int mZDistanceBetweenElements;
+    private int mZBasicHeight;
+
+    private StackIndentationFunctor mTopStackIndentationFunctor;
+    private StackIndentationFunctor mBottomStackIndentationFunctor;
+
+    private float mLayoutHeight;
+    private StackScrollAlgorithmState mTempAlgorithmState = new StackScrollAlgorithmState();
+
+    public StackScrollAlgorithm(Context context) {
+        initConstants(context);
+    }
+
+    private void initConstants(Context context) {
+
+        // currently the padding is in the elements themself
+        mPaddingBetweenElements = 0;
+        mCollapsedSize = context.getResources()
+                .getDimensionPixelSize(R.dimen.notification_row_min_height);
+        mTopStackPeekSize = context.getResources()
+                .getDimensionPixelSize(R.dimen.top_stack_peek_amount);
+        mBottomStackPeekSize = context.getResources()
+                .getDimensionPixelSize(R.dimen.bottom_stack_peek_amount);
+        mZDistanceBetweenElements = context.getResources()
+                .getDimensionPixelSize(R.dimen.z_distance_between_notifications);
+        mZBasicHeight = (MAX_ITEMS_IN_BOTTOM_STACK + 1) * mZDistanceBetweenElements;
+
+        mTopStackIndentationFunctor = new PiecewiseLinearIndentationFunctor(
+                MAX_ITEMS_IN_TOP_STACK,
+                mTopStackPeekSize,
+                mCollapsedSize + mPaddingBetweenElements,
+                0.5f);
+        mBottomStackIndentationFunctor = new PiecewiseLinearIndentationFunctor(
+                MAX_ITEMS_IN_BOTTOM_STACK,
+                mBottomStackPeekSize,
+                mBottomStackPeekSize,
+                0.5f);
+    }
+
+
+    public void getStackScrollState(StackScrollState resultState) {
+        // The state of the local variables are saved in an algorithmState to easily subdivide it
+        // into multiple phases.
+        StackScrollAlgorithmState algorithmState = mTempAlgorithmState;
+
+        // First we reset the view states to their default values.
+        resultState.resetViewStates();
+
+        // The first element is always in there so it's initialized with 1.0f.
+        algorithmState.itemsInTopStack = 1.0f;
+        algorithmState.partialInTop = 0.0f;
+        algorithmState.lastTopStackIndex = 0;
+        algorithmState.scrollY = resultState.getScrollY();
+        algorithmState.itemsInBottomStack = 0.0f;
+
+        // Phase 1:
+        findNumberOfItemsInTopStackAndUpdateState(resultState, algorithmState);
+
+        // Phase 2:
+        updatePositionsForState(resultState, algorithmState);
+
+        // Phase 3:
+        updateZValuesForState(resultState, algorithmState);
+
+        // Write the algorithm state to the result.
+        resultState.setScrollY(algorithmState.scrollY);
+    }
+
+    /**
+     * Determine the positions for the views. This is the main part of the algorithm.
+     *
+     * @param resultState The result state to update if a change to the properties of a child occurs
+     * @param algorithmState The state in which the current pass of the algorithm is currently in
+     *                       and which will be updated
+     */
+    private void updatePositionsForState(StackScrollState resultState,
+            StackScrollAlgorithmState algorithmState) {
+        float stackHeight = getLayoutHeight();
+
+        // The position where the bottom stack starts.
+        float transitioningPositionStart = stackHeight - mCollapsedSize - mBottomStackPeekSize;
+
+        // The y coordinate of the current child.
+        float currentYPosition = 0.0f;
+
+        // How far in is the element currently transitioning into the bottom stack.
+        float yPositionInScrollView = 0.0f;
+
+        ViewGroup hostView = resultState.getHostView();
+        int childCount = hostView.getChildCount();
+        int numberOfElementsCompletelyIn = (int) algorithmState.itemsInTopStack;
+        for (int i = 0; i < childCount; i++) {
+            View child = hostView.getChildAt(i);
+            StackScrollState.ViewState childViewState = resultState.getViewStateForView(child);
+            childViewState.yTranslation = currentYPosition;
+            int childHeight = child.getHeight();
+            // The y position after this element
+            float nextYPosition = currentYPosition + childHeight + mPaddingBetweenElements;
+            float yPositionInScrollViewAfterElement = yPositionInScrollView
+                    + childHeight
+                    + mPaddingBetweenElements;
+            float scrollOffset = yPositionInScrollViewAfterElement - algorithmState.scrollY;
+            if (i < algorithmState.lastTopStackIndex) {
+                // Case 1:
+                // We are in the top Stack
+                nextYPosition = updateStateForTopStackChild(algorithmState,
+                        numberOfElementsCompletelyIn,
+                        i, childViewState);
+
+            } else if (i == algorithmState.lastTopStackIndex) {
+                // Case 2:
+                // First element of regular scrollview comes next, so the position is just the
+                // scrolling position
+                nextYPosition = scrollOffset;
+            } else if (nextYPosition >= transitioningPositionStart) {
+                if (currentYPosition >= transitioningPositionStart) {
+                    // Case 3:
+                    // According to the regular scroll view we are fully translated out of the
+                    // bottom of the screen so we are fully in the bottom stack
+                    nextYPosition = updateStateForChildFullyInBottomStack(algorithmState,
+                            transitioningPositionStart, childViewState, childHeight);
+
+
+                } else {
+                    // Case 4:
+                    // According to the regular scroll view we are currently translating out of /
+                    // into the bottom of the screen
+                    nextYPosition = updateStateForChildTransitioningInBottom(
+                            algorithmState, stackHeight, transitioningPositionStart,
+                            currentYPosition, childViewState,
+                            childHeight, nextYPosition);
+                }
+            }
+            currentYPosition = nextYPosition;
+            yPositionInScrollView = yPositionInScrollViewAfterElement;
+        }
+    }
+
+    private float updateStateForChildTransitioningInBottom(StackScrollAlgorithmState algorithmState,
+            float stackHeight, float transitioningPositionStart, float currentYPosition,
+            StackScrollState.ViewState childViewState, int childHeight, float nextYPosition) {
+        float newSize = transitioningPositionStart + mCollapsedSize - currentYPosition;
+        newSize = Math.min(childHeight, newSize);
+        // Transitioning element on top of bottom stack:
+        algorithmState.partialInBottom = 1.0f - (
+                (stackHeight - mBottomStackPeekSize - nextYPosition) / mCollapsedSize);
+        // Our element can be expanded, so we might even have to scroll further than
+        // mCollapsedSize
+        algorithmState.partialInBottom = Math.min(1.0f, algorithmState.partialInBottom);
+        float offset = mBottomStackIndentationFunctor.getValue(
+                algorithmState.partialInBottom);
+        nextYPosition = transitioningPositionStart + offset;
+        algorithmState.itemsInBottomStack += algorithmState.partialInBottom;
+        // TODO: only temporarily collapse
+        if (childHeight != (int) newSize) {
+            childViewState.height = (int) newSize;
+        }
+        return nextYPosition;
+    }
+
+    private float updateStateForChildFullyInBottomStack(StackScrollAlgorithmState algorithmState,
+            float transitioningPositionStart, StackScrollState.ViewState childViewState,
+            int childHeight) {
+
+        float nextYPosition;
+        algorithmState.itemsInBottomStack += 1.0f;
+        if (algorithmState.itemsInBottomStack < MAX_ITEMS_IN_BOTTOM_STACK) {
+            // We are visually entering the bottom stack
+            nextYPosition = transitioningPositionStart
+                    + mBottomStackIndentationFunctor.getValue(
+                            algorithmState.itemsInBottomStack);
+        } else {
+            // we are fully inside the stack
+            if (algorithmState.itemsInBottomStack > MAX_ITEMS_IN_BOTTOM_STACK + 2) {
+                childViewState.alpha = 0.0f;
+            } else if (algorithmState.itemsInBottomStack
+                    > MAX_ITEMS_IN_BOTTOM_STACK + 1) {
+                childViewState.alpha = 1.0f - algorithmState.partialInBottom;
+            }
+            nextYPosition = transitioningPositionStart + mBottomStackPeekSize;
+        }
+        // TODO: only temporarily collapse
+        if (childHeight != mCollapsedSize) {
+            childViewState.height = mCollapsedSize;
+        }
+        return nextYPosition;
+    }
+
+    private float updateStateForTopStackChild(StackScrollAlgorithmState algorithmState,
+            int numberOfElementsCompletelyIn, int i, StackScrollState.ViewState childViewState) {
+
+        float nextYPosition = 0;
+
+        // First we calculate the index relative to the current stack window of size at most
+        // {@link #MAX_ITEMS_IN_TOP_STACK}
+        int paddedIndex = i
+                - Math.max(numberOfElementsCompletelyIn - MAX_ITEMS_IN_TOP_STACK, 0);
+        if (paddedIndex >= 0) {
+            // We are currently visually entering the top stack
+            nextYPosition = mCollapsedSize + mPaddingBetweenElements -
+                    mTopStackIndentationFunctor.getValue(
+                            algorithmState.itemsInTopStack - i - 1);
+            if (paddedIndex == 0 && i != 0) {
+                childViewState.alpha = 1.0f - algorithmState.partialInTop;
+            }
+        } else {
+            // We are hidden behind the top card and faded out, so we can hide ourselfs
+            if (i != 0) {
+                childViewState.alpha = 0.0f;
+            }
+        }
+        return nextYPosition;
+    }
+
+    /**
+     * Find the number of items in the top stack and update the result state if needed.
+     *
+     * @param resultState The result state to update if a height change of an child occurs
+     * @param algorithmState The state in which the current pass of the algorithm is currently in
+     *                       and which will be updated
+     */
+    private void findNumberOfItemsInTopStackAndUpdateState(StackScrollState resultState,
+            StackScrollAlgorithmState algorithmState) {
+
+        // The y Position if the element would be in a regular scrollView
+        float yPositionInScrollView = 0.0f;
+        ViewGroup hostView = resultState.getHostView();
+        int childCount = hostView.getChildCount();
+
+        // find the number of elements in the top stack.
+        for (int i = 0; i < childCount; i++) {
+            View child = hostView.getChildAt(i);
+            StackScrollState.ViewState childViewState = resultState.getViewStateForView(child);
+            int childHeight = child.getHeight();
+            float yPositionInScrollViewAfterElement = yPositionInScrollView
+                    + childHeight
+                    + mPaddingBetweenElements;
+            if (yPositionInScrollView < algorithmState.scrollY) {
+                if (yPositionInScrollViewAfterElement <= algorithmState.scrollY) {
+                    // According to the regular scroll view we are fully off screen
+                    algorithmState.itemsInTopStack += 1.0f;
+                    if (childHeight != mCollapsedSize) {
+                        childViewState.height = mCollapsedSize;
+                    }
+                } else {
+                    // According to the regular scroll view we are partially off screen
+                    // If it is expanded we have to collapse it to a new size
+                    float newSize = yPositionInScrollViewAfterElement
+                            - mPaddingBetweenElements
+                            - algorithmState.scrollY;
+
+                    // How much did we scroll into this child
+                    algorithmState.partialInTop = (mCollapsedSize - newSize) / (mCollapsedSize
+                            + mPaddingBetweenElements);
+
+                    // Our element can be expanded, so this can get negative
+                    algorithmState.partialInTop = Math.max(0.0f, algorithmState.partialInTop);
+                    algorithmState.itemsInTopStack += algorithmState.partialInTop;
+                    // TODO: handle overlapping sizes with end stack
+                    newSize = Math.max(mCollapsedSize, newSize);
+                    // TODO: only temporarily collapse
+                    if (newSize != childHeight) {
+                        childViewState.height = (int) newSize;
+
+                        // We decrease scrollY by the same amount we made this child smaller.
+                        // The new scroll position is therefore the start of the element
+                        algorithmState.scrollY = (int) yPositionInScrollView;
+                        resultState.setScrollY(algorithmState.scrollY);
+                    }
+                    if (childHeight > mCollapsedSize) {
+                        // If we are just resizing this child, this element is not treated to be
+                        // transitioning into the stack and therefore it is the last element in
+                        // the stack.
+                        algorithmState.lastTopStackIndex = i;
+                        break;
+                    }
+                }
+            } else {
+                algorithmState.lastTopStackIndex = i;
+
+                // We are already past the stack so we can end the loop
+                break;
+            }
+            yPositionInScrollView = yPositionInScrollViewAfterElement;
+        }
+    }
+
+    /**
+     * Calculate the Z positions for all children based on the number of items in both stacks and
+     * save it in the resultState
+     *
+     * @param resultState The result state to update the zTranslation values
+     * @param algorithmState The state in which the current pass of the algorithm is currently in
+     */
+    private void updateZValuesForState(StackScrollState resultState,
+            StackScrollAlgorithmState algorithmState) {
+        ViewGroup hostView = resultState.getHostView();
+        int childCount = hostView.getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View child = hostView.getChildAt(i);
+            StackScrollState.ViewState childViewState = resultState.getViewStateForView(child);
+            if (i < algorithmState.itemsInTopStack) {
+                float stackIndex = algorithmState.itemsInTopStack - i;
+                stackIndex = Math.min(stackIndex, MAX_ITEMS_IN_TOP_STACK + 2);
+                childViewState.zTranslation = mZBasicHeight
+                        + stackIndex * mZDistanceBetweenElements;
+            } else if (i > (childCount - 1 - algorithmState.itemsInBottomStack)) {
+                float numItemsAbove = i - (childCount - 1 - algorithmState.itemsInBottomStack);
+                float translationZ = mZBasicHeight
+                        - numItemsAbove * mZDistanceBetweenElements;
+                childViewState.zTranslation = translationZ;
+            } else {
+                childViewState.zTranslation = mZBasicHeight;
+            }
+        }
+    }
+
+    public float getLayoutHeight() {
+        return mLayoutHeight;
+    }
+
+    public void setLayoutHeight(float layoutHeight) {
+        this.mLayoutHeight = layoutHeight;
+    }
+
+    class StackScrollAlgorithmState {
+
+        /**
+         * The scroll position of the algorithm
+         */
+        public int scrollY;
+
+        /**
+         *  The quantity of items which are in the top stack.
+         */
+        public float itemsInTopStack;
+
+        /**
+         * how far in is the element currently transitioning into the top stack
+         */
+        public float partialInTop;
+
+        /**
+         * The last item index which is in the top stack.
+         * NOTE: In the top stack the item after the transitioning element is also in the stack!
+         * This is needed to ensure a smooth transition between the y position in the regular
+         * scrollview and the one in the stack.
+         */
+        public int lastTopStackIndex;
+
+        /**
+         * The quantity of items which are in the bottom stack.
+         */
+        public float itemsInBottomStack;
+
+        /**
+         * how far in is the element currently transitioning into the bottom stack
+         */
+        public float partialInBottom;
+    }
+
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollState.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollState.java
new file mode 100644
index 0000000..f72a52f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollState.java
@@ -0,0 +1,153 @@
+/*
+ * 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.stack;
+
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A state of a {@link com.android.systemui.statusbar.stack.NotificationStackScrollLayout} which
+ * can be applied to a viewGroup.
+ */
+public class StackScrollState {
+
+    private static final String CHILD_NOT_FOUND_TAG = "StackScrollStateNoSuchChild";
+
+    private final ViewGroup mHostView;
+    private Map<View, ViewState> mStateMap;
+    private int mScrollY;
+
+    public int getScrollY() {
+        return mScrollY;
+    }
+
+    public void setScrollY(int scrollY) {
+        this.mScrollY = scrollY;
+    }
+
+    public StackScrollState(ViewGroup hostView) {
+        mHostView = hostView;
+        mStateMap = new HashMap<View, ViewState>(mHostView.getChildCount());
+    }
+
+    public ViewGroup getHostView() {
+        return mHostView;
+    }
+
+    public void resetViewStates() {
+        int numChildren = mHostView.getChildCount();
+        for (int i = 0; i < numChildren; i++) {
+            View child = 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.alpha = 1.0f;
+        }
+    }
+
+
+    public ViewState getViewStateForView(View requestedView) {
+        return mStateMap.get(requestedView);
+    }
+
+    /**
+     * Apply the properties saved in {@link #mStateMap} to the children of the {@link #mHostView}.
+     * The properties are only applied if they effectively changed.
+     */
+    public void apply() {
+        int numChildren = mHostView.getChildCount();
+        for (int i = 0; i < numChildren; i++) {
+            View child = mHostView.getChildAt(i);
+            ViewState state = mStateMap.get(child);
+            if (state != null) {
+                float alpha = child.getAlpha();
+                float yTranslation = child.getTranslationY();
+                float zTranslation = child.getTranslationZ();
+                int height = child.getHeight();
+                float newAlpha = state.alpha;
+                float newYTranslation = state.yTranslation;
+                float newZTranslation = state.zTranslation;
+                int newHeight = state.height;
+                boolean becomesInvisible = newAlpha == 0.0f;
+                if (alpha != newAlpha) {
+                    // apply layer type
+                    boolean becomesFullyVisible = newAlpha == 1.0f;
+                    boolean newLayerTypeIsHardware = !becomesInvisible && !becomesFullyVisible;
+                    int layerType = child.getLayerType();
+                    int newLayerType = newLayerTypeIsHardware
+                            ? View.LAYER_TYPE_HARDWARE
+                            : View.LAYER_TYPE_NONE;
+                    if (layerType != newLayerType) {
+                        child.setLayerType(newLayerType, null);
+                    }
+
+                    // apply alpha
+                    if (!becomesInvisible) {
+                        child.setAlpha(newAlpha);
+                    }
+                }
+
+                // apply visibility
+                int oldVisibility = child.getVisibility();
+                int newVisibility = becomesInvisible ? View.INVISIBLE : View.VISIBLE;
+                if (newVisibility != oldVisibility) {
+                    child.setVisibility(newVisibility);
+                }
+
+                // apply yTranslation
+                if (yTranslation != newYTranslation) {
+                    child.setTranslationY(newYTranslation);
+                }
+
+                // apply zTranslation
+                if (zTranslation != newZTranslation) {
+                    child.setTranslationZ(newZTranslation);
+                }
+
+                // apply height
+                if (height != newHeight) {
+                    applyNewHeight(child, newHeight);
+                }
+            } else {
+                Log.wtf(CHILD_NOT_FOUND_TAG, "No child state was found when applying this state " +
+                        "to the hostView");
+            }
+        }
+    }
+
+    private void applyNewHeight(View child, int newHeight) {
+        ViewGroup.LayoutParams lp = child.getLayoutParams();
+        lp.height = newHeight;
+        child.setLayoutParams(lp);
+    }
+
+
+    public class ViewState {
+        float alpha;
+        float yTranslation;
+        float zTranslation;
+        int height;
+    }
+}