Animate CalculatorDisplay text on pulldown.

Bug: 31623549
Bug: 32584801

Change-Id: I07a54cad38c026357082b86ad026392f72693e22
diff --git a/res/layout/activity_calculator_port.xml b/res/layout/activity_calculator_port.xml
index eff5c96..30aaf00 100644
--- a/res/layout/activity_calculator_port.xml
+++ b/res/layout/activity_calculator_port.xml
@@ -43,4 +43,4 @@
 
     </com.android.calculator2.CalculatorPadViewPager>
 
-</LinearLayout>
\ No newline at end of file
+</LinearLayout>
diff --git a/res/layout/display_two_line.xml b/res/layout/display_two_line.xml
index 55e6f7b..3f7338d 100644
--- a/res/layout/display_two_line.xml
+++ b/res/layout/display_two_line.xml
@@ -38,12 +38,9 @@
             style="@style/DisplayTextStyle.Formula"
             android:layout_width="wrap_content"
             android:layout_height="match_parent"
-            android:layout_gravity="bottom|end"
             android:ellipsize="none"
-            android:longClickable="true"
-            android:singleLine="true"
-            android:textColor="@color/display_formula_text_color"
-            android:textIsSelectable="false" />
+            android:maxLines="1"
+            android:textColor="@color/display_formula_text_color" />
 
     </com.android.calculator2.CalculatorScrollView>
 
diff --git a/res/layout/fragment_history.xml b/res/layout/fragment_history.xml
index 35d2691..8bb7218 100644
--- a/res/layout/fragment_history.xml
+++ b/res/layout/fragment_history.xml
@@ -16,10 +16,10 @@
   -->
 <LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:background="@color/display_background_color"
-    android:clickable="true"
     android:orientation="vertical">
 
     <Toolbar
@@ -34,10 +34,12 @@
         android:theme="@android:style/ThemeOverlay.Material.Dark.ActionBar"
         android:title="@string/title_history" />
 
-    <TextView
+    <android.support.v7.widget.RecyclerView
+        android:id="@+id/history_recycler_view"
         android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:gravity="center"
-        android:text="No History" />
+        android:layout_height="wrap_content"
+        android:clipChildren="false"
+        app:layoutManager="LinearLayoutManager"
+        app:stackFromEnd="true" />
 
 </LinearLayout>
diff --git a/res/layout/history_item.xml b/res/layout/history_item.xml
new file mode 100644
index 0000000..1d45a14
--- /dev/null
+++ b/res/layout/history_item.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2016 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.
+  -->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:clipChildren="false"
+    android:orientation="vertical">
+
+    <TextView
+        android:id="@+id/history_date"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:fontFamily="sans-serif-medium"
+        android:text="@string/title_current_expression"
+        android:textColor="?android:attr/colorAccent"
+        android:textSize="14dp" />
+
+    <com.android.calculator2.CalculatorScrollView
+        android:id="@+id/history_formula_container"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:overScrollMode="never"
+        android:scrollbars="none">
+
+        <com.android.calculator2.CalculatorFormula
+            android:id="@+id/history_formula"
+            style="@style/HistoryItemTextStyle"
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent"
+            android:ellipsize="none"
+            android:textColor="@color/display_formula_text_color" />
+
+    </com.android.calculator2.CalculatorScrollView>
+
+    <com.android.calculator2.CalculatorResult
+        android:id="@+id/history_result"
+        style="@style/HistoryItemTextStyle"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:bufferType="spannable"
+        android:textColor="@color/display_result_text_color" />
+
+</LinearLayout>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 5218acd..9de65d5 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -20,4 +20,9 @@
     <!-- The margin between the pad pages when displayed using a view pager. -->
     <dimen name="pad_page_margin">24dip</dimen>
 
+    <dimen name="history_item_text_padding_top">8dip</dimen>
+    <dimen name="history_item_text_padding_bottom">16dip</dimen>
+    <dimen name="history_item_text_padding_start">16dip</dimen>
+    <dimen name="history_item_text_padding_end">24dip</dimen>
+
 </resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index f6cfad1..8a053cc 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -299,4 +299,7 @@
     <!-- Title for alert dialog when calculation takes too long (timeout). [CHAR_LIMIT=30] -->
     <string name="dialog_timeout">Timeout</string>
 
+    <!-- Title for "current expression" in history page. [CHAR_LIMIT=40] -->
+    <string name="title_current_expression">Current Expression</string>
+
 </resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index a15cc31..25a3cd0 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -18,7 +18,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android">
 
     <style name="DisplayTextStyle" parent="@android:style/Widget.Material.Light.TextView">
-        <item name="android:background">@android:color/transparent</item>
         <item name="android:cursorVisible">false</item>
         <item name="android:fontFamily">sans-serif-light</item>
         <item name="android:includeFontPadding">false</item>
@@ -46,6 +45,14 @@
         <item name="android:textSize">@dimen/result_textsize</item>
     </style>
 
+    <style name="HistoryItemTextStyle" parent="DisplayTextStyle">
+        <item name="android:paddingTop">@dimen/history_item_text_padding_top</item>
+        <item name="android:paddingBottom">@dimen/history_item_text_padding_bottom</item>
+        <item name="android:paddingStart">@dimen/history_item_text_padding_start</item>
+        <item name="android:paddingEnd">@dimen/history_item_text_padding_end</item>
+        <item name="android:textSize">@dimen/result_textsize</item>
+    </style>
+
     <style name="PadButtonStyle" parent="@android:style/Widget.Material.Light.Button.Borderless">
         <item name="android:layout_width">0dip</item>
         <item name="android:layout_height">0dip</item>
diff --git a/src/com/android/calculator2/Calculator.java b/src/com/android/calculator2/Calculator.java
index 8e4b240..8aac061 100644
--- a/src/com/android/calculator2/Calculator.java
+++ b/src/com/android/calculator2/Calculator.java
@@ -57,12 +57,14 @@
 import android.view.KeyEvent;
 import android.view.Menu;
 import android.view.MenuItem;
+import android.view.MotionEvent;
 import android.view.View;
 import android.view.View.OnLongClickListener;
 import android.view.ViewAnimationUtils;
 import android.view.ViewGroupOverlay;
 import android.view.ViewTreeObserver;
 import android.view.animation.AccelerateDecelerateInterpolator;
+import android.widget.FrameLayout;
 import android.widget.HorizontalScrollView;
 import android.widget.TextView;
 import android.widget.Toolbar;
@@ -174,6 +176,44 @@
         }
     };
 
+    private final DragLayout.DragCallback mDragCallback = new DragLayout.DragCallback() {
+        @Override
+        public void onStartDragging() {
+            showHistoryFragment(FragmentTransaction.TRANSIT_NONE);
+        }
+
+        @Override
+        public void whileDragging(float yFraction) {
+            // no-op
+        }
+
+        @Override
+        public void onClosed() {
+            getFragmentManager().popBackStack();
+        }
+
+        @Override
+        public boolean allowDrag(MotionEvent event) {
+            return isViewTarget(mHistoryFrame, event) || isViewTarget(mDisplayView, event);
+        }
+
+        @Override
+        public boolean shouldInterceptTouchEvent(MotionEvent event) {
+            return isViewTarget(mHistoryFrame, event) || isViewTarget(mDisplayView, event);
+        }
+
+        @Override
+        public int getDisplayHeight() {
+            return mDisplayView.getMeasuredHeight();
+        }
+
+        public void onLayout(int translation) {
+            mHistoryFrame.setTranslationY(translation + mDisplayView.getBottom());
+        }
+    };
+
+    private final Rect mHitRect = new Rect();
+
     private CalculatorState mCurrentState;
     private Evaluator mEvaluator;
 
@@ -183,6 +223,7 @@
     private CalculatorResult mResultText;
     private HorizontalScrollView mFormulaContainer;
     private DragLayout mDragLayout;
+    private FrameLayout mHistoryFrame;
 
     private ViewPager mPadViewPager;
     private View mDeleteButton;
@@ -272,17 +313,10 @@
         KeyMaps.setActivity(this);
 
         mDragLayout = (DragLayout) findViewById(R.id.drag_layout);
-        mDragLayout.setOnDragCallback(new DragLayout.OnDragCallback() {
-            @Override
-            public void onStartDragging() {
-                showHistoryFragment(FragmentTransaction.TRANSIT_NONE);
-            }
+        mDragLayout.removeDragCallback(mDragCallback);
+        mDragLayout.addDragCallback(mDragCallback);
 
-            @Override
-            public void onDragToClose() {
-                getFragmentManager().popBackStack();
-            }
-        });
+        mHistoryFrame = (FrameLayout) findViewById(R.id.history_frame);
 
         if (savedInstanceState != null) {
             setState(CalculatorState.values()[
@@ -422,6 +456,12 @@
     }
 
     @Override
+    protected void onDestroy() {
+        mDragLayout.removeDragCallback(mDragCallback);
+        super.onDestroy();
+    }
+
+    @Override
     public void onActionModeStarted(ActionMode mode) {
         super.onActionModeStarted(mode);
         if (mode.getTag() == CalculatorFormula.TAG_ACTION_MODE) {
@@ -1136,11 +1176,13 @@
     }
 
     private void showHistoryFragment(int transit) {
-        getFragmentManager().beginTransaction()
-                .replace(R.id.history_frame, mHistoryFragment, HistoryFragment.TAG)
-                .setTransition(transit)
-                .addToBackStack(HistoryFragment.TAG)
-                .commit();
+        if (!mDragLayout.isOpen()) {
+            getFragmentManager().beginTransaction()
+                    .replace(R.id.history_frame, mHistoryFragment, HistoryFragment.TAG)
+                    .setTransition(transit)
+                    .addToBackStack(HistoryFragment.TAG)
+                    .commit();
+        }
     }
 
     private void displayMessage(String title, String message) {
@@ -1263,6 +1305,12 @@
         }
     }
 
+    private boolean isViewTarget(View view, MotionEvent event) {
+        mHitRect.set(0, 0, view.getWidth(), view.getHeight());
+        mDragLayout.offsetDescendantRectToMyCoords(view, mHitRect);
+        return mHitRect.contains((int) event.getX(), (int) event.getY());
+    }
+
     @Override
     public boolean onPaste(ClipData clip) {
         final ClipData.Item item = clip.getItemCount() == 0 ? null : clip.getItemAt(0);
diff --git a/src/com/android/calculator2/DragController.java b/src/com/android/calculator2/DragController.java
new file mode 100644
index 0000000..d448cbc
--- /dev/null
+++ b/src/com/android/calculator2/DragController.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2016 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.calculator2;
+
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+import android.widget.TextView;
+
+/**
+ * Contains the logic for animating the recyclerview elements on drag.
+ */
+public final class DragController {
+
+    // References to views from the Calculator Display.
+    private CalculatorFormula mDisplayFormula;
+    private CalculatorResult mDisplayResult;
+    private View mToolbar;
+
+    private int mFormulaTranslationY;
+    private int mFormulaTranslationX;
+    private float mFormulaScale;
+
+    private int mResultTranslationY;
+    private int mResultTranslationX;
+
+    private boolean mAnimationInitialized;
+
+    public void setDisplayFormula(CalculatorFormula formula) {
+        mDisplayFormula = formula;
+    }
+
+    public void setDisplayResult(CalculatorResult result) {
+        mDisplayResult = result;
+    }
+
+    public void setToolbar(View toolbar) {
+        mToolbar = toolbar;
+    }
+
+    public void animateViews(float yFraction, RecyclerView recyclerView, int itemCount) {
+        final HistoryAdapter.ViewHolder vh = (HistoryAdapter.ViewHolder)
+                recyclerView.findViewHolderForAdapterPosition(itemCount - 1);
+        if (vh != null) {
+            final CalculatorFormula formula = vh.getFormula();
+            final CalculatorResult result = vh.getResult();
+            final TextView date = vh.getDate();
+
+            if (!mAnimationInitialized) {
+                // Calculate the scale for the text
+                mFormulaScale = (mDisplayFormula.getTextSize() * 1.0f) / formula.getTextSize();
+
+                // Baseline of formula moves by the difference in formula bottom padding.
+                mFormulaTranslationY =
+                        mDisplayFormula.getPaddingBottom() - formula.getPaddingBottom()
+                        + mDisplayResult.getHeight() - result.getHeight();
+
+                // Right border of formula moves by the difference in formula end padding.
+                mFormulaTranslationX = mDisplayFormula.getPaddingEnd() - formula.getPaddingEnd();
+
+                // Baseline of result moves by the difference in result bottom padding.
+                mResultTranslationY = mDisplayResult.getPaddingBottom() - result.getPaddingBottom();
+
+                mResultTranslationX = mDisplayResult.getPaddingEnd() - result.getPaddingEnd();
+
+                mAnimationInitialized = true;
+            }
+
+            if (mAnimationInitialized) {
+                formula.setPivotX(formula.getWidth() - formula.getPaddingEnd());
+                formula.setPivotY(formula.getHeight() - formula.getPaddingBottom());
+
+                result.setPivotX(result.getWidth() - result.getPaddingEnd());
+                result.setPivotY(result.getHeight() - result.getPaddingBottom());
+
+                final float resultTranslationX = (mResultTranslationX * yFraction)
+                        - mResultTranslationX;
+                result.setTranslationX(resultTranslationX);
+
+                // Scale linearly between -mResultTranslationY and 0.
+                final float resultTranslationY =
+                        (mResultTranslationY * yFraction) - mResultTranslationY;
+                result.setTranslationY(resultTranslationY);
+
+                final float scale = mFormulaScale - (mFormulaScale * yFraction) + yFraction;
+                formula.setScaleY(scale);
+                formula.setScaleX(scale);
+
+                final float formulaTranslationX = (mFormulaTranslationX * yFraction)
+                        - mFormulaTranslationX;
+                formula.setTranslationX(formulaTranslationX);
+
+                // Scale linearly between -FormulaTranslationY and 0.
+                final float formulaTranslationY =
+                        (mFormulaTranslationY * yFraction) - mFormulaTranslationY;
+                formula.setTranslationY(formulaTranslationY);
+
+                // We want the date to start out above the visible screen with
+                // this distance decreasing as it's pulled down.
+                final float dateTranslationY =
+                        - mToolbar.getHeight() * (1 - yFraction)
+                        + formulaTranslationY
+                        - mDisplayFormula.getPaddingTop()
+                        + (mDisplayFormula.getPaddingTop() * yFraction);
+                date.setTranslationY(dateTranslationY);
+
+                // Move up all ViewHolders above the current expression.
+                for (int i = recyclerView.getChildCount() - 2; i >= 0; --i) {
+                    final RecyclerView.ViewHolder vh2 =
+                            recyclerView.getChildViewHolder(recyclerView.getChildAt(i));
+                    if (vh2 != null) {
+                        final View view = vh2.itemView;
+                        if (view != null){
+                            view.setTranslationY(dateTranslationY);
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/src/com/android/calculator2/DragLayout.java b/src/com/android/calculator2/DragLayout.java
index 93a5974..101b62e 100644
--- a/src/com/android/calculator2/DragLayout.java
+++ b/src/com/android/calculator2/DragLayout.java
@@ -17,7 +17,6 @@
 package com.android.calculator2;
 
 import android.content.Context;
-import android.graphics.Rect;
 import android.os.Bundle;
 import android.os.Parcelable;
 import android.support.v4.view.ViewCompat;
@@ -28,25 +27,30 @@
 import android.widget.FrameLayout;
 import android.widget.RelativeLayout;
 
+import java.util.ArrayList;
+import java.util.List;
+
 public class DragLayout extends RelativeLayout {
 
+    private static final String TAG = "DragLayout";
     private static final double AUTO_OPEN_SPEED_LIMIT = 800.0;
     private static final String KEY_IS_OPEN = "IS_OPEN";
     private static final String KEY_SUPER_STATE = "SUPER_STATE";
 
-    private final Rect mHitRect = new Rect();
-
-    private CalculatorDisplay mCalculatorDisplay;
     private FrameLayout mHistoryFrame;
     private ViewDragHelper mDragHelper;
 
-    private OnDragCallback mOnDragCallback;
+    private final List<DragCallback> mDragCallbacks = new ArrayList<>();
 
     private int mDraggingState = ViewDragHelper.STATE_IDLE;
     private int mDraggingBorder;
     private int mVerticalRange;
     private boolean mIsOpen;
 
+    // Used to determine whether a touch event should be intercepted.
+    private float mInitialDownX;
+    private float mInitialDownY;
+
     public DragLayout(Context context, AttributeSet attrs) {
         super(context, attrs);
     }
@@ -55,7 +59,6 @@
     protected void onFinishInflate() {
         mDragHelper = ViewDragHelper.create(this, 1.0f, new DragHelperCallback());
         mHistoryFrame = (FrameLayout) findViewById(R.id.history_frame);
-        mCalculatorDisplay = (CalculatorDisplay) findViewById(R.id.display);
         super.onFinishInflate();
     }
 
@@ -63,17 +66,24 @@
     protected void onLayout(boolean changed, int l, int t, int r, int b) {
         super.onLayout(changed, l, t, r, b);
         if (changed) {
-            mHistoryFrame.setTranslationY(-(b - t) + mCalculatorDisplay.getBottom());
-
+            for (DragCallback c : mDragCallbacks) {
+               c.onLayout(t-b);
+            }
             if (mIsOpen) {
                 setOpen();
+            } else {
+                setClosed();
             }
         }
     }
 
     @Override
     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
-        mVerticalRange = h - mCalculatorDisplay.getMeasuredHeight();
+        int height = 0;
+        for (DragCallback c : mDragCallbacks) {
+            height += c.getDisplayHeight();
+        }
+        mVerticalRange = h - height;
         super.onSizeChanged(w, h, oldw, oldh);
     }
 
@@ -97,13 +107,41 @@
 
     @Override
     public boolean onInterceptTouchEvent(MotionEvent event) {
-        return (isDisplayTarget(event) || isHistoryTarget(event))
-                && mDragHelper.shouldInterceptTouchEvent(event);
+        // First verify that we don't have a large deltaX (that the user is not trying to
+        // horizontally scroll).
+        final float x = event.getX();
+        final float y = event.getY();
+        final int action = event.getAction();
+
+        switch (action) {
+            case MotionEvent.ACTION_DOWN:
+                mInitialDownX = x;
+                mInitialDownY = y;
+                break;
+            case MotionEvent.ACTION_MOVE:
+                final float deltaX = Math.abs(x - mInitialDownX);
+                final float deltaY = Math.abs(y - mInitialDownY);
+                final int slop = mDragHelper.getTouchSlop();
+                if (deltaY > slop && deltaX > deltaY) {
+                    mDragHelper.cancel();
+                    return false;
+                }
+        }
+
+        boolean doDrag = true;
+        for (DragCallback c : mDragCallbacks) {
+            doDrag &= c.allowDrag(event);
+        }
+        return doDrag && mDragHelper.shouldInterceptTouchEvent(event);
     }
 
     @Override
     public boolean onTouchEvent(MotionEvent event) {
-        if (isDisplayTarget(event) || isHistoryTarget(event) || isMoving()) {
+        boolean doIntercept = true;
+        for (DragCallback c : mDragCallbacks) {
+            doIntercept &= c.shouldInterceptTouchEvent(event);
+        }
+        if (doIntercept || isMoving()) {
             mDragHelper.processTouchEvent(event);
             return true;
         } else {
@@ -119,24 +157,12 @@
     }
 
     private void onStartDragging() {
-        mOnDragCallback.onStartDragging();
+        for (DragCallback c : mDragCallbacks) {
+            c.onStartDragging();
+        }
         mHistoryFrame.setVisibility(VISIBLE);
     }
 
-    private boolean isViewTarget(View view, MotionEvent event) {
-        view.getHitRect(mHitRect);
-        offsetDescendantRectToMyCoords(view, mHitRect);
-        return mHitRect.contains((int) event.getRawX(), (int) event.getRawY());
-    }
-
-    private boolean isDisplayTarget(MotionEvent event) {
-        return isViewTarget(mCalculatorDisplay, event);
-    }
-
-    private boolean isHistoryTarget(MotionEvent event) {
-        return isViewTarget(mHistoryFrame, event);
-    }
-
     public boolean isMoving() {
         return mDraggingState == ViewDragHelper.STATE_DRAGGING
                 || mDraggingState == ViewDragHelper.STATE_SETTLING;
@@ -153,22 +179,45 @@
     }
 
     public void setClosed() {
+        // Scroll the RecyclerView to the bottom.
+        for (DragCallback c : mDragCallbacks) {
+            c.onClosed();
+        }
         mDragHelper.smoothSlideViewTo(mHistoryFrame, 0, 0);
-        mIsOpen = false;
-        mOnDragCallback.onDragToClose();
         mHistoryFrame.setVisibility(GONE);
+        mIsOpen = false;
     }
 
-    public void setOnDragCallback(OnDragCallback callback) {
-        mOnDragCallback = callback;
+    public void addDragCallback(DragCallback callback) {
+        mDragCallbacks.add(callback);
     }
 
-    public interface OnDragCallback {
+    public void removeDragCallback(DragCallback callback) {
+        mDragCallbacks.remove(callback);
+    }
+
+    /**
+     * Callbacks for coordinating with the RecyclerView or HistoryFragment.
+     */
+    public interface DragCallback {
         // Callback when a drag in any direction begins.
         void onStartDragging();
 
-        // Callback when a drag is used to close.
-        void onDragToClose();
+        // Animate the RecyclerView text.
+        void whileDragging(float yFraction);
+
+        // Scroll the RecyclerView to the bottom before closing the frame.
+        void onClosed();
+
+        // Whether we should allow the drag to happen
+        boolean allowDrag(MotionEvent event);
+
+        // Whether we should intercept the touch event
+        boolean shouldInterceptTouchEvent(MotionEvent event);
+
+        int getDisplayHeight();
+
+        void onLayout(int translation);
     }
 
     public class DragHelperCallback extends ViewDragHelper.Callback {
@@ -197,6 +246,11 @@
         @Override
         public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
             mDraggingBorder = top;
+
+            // Animate RecyclerView text.
+            for (DragCallback c : mDragCallbacks) {
+                c.whileDragging(top / (mVerticalRange * 1.0f));
+            }
         }
 
         @Override
@@ -218,14 +272,6 @@
 
         @Override
         public void onViewReleased(View releasedChild, float xvel, float yvel) {
-            if (mDraggingBorder == 0) {
-                setClosed();
-                return;
-            }
-            if (mDraggingBorder == mVerticalRange) {
-                setOpen();
-                return;
-            }
             boolean settleToOpen = false;
             final float threshold = mVerticalRange / 2;
             if (yvel > AUTO_OPEN_SPEED_LIMIT) {
diff --git a/src/com/android/calculator2/HistoryAdapter.java b/src/com/android/calculator2/HistoryAdapter.java
new file mode 100644
index 0000000..6a630d9
--- /dev/null
+++ b/src/com/android/calculator2/HistoryAdapter.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2016 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.calculator2;
+
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.List;
+
+/**
+ * Adapter for RecyclerView of HistoryItems.
+ */
+public class HistoryAdapter extends RecyclerView.Adapter<HistoryAdapter.ViewHolder> {
+
+    private final List<HistoryItem> mDataSet = new ArrayList<>();
+    private String mCurrentExpression;
+
+    public HistoryAdapter(int[] dataset, String currentExpression) {
+        mCurrentExpression = currentExpression;
+        // Temporary dataset
+        final Calendar calendar = Calendar.getInstance();
+        for (int i: dataset) {
+            calendar.set(2016, 10, i);
+            mDataSet.add(new HistoryItem(calendar.getTimeInMillis(), Integer.toString(i) + "+1",
+                    Integer.toString(i+1)));
+        }
+    }
+
+    @Override
+    public HistoryAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+        final View v = LayoutInflater.from(parent.getContext())
+                .inflate(R.layout.history_item, parent, false);
+        return new ViewHolder(v);
+    }
+
+    @Override
+    public void onBindViewHolder(HistoryAdapter.ViewHolder holder, int position) {
+        final HistoryItem item = mDataSet.get(position);
+
+        if (!isCurrentExpressionItem(position)) {
+            holder.mDate.setText(item.getDateString());
+            holder.mDate.setContentDescription(item.getDateDescription());
+        } else {
+            holder.mDate.setText(mCurrentExpression);
+            holder.mDate.setContentDescription(mCurrentExpression);
+        }
+        holder.mFormula.setText(item.getFormula());
+        holder.mResult.setText(item.getResult());
+    }
+
+    @Override
+    public void onViewRecycled(ViewHolder holder) {
+        holder.mDate.setContentDescription(null);
+        holder.mDate.setText(null);
+        holder.mFormula.setText(null);
+        holder.mResult.setText(null);
+
+        super.onViewRecycled(holder);
+    }
+
+    @Override
+    public int getItemCount() {
+        return mDataSet.size();
+    }
+
+    private boolean isCurrentExpressionItem(int position) {
+        return position == mDataSet.size() - 1;
+    }
+
+    public static class ViewHolder extends RecyclerView.ViewHolder {
+
+        private final TextView mDate;
+        private final CalculatorFormula mFormula;
+        private final CalculatorResult mResult;
+
+        public ViewHolder(View v) {
+            super(v);
+            mDate = (TextView) v.findViewById(R.id.history_date);
+            mFormula = (CalculatorFormula) v.findViewById(R.id.history_formula);
+            mResult = (CalculatorResult) v.findViewById(R.id.history_result);
+        }
+
+        public CalculatorFormula getFormula() {
+            return mFormula;
+        }
+
+        public CalculatorResult getResult() {
+            return mResult;
+        }
+
+        public TextView getDate() {
+            return mDate;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/calculator2/HistoryFragment.java b/src/com/android/calculator2/HistoryFragment.java
index ef75ed6..3fc8e89 100644
--- a/src/com/android/calculator2/HistoryFragment.java
+++ b/src/com/android/calculator2/HistoryFragment.java
@@ -21,9 +21,11 @@
 import android.app.Fragment;
 import android.app.FragmentTransaction;
 import android.os.Bundle;
+import android.support.v7.widget.RecyclerView;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.MenuItem;
+import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.Toolbar;
@@ -32,12 +34,72 @@
 
     public static final String TAG = "HistoryFragment";
 
+    private final DragLayout.DragCallback mDragCallback =
+            new DragLayout.DragCallback() {
+                @Override
+                public void onStartDragging() {
+                    // no-op
+                }
+
+                @Override
+                public void whileDragging(float yFraction) {
+                    mDragController.animateViews(yFraction, mRecyclerView, mAdapter.getItemCount());
+                }
+
+                @Override
+                public void onClosed() {
+                    mRecyclerView.scrollToPosition(mAdapter.getItemCount() - 1);
+                }
+
+                @Override
+                public boolean allowDrag(MotionEvent event) {
+                    // Do not allow drag if the recycler view can move down more
+                    return !mRecyclerView.canScrollVertically(1);
+                }
+
+                @Override
+                public boolean shouldInterceptTouchEvent(MotionEvent event) {
+                    return true;
+                }
+
+                @Override
+                public int getDisplayHeight() {
+                    return 0;
+                }
+
+                @Override
+                public void onLayout(int translation) {
+                    // no-op
+                }
+            };
+
+    private final DragController mDragController = new DragController();
+
+    private RecyclerView mRecyclerView;
+    private HistoryAdapter mAdapter;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        // Temporary data
+        final int[] testArray = {1, 2, 3, 4, 5, 6, 7};
+        mAdapter = new HistoryAdapter(testArray,
+                getContext().getResources().getString(R.string.title_current_expression));
+    }
+
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container,
             Bundle savedInstanceState) {
         final View view = inflater.inflate(
                 R.layout.fragment_history, container, false /* attachToRoot */);
 
+        mRecyclerView = (RecyclerView) view.findViewById(R.id.history_recycler_view);
+
+        // The size of the RecyclerView is not affected by the adapter's contents.
+        mRecyclerView.setHasFixedSize(true);
+        mRecyclerView.setAdapter(mAdapter);
+
         final Toolbar toolbar = (Toolbar) view.findViewById(R.id.history_toolbar);
         toolbar.inflateMenu(R.menu.fragment_history);
         toolbar.setOnMenuItemClickListener(new Toolbar.OnMenuItemClickListener() {
@@ -61,6 +123,16 @@
     }
 
     @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+
+        initializeController();
+        final DragLayout dragLayout = (DragLayout) getActivity().findViewById(R.id.drag_layout);
+        dragLayout.removeDragCallback(mDragCallback);
+        dragLayout.addDragCallback(mDragCallback);
+    }
+
+    @Override
     public Animator onCreateAnimator(int transit, boolean enter, int nextAnim) {
         final View view = getView();
         final int height = getResources().getDisplayMetrics().heightPixels;
@@ -72,6 +144,29 @@
         return null;
     }
 
+    @Override
+    public void onDestroyView() {
+        final DragLayout dragLayout = (DragLayout) getActivity().findViewById(R.id.drag_layout);
+        if (dragLayout != null) {
+            dragLayout.removeDragCallback(mDragCallback);
+        }
+        super.onDestroy();
+    }
+
+    private void initializeController() {
+        mDragController.setDisplayFormula(
+                (CalculatorFormula) getActivity().findViewById(R.id.formula));
+
+        mDragController.setDisplayResult(
+                (CalculatorResult) getActivity().findViewById(R.id.result));
+
+        mDragController.setToolbar(getActivity().findViewById(R.id.toolbar));
+
+        // Initialize the current expression element to dimensions that match the display to avoid
+        // flickering and scrolling when elements expand on drag start.
+        mDragController.animateViews(1.0f, mRecyclerView, mAdapter.getItemCount());
+    }
+
     private void clearHistory() {
         Log.d(TAG, "Dropping history table");
     }
diff --git a/src/com/android/calculator2/HistoryItem.java b/src/com/android/calculator2/HistoryItem.java
new file mode 100644
index 0000000..0e46293
--- /dev/null
+++ b/src/com/android/calculator2/HistoryItem.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2016 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.calculator2;
+
+import android.text.format.DateFormat;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+public class HistoryItem {
+
+    private static final String dateFormat = "EEEMMMd";
+    private static final String descriptionFormat = "EEEEMMMMd";
+
+    private Date mDate;
+    private String mFormula;
+    private String mResult;
+
+    public HistoryItem(long millis, String formula, String result) {
+        mDate = new Date(millis);
+        mFormula = formula;
+        mResult = result;
+    }
+
+    public String getDateString() {
+        // TODO: Use DateUtils?
+        final Locale l = Locale.getDefault();
+        final String datePattern = DateFormat.getBestDateTimePattern(l, dateFormat);
+        return new SimpleDateFormat(datePattern, l).format(mDate);
+    }
+
+    public String getDateDescription() {
+        final Locale l = Locale.getDefault();
+        final String descriptionPattern = DateFormat.getBestDateTimePattern(l, descriptionFormat);
+        return new SimpleDateFormat(descriptionPattern, l).format(mDate);
+    }
+
+    public String getFormula() {
+        return mFormula;
+    }
+
+    public String getResult() {
+        return mResult;
+    }
+}
\ No newline at end of file