Bug 1310081 - 1. Make the tabs list grid view a RecyclerView. r?sebastian draft
authorTom Klein <twointofive@gmail.com>
Mon, 12 Sep 2016 11:21:51 -0500
changeset 441610 e77eeb59c014f4a7759a84c6e1a6559933031b18
parent 440454 05e5b12f41df270b31955ff7e6d09245c1f83a7a
child 441611 f9e68ff4caf16235a81a934681c6d652e3394533
push id36467
push userbmo:twointofive@gmail.com
push dateSun, 20 Nov 2016 03:37:17 +0000
reviewerssebastian
bugs1310081
milestone53.0a1
Bug 1310081 - 1. Make the tabs list grid view a RecyclerView. r?sebastian Our previous GridLayout settings gave extra horizontal space to the padding between items, but GridLayoutManager by default simply left aligns fixed width items in their column, so the item's width has been changed to fill_parent and the item title has been switched to fixed width (since otherwise it looks broken when it expands to an item width larger than the thumbnail width). The drawback is that clicking on the extra width part of an item activates the tab, even though it would seem from what's being displayed that the item should end at the vertical edge of the thumbnail - that will be fixed in a future commit. Both the list and grid tabs panel views are now RecyclerViews, so move TabsLayoutRecyclerAdapter.java to TabsLayoutAdapter.java. MozReview-Commit-ID: CBrxw1HfRcP
mobile/android/base/java/org/mozilla/gecko/tabs/TabsGridLayout.java
mobile/android/base/java/org/mozilla/gecko/tabs/TabsGridLayoutAnimator.java
mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayout.java
mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayoutAdapter.java
mobile/android/base/java/org/mozilla/gecko/tabs/TabsListLayout.java
mobile/android/base/moz.build
mobile/android/base/resources/layout/tabs_layout_item_view.xml
mobile/android/base/resources/values-land/dimens.xml
mobile/android/base/resources/values-sw240dp/dimens.xml
mobile/android/base/resources/values-sw360dp/dimens.xml
mobile/android/base/resources/values-sw400dp/dimens.xml
mobile/android/base/resources/values-v11/themes.xml
mobile/android/base/resources/values-v21/themes.xml
mobile/android/base/resources/values-xlarge-land-v11/dimens.xml
mobile/android/base/resources/values-xlarge-v11/dimens.xml
mobile/android/base/resources/values/attrs.xml
mobile/android/base/resources/values/dimens.xml
mobile/android/base/resources/values/styles.xml
mobile/android/base/resources/values/themes.xml
mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/BaseTest.java
--- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsGridLayout.java
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsGridLayout.java
@@ -1,712 +1,77 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.tabs;
 
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.R;
-import org.mozilla.gecko.Tab;
-import org.mozilla.gecko.Tabs;
-import org.mozilla.gecko.animation.PropertyAnimator;
-import org.mozilla.gecko.tabs.TabsPanel.TabsLayout;
-import org.mozilla.gecko.widget.themed.ThemedRelativeLayout;
 
 import android.content.Context;
 import android.content.res.Resources;
-import android.content.res.TypedArray;
-import android.graphics.PointF;
-import android.graphics.Rect;
-import android.os.Build;
+import android.support.v7.widget.GridLayoutManager;
 import android.util.AttributeSet;
-import android.util.SparseArray;
-import android.view.MotionEvent;
-import android.view.VelocityTracker;
-import android.view.View;
-import android.view.ViewConfiguration;
-import android.view.ViewGroup;
-import android.view.ViewTreeObserver;
-import android.view.animation.DecelerateInterpolator;
-import android.widget.AbsListView;
-import android.widget.AdapterView;
-import android.widget.Button;
-import android.widget.GridView;
-import android.animation.Animator;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.animation.PropertyValuesHolder;
-import android.animation.ValueAnimator;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * A tabs layout implementation for the tablet redesign (bug 1014156) and later ported to mobile (bug 1193745).
- */
-
-class TabsGridLayout extends GridView
-                     implements TabsLayout,
-                                Tabs.OnTabsChangedListener {
-
-    private static final String LOGTAG = "Gecko" + TabsGridLayout.class.getSimpleName();
-
-    public static final int ANIM_DELAY_MULTIPLE_MS = 20;
-    private static final int ANIM_TIME_MS = 200;
-    private static final DecelerateInterpolator ANIM_INTERPOLATOR = new DecelerateInterpolator();
-
-    private final SparseArray<PointF> tabLocations = new SparseArray<PointF>();
-    private final boolean isPrivate;
-    private final TabsLayoutAdapter tabsAdapter;
-    private final int columnWidth;
-    private TabsPanel tabsPanel;
-    private int lastSelectedTabId;
-
-    public TabsGridLayout(final Context context, final AttributeSet attrs) {
-        super(context, attrs, R.attr.tabGridLayoutViewStyle);
-
-        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabsLayout);
-        isPrivate = (a.getInt(R.styleable.TabsLayout_tabs, 0x0) == 1);
-        a.recycle();
-
-        tabsAdapter = new TabsGridLayoutAdapter(context);
-        setAdapter(tabsAdapter);
-
-        setRecyclerListener(new RecyclerListener() {
-            @Override
-            public void onMovedToScrapHeap(View view) {
-                TabsLayoutItemView item = (TabsLayoutItemView) view;
-                item.setThumbnail(null);
-            }
-        });
-
-        // The clipToPadding setting in the styles.xml doesn't seem to be working (bug 1101784)
-        // so lets set it manually in code for the moment as it's needed for the padding animation
-        setClipToPadding(false);
-
-        setVerticalFadingEdgeEnabled(false);
-
-        final Resources resources = getResources();
-        columnWidth = resources.getDimensionPixelSize(R.dimen.tab_panel_column_width);
 
-        final int padding = resources.getDimensionPixelSize(R.dimen.tab_panel_grid_padding);
-        final int paddingTop = resources.getDimensionPixelSize(R.dimen.tab_panel_grid_padding_top);
-
-        // Lets set double the top padding on the bottom so that the last row shows up properly!
-        // Your demise, GridView, cannot come fast enough.
-        final int paddingBottom = paddingTop * 2;
-
-        setPadding(padding, paddingTop, padding, paddingBottom);
-
-        setOnItemClickListener(new OnItemClickListener() {
-            @Override
-            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
-                final TabsLayoutItemView tabView = (TabsLayoutItemView) view;
-                final int tabId = tabView.getTabId();
-                final Tab tab = Tabs.getInstance().selectTab(tabId);
-                if (tab == null) {
-                    return;
-                }
-                autoHidePanel();
-                Tabs.getInstance().notifyListeners(tab, Tabs.TabEvents.OPENED_FROM_TABS_TRAY);
-            }
-        });
-
-        TabSwipeGestureListener mSwipeListener = new TabSwipeGestureListener();
-        setOnTouchListener(mSwipeListener);
-        setOnScrollListener(mSwipeListener.makeScrollListener());
-    }
-
-    private void populateTabLocations(final Tab removedTab) {
-        tabLocations.clear();
-
-        final int firstPosition = getFirstVisiblePosition();
-        final int lastPosition = getLastVisiblePosition();
-        final int numberOfColumns = getNumColumns();
-        final int childCount = getChildCount();
-        final int removedPosition = tabsAdapter.getPositionForTab(removedTab);
+public class TabsGridLayout extends TabsLayout {
+    private final int desiredColumnWidth;
 
-        for (int x = 1, i = (removedPosition - firstPosition) + 1; i < childCount; i++, x++) {
-            final View child = getChildAt(i);
-            if (child != null) {
-                // Reset the transformations here in case the user is swiping tabs away fast and they swipe a tab
-                // before the last animation has finished (bug 1179195).
-                resetTransforms(child);
-
-                tabLocations.append(x, new PointF(child.getX(), child.getY()));
-            }
-        }
-
-        final boolean firstChildOffScreen = ((firstPosition > 0) || getChildAt(0).getY() < 0);
-        final boolean lastChildVisible = (lastPosition - childCount == firstPosition - 1);
-        final boolean oneItemOnLastRow = (lastPosition % numberOfColumns == 0);
-        if (firstChildOffScreen && lastChildVisible && oneItemOnLastRow) {
-            // We need to set the view's bottom padding to prevent a sudden jump as the
-            // last item in the row is being removed. We then need to remove the padding
-            // via a sweet animation
+    public TabsGridLayout(Context context, AttributeSet attrs) {
+        super(context, attrs, R.layout.tabs_layout_item_view);
 
-            final int removedHeight = getChildAt(0).getMeasuredHeight();
-            final int verticalSpacing =
-                    getResources().getDimensionPixelOffset(R.dimen.tab_panel_grid_vspacing);
-
-            ValueAnimator paddingAnimator = ValueAnimator.ofInt(getPaddingBottom() + removedHeight + verticalSpacing, getPaddingBottom());
-            paddingAnimator.setDuration(ANIM_TIME_MS * 2);
-
-            paddingAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
-
-                @Override
-                public void onAnimationUpdate(ValueAnimator animation) {
-                    setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), (Integer) animation.getAnimatedValue());
-                }
-            });
-            paddingAnimator.start();
-        }
-    }
-
-    @Override
-    public void setTabsPanel(TabsPanel panel) {
-        tabsPanel = panel;
-    }
+        final Resources resources = context.getResources();
 
-    @Override
-    public void show() {
-        setVisibility(View.VISIBLE);
-        Tabs.getInstance().refreshThumbnails();
-        Tabs.registerOnTabsChangedListener(this);
-        refreshTabsData();
-
-        final Tab currentlySelectedTab = Tabs.getInstance().getSelectedTab();
-        final int position =  currentlySelectedTab != null ? tabsAdapter.getPositionForTab(currentlySelectedTab) : -1;
-        if (position != -1) {
-            final boolean selectionChanged = lastSelectedTabId != currentlySelectedTab.getId();
-            final boolean positionIsVisible = position >= getFirstVisiblePosition() && position <= getLastVisiblePosition();
-
-            if (selectionChanged || !positionIsVisible) {
-                smoothScrollToPosition(position);
-            }
-        }
-    }
-
-    @Override
-    public void hide() {
-        lastSelectedTabId = Tabs.getInstance().getSelectedTab().getId();
-        setVisibility(View.GONE);
-        Tabs.unregisterOnTabsChangedListener(this);
-        GeckoAppShell.notifyObservers("Tab:Screenshot:Cancel", "");
-        tabsAdapter.clear();
-    }
+        setLayoutManager(new GridLayoutManager(context, 1));
 
-    @Override
-    public boolean shouldExpand() {
-        return true;
-    }
-
-    private void autoHidePanel() {
-        tabsPanel.autoHidePanel();
-    }
-
-    @Override
-    public void onTabChanged(Tab tab, Tabs.TabEvents msg, String data) {
-        switch (msg) {
-            case ADDED:
-                // Refresh only if panel is shown. show() will call refreshTabsData() later again.
-                if (tabsPanel.isShown()) {
-                    // Refresh the list to make sure the new tab is added in the right position.
-                    refreshTabsData();
-                }
-                break;
-
-            case CLOSED:
-
-                // This is limited to >= ICS as animations on GB devices are generally pants
-                if (Build.VERSION.SDK_INT >= 11 && tabsAdapter.getCount() > 0) {
-                    animateRemoveTab(tab);
-                }
-
-                final Tabs tabsInstance = Tabs.getInstance();
+        desiredColumnWidth = resources.getDimensionPixelSize(R.dimen.tab_panel_item_width);
+        final int viewPaddingHorizontal = resources.getDimensionPixelSize(R.dimen.tab_panel_grid_hpadding);
+        final int viewPaddingVertical = resources.getDimensionPixelSize(R.dimen.tab_panel_grid_vpadding);
 
-                if (tabsAdapter.removeTab(tab)) {
-                    if (tab.isPrivate() == isPrivate && tabsAdapter.getCount() > 0) {
-                        int selected = tabsAdapter.getPositionForTab(tabsInstance.getSelectedTab());
-                        updateSelectedStyle(selected);
-                    }
-                    if (!tab.isPrivate()) {
-                        // Make sure we always have at least one normal tab
-                        final Iterable<Tab> tabs = tabsInstance.getTabsInOrder();
-                        boolean removedTabIsLastNormalTab = true;
-                        for (Tab singleTab : tabs) {
-                            if (!singleTab.isPrivate()) {
-                                removedTabIsLastNormalTab = false;
-                                break;
-                            }
-                        }
-                        if (removedTabIsLastNormalTab) {
-                            tabsInstance.addTab();
-                        }
-                    }
-                }
-                break;
-
-            case SELECTED:
-                // Update the selected position, then fall through...
-                updateSelectedPosition();
-            case UNSELECTED:
-                // We just need to update the style for the unselected tab...
-            case THUMBNAIL:
-            case TITLE:
-            case RECORDING_CHANGE:
-            case AUDIO_PLAYING_CHANGE:
-                View view = getChildAt(tabsAdapter.getPositionForTab(tab) - getFirstVisiblePosition());
-                if (view == null)
-                    return;
-
-                ((TabsLayoutItemView) view).assignValues(tab);
-                break;
-        }
-    }
-
-    // Updates the selected position in the list so that it will be scrolled to the right place.
-    private void updateSelectedPosition() {
-        int selected = tabsAdapter.getPositionForTab(Tabs.getInstance().getSelectedTab());
-        updateSelectedStyle(selected);
+        setPadding(viewPaddingHorizontal, viewPaddingVertical, viewPaddingHorizontal, viewPaddingVertical);
+        setClipToPadding(false);
+        setScrollBarStyle(SCROLLBARS_OUTSIDE_OVERLAY);
 
-        if (selected != -1) {
-            setSelection(selected);
-        }
-    }
-
-    /**
-     * Updates the selected/unselected style for the tabs.
-     *
-     * @param selected position of the selected tab
-     */
-    private void updateSelectedStyle(final int selected) {
-        post(new Runnable() {
-            @Override
-            public void run() {
-                final int displayCount = tabsAdapter.getCount();
+        setItemAnimator(new TabsGridLayoutAnimator());
 
-                for (int i = 0; i < displayCount; i++) {
-                    final Tab tab = tabsAdapter.getItem(i);
-                    final boolean checked = displayCount == 1 || i == selected;
-                    final View tabView = getViewForTab(tab);
-                    if (tabView != null) {
-                        ((TabsLayoutItemView) tabView).setChecked(checked);
-                    }
-                    // setItemChecked doesn't exist until API 11, despite what the API docs say!
-                    setItemChecked(i, checked);
-                }
-            }
-        });
-    }
-
-    private void refreshTabsData() {
-        // Store a different copy of the tabs, so that we don't have to worry about
-        // accidentally updating it on the wrong thread.
-        ArrayList<Tab> tabData = new ArrayList<>();
-
-        Iterable<Tab> allTabs = Tabs.getInstance().getTabsInOrder();
-        for (Tab tab : allTabs) {
-            if (tab.isPrivate() == isPrivate)
-                tabData.add(tab);
-        }
-
-        tabsAdapter.setTabs(tabData);
-        updateSelectedPosition();
-    }
-
-    private void resetTransforms(View view) {
-        view.setAlpha(1);
-        view.setTranslationX(0);
-        view.setTranslationY(0);
-
-        ((TabsLayoutItemView) view).setCloseVisible(true);
+        // TODO Add ItemDecoration.
     }
 
     @Override
     public void closeAll() {
-
         autoHidePanel();
 
-        if (getChildCount() == 0) {
+        closeAllTabs();
+    }
+
+    @Override
+    protected boolean addAtIndexRequiresScroll(int index) {
+        final GridLayoutManager layoutManager = (GridLayoutManager) getLayoutManager();
+        final int spanCount = layoutManager.getSpanCount();
+        final int firstVisibleIndex = layoutManager.findFirstVisibleItemPosition();
+        // When you add an item at the first visible position to a GridLayoutManager and there's
+        // room to scroll, RecyclerView scrolls the new position to anywhere from near the bottom of
+        // its row to completely offscreen (for unknown reasons), so we need to scroll to fix that.
+        // We also scroll when the item being added is the only item on the final row.
+        return index == firstVisibleIndex ||
+                (index == getAdapter().getItemCount() - 1 && index % spanCount == 0);
+    }
+
+    @Override
+    public void onSizeChanged(int w, int h, int oldw, int oldh) {
+        super.onSizeChanged(w, h, oldw, oldh);
+
+        if (w == oldw) {
             return;
         }
 
-        final Iterable<Tab> tabs = Tabs.getInstance().getTabsInOrder();
-        for (Tab tab : tabs) {
-            // In the normal panel we want to close all tabs (both private and normal),
-            // but in the private panel we only want to close private tabs.
-            if (!isPrivate || tab.isPrivate()) {
-                Tabs.getInstance().closeTab(tab, false);
-            }
-        }
-    }
-
-    private View getViewForTab(Tab tab) {
-        final int position = tabsAdapter.getPositionForTab(tab);
-        return getChildAt(position - getFirstVisiblePosition());
-    }
-
-    void closeTab(View v) {
-        if (tabsAdapter.getCount() == 1) {
-            autoHidePanel();
-        }
-
-        TabsLayoutItemView itemView = (TabsLayoutItemView) v.getTag();
-        Tab tab = Tabs.getInstance().getTab(itemView.getTabId());
-
-        Tabs.getInstance().closeTab(tab, true);
-    }
-
-    private void animateRemoveTab(final Tab removedTab) {
-        final int removedPosition = tabsAdapter.getPositionForTab(removedTab);
-
-        final View removedView = getViewForTab(removedTab);
-
-        // The removed position might not have a matching child view
-        // when it's not within the visible range of positions in the strip.
-        if (removedView == null) {
+        // TODO This is temporary - we need to take into account item padding and we'll also try to
+        // match the previous GridLayout span count.
+        final int nonPaddingWidth = w - getPaddingLeft() - getPaddingRight();
+        // Adjust span based on space available (what GridView does when you say numColumns="auto_fit").
+        final int spanCount = Math.max(1, nonPaddingWidth / desiredColumnWidth);
+        final GridLayoutManager layoutManager = (GridLayoutManager) getLayoutManager();
+        if (spanCount == layoutManager.getSpanCount()) {
             return;
         }
-        final int removedHeight = removedView.getMeasuredHeight();
-
-        populateTabLocations(removedTab);
-
-        getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
-            @Override
-            public boolean onPreDraw() {
-                getViewTreeObserver().removeOnPreDrawListener(this);
-                // We don't animate the removed child view (it just disappears)
-                // but we still need its size to animate all affected children
-                // within the visible viewport.
-                final int childCount = getChildCount();
-                final int firstPosition = getFirstVisiblePosition();
-                final int numberOfColumns = getNumColumns();
-
-                final List<Animator> childAnimators = new ArrayList<>();
-
-                PropertyValuesHolder translateX, translateY;
-                for (int x = 0, i = removedPosition - firstPosition; i < childCount; i++, x++) {
-                    final View child = getChildAt(i);
-                    ObjectAnimator animator;
-
-                    if (i % numberOfColumns == numberOfColumns - 1) {
-                        // Animate X & Y
-                        translateX = PropertyValuesHolder.ofFloat("translationX", -(columnWidth * numberOfColumns), 0);
-                        translateY = PropertyValuesHolder.ofFloat("translationY", removedHeight, 0);
-                        animator = ObjectAnimator.ofPropertyValuesHolder(child, translateX, translateY);
-                    } else {
-                        // Just animate X
-                        translateX = PropertyValuesHolder.ofFloat("translationX", columnWidth, 0);
-                        animator = ObjectAnimator.ofPropertyValuesHolder(child, translateX);
-                    }
-                    animator.setStartDelay(x * ANIM_DELAY_MULTIPLE_MS);
-                    childAnimators.add(animator);
-                }
-
-                final AnimatorSet animatorSet = new AnimatorSet();
-                animatorSet.playTogether(childAnimators);
-                animatorSet.setDuration(ANIM_TIME_MS);
-                animatorSet.setInterpolator(ANIM_INTERPOLATOR);
-                animatorSet.start();
-
-                // Set the starting position of the child views - because we are delaying the start
-                // of the animation, we need to prevent the items being drawn in their final position
-                // prior to the animation starting
-                for (int x = 1, i = (removedPosition - firstPosition) + 1; i < childCount; i++, x++) {
-                    final View child = getChildAt(i);
-
-                    final PointF targetLocation = tabLocations.get(x + 1);
-                    if (targetLocation == null) {
-                        continue;
-                    }
-
-                    child.setX(targetLocation.x);
-                    child.setY(targetLocation.y);
-                }
-
-                return true;
-            }
-        });
-    }
-
-
-    private void animateCancel(final View view) {
-        PropertyAnimator animator = new PropertyAnimator(ANIM_TIME_MS);
-        animator.attach(view, PropertyAnimator.Property.ALPHA, 1);
-        animator.attach(view, PropertyAnimator.Property.TRANSLATION_X, 0);
-
-        animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
-            @Override
-            public void onPropertyAnimationStart() {
-            }
-
-            @Override
-            public void onPropertyAnimationEnd() {
-                TabsLayoutItemView tab = (TabsLayoutItemView) view;
-                tab.setCloseVisible(true);
-            }
-        });
-
-        animator.start();
-    }
-
-    private class TabsGridLayoutAdapter extends TabsLayoutAdapter {
-
-        final private Button.OnClickListener mCloseClickListener;
-
-        public TabsGridLayoutAdapter(Context context) {
-            super(context, R.layout.tabs_layout_item_view);
-
-            mCloseClickListener = new Button.OnClickListener() {
-                @Override
-                public void onClick(View v) {
-                    closeTab(v);
-                }
-            };
-        }
-
-        @Override
-        TabsLayoutItemView newView(int position, ViewGroup parent) {
-            final TabsLayoutItemView item = super.newView(position, parent);
-
-            item.setCloseOnClickListener(mCloseClickListener);
-            ((ThemedRelativeLayout) item.findViewById(R.id.wrapper)).setPrivateMode(isPrivate);
-
-            return item;
-        }
-
-        @Override
-        public void bindView(TabsLayoutItemView view, Tab tab) {
-            super.bindView(view, tab);
-
-            // If we're recycling this view, there's a chance it was transformed during
-            // the close animation. Remove any of those properties.
-            resetTransforms(view);
-        }
-    }
-
-    private class TabSwipeGestureListener implements View.OnTouchListener {
-        // same value the stock browser uses for after drag animation velocity in pixels/sec
-        // http://androidxref.com/4.0.4/xref/packages/apps/Browser/src/com/android/browser/NavTabScroller.java#61
-        private static final float MIN_VELOCITY = 750;
-
-        private final int mSwipeThreshold;
-        private final int mMinFlingVelocity;
-
-        private final int mMaxFlingVelocity;
-        private VelocityTracker mVelocityTracker;
-
-        private int mTabWidth = 1;
-
-        private View mSwipeView;
-        private Runnable mPendingCheckForTap;
-
-        private float mSwipeStartX;
-        private boolean mSwiping;
-        private boolean mEnabled;
-
-        public TabSwipeGestureListener() {
-            mEnabled = true;
-
-            ViewConfiguration vc = ViewConfiguration.get(TabsGridLayout.this.getContext());
-            mSwipeThreshold = vc.getScaledTouchSlop();
-            mMinFlingVelocity = (int) (TabsGridLayout.this.getContext().getResources().getDisplayMetrics().density * MIN_VELOCITY);
-            mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();
-        }
-
-        public void setEnabled(boolean enabled) {
-            mEnabled = enabled;
-        }
-
-        public OnScrollListener makeScrollListener() {
-            return new OnScrollListener() {
-                @Override
-                public void onScrollStateChanged(AbsListView view, int scrollState) {
-                    setEnabled(scrollState != GridView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
-                }
-
-                @Override
-                public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
-
-                }
-            };
-        }
-
-        @Override
-        public boolean onTouch(View view, MotionEvent e) {
-            if (!mEnabled) {
-                return false;
-            }
-
-            switch (e.getActionMasked()) {
-                case MotionEvent.ACTION_DOWN: {
-                    // Check if we should set pressed state on the
-                    // touched view after a standard delay.
-                    triggerCheckForTap();
-
-                    final float x = e.getRawX();
-                    final float y = e.getRawY();
-
-                    // Find out which view is being touched
-                    mSwipeView = findViewAt(x, y);
-
-                    if (mSwipeView != null) {
-                        if (mTabWidth < 2) {
-                            mTabWidth = mSwipeView.getWidth();
-                        }
-
-                        mSwipeStartX = e.getRawX();
-
-                        mVelocityTracker = VelocityTracker.obtain();
-                        mVelocityTracker.addMovement(e);
-                    }
-
-                    view.onTouchEvent(e);
-                    return true;
-                }
-
-                case MotionEvent.ACTION_UP: {
-                    if (mSwipeView == null) {
-                        break;
-                    }
-
-                    cancelCheckForTap();
-                    mSwipeView.setPressed(false);
-
-                    if (!mSwiping) {
-                        final TabsLayoutItemView item = (TabsLayoutItemView) mSwipeView;
-                        final int tabId = item.getTabId();
-                        final Tab tab = Tabs.getInstance().selectTab(tabId);
-                        if (tab != null) {
-                            autoHidePanel();
-                            Tabs.getInstance().notifyListeners(tab, Tabs.TabEvents.OPENED_FROM_TABS_TRAY);
-                        }
-
-                        mVelocityTracker.recycle();
-                        mVelocityTracker = null;
-                        break;
-                    }
-
-                    mVelocityTracker.addMovement(e);
-                    mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
-
-                    float velocityX = Math.abs(mVelocityTracker.getXVelocity());
-
-                    boolean dismiss = false;
-
-                    float deltaX = mSwipeView.getTranslationX();
-
-                    if (Math.abs(deltaX) > mTabWidth / 2) {
-                        dismiss = true;
-                    } else if (mMinFlingVelocity <= velocityX && velocityX <= mMaxFlingVelocity) {
-                        dismiss = mSwiping && (deltaX * mVelocityTracker.getYVelocity() > 0);
-                    }
-                    if (dismiss) {
-                        closeTab(mSwipeView.findViewById(R.id.close));
-                    } else {
-                        animateCancel(mSwipeView);
-                    }
-                    mVelocityTracker.recycle();
-                    mVelocityTracker = null;
-                    mSwipeView = null;
-
-                    mSwipeStartX = 0;
-                    mSwiping = false;
-                }
-
-                case MotionEvent.ACTION_MOVE: {
-                    if (mSwipeView == null || mVelocityTracker == null) {
-                        break;
-                    }
-
-                    mVelocityTracker.addMovement(e);
-
-                    float delta = e.getRawX() - mSwipeStartX;
-
-                    boolean isScrollingX = Math.abs(delta) > mSwipeThreshold;
-                    boolean isSwipingToClose = isScrollingX;
-
-                    // If we're actually swiping, make sure we don't
-                    // set pressed state on the swiped view.
-                    if (isScrollingX) {
-                        cancelCheckForTap();
-                    }
-
-                    if (isSwipingToClose) {
-                        mSwiping = true;
-                        TabsGridLayout.this.requestDisallowInterceptTouchEvent(true);
-
-                        ((TabsLayoutItemView) mSwipeView).setCloseVisible(false);
-
-                        // Stops listview from highlighting the touched item
-                        // in the list when swiping.
-                        MotionEvent cancelEvent = MotionEvent.obtain(e);
-                        cancelEvent.setAction(MotionEvent.ACTION_CANCEL |
-                                (e.getActionIndex() << MotionEvent.ACTION_POINTER_INDEX_SHIFT));
-                        TabsGridLayout.this.onTouchEvent(cancelEvent);
-                        cancelEvent.recycle();
-                    }
-
-                    if (mSwiping) {
-                        mSwipeView.setTranslationX(delta);
-
-                        mSwipeView.setAlpha(Math.min(1f, 1f - 2f * Math.abs(delta) / mTabWidth));
-
-                        return true;
-                    }
-
-                    break;
-                }
-            }
-            return false;
-        }
-
-        private View findViewAt(float rawX, float rawY) {
-            Rect rect = new Rect();
-
-            int[] listViewCoords = new int[2];
-            TabsGridLayout.this.getLocationOnScreen(listViewCoords);
-
-            int x = (int) rawX - listViewCoords[0];
-            int y = (int) rawY - listViewCoords[1];
-
-            for (int i = 0; i < TabsGridLayout.this.getChildCount(); i++) {
-                View child = TabsGridLayout.this.getChildAt(i);
-                child.getHitRect(rect);
-
-                if (rect.contains(x, y)) {
-                    return child;
-                }
-            }
-
-            return null;
-        }
-
-        private void triggerCheckForTap() {
-            if (mPendingCheckForTap == null) {
-                mPendingCheckForTap = new CheckForTap();
-            }
-
-            TabsGridLayout.this.postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
-        }
-
-        private void cancelCheckForTap() {
-            if (mPendingCheckForTap == null) {
-                return;
-            }
-
-            TabsGridLayout.this.removeCallbacks(mPendingCheckForTap);
-        }
-
-        private class CheckForTap implements Runnable {
-            @Override
-            public void run() {
-                if (!mSwiping && mSwipeView != null && mEnabled) {
-                    mSwipeView.setPressed(true);
-                }
-            }
-        }
+        layoutManager.setSpanCount(spanCount);
     }
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsGridLayoutAnimator.java
@@ -0,0 +1,21 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.tabs;
+
+import org.mozilla.gecko.widget.DefaultItemAnimatorBase;
+
+import android.support.v7.widget.RecyclerView;
+
+class TabsGridLayoutAnimator extends DefaultItemAnimatorBase {
+    public TabsGridLayoutAnimator() {
+        setSupportsChangeAnimations(false);
+    }
+
+    @Override
+    protected boolean preAnimateRemoveImpl(final RecyclerView.ViewHolder holder) {
+        return false;
+    }
+}
--- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayout.java
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayout.java
@@ -25,26 +25,26 @@ public abstract class TabsLayout extends
         Tabs.OnTabsChangedListener,
         RecyclerViewClickSupport.OnItemClickListener,
         TabsTouchHelperCallback.DismissListener {
 
     private static final String LOGTAG = "Gecko" + TabsLayout.class.getSimpleName();
 
     private final boolean isPrivate;
     private TabsPanel tabsPanel;
-    private final TabsLayoutRecyclerAdapter tabsAdapter;
+    private final TabsLayoutAdapter tabsAdapter;
 
     public TabsLayout(Context context, AttributeSet attrs, int itemViewLayoutResId) {
         super(context, attrs);
 
         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabsLayout);
         isPrivate = (a.getInt(R.styleable.TabsLayout_tabs, 0x0) == 1);
         a.recycle();
 
-        tabsAdapter = new TabsLayoutRecyclerAdapter(context, itemViewLayoutResId, isPrivate,
+        tabsAdapter = new TabsLayoutAdapter(context, itemViewLayoutResId, isPrivate,
                 /* close on click listener */
                 new Button.OnClickListener() {
                     @Override
                     public void onClick(View v) {
                         // The view here is the close button, which has a reference
                         // to the parent TabsLayoutItemView in its tag, hence the getTag() call.
                         TabsLayoutItemView itemView = (TabsLayoutItemView) v.getTag();
                         closeTab(itemView);
@@ -96,18 +96,18 @@ public abstract class TabsLayout extends
 
     @Override
     public void onTabChanged(Tab tab, Tabs.TabEvents msg, String data) {
         switch (msg) {
             case ADDED:
                 final int tabIndex = Integer.parseInt(data);
                 tabsAdapter.notifyTabInserted(tab, tabIndex);
                 if (addAtIndexRequiresScroll(tabIndex)) {
-                    // (The current Tabs implementation updates the SELECTED tab *after* this
-                    // call to ADDED, so don't just call updateSelectedPosition().)
+                    // (The SELECTED tab is updated *after* this call to ADDED, so don't just call
+                    // updateSelectedPosition().)
                     scrollToPosition(tabIndex);
                 }
                 break;
 
             case CLOSED:
                 if (tab.isPrivate() == isPrivate && tabsAdapter.getItemCount() > 0) {
                     tabsAdapter.removeTab(tab);
                 }
@@ -119,38 +119,44 @@ public abstract class TabsLayout extends
             case TITLE:
             case RECORDING_CHANGE:
             case AUDIO_PLAYING_CHANGE:
                 tabsAdapter.notifyTabChanged(tab);
                 break;
         }
     }
 
-    // Addition of a tab at selected positions (dependent on LayoutManager) will result in a tab
-    // being added out of view - return true if index is such a position.
+    /**
+     * Addition of a tab at selected positions (dependent on LayoutManager) will result in a tab
+     * being added out of view - return true if {@code index} is such a position.
+     */
     abstract protected boolean addAtIndexRequiresScroll(int index);
 
+    protected int getSelectedAdapterPosition() {
+        return tabsAdapter.getPositionForTab(Tabs.getInstance().getSelectedTab());
+    }
+
     @Override
     public void onItemClicked(RecyclerView recyclerView, int position, View v) {
         final TabsLayoutItemView item = (TabsLayoutItemView) v;
         final int tabId = item.getTabId();
         final Tab tab = Tabs.getInstance().selectTab(tabId);
         if (tab == null) {
             // The tab that was clicked no longer exists in the tabs list (which can happen if you
             // tap on a tab while its remove animation is running), so ignore the click.
             return;
         }
 
         autoHidePanel();
         Tabs.getInstance().notifyListeners(tab, Tabs.TabEvents.OPENED_FROM_TABS_TRAY);
     }
 
-    // Updates the selected position in the list so that it will be scrolled to the right place.
+    /** Updates the selected position in the list so that it will be scrolled to the right place. */
     private void updateSelectedPosition() {
-        final int selected = tabsAdapter.getPositionForTab(Tabs.getInstance().getSelectedTab());
+        final int selected = getSelectedAdapterPosition();
         if (selected != NO_POSITION) {
             scrollToPosition(selected);
         }
     }
 
     private void refreshTabsData() {
         // Store a different copy of the tabs, so that we don't have to worry about
         // accidentally updating it on the wrong thread.
@@ -194,16 +200,25 @@ public abstract class TabsLayout extends
         }
     }
 
     @Override
     public void onItemDismiss(View view) {
         closeTab(view);
     }
 
+    @Override
+    public void onChildAttachedToWindow(View child) {
+        // Make sure we reset any attributes that may have been animated in this child's previous
+        // incarnation.
+        child.setTranslationX(0);
+        child.setTranslationY(0);
+        child.setAlpha(1);
+    }
+
     private Tab getTabForView(View view) {
         if (view == null) {
             return null;
         }
         return Tabs.getInstance().getTab(((TabsLayoutItemView) view).getTabId());
     }
 
     @Override
--- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayoutAdapter.java
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayoutAdapter.java
@@ -1,100 +1,127 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.tabs;
 
-import org.mozilla.gecko.R;
 import org.mozilla.gecko.Tab;
 
 import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
-import android.widget.BaseAdapter;
+import android.widget.Button;
 
 import java.util.ArrayList;
 
-// Adapter to bind tabs into a list
-public class TabsLayoutAdapter extends BaseAdapter {
-    public static final String LOGTAG = "Gecko" + TabsLayoutAdapter.class.getSimpleName();
+public class TabsLayoutAdapter
+        extends RecyclerView.Adapter<TabsLayoutAdapter.TabsListViewHolder> {
+
+    private static final String LOGTAG = "Gecko" + TabsLayoutAdapter.class.getSimpleName();
+
+    private final int tabLayoutId;
+    private @NonNull ArrayList<Tab> tabs;
+    private final LayoutInflater inflater;
+    private final boolean isPrivate;
+    // Click listener for the close button on itemViews.
+    private final Button.OnClickListener closeOnClickListener;
 
-    private final Context mContext;
-    private final int mTabLayoutId;
-    private ArrayList<Tab> mTabs;
-    private final LayoutInflater mInflater;
+    // The TabsLayoutItemView takes care of caching its own Views, so we don't need to do anything
+    // here except not be abstract.
+    public static class TabsListViewHolder extends RecyclerView.ViewHolder {
+        public TabsListViewHolder(View itemView) {
+            super(itemView);
+        }
+    }
 
-    public TabsLayoutAdapter (Context context, int tabLayoutId) {
-        mContext = context;
-        mInflater = LayoutInflater.from(mContext);
-        mTabLayoutId = tabLayoutId;
+    public TabsLayoutAdapter(Context context, int tabLayoutId, boolean isPrivate,
+                             Button.OnClickListener closeOnClickListener) {
+        inflater = LayoutInflater.from(context);
+        this.tabLayoutId = tabLayoutId;
+        this.isPrivate = isPrivate;
+        this.closeOnClickListener = closeOnClickListener;
+        tabs = new ArrayList<>(0);
+    }
+
+    /* package */ final void setTabs(@NonNull ArrayList<Tab> tabs) {
+        this.tabs = tabs;
+        notifyDataSetChanged();
+    }
+
+    /* package */ final void clear() {
+        tabs = new ArrayList<>(0);
+        notifyDataSetChanged();
     }
 
-    final void setTabs (ArrayList<Tab> tabs) {
-        mTabs = tabs;
-        notifyDataSetChanged(); // Be sure to call this whenever mTabs changes.
+    /* package */ final boolean removeTab(Tab tab) {
+        final int position = getPositionForTab(tab);
+        if (position == -1) {
+            return false;
+        }
+        tabs.remove(position);
+        notifyItemRemoved(position);
+        return true;
+    }
+
+    /* package */ final int getPositionForTab(Tab tab) {
+        if (tab == null) {
+            return -1;
+        }
+
+        return tabs.indexOf(tab);
     }
 
-    final boolean removeTab (Tab tab) {
-        boolean tabRemoved = mTabs.remove(tab);
-        if (tabRemoved) {
-            notifyDataSetChanged(); // Be sure to call this whenever mTabs changes.
+    /* package */ void notifyTabChanged(Tab tab) {
+        final int position = getPositionForTab(tab);
+        if (position != -1) {
+            notifyItemChanged(position);
         }
-        return tabRemoved;
     }
 
-    final void clear() {
-        mTabs = null;
-
-        notifyDataSetChanged(); // Be sure to call this whenever mTabs changes.
+    /* package */ void notifyTabInserted(Tab tab, int index) {
+        if (index >= 0 && index <= tabs.size()) {
+            tabs.add(index, tab);
+            notifyItemInserted(index);
+        } else {
+            // Add to the end.
+            tabs.add(tab);
+            notifyItemInserted(tabs.size() - 1);
+            // index == -1 is a valid way to add to the end, the other cases are errors.
+            if (index != -1) {
+                Log.e(LOGTAG, "Tab was inserted at an invalid position: " + Integer.toString(index));
+            }
+        }
     }
 
     @Override
-    public int getCount() {
-        return (mTabs == null ? 0 : mTabs.size());
-    }
-
-    @Override
-    public Tab getItem(int position) {
-        return mTabs.get(position);
+    public int getItemCount() {
+        return tabs.size();
     }
 
-    @Override
-    public long getItemId(int position) {
-        return position;
-    }
-
-    final int getPositionForTab(Tab tab) {
-        if (mTabs == null || tab == null)
-            return -1;
-
-        return mTabs.indexOf(tab);
+    private Tab getItem(int position) {
+        return tabs.get(position);
     }
 
     @Override
-    public boolean isEnabled(int position) {
-        return true;
+    public void onBindViewHolder(TabsListViewHolder viewHolder, int position) {
+        final Tab tab = getItem(position);
+        final TabsLayoutItemView itemView = (TabsLayoutItemView) viewHolder.itemView;
+        itemView.assignValues(tab);
+        // Be careful (re)setting position values here: bind is called on each notifyItemChanged,
+        // so you could be stomping on values that have been set in support of other animations
+        // that are already underway.
     }
 
     @Override
-    final public TabsLayoutItemView getView(int position, View convertView, ViewGroup parent) {
-        final TabsLayoutItemView view;
-        if (convertView == null) {
-            view = newView(position, parent);
-        } else {
-            view = (TabsLayoutItemView) convertView;
-        }
-        final Tab tab = mTabs.get(position);
-        bindView(view, tab);
-        return view;
+    public TabsListViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+        final TabsLayoutItemView viewItem = (TabsLayoutItemView) inflater.inflate(tabLayoutId, parent, false);
+        viewItem.setPrivateMode(isPrivate);
+        viewItem.setCloseOnClickListener(closeOnClickListener);
+
+        return new TabsListViewHolder(viewItem);
     }
-
-    TabsLayoutItemView newView(int position, ViewGroup parent) {
-        return (TabsLayoutItemView) mInflater.inflate(mTabLayoutId, parent, false);
-    }
-
-    void bindView(TabsLayoutItemView view, Tab tab) {
-        view.assignValues(tab);
-    }
-}
\ No newline at end of file
+}
--- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsListLayout.java
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsListLayout.java
@@ -101,18 +101,9 @@ public class TabsListLayout extends Tabs
             cascadeDelay += ANIMATION_CASCADE_DELAY;
         }
     }
 
     @Override
     protected boolean addAtIndexRequiresScroll(int index) {
         return index == 0 || index == getAdapter().getItemCount() - 1;
     }
-
-    @Override
-    public void onChildAttachedToWindow(View child) {
-        // Make sure we reset any attributes that may have been animated in this child's previous
-        // incarnation.
-        child.setTranslationX(0);
-        child.setTranslationY(0);
-        child.setAlpha(1);
-    }
 }
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -680,20 +680,20 @@ gbjar.sources += ['java/org/mozilla/geck
     'tabs/PrivateTabsPanel.java',
     'tabs/TabCurve.java',
     'tabs/TabHistoryController.java',
     'tabs/TabHistoryFragment.java',
     'tabs/TabHistoryItemRow.java',
     'tabs/TabHistoryPage.java',
     'tabs/TabPanelBackButton.java',
     'tabs/TabsGridLayout.java',
+    'tabs/TabsGridLayoutAnimator.java',
     'tabs/TabsLayout.java',
     'tabs/TabsLayoutAdapter.java',
     'tabs/TabsLayoutItemView.java',
-    'tabs/TabsLayoutRecyclerAdapter.java',
     'tabs/TabsListLayout.java',
     'tabs/TabsListLayoutAnimator.java',
     'tabs/TabsPanel.java',
     'tabs/TabsPanelThumbnailView.java',
     'tabs/TabsTouchHelperCallback.java',
     'Telemetry.java',
     'telemetry/measurements/CampaignIdMeasurements.java',
     'telemetry/measurements/SearchCountMeasurements.java',
--- a/mobile/android/base/resources/layout/tabs_layout_item_view.xml
+++ b/mobile/android/base/resources/layout/tabs_layout_item_view.xml
@@ -2,27 +2,25 @@
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 <org.mozilla.gecko.tabs.TabsLayoutItemView xmlns:android="http://schemas.android.com/apk/res/android"
                                            xmlns:gecko="http://schemas.android.com/apk/res-auto"
                                            style="@style/TabsItem"
                                            android:id="@+id/info"
-                                           android:layout_width="wrap_content"
+                                           android:layout_width="fill_parent"
                                            android:layout_height="wrap_content"
                                            android:gravity="center"
                                            android:orientation="vertical">
 
-    <LinearLayout android:layout_width="fill_parent"
+    <LinearLayout android:layout_width="@dimen/tab_thumbnail_width"
                   android:layout_height="wrap_content"
                   android:orientation="horizontal"
                   android:duplicateParentState="true"
-                  android:paddingLeft="@dimen/tab_highlight_stroke_width"
-                  android:paddingRight="@dimen/tab_highlight_stroke_width"
                   android:paddingBottom="@dimen/tab_highlight_stroke_width">
 
        <org.mozilla.gecko.widget.FadedSingleColorTextView
                android:id="@+id/title"
                android:layout_width="0dip"
                android:layout_height="wrap_content"
                android:layout_weight="1.0"
                style="@style/TabLayoutItemTextAppearance"
--- a/mobile/android/base/resources/values-land/dimens.xml
+++ b/mobile/android/base/resources/values-land/dimens.xml
@@ -2,10 +2,10 @@
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 <resources>
 
     <!-- Remote Tabs static view top padding. Less in landscape on phones. -->
     <dimen name="home_remote_tabs_top_padding">16dp</dimen>
-    <dimen name="tab_panel_grid_padding">48dp</dimen>
+    <dimen name="tab_panel_grid_hpadding">48dp</dimen>
 </resources>
--- a/mobile/android/base/resources/values-sw240dp/dimens.xml
+++ b/mobile/android/base/resources/values-sw240dp/dimens.xml
@@ -1,10 +1,10 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 <resources>
-    <dimen name="tab_panel_column_width">143dip</dimen>
+    <dimen name="tab_panel_item_width">143dip</dimen>
     <dimen name="tab_thumbnail_height">100dip</dimen>
     <dimen name="tab_thumbnail_width">135dip</dimen>
 </resources>
--- a/mobile/android/base/resources/values-sw360dp/dimens.xml
+++ b/mobile/android/base/resources/values-sw360dp/dimens.xml
@@ -1,13 +1,13 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 <resources>
-    <dimen name="tab_panel_column_width">156dip</dimen>
+    <dimen name="tab_panel_item_width">156dip</dimen>
     <dimen name="tab_thumbnail_height">110dip</dimen>
     <dimen name="tab_thumbnail_width">148dip</dimen>
 
     <dimen name="firstrun_background_height">180dp</dimen>
     <dimen name="firstrun_min_height">180dp</dimen>
 </resources>
--- a/mobile/android/base/resources/values-sw400dp/dimens.xml
+++ b/mobile/android/base/resources/values-sw400dp/dimens.xml
@@ -1,10 +1,10 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 <resources>
-    <dimen name="tab_panel_column_width">176dip</dimen>
+    <dimen name="tab_panel_item_width">176dip</dimen>
     <dimen name="tab_thumbnail_height">120dip</dimen>
     <dimen name="tab_thumbnail_width">168dip</dimen>
 </resources>
--- a/mobile/android/base/resources/values-v11/themes.xml
+++ b/mobile/android/base/resources/values-v11/themes.xml
@@ -34,12 +34,11 @@
         <item name="android:actionModeCutDrawable">@drawable/ab_cut</item>
         <item name="android:actionModePasteDrawable">@drawable/ab_paste</item>
         <item name="android:listViewStyle">@style/Widget.ListView</item>
         <item name="android:spinnerDropDownItemStyle">@style/Widget.DropDownItem.Spinner</item>
         <item name="android:spinnerItemStyle">@style/Widget.TextView.SpinnerItem</item>
         <item name="menuItemSwitcherLayoutStyle">@style/Widget.MenuItemSwitcherLayout</item>
         <item name="menuItemDefaultStyle">@style/Widget.MenuItemDefault</item>
         <item name="menuItemSecondaryActionBarStyle">@style/Widget.MenuItemSecondaryActionBar</item>
-        <item name="tabGridLayoutViewStyle">@style/Widget.TabsGridLayout</item>
     </style>
 
 </resources>
--- a/mobile/android/base/resources/values-v21/themes.xml
+++ b/mobile/android/base/resources/values-v21/themes.xml
@@ -29,12 +29,11 @@
     <style name="GeckoAppBase" parent="Gecko">
         <item name="android:actionButtonStyle">@style/GeckoActionBar.Button</item>
         <item name="android:listViewStyle">@style/Widget.ListView</item>
         <item name="android:spinnerDropDownItemStyle">@style/Widget.DropDownItem.Spinner</item>
         <item name="android:spinnerItemStyle">@style/Widget.TextView.SpinnerItem</item>
         <item name="menuItemSwitcherLayoutStyle">@style/Widget.MenuItemSwitcherLayout</item>
         <item name="menuItemDefaultStyle">@style/Widget.MenuItemDefault</item>
         <item name="menuItemSecondaryActionBarStyle">@style/Widget.MenuItemSecondaryActionBar</item>
-        <item name="tabGridLayoutViewStyle">@style/Widget.TabsGridLayout</item>
     </style>
 
 </resources>
--- a/mobile/android/base/resources/values-xlarge-land-v11/dimens.xml
+++ b/mobile/android/base/resources/values-xlarge-land-v11/dimens.xml
@@ -1,10 +1,10 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 <resources>
 
-    <dimen name="tab_panel_grid_padding">64dp</dimen>
+    <dimen name="tab_panel_grid_hpadding">64dp</dimen>
 
 </resources>
--- a/mobile/android/base/resources/values-xlarge-v11/dimens.xml
+++ b/mobile/android/base/resources/values-xlarge-v11/dimens.xml
@@ -1,11 +1,11 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 <resources>
 
     <dimen name="panel_grid_view_column_width">250dp</dimen>
-    <dimen name="tab_panel_grid_padding">48dp</dimen>
+    <dimen name="tab_panel_grid_hpadding">48dp</dimen>
 
 </resources>
--- a/mobile/android/base/resources/values/attrs.xml
+++ b/mobile/android/base/resources/values/attrs.xml
@@ -29,19 +29,16 @@
         <attr name="bookmarksListViewStyle" format="reference" />
 
         <!-- Default style for the TopSitesGridItemView -->
         <attr name="topSitesGridItemViewStyle" format="reference" />
 
         <!-- Styles for dynamic panel grid views -->
         <attr name="panelIconViewStyle" format="reference" />
 
-        <!-- Style for the TabsGridLayout -->
-        <attr name="tabGridLayoutViewStyle" format="reference" />
-
         <!-- Default style for the TopSitesGridView -->
         <attr name="topSitesGridViewStyle" format="reference" />
 
         <!-- Default style for the TopSitesThumbnailView -->
         <attr name="topSitesThumbnailViewStyle" format="reference" />
 
         <!-- Default style for the HomeListView -->
         <attr name="homeListViewStyle" format="reference" />
--- a/mobile/android/base/resources/values/dimens.xml
+++ b/mobile/android/base/resources/values/dimens.xml
@@ -139,20 +139,21 @@
     <dimen name="tabs_strip_button_width">100dp</dimen>
     <dimen name="tabs_strip_button_padding">18dp</dimen>
     <dimen name="tabs_strip_shadow_size">1dp</dimen>
     <dimen name="validation_message_height">50dp</dimen>
     <dimen name="validation_message_margin_top">6dp</dimen>
 
     <dimen name="tab_thumbnail_width">121dp</dimen>
     <dimen name="tab_thumbnail_height">90dp</dimen>
-    <dimen name="tab_panel_column_width">129dp</dimen>
-    <dimen name="tab_panel_grid_padding">20dp</dimen>
-    <dimen name="tab_panel_grid_vspacing">20dp</dimen>
-    <dimen name="tab_panel_grid_padding_top">19dp</dimen>
+    <dimen name="tab_panel_item_width">129dp</dimen>
+    <dimen name="tab_panel_grid_hpadding">20dp</dimen>
+    <dimen name="tab_panel_grid_vpadding">19dp</dimen>
+    <dimen name="tab_panel_grid_item_hpadding">1dp</dimen>
+    <dimen name="tab_panel_grid_item_vpadding">10dp</dimen>
 
     <dimen name="tab_highlight_stroke_width">4dp</dimen>
 
     <!-- PageActionButtons dimensions -->
     <dimen name="page_action_button_width">32dp</dimen>
 
     <!-- Banner -->
     <dimen name="home_banner_height">72dp</dimen>
--- a/mobile/android/base/resources/values/styles.xml
+++ b/mobile/android/base/resources/values/styles.xml
@@ -175,31 +175,16 @@
 
     <style name="Widget.TopSitesGridItemView">
         <item name="android:layout_width">match_parent</item>
         <item name="android:layout_height">match_parent</item>
         <item name="android:padding">5dip</item>
         <item name="android:orientation">vertical</item>
     </style>
 
-    <style name="Widget.TabsGridLayout" parent="Widget.GridView">
-        <item name="android:layout_width">match_parent</item>
-        <item name="android:layout_height">match_parent</item>
-        <item name="android:paddingTop">0dp</item>
-        <item name="android:stretchMode">spacingWidth</item>
-        <item name="android:scrollbarStyle">outsideOverlay</item>
-        <item name="android:gravity">center</item>
-        <item name="android:numColumns">auto_fit</item>
-        <item name="android:columnWidth">@dimen/tab_panel_column_width</item>
-        <item name="android:horizontalSpacing">2dp</item>
-        <item name="android:verticalSpacing">@dimen/tab_panel_grid_vspacing</item>
-        <item name="android:drawSelectorOnTop">true</item>
-        <item name="android:clipToPadding">false</item>
-    </style>
-
     <style name="Widget.BookmarkItemView" parent="Widget.TwoLinePageRow"/>
 
     <style name="Widget.BookmarksListView" parent="Widget.HomeListView"/>
 
     <style name="Widget.TopSitesThumbnailView">
       <item name="android:padding">0dip</item>
       <item name="android:scaleType">centerCrop</item>
     </style>
--- a/mobile/android/base/resources/values/themes.xml
+++ b/mobile/android/base/resources/values/themes.xml
@@ -90,17 +90,16 @@
     </style>
 
     <!-- All customizations that are NOT specific to a particular API-level can go here. -->
     <style name="Gecko.App" parent="GeckoAppBase">
         <item name="android:gridViewStyle">@style/Widget.GridView</item>
         <item name="android:spinnerStyle">@style/Widget.Spinner</item>
         <item name="android:windowBackground">@android:color/white</item>
         <item name="bookmarksListViewStyle">@style/Widget.BookmarksListView</item>
-        <item name="tabGridLayoutViewStyle">@style/Widget.TabsGridLayout</item>
         <item name="geckoMenuListViewStyle">@style/Widget.GeckoMenuListView</item>
         <item name="homeListViewStyle">@style/Widget.HomeListView</item>
         <item name="menuItemActionBarStyle">@style/Widget.MenuItemActionBar</item>
         <item name="menuItemActionModeStyle">@style/GeckoActionBar.Button</item>
         <item name="topSitesGridItemViewStyle">@style/Widget.TopSitesGridItemView</item>
         <item name="topSitesGridViewStyle">@style/Widget.TopSitesGridView</item>
         <item name="topSitesThumbnailViewStyle">@style/Widget.TopSitesThumbnailView</item>
     </style>
--- a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/BaseTest.java
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/BaseTest.java
@@ -607,89 +607,52 @@ abstract class BaseTest extends BaseRobo
     }
 
     public void closeAddedTabs() {
         for(int tabID : mKnownTabIDs) {
             closeTab(tabID);
         }
     }
 
-    // A temporary tabs list/grid holder while the list and grid views are being transitioned to
-    // RecyclerViews (bug 1116415 and bug 1310081).
-    private static class TabsView {
-        private AdapterView<ListAdapter> gridView;
-        private RecyclerView listView;
-
-        public TabsView(View view) {
-            if (view instanceof RecyclerView) {
-                listView = (RecyclerView) view;
-            } else {
-                gridView = (AdapterView<ListAdapter>) view;
-            }
-        }
-
-        public void bringPositionIntoView(int index) {
-            if (gridView != null) {
-                gridView.setSelection(index);
-            } else {
-                listView.scrollToPosition(index);
-            }
-        }
-
-        public View getViewAtIndex(int index) {
-            if (gridView != null) {
-                return gridView.getChildAt(index - gridView.getFirstVisiblePosition());
-            } else {
-                final RecyclerView.ViewHolder itemViewHolder = listView.findViewHolderForLayoutPosition(index);
-                return itemViewHolder == null ? null : itemViewHolder.itemView;
-            }
-        }
-
-        public void post(Runnable runnable) {
-            if (gridView != null) {
-                gridView.post(runnable);
-            } else {
-                listView.post(runnable);
-            }
-        }
-    }
     /**
-     * Gets the AdapterView of the tabs list.
+     * Gets the RecyclerView of the tabs list.
      *
      * @return List view in the tabs panel
      */
-    private final TabsView getTabsLayout() {
+    private final RecyclerView getTabsLayout() {
         Element tabs = mDriver.findElement(getActivity(), R.id.tabs);
         tabs.click();
-        return new TabsView(getActivity().findViewById(R.id.normal_tabs));
+        return (RecyclerView) getActivity().findViewById(R.id.normal_tabs);
     }
 
     /**
      * Gets the view in the tabs panel at the specified index.
      *
      * @return View at index
      */
     private View getTabViewAt(final int index) {
         final View[] childView = { null };
 
-        final TabsView view = getTabsLayout();
+        final RecyclerView view = getTabsLayout();
 
         runOnUiThreadSync(new Runnable() {
             @Override
             public void run() {
-                view.bringPositionIntoView(index);
+                view.scrollToPosition(index);
 
                 // The selection isn't updated synchronously; posting a
                 // runnable to the view's queue guarantees we'll run after the
                 // layout pass.
                 view.post(new Runnable() {
                     @Override
                     public void run() {
                         // Index is relative to all views in the list.
-                        childView[0] = view.getViewAtIndex(index);
+                        final RecyclerView.ViewHolder itemViewHolder =
+                                view.findViewHolderForLayoutPosition(index);
+                        childView[0] = itemViewHolder == null ? null : itemViewHolder.itemView;
                     }
                 });
             }
         });
 
         boolean result = waitForCondition(new Condition() {
             @Override
             public boolean isSatisfied() {