Bug 1116415 - 1. Create new RecyclerView and adapter base classes for tabs panels. r?sebastian
TabsListRecyclerAdapter.java will replace TabsLayoutAdapter.java when the time
comes.
Note from the future: the previous tabs layouts did a scroll each time an item
was added or selected, but GridLayoutManager sometimes does scrolls that aren't
necessary (like even when the position being scrolled to is already completely
in view), so we've adopted the approach of only scrolling when RecyclerView
makes it necessary.
MozReview-Commit-ID: JisX974zt88
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayout.java
@@ -0,0 +1,191 @@
+/* -*- 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.GeckoAppShell;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.Tab;
+import org.mozilla.gecko.Tabs;
+import org.mozilla.gecko.widget.RecyclerViewClickSupport;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.support.v7.widget.RecyclerView;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.Button;
+
+import java.util.ArrayList;
+
+public abstract class TabsLayout extends RecyclerView
+ implements TabsPanel.TabsLayout,
+ Tabs.OnTabsChangedListener,
+ RecyclerViewClickSupport.OnItemClickListener {
+
+ private static final String LOGTAG = "Gecko" + TabsLayout.class.getSimpleName();
+
+ private final boolean isPrivate;
+ private TabsPanel tabsPanel;
+ private final TabsLayoutRecyclerAdapter tabsAdapter;
+
+ public TabsLayout(Context context, AttributeSet attrs, int itemViewLayoutResId) {
+ super(context, attrs);
+
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabsLayout);
+ isPrivate = (a.getInt(R.styleable.TabsLayout_tabs, 0x0) == 1);
+ a.recycle();
+
+ tabsAdapter = new TabsLayoutRecyclerAdapter(context, itemViewLayoutResId, isPrivate,
+ /* close on click listener */
+ new Button.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // The view here is the close button, which has a reference
+ // to the parent TabsLayoutItemView in its tag, hence the getTag() call.
+ TabsLayoutItemView itemView = (TabsLayoutItemView) v.getTag();
+ closeTab(itemView);
+ }
+ });
+ setAdapter(tabsAdapter);
+
+ RecyclerViewClickSupport.addTo(this).setOnItemClickListener(this);
+
+ setRecyclerListener(new RecyclerListener() {
+ @Override
+ public void onViewRecycled(RecyclerView.ViewHolder holder) {
+ final TabsLayoutItemView itemView = (TabsLayoutItemView) holder.itemView;
+ itemView.setThumbnail(null);
+ itemView.setCloseVisible(true);
+ }
+ });
+ }
+
+ @Override
+ public void setTabsPanel(TabsPanel panel) {
+ tabsPanel = panel;
+ }
+
+ @Override
+ public void show() {
+ setVisibility(View.VISIBLE);
+ Tabs.getInstance().refreshThumbnails();
+ Tabs.registerOnTabsChangedListener(this);
+ refreshTabsData();
+ }
+
+ @Override
+ public void hide() {
+ setVisibility(View.GONE);
+ Tabs.unregisterOnTabsChangedListener(this);
+ GeckoAppShell.notifyObservers("Tab:Screenshot:Cancel", "");
+ tabsAdapter.clear();
+ }
+
+ @Override
+ public boolean shouldExpand() {
+ return true;
+ }
+
+ protected void autoHidePanel() {
+ tabsPanel.autoHidePanel();
+ }
+
+ @Override
+ public void onTabChanged(Tab tab, Tabs.TabEvents msg, String data) {
+ switch (msg) {
+ case ADDED:
+ // Refresh the list to make sure the new tab is added in the right position.
+ refreshTabsData();
+ break;
+
+ case CLOSED:
+ if (tab.isPrivate() == isPrivate && tabsAdapter.getItemCount() > 0) {
+ tabsAdapter.removeTab(tab);
+ }
+ break;
+
+ case SELECTED:
+ case UNSELECTED:
+ case THUMBNAIL:
+ case TITLE:
+ case RECORDING_CHANGE:
+ case AUDIO_PLAYING_CHANGE:
+ tabsAdapter.notifyTabChanged(tab);
+ break;
+ }
+ }
+
+ @Override
+ public void onItemClicked(RecyclerView recyclerView, int position, View v) {
+ final TabsLayoutItemView item = (TabsLayoutItemView) v;
+ final int tabId = item.getTabId();
+ final Tabs tabs = Tabs.getInstance();
+ tabs.selectTab(tabId);
+ autoHidePanel();
+ tabs.notifyListeners(tabs.getTab(tabId), Tabs.TabEvents.OPENED_FROM_TABS_TRAY);
+ }
+
+ // 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());
+ if (selected != NO_POSITION) {
+ scrollToPosition(selected);
+ }
+ }
+
+ private void refreshTabsData() {
+ // Store a different copy of the tabs, so that we don't have to worry about
+ // accidentally updating it on the wrong thread.
+ final ArrayList<Tab> tabData = new ArrayList<>();
+ final Iterable<Tab> allTabs = Tabs.getInstance().getTabsInOrder();
+
+ for (final Tab tab : allTabs) {
+ if (tab.isPrivate() == isPrivate) {
+ tabData.add(tab);
+ }
+ }
+
+ tabsAdapter.setTabs(tabData);
+ updateSelectedPosition();
+ }
+
+ private void closeTab(View view) {
+ final TabsLayoutItemView itemView = (TabsLayoutItemView) view;
+ final Tab tab = getTabForView(itemView);
+ final boolean closingLastTab = tabsAdapter.getItemCount() == 1;
+ Tabs.getInstance().closeTab(tab, true);
+
+ if (closingLastTab) {
+ autoHidePanel();
+ }
+ }
+
+ protected void closeAllTabs() {
+ final Iterable<Tab> tabs = Tabs.getInstance().getTabsInOrder();
+ for (final 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 Tab getTabForView(View view) {
+ if (view == null) {
+ return null;
+ }
+ return Tabs.getInstance().getTab(((TabsLayoutItemView) view).getTabId());
+ }
+
+ @Override
+ public void setEmptyView(View emptyView) {
+ // We never display an empty view.
+ }
+
+ @Override
+ abstract public void closeAll();
+}
--- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayoutItemView.java
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayoutItemView.java
@@ -5,16 +5,17 @@
package org.mozilla.gecko.tabs;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.R;
import org.mozilla.gecko.Tab;
import org.mozilla.gecko.Tabs;
import org.mozilla.gecko.util.HardwareUtils;
import org.mozilla.gecko.widget.TabThumbnailWrapper;
+import org.mozilla.gecko.widget.themed.ThemedRelativeLayout;
import android.content.Context;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.TouchDelegate;
import android.view.View;
@@ -161,9 +162,13 @@ public class TabsLayoutItemView extends
public void setThumbnail(Drawable thumbnail) {
mThumbnail.setImageDrawable(thumbnail);
}
public void setCloseVisible(boolean visible) {
mCloseButton.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
}
+
+ public void setPrivateMode(boolean isPrivate) {
+ ((ThemedRelativeLayout) findViewById(R.id.wrapper)).setPrivateMode(isPrivate);
+ }
}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayoutRecyclerAdapter.java
@@ -0,0 +1,107 @@
+/* -*- 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.Tab;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+
+import java.util.ArrayList;
+
+public class TabsLayoutRecyclerAdapter
+ extends RecyclerView.Adapter<TabsLayoutRecyclerAdapter.TabsListViewHolder> {
+
+ 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;
+
+ // The TabsLayoutItemView takes care of caching its own Views, so we don't need to do anything
+ // here except not be abstract.
+ public static class TabsListViewHolder extends RecyclerView.ViewHolder {
+ public TabsListViewHolder(View itemView) {
+ super(itemView);
+ }
+ }
+
+ public TabsLayoutRecyclerAdapter(Context context, int tabLayoutId, boolean isPrivate,
+ Button.OnClickListener closeOnClickListener) {
+ inflater = LayoutInflater.from(context);
+ this.tabLayoutId = tabLayoutId;
+ this.isPrivate = isPrivate;
+ this.closeOnClickListener = closeOnClickListener;
+ tabs = new ArrayList<>(0);
+ }
+
+ /* package */ final void setTabs(@NonNull ArrayList<Tab> tabs) {
+ this.tabs = tabs;
+ notifyDataSetChanged();
+ }
+
+ /* package */ final void clear() {
+ tabs = new ArrayList<>(0);
+ notifyDataSetChanged();
+ }
+
+ /* package */ final boolean removeTab(Tab tab) {
+ final int position = getPositionForTab(tab);
+ if (position == -1) {
+ return false;
+ }
+ tabs.remove(position);
+ notifyItemRemoved(position);
+ return true;
+ }
+
+ /* package */ final int getPositionForTab(Tab tab) {
+ if (tab == null) {
+ return -1;
+ }
+
+ return tabs.indexOf(tab);
+ }
+
+ /* package */ void notifyTabChanged(Tab tab) {
+ notifyItemChanged(getPositionForTab(tab));
+ }
+
+ @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);
+ // Make sure we didn't miss any resets after animations and swipes:
+ itemView.setAlpha(1);
+ itemView.setTranslationX(0);
+ itemView.setTranslationY(0);
+ }
+
+ @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);
+ }
+}
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -662,18 +662,20 @@ gbjar.sources += ['java/org/mozilla/geck
'tabs/PrivateTabsPanel.java',
'tabs/TabCurve.java',
'tabs/TabHistoryController.java',
'tabs/TabHistoryFragment.java',
'tabs/TabHistoryItemRow.java',
'tabs/TabHistoryPage.java',
'tabs/TabPanelBackButton.java',
'tabs/TabsGridLayout.java',
+ 'tabs/TabsLayout.java',
'tabs/TabsLayoutAdapter.java',
'tabs/TabsLayoutItemView.java',
+ 'tabs/TabsLayoutRecyclerAdapter.java',
'tabs/TabsListLayout.java',
'tabs/TabsPanel.java',
'tabs/TabsPanelThumbnailView.java',
'Telemetry.java',
'telemetry/measurements/CampaignIdMeasurements.java',
'telemetry/measurements/SearchCountMeasurements.java',
'telemetry/measurements/SessionMeasurements.java',
'telemetry/pingbuilders/TelemetryCorePingBuilder.java',