Example activity for RecyclerView

Also includes a very basic custom RecyclerView LayoutManager.

Change-Id: Ic6dca39a4e71dbe8b3b82965299bc554fad18a43
diff --git a/samples/Support7Demos/Android.mk b/samples/Support7Demos/Android.mk
index 0e43e3d..ca8310f 100644
--- a/samples/Support7Demos/Android.mk
+++ b/samples/Support7Demos/Android.mk
@@ -26,7 +26,8 @@
         android-support-v4 \
         android-support-v7-appcompat \
         android-support-v7-gridlayout \
-        android-support-v7-mediarouter
+        android-support-v7-mediarouter \
+        android-support-v7-recyclerview
 LOCAL_RESOURCE_DIR = \
         $(LOCAL_PATH)/res \
         frameworks/support/v7/appcompat/res \
diff --git a/samples/Support7Demos/AndroidManifest.xml b/samples/Support7Demos/AndroidManifest.xml
index 4735433..4ce63b7 100644
--- a/samples/Support7Demos/AndroidManifest.xml
+++ b/samples/Support7Demos/AndroidManifest.xml
@@ -172,5 +172,15 @@
             </intent-filter>
         </activity>
 
+        <!-- RecyclerView samples -->
+        <activity android:name=".widget.RecyclerViewActivity"
+                android:label="@string/recycler_view"
+                android:theme="@style/Theme.AppCompat">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="com.example.android.supportv7.SAMPLE_CODE" />
+            </intent-filter>
+        </activity>
+
     </application>
 </manifest>
diff --git a/samples/Support7Demos/res/values/strings.xml b/samples/Support7Demos/res/values/strings.xml
index 1b13623..c36809b 100644
--- a/samples/Support7Demos/res/values/strings.xml
+++ b/samples/Support7Demos/res/values/strings.xml
@@ -95,4 +95,5 @@
     <string name="sample_media_route_activity_local">Local Playback</string>
     <string name="sample_media_route_activity_presentation">Local Playback on Presentation Display</string>
 
+    <string name="recycler_view">RecyclerView</string>
 </resources>
diff --git a/samples/Support7Demos/src/com/example/android/supportv7/widget/RecyclerViewActivity.java b/samples/Support7Demos/src/com/example/android/supportv7/widget/RecyclerViewActivity.java
new file mode 100644
index 0000000..7497c9a
--- /dev/null
+++ b/samples/Support7Demos/src/com/example/android/supportv7/widget/RecyclerViewActivity.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2013 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.example.android.supportv7.widget;
+
+import android.R;
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.support.v4.view.MenuItemCompat;
+import android.support.v7.widget.RecyclerView;
+import android.util.DisplayMetrics;
+import android.util.TypedValue;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.View.MeasureSpec;
+import android.widget.TextView;
+
+public class RecyclerViewActivity extends Activity {
+    private RecyclerView mRecyclerView;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        final RecyclerView rv = new RecyclerView(this);
+        rv.setLayoutManager(new MyLayoutManager(this));
+        rv.setHasFixedSize(true);
+        rv.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+                ViewGroup.LayoutParams.MATCH_PARENT));
+        rv.setAdapter(new MyAdapter());
+        setContentView(rv);
+
+        mRecyclerView = rv;
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        super.onCreateOptionsMenu(menu);
+        MenuItemCompat.setShowAsAction(menu.add("Layout"), MenuItemCompat.SHOW_AS_ACTION_IF_ROOM);
+        return true;
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        mRecyclerView.requestLayout();
+        return super.onOptionsItemSelected(item);
+    }
+
+    private static final int SCROLL_DISTANCE = 80; // dp
+
+    /**
+     * A basic ListView-style LayoutManager.
+     */
+    class MyLayoutManager extends RecyclerView.LayoutManager {
+        private static final String TAG = "MyLayoutManager";
+        private int mFirstPosition;
+        private final int mScrollDistance;
+
+        public MyLayoutManager(Context c) {
+            final DisplayMetrics dm = c.getResources().getDisplayMetrics();
+            mScrollDistance = (int) (SCROLL_DISTANCE * dm.density + 0.5f);
+        }
+
+        @Override
+        public void layoutChildren(RecyclerView.Adapter adapter, RecyclerView.Recycler recycler) {
+            final RecyclerView parent = getRecyclerView();
+            final int parentBottom = parent.getHeight() - parent.getPaddingBottom();
+
+            recycler.scrapAllViewsAttached();
+
+            int top = parent.getPaddingTop();
+            int bottom;
+            final int left = parent.getPaddingLeft();
+            final int right = parent.getWidth() - parent.getPaddingRight();
+
+            final int count = adapter.getItemCount();
+            for (int i = 0; mFirstPosition + i < count && top < parentBottom; i++, top = bottom) {
+                View v = recycler.getViewForPosition(mFirstPosition + i);
+                addView(v, i);
+                v.measure(MeasureSpec.makeMeasureSpec(right - left, MeasureSpec.EXACTLY),
+                        MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+                bottom = top + v.getMeasuredHeight();
+                v.layout(left, top, right, bottom);
+            }
+
+            recycler.detachDirtyScrapViews();
+        }
+
+        @Override
+        public RecyclerView.LayoutParams generateDefaultLayoutParams() {
+            return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+                    ViewGroup.LayoutParams.WRAP_CONTENT);
+        }
+
+        @Override
+        public boolean canScrollVertically() {
+            return true;
+        }
+
+        @Override
+        public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler) {
+            final RecyclerView parent = getRecyclerView();
+            if (parent.getChildCount() == 0) {
+                return 0;
+            }
+
+            int scrolled = 0;
+            final int left = parent.getPaddingLeft();
+            final int right = parent.getWidth() - parent.getPaddingRight();
+            if (dy < 0) {
+                while (scrolled > dy) {
+                    final View topView = parent.getChildAt(0);
+                    final int hangingTop = Math.max(-topView.getTop(), 0);
+                    final int scrollBy = Math.min(scrolled - dy, hangingTop);
+                    scrolled -= scrollBy;
+                    parent.offsetChildrenVertical(scrollBy);
+                    if (mFirstPosition > 0 && scrolled > dy) {
+                        mFirstPosition--;
+                        View v = recycler.getViewForPosition(mFirstPosition);
+                        addView(v, 0);
+                        v.measure(MeasureSpec.makeMeasureSpec(right - left, MeasureSpec.EXACTLY),
+                                MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+                        final int bottom = topView.getTop(); // TODO decorated top?
+                        final int top = bottom - v.getMeasuredHeight();
+                        v.layout(left, top, right, bottom);
+                    } else {
+                        break;
+                    }
+                }
+            } else if (dy > 0) {
+                final int parentHeight = parent.getHeight();
+                while (scrolled < dy) {
+                    final View bottomView = parent.getChildAt(parent.getChildCount() - 1);
+                    final int hangingBottom = Math.max(bottomView.getBottom() - parentHeight, 0);
+                    final int scrollBy = -Math.min(dy - scrolled, hangingBottom);
+                    scrolled -= scrollBy;
+                    parent.offsetChildrenVertical(scrollBy);
+                    if (scrolled < dy &&
+                            parent.getAdapter().getItemCount() > mFirstPosition + parent.getChildCount()) {
+                        View v = recycler.getViewForPosition(
+                                mFirstPosition + parent.getChildCount());
+                        final int top = parent.getChildAt(parent.getChildCount() - 1).getBottom();
+                        addView(v);
+                        v.measure(MeasureSpec.makeMeasureSpec(right - left, MeasureSpec.EXACTLY),
+                                MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+                        final int bottom = top + v.getMeasuredHeight();
+                        v.layout(left, top, right, bottom);
+                    } else {
+                        break;
+                    }
+                }
+            }
+            detachAndScrapViewsOutOfBounds(recycler);
+            return scrolled;
+        }
+
+        @Override
+        public View onFocusSearchFailed(View focused, int direction,
+                RecyclerView.Recycler recycler) {
+            final RecyclerView rv = getRecyclerView();
+            final int oldFirstPosition = mFirstPosition;
+            final int oldCount = rv.getChildCount();
+
+            if (oldCount == 0) {
+                return null;
+            }
+
+            final int left = rv.getPaddingLeft();
+            final int right = rv.getWidth() - rv.getPaddingRight();
+
+            View toFocus = null;
+            int newViewsHeight = 0;
+            if (direction == View.FOCUS_UP || direction == View.FOCUS_BACKWARD) {
+                while (mFirstPosition > 0 && newViewsHeight < mScrollDistance) {
+                    mFirstPosition--;
+                    View v = recycler.getViewForPosition(mFirstPosition);
+                    final int bottom = rv.getChildAt(0).getTop(); // TODO decorated top?
+                    addView(v, 0);
+                    v.measure(MeasureSpec.makeMeasureSpec(right - left, MeasureSpec.EXACTLY),
+                            MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+                    final int top = bottom - v.getMeasuredHeight();
+                    v.layout(left, top, right, bottom);
+                    if (v.isFocusable()) {
+                        toFocus = v;
+                        break;
+                    }
+                }
+            }
+            if (direction == View.FOCUS_DOWN || direction == View.FOCUS_FORWARD) {
+                while (mFirstPosition + rv.getChildCount() < rv.getAdapter().getItemCount() &&
+                        newViewsHeight < mScrollDistance) {
+                    View v = recycler.getViewForPosition(mFirstPosition + rv.getChildCount());
+                    final int top = rv.getChildAt(rv.getChildCount() - 1).getBottom();
+                    addView(v);
+                    v.measure(MeasureSpec.makeMeasureSpec(right - left, MeasureSpec.EXACTLY),
+                            MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+                    final int bottom = top + v.getMeasuredHeight();
+                    v.layout(left, top, right, bottom);
+                    if (v.isFocusable()) {
+                        toFocus = v;
+                        break;
+                    }
+                }
+            }
+
+            return toFocus;
+        }
+
+        public void detachAndScrapViewsOutOfBounds(RecyclerView.Recycler recycler) {
+            final RecyclerView parent = getRecyclerView();
+            final int childCount = parent.getChildCount();
+            final int parentWidth = parent.getWidth();
+            final int parentHeight = parent.getHeight();
+            boolean foundFirst = false;
+            int first = 0;
+            int last = 0;
+            for (int i = 0; i < childCount; i++) {
+                final View v = parent.getChildAt(i);
+                if (v.hasFocus() || (v.getRight() >= 0 && v.getLeft() <= parentWidth &&
+                        v.getBottom() >= 0 && v.getTop() <= parentHeight)) {
+                    if (!foundFirst) {
+                        first = i;
+                        foundFirst = true;
+                    }
+                    last = i;
+                }
+            }
+            for (int i = childCount - 1; i > last; i--) {
+                recycler.detachAndScrapView(parent.getChildAt(i));
+            }
+            for (int i = 0; i < first; i++) {
+                recycler.detachAndScrapView(parent.getChildAt(i));
+            }
+            if (parent.getChildCount() == 0) {
+                mFirstPosition = 0;
+            } else {
+                mFirstPosition += first;
+            }
+        }
+    }
+
+    class MyAdapter extends RecyclerView.Adapter {
+        private int mBackground;
+
+        public MyAdapter() {
+            TypedValue val = new TypedValue();
+            RecyclerViewActivity.this.getTheme().resolveAttribute(
+                    R.attr.selectableItemBackground, val, true);
+            mBackground = val.resourceId;
+        }
+
+        @Override
+        public RecyclerView.ViewHolder createViewHolder(ViewGroup parent, int viewType) {
+            ViewHolder h = new ViewHolder(new TextView(RecyclerViewActivity.this));
+            h.textView.setMinimumHeight(128);
+            h.textView.setFocusable(true);
+            h.textView.setBackgroundResource(mBackground);
+            return h;
+        }
+
+        @Override
+        public void bindViewHolder(RecyclerView.ViewHolder holder, int position) {
+            ((ViewHolder) holder).textView.setText("Item " + position);
+        }
+
+        @Override
+        public int getItemCount() {
+            return 100;
+        }
+    }
+
+    static class ViewHolder extends RecyclerView.ViewHolder {
+        public TextView textView;
+
+        public ViewHolder(TextView v) {
+            super(v);
+            textView = v;
+        }
+    }
+}