зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
0a6ae7ed4e
Коммит
36a8b1bc24
|
@ -5,708 +5,73 @@
|
||||||
|
|
||||||
package org.mozilla.gecko.tabs;
|
package org.mozilla.gecko.tabs;
|
||||||
|
|
||||||
import org.mozilla.gecko.AppConstants;
|
|
||||||
import org.mozilla.gecko.GeckoAppShell;
|
|
||||||
import org.mozilla.gecko.R;
|
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.Context;
|
||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
import android.content.res.TypedArray;
|
import android.support.v7.widget.GridLayoutManager;
|
||||||
import android.graphics.PointF;
|
|
||||||
import android.graphics.Rect;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.util.AttributeSet;
|
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;
|
public class TabsGridLayout extends TabsLayout {
|
||||||
import java.util.List;
|
private final int desiredColumnWidth;
|
||||||
|
|
||||||
/**
|
public TabsGridLayout(Context context, AttributeSet attrs) {
|
||||||
* A tabs layout implementation for the tablet redesign (bug 1014156) and later ported to mobile (bug 1193745).
|
super(context, attrs, R.layout.tabs_layout_item_view);
|
||||||
*/
|
|
||||||
|
|
||||||
class TabsGridLayout extends GridView
|
final Resources resources = context.getResources();
|
||||||
implements TabsLayout,
|
|
||||||
Tabs.OnTabsChangedListener {
|
|
||||||
|
|
||||||
private static final String LOGTAG = "Gecko" + TabsGridLayout.class.getSimpleName();
|
setLayoutManager(new GridLayoutManager(context, 1));
|
||||||
|
|
||||||
public static final int ANIM_DELAY_MULTIPLE_MS = 20;
|
desiredColumnWidth = resources.getDimensionPixelSize(R.dimen.tab_panel_item_width);
|
||||||
private static final int ANIM_TIME_MS = 200;
|
final int viewPaddingHorizontal = resources.getDimensionPixelSize(R.dimen.tab_panel_grid_hpadding);
|
||||||
private static final DecelerateInterpolator ANIM_INTERPOLATOR = new DecelerateInterpolator();
|
final int viewPaddingVertical = resources.getDimensionPixelSize(R.dimen.tab_panel_grid_vpadding);
|
||||||
|
|
||||||
private final SparseArray<PointF> tabLocations = new SparseArray<PointF>();
|
setPadding(viewPaddingHorizontal, viewPaddingVertical, viewPaddingHorizontal, viewPaddingVertical);
|
||||||
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);
|
setClipToPadding(false);
|
||||||
|
setScrollBarStyle(SCROLLBARS_OUTSIDE_OVERLAY);
|
||||||
|
|
||||||
setVerticalFadingEdgeEnabled(false);
|
setItemAnimator(new TabsGridLayoutAnimator());
|
||||||
|
|
||||||
final Resources resources = getResources();
|
// TODO Add ItemDecoration.
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void closeAll() {
|
public void closeAll() {
|
||||||
|
|
||||||
autoHidePanel();
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final Iterable<Tab> tabs = Tabs.getInstance().getTabsInOrder();
|
// TODO This is temporary - we need to take into account item padding and we'll also try to
|
||||||
for (Tab tab : tabs) {
|
// match the previous GridLayout span count.
|
||||||
// In the normal panel we want to close all tabs (both private and normal),
|
final int nonPaddingWidth = w - getPaddingLeft() - getPaddingRight();
|
||||||
// but in the private panel we only want to close private tabs.
|
// Adjust span based on space available (what GridView does when you say numColumns="auto_fit").
|
||||||
if (!isPrivate || tab.isPrivate()) {
|
final int spanCount = Math.max(1, nonPaddingWidth / desiredColumnWidth);
|
||||||
Tabs.getInstance().closeTab(tab, false);
|
final GridLayoutManager layoutManager = (GridLayoutManager) getLayoutManager();
|
||||||
}
|
if (spanCount == layoutManager.getSpanCount()) {
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final int removedHeight = removedView.getMeasuredHeight();
|
layoutManager.setSpanCount(spanCount);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 final boolean isPrivate;
|
||||||
private TabsPanel tabsPanel;
|
private TabsPanel tabsPanel;
|
||||||
private final TabsLayoutRecyclerAdapter tabsAdapter;
|
private final TabsLayoutAdapter tabsAdapter;
|
||||||
|
|
||||||
public TabsLayout(Context context, AttributeSet attrs, int itemViewLayoutResId) {
|
public TabsLayout(Context context, AttributeSet attrs, int itemViewLayoutResId) {
|
||||||
super(context, attrs);
|
super(context, attrs);
|
||||||
|
@ -39,7 +39,7 @@ public abstract class TabsLayout extends RecyclerView
|
||||||
isPrivate = (a.getInt(R.styleable.TabsLayout_tabs, 0x0) == 1);
|
isPrivate = (a.getInt(R.styleable.TabsLayout_tabs, 0x0) == 1);
|
||||||
a.recycle();
|
a.recycle();
|
||||||
|
|
||||||
tabsAdapter = new TabsLayoutRecyclerAdapter(context, itemViewLayoutResId, isPrivate,
|
tabsAdapter = new TabsLayoutAdapter(context, itemViewLayoutResId, isPrivate,
|
||||||
/* close on click listener */
|
/* close on click listener */
|
||||||
new Button.OnClickListener() {
|
new Button.OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
|
@ -101,8 +101,8 @@ public abstract class TabsLayout extends RecyclerView
|
||||||
final int tabIndex = Integer.parseInt(data);
|
final int tabIndex = Integer.parseInt(data);
|
||||||
tabsAdapter.notifyTabInserted(tab, tabIndex);
|
tabsAdapter.notifyTabInserted(tab, tabIndex);
|
||||||
if (addAtIndexRequiresScroll(tabIndex)) {
|
if (addAtIndexRequiresScroll(tabIndex)) {
|
||||||
// (The current Tabs implementation updates the SELECTED tab *after* this
|
// (The SELECTED tab is updated *after* this call to ADDED, so don't just call
|
||||||
// call to ADDED, so don't just call updateSelectedPosition().)
|
// updateSelectedPosition().)
|
||||||
scrollToPosition(tabIndex);
|
scrollToPosition(tabIndex);
|
||||||
}
|
}
|
||||||
break;
|
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);
|
abstract protected boolean addAtIndexRequiresScroll(int index);
|
||||||
|
|
||||||
|
protected int getSelectedAdapterPosition() {
|
||||||
|
return tabsAdapter.getPositionForTab(Tabs.getInstance().getSelectedTab());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onItemClicked(RecyclerView recyclerView, int position, View v) {
|
public void onItemClicked(RecyclerView recyclerView, int position, View v) {
|
||||||
final TabsLayoutItemView item = (TabsLayoutItemView) 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);
|
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() {
|
private void updateSelectedPosition() {
|
||||||
final int selected = tabsAdapter.getPositionForTab(Tabs.getInstance().getSelectedTab());
|
final int selected = getSelectedAdapterPosition();
|
||||||
if (selected != NO_POSITION) {
|
if (selected != NO_POSITION) {
|
||||||
scrollToPosition(selected);
|
scrollToPosition(selected);
|
||||||
}
|
}
|
||||||
|
@ -199,6 +205,15 @@ public abstract class TabsLayout extends RecyclerView
|
||||||
closeTab(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) {
|
private Tab getTabForView(View view) {
|
||||||
if (view == null) {
|
if (view == null) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -5,96 +5,123 @@
|
||||||
|
|
||||||
package org.mozilla.gecko.tabs;
|
package org.mozilla.gecko.tabs;
|
||||||
|
|
||||||
import org.mozilla.gecko.R;
|
|
||||||
import org.mozilla.gecko.Tab;
|
import org.mozilla.gecko.Tab;
|
||||||
|
|
||||||
import android.content.Context;
|
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.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.BaseAdapter;
|
import android.widget.Button;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
||||||
// Adapter to bind tabs into a list
|
public class TabsLayoutAdapter
|
||||||
public class TabsLayoutAdapter extends BaseAdapter {
|
extends RecyclerView.Adapter<TabsLayoutAdapter.TabsListViewHolder> {
|
||||||
public static final String LOGTAG = "Gecko" + TabsLayoutAdapter.class.getSimpleName();
|
|
||||||
|
|
||||||
private final Context mContext;
|
private static final String LOGTAG = "Gecko" + TabsLayoutAdapter.class.getSimpleName();
|
||||||
private final int mTabLayoutId;
|
|
||||||
private ArrayList<Tab> mTabs;
|
|
||||||
private final LayoutInflater mInflater;
|
|
||||||
|
|
||||||
public TabsLayoutAdapter (Context context, int tabLayoutId) {
|
private final int tabLayoutId;
|
||||||
mContext = context;
|
private @NonNull ArrayList<Tab> tabs;
|
||||||
mInflater = LayoutInflater.from(mContext);
|
private final LayoutInflater inflater;
|
||||||
mTabLayoutId = tabLayoutId;
|
private final boolean isPrivate;
|
||||||
}
|
// Click listener for the close button on itemViews.
|
||||||
|
private final Button.OnClickListener closeOnClickListener;
|
||||||
|
|
||||||
final void setTabs (ArrayList<Tab> tabs) {
|
// The TabsLayoutItemView takes care of caching its own Views, so we don't need to do anything
|
||||||
mTabs = tabs;
|
// here except not be abstract.
|
||||||
notifyDataSetChanged(); // Be sure to call this whenever mTabs changes.
|
public static class TabsListViewHolder extends RecyclerView.ViewHolder {
|
||||||
}
|
public TabsListViewHolder(View itemView) {
|
||||||
|
super(itemView);
|
||||||
final boolean removeTab (Tab tab) {
|
|
||||||
boolean tabRemoved = mTabs.remove(tab);
|
|
||||||
if (tabRemoved) {
|
|
||||||
notifyDataSetChanged(); // Be sure to call this whenever mTabs changes.
|
|
||||||
}
|
}
|
||||||
return tabRemoved;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final void clear() {
|
public TabsLayoutAdapter(Context context, int tabLayoutId, boolean isPrivate,
|
||||||
mTabs = null;
|
Button.OnClickListener closeOnClickListener) {
|
||||||
|
inflater = LayoutInflater.from(context);
|
||||||
notifyDataSetChanged(); // Be sure to call this whenever mTabs changes.
|
this.tabLayoutId = tabLayoutId;
|
||||||
|
this.isPrivate = isPrivate;
|
||||||
|
this.closeOnClickListener = closeOnClickListener;
|
||||||
|
tabs = new ArrayList<>(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
/* package */ final void setTabs(@NonNull ArrayList<Tab> tabs) {
|
||||||
public int getCount() {
|
this.tabs = tabs;
|
||||||
return (mTabs == null ? 0 : mTabs.size());
|
notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
/* package */ final void clear() {
|
||||||
public Tab getItem(int position) {
|
tabs = new ArrayList<>(0);
|
||||||
return mTabs.get(position);
|
notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
/* package */ final boolean removeTab(Tab tab) {
|
||||||
public long getItemId(int position) {
|
final int position = getPositionForTab(tab);
|
||||||
return position;
|
if (position == -1) {
|
||||||
}
|
return false;
|
||||||
|
}
|
||||||
final int getPositionForTab(Tab tab) {
|
tabs.remove(position);
|
||||||
if (mTabs == null || tab == null)
|
notifyItemRemoved(position);
|
||||||
return -1;
|
|
||||||
|
|
||||||
return mTabs.indexOf(tab);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isEnabled(int position) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
/* package */ final int getPositionForTab(Tab tab) {
|
||||||
final public TabsLayoutItemView getView(int position, View convertView, ViewGroup parent) {
|
if (tab == null) {
|
||||||
final TabsLayoutItemView view;
|
return -1;
|
||||||
if (convertView == null) {
|
|
||||||
view = newView(position, parent);
|
|
||||||
} else {
|
|
||||||
view = (TabsLayoutItemView) convertView;
|
|
||||||
}
|
}
|
||||||
final Tab tab = mTabs.get(position);
|
|
||||||
bindView(view, tab);
|
return tabs.indexOf(tab);
|
||||||
return view;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TabsLayoutItemView newView(int position, ViewGroup parent) {
|
/* package */ void notifyTabChanged(Tab tab) {
|
||||||
return (TabsLayoutItemView) mInflater.inflate(mTabLayoutId, parent, false);
|
final int position = getPositionForTab(tab);
|
||||||
|
if (position != -1) {
|
||||||
|
notifyItemChanged(position);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void bindView(TabsLayoutItemView view, Tab tab) {
|
/* package */ void notifyTabInserted(Tab tab, int index) {
|
||||||
view.assignValues(tab);
|
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) {
|
protected boolean addAtIndexRequiresScroll(int index) {
|
||||||
return index == 0 || index == getAdapter().getItemCount() - 1;
|
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/TabHistoryPage.java',
|
||||||
'tabs/TabPanelBackButton.java',
|
'tabs/TabPanelBackButton.java',
|
||||||
'tabs/TabsGridLayout.java',
|
'tabs/TabsGridLayout.java',
|
||||||
|
'tabs/TabsGridLayoutAnimator.java',
|
||||||
'tabs/TabsLayout.java',
|
'tabs/TabsLayout.java',
|
||||||
'tabs/TabsLayoutAdapter.java',
|
'tabs/TabsLayoutAdapter.java',
|
||||||
'tabs/TabsLayoutItemView.java',
|
'tabs/TabsLayoutItemView.java',
|
||||||
'tabs/TabsLayoutRecyclerAdapter.java',
|
|
||||||
'tabs/TabsListLayout.java',
|
'tabs/TabsListLayout.java',
|
||||||
'tabs/TabsListLayoutAnimator.java',
|
'tabs/TabsListLayoutAnimator.java',
|
||||||
'tabs/TabsPanel.java',
|
'tabs/TabsPanel.java',
|
||||||
|
|
|
@ -7,17 +7,15 @@
|
||||||
xmlns:gecko="http://schemas.android.com/apk/res-auto"
|
xmlns:gecko="http://schemas.android.com/apk/res-auto"
|
||||||
style="@style/TabsItem"
|
style="@style/TabsItem"
|
||||||
android:id="@+id/info"
|
android:id="@+id/info"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="fill_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<LinearLayout android:layout_width="fill_parent"
|
<LinearLayout android:layout_width="@dimen/tab_thumbnail_width"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:duplicateParentState="true"
|
android:duplicateParentState="true"
|
||||||
android:paddingLeft="@dimen/tab_highlight_stroke_width"
|
|
||||||
android:paddingRight="@dimen/tab_highlight_stroke_width"
|
|
||||||
android:paddingBottom="@dimen/tab_highlight_stroke_width">
|
android:paddingBottom="@dimen/tab_highlight_stroke_width">
|
||||||
|
|
||||||
<org.mozilla.gecko.widget.FadedSingleColorTextView
|
<org.mozilla.gecko.widget.FadedSingleColorTextView
|
||||||
|
|
|
@ -7,5 +7,5 @@
|
||||||
|
|
||||||
<!-- Remote Tabs static view top padding. Less in landscape on phones. -->
|
<!-- Remote Tabs static view top padding. Less in landscape on phones. -->
|
||||||
<dimen name="home_remote_tabs_top_padding">16dp</dimen>
|
<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>
|
</resources>
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||||
|
|
||||||
<resources>
|
<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_height">100dip</dimen>
|
||||||
<dimen name="tab_thumbnail_width">135dip</dimen>
|
<dimen name="tab_thumbnail_width">135dip</dimen>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||||
|
|
||||||
<resources>
|
<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_height">110dip</dimen>
|
||||||
<dimen name="tab_thumbnail_width">148dip</dimen>
|
<dimen name="tab_thumbnail_width">148dip</dimen>
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||||
|
|
||||||
<resources>
|
<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_height">120dip</dimen>
|
||||||
<dimen name="tab_thumbnail_width">168dip</dimen>
|
<dimen name="tab_thumbnail_width">168dip</dimen>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -39,7 +39,6 @@
|
||||||
<item name="menuItemSwitcherLayoutStyle">@style/Widget.MenuItemSwitcherLayout</item>
|
<item name="menuItemSwitcherLayoutStyle">@style/Widget.MenuItemSwitcherLayout</item>
|
||||||
<item name="menuItemDefaultStyle">@style/Widget.MenuItemDefault</item>
|
<item name="menuItemDefaultStyle">@style/Widget.MenuItemDefault</item>
|
||||||
<item name="menuItemSecondaryActionBarStyle">@style/Widget.MenuItemSecondaryActionBar</item>
|
<item name="menuItemSecondaryActionBarStyle">@style/Widget.MenuItemSecondaryActionBar</item>
|
||||||
<item name="tabGridLayoutViewStyle">@style/Widget.TabsGridLayout</item>
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -34,7 +34,6 @@
|
||||||
<item name="menuItemSwitcherLayoutStyle">@style/Widget.MenuItemSwitcherLayout</item>
|
<item name="menuItemSwitcherLayoutStyle">@style/Widget.MenuItemSwitcherLayout</item>
|
||||||
<item name="menuItemDefaultStyle">@style/Widget.MenuItemDefault</item>
|
<item name="menuItemDefaultStyle">@style/Widget.MenuItemDefault</item>
|
||||||
<item name="menuItemSecondaryActionBarStyle">@style/Widget.MenuItemSecondaryActionBar</item>
|
<item name="menuItemSecondaryActionBarStyle">@style/Widget.MenuItemSecondaryActionBar</item>
|
||||||
<item name="tabGridLayoutViewStyle">@style/Widget.TabsGridLayout</item>
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -5,6 +5,6 @@
|
||||||
|
|
||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<dimen name="tab_panel_grid_padding">64dp</dimen>
|
<dimen name="tab_panel_grid_hpadding">64dp</dimen>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -6,6 +6,6 @@
|
||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<dimen name="panel_grid_view_column_width">250dp</dimen>
|
<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>
|
</resources>
|
||||||
|
|
|
@ -34,9 +34,6 @@
|
||||||
<!-- Styles for dynamic panel grid views -->
|
<!-- Styles for dynamic panel grid views -->
|
||||||
<attr name="panelIconViewStyle" format="reference" />
|
<attr name="panelIconViewStyle" format="reference" />
|
||||||
|
|
||||||
<!-- Style for the TabsGridLayout -->
|
|
||||||
<attr name="tabGridLayoutViewStyle" format="reference" />
|
|
||||||
|
|
||||||
<!-- Default style for the TopSitesGridView -->
|
<!-- Default style for the TopSitesGridView -->
|
||||||
<attr name="topSitesGridViewStyle" format="reference" />
|
<attr name="topSitesGridViewStyle" format="reference" />
|
||||||
|
|
||||||
|
|
|
@ -144,10 +144,11 @@
|
||||||
|
|
||||||
<dimen name="tab_thumbnail_width">121dp</dimen>
|
<dimen name="tab_thumbnail_width">121dp</dimen>
|
||||||
<dimen name="tab_thumbnail_height">90dp</dimen>
|
<dimen name="tab_thumbnail_height">90dp</dimen>
|
||||||
<dimen name="tab_panel_column_width">129dp</dimen>
|
<dimen name="tab_panel_item_width">129dp</dimen>
|
||||||
<dimen name="tab_panel_grid_padding">20dp</dimen>
|
<dimen name="tab_panel_grid_hpadding">20dp</dimen>
|
||||||
<dimen name="tab_panel_grid_vspacing">20dp</dimen>
|
<dimen name="tab_panel_grid_vpadding">19dp</dimen>
|
||||||
<dimen name="tab_panel_grid_padding_top">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>
|
<dimen name="tab_highlight_stroke_width">4dp</dimen>
|
||||||
|
|
||||||
|
|
|
@ -180,21 +180,6 @@
|
||||||
<item name="android:orientation">vertical</item>
|
<item name="android:orientation">vertical</item>
|
||||||
</style>
|
</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.BookmarkItemView" parent="Widget.TwoLinePageRow"/>
|
||||||
|
|
||||||
<style name="Widget.BookmarksListView" parent="Widget.HomeListView"/>
|
<style name="Widget.BookmarksListView" parent="Widget.HomeListView"/>
|
||||||
|
|
|
@ -95,7 +95,6 @@
|
||||||
<item name="android:spinnerStyle">@style/Widget.Spinner</item>
|
<item name="android:spinnerStyle">@style/Widget.Spinner</item>
|
||||||
<item name="android:windowBackground">@android:color/white</item>
|
<item name="android:windowBackground">@android:color/white</item>
|
||||||
<item name="bookmarksListViewStyle">@style/Widget.BookmarksListView</item>
|
<item name="bookmarksListViewStyle">@style/Widget.BookmarksListView</item>
|
||||||
<item name="tabGridLayoutViewStyle">@style/Widget.TabsGridLayout</item>
|
|
||||||
<item name="geckoMenuListViewStyle">@style/Widget.GeckoMenuListView</item>
|
<item name="geckoMenuListViewStyle">@style/Widget.GeckoMenuListView</item>
|
||||||
<item name="homeListViewStyle">@style/Widget.HomeListView</item>
|
<item name="homeListViewStyle">@style/Widget.HomeListView</item>
|
||||||
<item name="menuItemActionBarStyle">@style/Widget.MenuItemActionBar</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
|
* @return List view in the tabs panel
|
||||||
*/
|
*/
|
||||||
private final TabsView getTabsLayout() {
|
private final RecyclerView getTabsLayout() {
|
||||||
Element tabs = mDriver.findElement(getActivity(), R.id.tabs);
|
Element tabs = mDriver.findElement(getActivity(), R.id.tabs);
|
||||||
tabs.click();
|
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) {
|
private View getTabViewAt(final int index) {
|
||||||
final View[] childView = { null };
|
final View[] childView = { null };
|
||||||
|
|
||||||
final TabsView view = getTabsLayout();
|
final RecyclerView view = getTabsLayout();
|
||||||
|
|
||||||
runOnUiThreadSync(new Runnable() {
|
runOnUiThreadSync(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
view.bringPositionIntoView(index);
|
view.scrollToPosition(index);
|
||||||
|
|
||||||
// The selection isn't updated synchronously; posting a
|
// The selection isn't updated synchronously; posting a
|
||||||
// runnable to the view's queue guarantees we'll run after the
|
// runnable to the view's queue guarantees we'll run after the
|
||||||
|
@ -684,7 +645,9 @@ abstract class BaseTest extends BaseRobocopTest {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
// Index is relative to all views in the list.
|
// 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;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Загрузка…
Ссылка в новой задаче