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

--HG--
extra : rebase_source : 009e98a71b1644fbe39e36dab10bfbf371329fa8
extra : source : 1c6a92e7614c2b0981db9ccfbc1d673656c88daf
This commit is contained in:
Tom Klein 2016-09-12 11:21:51 -05:00
Родитель 0a6ae7ed4e
Коммит 36a8b1bc24
20 изменённых файлов: 199 добавлений и 839 удалений

Просмотреть файл

@ -5,708 +5,73 @@
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;
public class TabsGridLayout extends TabsLayout {
private final int desiredColumnWidth;
/**
* A tabs layout implementation for the tablet redesign (bug 1014156) and later ported to mobile (bug 1193745).
*/
public TabsGridLayout(Context context, AttributeSet attrs) {
super(context, attrs, R.layout.tabs_layout_item_view);
class TabsGridLayout extends GridView
implements TabsLayout,
Tabs.OnTabsChangedListener {
final Resources resources = context.getResources();
private static final String LOGTAG = "Gecko" + TabsGridLayout.class.getSimpleName();
setLayoutManager(new GridLayoutManager(context, 1));
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();
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);
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
setPadding(viewPaddingHorizontal, viewPaddingVertical, viewPaddingHorizontal, viewPaddingVertical);
setClipToPadding(false);
setScrollBarStyle(SCROLLBARS_OUTSIDE_OVERLAY);
setVerticalFadingEdgeEnabled(false);
setItemAnimator(new TabsGridLayoutAnimator());
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);
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
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;
}
@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();
}
@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();
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);
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();
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);
}
}

Просмотреть файл

@ -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;
}
}

Просмотреть файл

@ -30,7 +30,7 @@ public abstract class TabsLayout extends RecyclerView
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);
@ -39,7 +39,7 @@ public abstract class TabsLayout extends RecyclerView
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
@ -101,8 +101,8 @@ public abstract class TabsLayout extends RecyclerView
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;
@ -124,10 +124,16 @@ public abstract class TabsLayout extends RecyclerView
}
}
// 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;
@ -143,9 +149,9 @@ public abstract class TabsLayout extends RecyclerView
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);
}
@ -199,6 +205,15 @@ public abstract class TabsLayout extends RecyclerView
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;

Просмотреть файл

@ -5,96 +5,123 @@
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 final Context mContext;
private final int mTabLayoutId;
private ArrayList<Tab> mTabs;
private final LayoutInflater mInflater;
private static final String LOGTAG = "Gecko" + TabsLayoutAdapter.class.getSimpleName();
public TabsLayoutAdapter (Context context, int tabLayoutId) {
mContext = context;
mInflater = LayoutInflater.from(mContext);
mTabLayoutId = tabLayoutId;
}
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;
final void setTabs (ArrayList<Tab> tabs) {
mTabs = tabs;
notifyDataSetChanged(); // Be sure to call this whenever mTabs changes.
}
final boolean removeTab (Tab tab) {
boolean tabRemoved = mTabs.remove(tab);
if (tabRemoved) {
notifyDataSetChanged(); // Be sure to call this whenever mTabs changes.
// 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);
}
return tabRemoved;
}
final void clear() {
mTabs = null;
notifyDataSetChanged(); // Be sure to call this whenever mTabs changes.
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);
}
@Override
public int getCount() {
return (mTabs == null ? 0 : mTabs.size());
/* package */ final void setTabs(@NonNull ArrayList<Tab> tabs) {
this.tabs = tabs;
notifyDataSetChanged();
}
@Override
public Tab getItem(int position) {
return mTabs.get(position);
/* package */ final void clear() {
tabs = new ArrayList<>(0);
notifyDataSetChanged();
}
@Override
public long getItemId(int position) {
return position;
}
final int getPositionForTab(Tab tab) {
if (mTabs == null || tab == null)
return -1;
return mTabs.indexOf(tab);
}
@Override
public boolean isEnabled(int position) {
/* package */ final boolean removeTab(Tab tab) {
final int position = getPositionForTab(tab);
if (position == -1) {
return false;
}
tabs.remove(position);
notifyItemRemoved(position);
return true;
}
@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;
/* package */ final int getPositionForTab(Tab tab) {
if (tab == null) {
return -1;
}
final Tab tab = mTabs.get(position);
bindView(view, tab);
return view;
return tabs.indexOf(tab);
}
TabsLayoutItemView newView(int position, ViewGroup parent) {
return (TabsLayoutItemView) mInflater.inflate(mTabLayoutId, parent, false);
/* package */ void notifyTabChanged(Tab tab) {
final int position = getPositionForTab(tab);
if (position != -1) {
notifyItemChanged(position);
}
}
void bindView(TabsLayoutItemView view, Tab tab) {
view.assignValues(tab);
/* 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 getItemCount() {
return tabs.size();
}
private Tab getItem(int position) {
return tabs.get(position);
}
@Override
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
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);
}
}

Просмотреть файл

@ -106,13 +106,4 @@ public class TabsListLayout extends TabsLayout {
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);
}
}

Просмотреть файл

@ -685,10 +685,10 @@ gbjar.sources += ['java/org/mozilla/gecko/' + x for x in [
'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',

Просмотреть файл

@ -7,17 +7,15 @@
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

Просмотреть файл

@ -7,5 +7,5 @@
<!-- 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>

Просмотреть файл

@ -4,7 +4,7 @@
- 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>

Просмотреть файл

@ -4,7 +4,7 @@
- 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>

Просмотреть файл

@ -4,7 +4,7 @@
- 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>

Просмотреть файл

@ -39,7 +39,6 @@
<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>

Просмотреть файл

@ -34,7 +34,6 @@
<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>

Просмотреть файл

@ -5,6 +5,6 @@
<resources>
<dimen name="tab_panel_grid_padding">64dp</dimen>
<dimen name="tab_panel_grid_hpadding">64dp</dimen>
</resources>

Просмотреть файл

@ -6,6 +6,6 @@
<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>

Просмотреть файл

@ -34,9 +34,6 @@
<!-- 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" />

Просмотреть файл

@ -144,10 +144,11 @@
<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>

Просмотреть файл

@ -180,21 +180,6 @@
<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"/>

Просмотреть файл

@ -95,7 +95,6 @@
<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>

Просмотреть файл

@ -612,54 +612,15 @@ abstract class BaseTest extends BaseRobocopTest {
}
}
// 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);
}
/**
@ -670,12 +631,12 @@ abstract class BaseTest extends BaseRobocopTest {
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
@ -684,7 +645,9 @@ abstract class BaseTest extends BaseRobocopTest {
@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;
}
});
}