Bug 1116415 - 1. Create new RecyclerView and adapter base classes for tabs panels. r?sebastian draft
authorTom Klein <twointofive@gmail.com>
Mon, 12 Sep 2016 10:33:02 -0500
changeset 424886 22a3bca7516429880b97b8aee1f60fc2f111dfda
parent 424813 99c64f6f475b87e9cb22edaa530d938884030068
child 424887 befde9a29c936f5afc87565f62aa3635fc05316e
child 425500 4a2b54b24d24495875121d764f3e07f07be46f7b
push id32276
push userbmo:twointofive@gmail.com
push dateThu, 13 Oct 2016 17:14:18 +0000
reviewerssebastian
bugs1116415
milestone52.0a1
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
mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayout.java
mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayoutItemView.java
mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayoutRecyclerAdapter.java
mobile/android/base/moz.build
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',