1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
4  * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 
6 package org.mozilla.gecko.tabs;
7 
8 import org.mozilla.gecko.R;
9 import org.mozilla.gecko.Tab;
10 import org.mozilla.gecko.Tabs;
11 import org.mozilla.gecko.widget.RecyclerViewClickSupport;
12 
13 import android.content.Context;
14 import android.content.res.TypedArray;
15 import android.support.v7.widget.LinearLayoutManager;
16 import android.support.v7.widget.RecyclerView;
17 import android.util.AttributeSet;
18 import android.view.View;
19 import android.widget.Button;
20 
21 import java.util.ArrayList;
22 
23 public abstract class TabsLayout extends RecyclerView
24         implements TabsPanel.TabsLayout,
25         Tabs.OnTabsChangedListener,
26         RecyclerViewClickSupport.OnItemClickListener,
27         TabsTouchHelperCallback.DismissListener,
28         TabsTouchHelperCallback.DragListener {
29 
30     private static final String LOGTAG = "Gecko" + TabsLayout.class.getSimpleName();
31 
32     private final boolean isPrivate;
33     private TabsPanel tabsPanel;
34     private final TabsLayoutAdapter tabsAdapter;
35     private View emptyView;
36 
TabsLayout(Context context, AttributeSet attrs, int itemViewLayoutResId)37     public TabsLayout(Context context, AttributeSet attrs, int itemViewLayoutResId) {
38         super(context, attrs);
39 
40         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabsLayout);
41         isPrivate = (a.getInt(R.styleable.TabsLayout_tabs, 0x0) == 1);
42         a.recycle();
43 
44         tabsAdapter = new TabsLayoutAdapter(context, itemViewLayoutResId, isPrivate,
45                 /* close on click listener */
46                 new Button.OnClickListener() {
47                     @Override
48                     public void onClick(View v) {
49                         // The view here is the close button, which has a reference
50                         // to the parent TabsLayoutItemView in its tag, hence the getTag() call.
51                         TabsLayoutItemView itemView = (TabsLayoutItemView) v.getTag();
52                         closeTab(itemView);
53                     }
54                 });
55         setAdapter(tabsAdapter);
56 
57         RecyclerViewClickSupport.addTo(this).setOnItemClickListener(this);
58 
59         setRecyclerListener(new RecyclerListener() {
60             @Override
61             public void onViewRecycled(RecyclerView.ViewHolder holder) {
62                 final TabsLayoutItemView itemView = (TabsLayoutItemView) holder.itemView;
63                 itemView.setThumbnail(null);
64                 itemView.setCloseVisible(true);
65             }
66         });
67     }
68 
69     @Override
setTabsPanel(TabsPanel panel)70     public void setTabsPanel(TabsPanel panel) {
71         tabsPanel = panel;
72     }
73 
74     @Override
show()75     public void show() {
76         final boolean hasTabs = (tabsAdapter.getItemCount() > 0);
77         setVisibility(hasTabs ? VISIBLE : GONE);
78 
79         if (emptyView != null) {
80             emptyView.setVisibility(hasTabs ? GONE : VISIBLE);
81         }
82 
83         Tabs.getInstance().refreshThumbnails();
84         Tabs.registerOnTabsChangedListener(this);
85         refreshTabsData();
86     }
87 
88     @Override
hide()89     public void hide() {
90         setVisibility(View.GONE);
91         Tabs.unregisterOnTabsChangedListener(this);
92         tabsAdapter.clear();
93 
94         if (emptyView != null) {
95             emptyView.setVisibility(VISIBLE);
96         }
97     }
98 
99     @Override
shouldExpand()100     public boolean shouldExpand() {
101         return true;
102     }
103 
autoHidePanel()104     protected void autoHidePanel() {
105         tabsPanel.autoHidePanel();
106     }
107 
108     @Override
onTabChanged(Tab tab, Tabs.TabEvents msg, String data)109     public void onTabChanged(Tab tab, Tabs.TabEvents msg, String data) {
110         switch (msg) {
111             case ADDED:
112                 int tabIndex = Integer.parseInt(data);
113                 tabIndex = tabIndex == Tabs.NEW_LAST_INDEX ? tabsAdapter.getItemCount() : tabIndex;
114                 tabsAdapter.notifyTabInserted(tab, tabIndex);
115                 if (addAtIndexRequiresScroll(tabIndex)) {
116                     // (The SELECTED tab is updated *after* this call to ADDED, so don't just call
117                     // scrollSelectedTabToTopOfTray().)
118                     scrollToPosition(tabIndex);
119                 }
120                 break;
121 
122             case CLOSED:
123                 if (tab.isPrivate() == isPrivate && tabsAdapter.getItemCount() > 0) {
124                     tabsAdapter.removeTab(tab);
125                 }
126                 break;
127 
128             case SELECTED:
129             case UNSELECTED:
130             case THUMBNAIL:
131             case TITLE:
132             case RECORDING_CHANGE:
133             case AUDIO_PLAYING_CHANGE:
134                 tabsAdapter.notifyTabChanged(tab);
135                 break;
136         }
137     }
138 
139     /**
140      * Addition of a tab at selected positions (dependent on LayoutManager) can result in a tab
141      * being added out of view - return true if {@code index} is such a position.  This should be
142      * called only after the add has occurred.
143      */
addAtIndexRequiresScroll(int index)144     abstract protected boolean addAtIndexRequiresScroll(int index);
145 
getSelectedAdapterPosition()146     protected int getSelectedAdapterPosition() {
147         return tabsAdapter.getPositionForTab(Tabs.getInstance().getSelectedTab());
148     }
149 
150     @Override
onItemClicked(RecyclerView recyclerView, int position, View v)151     public void onItemClicked(RecyclerView recyclerView, int position, View v) {
152         final TabsLayoutItemView item = (TabsLayoutItemView) v;
153         final int tabId = item.getTabId();
154         final Tab tab = Tabs.getInstance().selectTab(tabId);
155         if (tab == null) {
156             // The tab that was clicked no longer exists in the tabs list (which can happen if you
157             // tap on a tab while its remove animation is running), so ignore the click.
158             return;
159         }
160 
161         autoHidePanel();
162         Tabs.getInstance().notifyListeners(tab, Tabs.TabEvents.OPENED_FROM_TABS_TRAY);
163     }
164 
165     @Override
onItemDismiss(View view)166     public void onItemDismiss(View view) {
167         closeTab(view);
168     }
169 
170     @Override
onItemMove(int fromPosition, int toPosition)171     public boolean onItemMove(int fromPosition, int toPosition) {
172         return tabsAdapter.moveTab(fromPosition, toPosition);
173     }
174 
175     /**
176      * Scroll the selected tab to the top of the tray.
177      * One of the motivations for scrolling to the top is so that, as often as possible, if we open
178      * a background tab from the selected tab, when we return to the tabs tray the new tab will be
179      * visible for selecting without requiring additional scrolling.
180      */
scrollSelectedTabToTopOfTray()181     protected void scrollSelectedTabToTopOfTray() {
182         final int selected = getSelectedAdapterPosition();
183         if (selected != NO_POSITION) {
184             ((LinearLayoutManager)getLayoutManager()).scrollToPositionWithOffset(selected, 0);
185         }
186     }
187 
refreshTabsData()188     private void refreshTabsData() {
189         // Store a different copy of the tabs, so that we don't have to worry about
190         // accidentally updating it on the wrong thread.
191         final ArrayList<Tab> tabData = new ArrayList<>();
192         final Iterable<Tab> allTabs = Tabs.getInstance().getTabsInOrder();
193 
194         for (final Tab tab : allTabs) {
195             if (tab.isPrivate() == isPrivate) {
196                 tabData.add(tab);
197             }
198         }
199 
200         tabsAdapter.setTabs(tabData);
201         scrollSelectedTabToTopOfTray();
202 
203         // Show empty view if we're in private panel and there's no private tabs.
204         boolean hasTabs = !tabData.isEmpty();
205         setVisibility(hasTabs ? VISIBLE : GONE);
206 
207         if (emptyView != null) {
208             emptyView.setVisibility(hasTabs ? GONE : VISIBLE);
209         }
210     }
211 
closeTab(View view)212     private void closeTab(View view) {
213         final TabsLayoutItemView itemView = (TabsLayoutItemView) view;
214         final Tab tab = getTabForView(itemView);
215         if (tab == null) {
216             // We can be null here if this is the second closeTab call resulting from a sufficiently
217             // fast double tap on the close tab button.
218             return;
219         }
220 
221         final boolean closingLastTab = tabsAdapter.getItemCount() == 1;
222         Tabs.getInstance().closeTab(tab, true);
223         if (closingLastTab) {
224             autoHidePanel();
225         }
226     }
227 
228     @Override
onChildAttachedToWindow(View child)229     public void onChildAttachedToWindow(View child) {
230         // Make sure we reset any attributes that may have been animated in this child's previous
231         // incarnation.
232         child.setTranslationX(0);
233         child.setTranslationY(0);
234         child.setAlpha(1);
235     }
236 
getTabForView(View view)237     private Tab getTabForView(View view) {
238         if (view == null) {
239             return null;
240         }
241         return Tabs.getInstance().getTab(((TabsLayoutItemView) view).getTabId());
242     }
243 
244     @Override
setEmptyView(View emptyView)245     public void setEmptyView(View emptyView) {
246         this.emptyView = emptyView;
247     }
248 
249     @Override
onCloseAll()250     abstract public void onCloseAll();
251 
isNormal()252     protected boolean isNormal() {
253         return !isPrivate;
254     }
255 }
256