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