summaryrefslogtreecommitdiffstats
path: root/mobile/android/base/java/org/mozilla/gecko/tabs
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /mobile/android/base/java/org/mozilla/gecko/tabs
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'mobile/android/base/java/org/mozilla/gecko/tabs')
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/tabs/PrivateTabsPanel.java63
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/tabs/TabCurve.java70
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryController.java87
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryFragment.java172
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryItemRow.java69
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryPage.java60
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/tabs/TabPanelBackButton.java55
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/tabs/TabStrip.java170
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/tabs/TabStripAdapter.java98
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/tabs/TabStripItemView.java254
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/tabs/TabStripView.java449
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/tabs/TabsGridLayout.java712
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayout.java216
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayoutAdapter.java100
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayoutItemView.java172
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayoutRecyclerAdapter.java124
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/tabs/TabsListLayout.java118
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/tabs/TabsListLayoutAnimator.java65
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/tabs/TabsPanel.java456
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/tabs/TabsPanelThumbnailView.java52
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/tabs/TabsTouchHelperCallback.java69
21 files changed, 3631 insertions, 0 deletions
diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/PrivateTabsPanel.java b/mobile/android/base/java/org/mozilla/gecko/tabs/PrivateTabsPanel.java
new file mode 100644
index 000000000..b7bd83376
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/PrivateTabsPanel.java
@@ -0,0 +1,63 @@
+/* -*- 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.R;
+import org.mozilla.gecko.tabs.TabsPanel.CloseAllPanelView;
+import org.mozilla.gecko.tabs.TabsPanel.TabsLayout;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.FrameLayout;
+
+/**
+ * A container that wraps the private tabs {@link android.widget.AdapterView} and empty
+ * {@link android.view.View} to manage both of their visibility states by changing the visibility of
+ * this container as calling {@link android.widget.AdapterView#setVisibility} does not affect the
+ * empty View's visibility.
+ */
+class PrivateTabsPanel extends FrameLayout implements CloseAllPanelView {
+ private final TabsLayout tabsLayout;
+
+ public PrivateTabsPanel(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+
+ LayoutInflater.from(context).inflate(R.layout.private_tabs_panel, this);
+ tabsLayout = (TabsLayout) findViewById(R.id.private_tabs_layout);
+
+ final View emptyTabsFrame = findViewById(R.id.private_tabs_empty);
+ tabsLayout.setEmptyView(emptyTabsFrame);
+ }
+
+ @Override
+ public void setTabsPanel(final TabsPanel panel) {
+ tabsLayout.setTabsPanel(panel);
+ }
+
+ @Override
+ public void show() {
+ tabsLayout.show();
+ setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void hide() {
+ setVisibility(View.GONE);
+ tabsLayout.hide();
+ }
+
+ @Override
+ public boolean shouldExpand() {
+ return tabsLayout.shouldExpand();
+ }
+
+ @Override
+ public void closeAll() {
+ tabsLayout.closeAll();
+ }
+}
diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabCurve.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabCurve.java
new file mode 100644
index 000000000..0b6a30d7a
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabCurve.java
@@ -0,0 +1,70 @@
+/* -*- 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 android.graphics.Path;
+
+/**
+ * Utility methods to draws Firefox's tab curve shape.
+ */
+public class TabCurve {
+
+ public enum Direction {
+ LEFT(-1),
+ RIGHT(1);
+
+ private final int value;
+
+ private Direction(int value) {
+ this.value = value;
+ }
+ }
+
+ // Curve's aspect ratio
+ private static final float ASPECT_RATIO = 0.729f;
+
+ // Width multipliers
+ private static final float W_M1 = 0.343f;
+ private static final float W_M2 = 0.514f;
+ private static final float W_M3 = 0.49f;
+ private static final float W_M4 = 0.545f;
+ private static final float W_M5 = 0.723f;
+
+ // Height multipliers
+ private static final float H_M1 = 0.25f;
+ private static final float H_M2 = 0.5f;
+ private static final float H_M3 = 0.72f;
+ private static final float H_M4 = 0.961f;
+
+ private TabCurve() {
+ }
+
+ public static float getWidthForHeight(float height) {
+ return (int) (height * ASPECT_RATIO);
+ }
+
+ public static void drawFromTop(Path path, float from, float height, Direction dir) {
+ final float width = getWidthForHeight(height);
+
+ path.cubicTo(from + width * W_M1 * dir.value, 0.0f,
+ from + width * W_M3 * dir.value, height * H_M1,
+ from + width * W_M2 * dir.value, height * H_M2);
+ path.cubicTo(from + width * W_M4 * dir.value, height * H_M3,
+ from + width * W_M5 * dir.value, height * H_M4,
+ from + width * dir.value, height);
+ }
+
+ public static void drawFromBottom(Path path, float from, float height, Direction dir) {
+ final float width = getWidthForHeight(height);
+
+ path.cubicTo(from + width * (1f - W_M5) * dir.value, height * H_M4,
+ from + width * (1f - W_M4) * dir.value, height * H_M3,
+ from + width * (1f - W_M2) * dir.value, height * H_M2);
+ path.cubicTo(from + width * (1f - W_M3) * dir.value, height * H_M1,
+ from + width * (1f - W_M1) * dir.value, 0.0f,
+ from + width * dir.value, 0.0f);
+ }
+}
diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryController.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryController.java
new file mode 100644
index 000000000..7b06c994c
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryController.java
@@ -0,0 +1,87 @@
+/* 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 java.util.ArrayList;
+import java.util.List;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.Tab;
+import org.mozilla.gecko.util.GeckoRequest;
+import org.mozilla.gecko.util.NativeJSObject;
+
+import android.util.Log;
+
+public class TabHistoryController {
+ private static final String LOGTAG = "TabHistoryController";
+ private final OnShowTabHistory showTabHistoryListener;
+
+ public static enum HistoryAction {
+ ALL,
+ BACK,
+ FORWARD
+ };
+
+ public interface OnShowTabHistory {
+ void onShowHistory(List<TabHistoryPage> historyPageList, int toIndex);
+ }
+
+ public TabHistoryController(OnShowTabHistory showTabHistoryListener) {
+ this.showTabHistoryListener = showTabHistoryListener;
+ }
+
+ /**
+ * This method will show the history for the current tab.
+ */
+ public boolean showTabHistory(final Tab tab, final HistoryAction action) {
+ JSONObject json = new JSONObject();
+ try {
+ json.put("action", action.name());
+ json.put("tabId", tab.getId());
+ } catch (JSONException e) {
+ Log.e(LOGTAG, "JSON error", e);
+ }
+
+ GeckoAppShell.sendRequestToGecko(new GeckoRequest("Session:GetHistory", json) {
+ @Override
+ public void onResponse(NativeJSObject nativeJSObject) {
+ /*
+ * The response from gecko request is of the form
+ * {
+ * "historyItems" : [
+ * {
+ * "title": "google",
+ * "url": "google.com",
+ * "selected": false
+ * }
+ * ],
+ * toIndex = 1
+ * }
+ */
+
+ final NativeJSObject[] historyItems = nativeJSObject.getObjectArray("historyItems");
+ if (historyItems.length == 0) {
+ // Empty history, return without showing the popup.
+ return;
+ }
+
+ final List<TabHistoryPage> historyPageList = new ArrayList<>(historyItems.length);
+ final int toIndex = nativeJSObject.getInt("toIndex");
+
+ for (NativeJSObject obj : historyItems) {
+ final String title = obj.getString("title");
+ final String url = obj.getString("url");
+ final boolean selected = obj.getBoolean("selected");
+ historyPageList.add(new TabHistoryPage(title, url, selected));
+ }
+
+ showTabHistoryListener.onShowHistory(historyPageList, toIndex);
+ }
+ });
+ return true;
+ }
+}
diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryFragment.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryFragment.java
new file mode 100644
index 000000000..e6deabdcf
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryFragment.java
@@ -0,0 +1,172 @@
+/* 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 java.util.ArrayList;
+import java.util.List;
+
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.GeckoApplication;
+import org.mozilla.gecko.R;
+
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentTransaction;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+
+public class TabHistoryFragment extends Fragment implements OnItemClickListener, OnClickListener {
+ private static final String ARG_LIST = "historyPageList";
+ private static final String ARG_INDEX = "index";
+ private static final String BACK_STACK_ID = "backStateId";
+
+ private List<TabHistoryPage> historyPageList;
+ private int toIndex;
+ private ListView dialogList;
+ private int backStackId = -1;
+ private ViewGroup parent;
+ private boolean dismissed;
+
+ public TabHistoryFragment() {
+
+ }
+
+ public static TabHistoryFragment newInstance(List<TabHistoryPage> historyPageList, int toIndex) {
+ final TabHistoryFragment fragment = new TabHistoryFragment();
+ final Bundle args = new Bundle();
+ args.putParcelableArrayList(ARG_LIST, (ArrayList<? extends Parcelable>) historyPageList);
+ args.putInt(ARG_INDEX, toIndex);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (savedInstanceState != null) {
+ backStackId = savedInstanceState.getInt(BACK_STACK_ID, -1);
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ this.parent = container;
+ parent.setVisibility(View.VISIBLE);
+ View view = inflater.inflate(R.layout.tab_history_layout, container, false);
+ view.setOnClickListener(this);
+ dialogList = (ListView) view.findViewById(R.id.tab_history_list);
+ dialogList.setDivider(null);
+ return view;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ Bundle bundle = getArguments();
+ historyPageList = bundle.getParcelableArrayList(ARG_LIST);
+ toIndex = bundle.getInt(ARG_INDEX);
+ final ArrayAdapter<TabHistoryPage> urlAdapter = new TabHistoryAdapter(getActivity(), historyPageList);
+ dialogList.setAdapter(urlAdapter);
+ dialogList.setOnItemClickListener(this);
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ String index = String.valueOf(toIndex - position);
+ GeckoAppShell.notifyObservers("Session:Navigate", index);
+ dismiss();
+ }
+
+ @Override
+ public void onClick(View v) {
+ // Since the fragment view fills the entire screen, any clicks outside of the history
+ // ListView will end up here.
+ dismiss();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ dismiss();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ dismiss();
+
+ GeckoApplication.watchReference(getActivity(), this);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ if (backStackId >= 0) {
+ outState.putInt(BACK_STACK_ID, backStackId);
+ }
+ }
+
+ // Function to add this fragment to activity state with containerViewId as parent.
+ // This similar in functionality to DialogFragment.show() except that containerId is provided here.
+ public void show(final int containerViewId, final FragmentTransaction transaction, final String tag) {
+ dismissed = false;
+ transaction.add(containerViewId, this, tag);
+ transaction.addToBackStack(tag);
+ // Populating the tab history requires a gecko call (which can be slow) - therefore the app
+ // state by the time we try to show this fragment is unknown, and we could be in the
+ // middle of shutting down:
+ backStackId = transaction.commitAllowingStateLoss();
+ }
+
+ // Pop the fragment from backstack if it exists.
+ public void dismiss() {
+ if (dismissed) {
+ return;
+ }
+
+ dismissed = true;
+
+ if (backStackId >= 0) {
+ getFragmentManager().popBackStackImmediate(backStackId, FragmentManager.POP_BACK_STACK_INCLUSIVE);
+ backStackId = -1;
+ }
+
+ if (parent != null) {
+ parent.setVisibility(View.GONE);
+ }
+ }
+
+ private static class TabHistoryAdapter extends ArrayAdapter<TabHistoryPage> {
+ private final List<TabHistoryPage> pages;
+ private final Context context;
+
+ public TabHistoryAdapter(Context context, List<TabHistoryPage> pages) {
+ super(context, R.layout.tab_history_item_row, pages);
+ this.context = context;
+ this.pages = pages;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ TabHistoryItemRow row = (TabHistoryItemRow) convertView;
+ if (row == null) {
+ row = new TabHistoryItemRow(context, null);
+ }
+
+ row.update(pages.get(position), position == 0, position == pages.size() - 1);
+ return row;
+ }
+ }
+}
diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryItemRow.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryItemRow.java
new file mode 100644
index 000000000..112dbc07d
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryItemRow.java
@@ -0,0 +1,69 @@
+/* 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 android.content.Context;
+import android.graphics.Typeface;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.icons.IconResponse;
+import org.mozilla.gecko.icons.Icons;
+import org.mozilla.gecko.util.ThreadUtils;
+import org.mozilla.gecko.widget.FaviconView;
+
+import java.util.concurrent.Future;
+
+public class TabHistoryItemRow extends RelativeLayout {
+ private final FaviconView favicon;
+ private final TextView title;
+ private final ImageView timeLineTop;
+ private final ImageView timeLineBottom;
+ private Future<IconResponse> ongoingIconLoad;
+
+ public TabHistoryItemRow(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ LayoutInflater.from(context).inflate(R.layout.tab_history_item_row, this);
+ favicon = (FaviconView) findViewById(R.id.tab_history_icon);
+ title = (TextView) findViewById(R.id.tab_history_title);
+ timeLineTop = (ImageView) findViewById(R.id.tab_history_timeline_top);
+ timeLineBottom = (ImageView) findViewById(R.id.tab_history_timeline_bottom);
+ }
+
+ // Update the views with historic page detail.
+ public void update(final TabHistoryPage historyPage, boolean isFirstElement, boolean isLastElement) {
+ ThreadUtils.assertOnUiThread();
+
+ timeLineTop.setVisibility(isFirstElement ? View.INVISIBLE : View.VISIBLE);
+ timeLineBottom.setVisibility(isLastElement ? View.INVISIBLE : View.VISIBLE);
+ title.setText(historyPage.getTitle());
+
+ if (historyPage.isSelected()) {
+ // Highlight title with bold font.
+ title.setTypeface(null, Typeface.BOLD);
+ } else {
+ // Clear previously set bold font.
+ title.setTypeface(null, Typeface.NORMAL);
+ }
+
+ favicon.setEnabled(historyPage.isSelected());
+ favicon.clearImage();
+
+ if (ongoingIconLoad != null) {
+ ongoingIconLoad.cancel(true);
+ }
+
+ ongoingIconLoad = Icons.with(getContext())
+ .pageUrl(historyPage.getUrl())
+ .skipNetwork()
+ .build()
+ .execute(favicon.createIconCallback());
+ }
+}
diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryPage.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryPage.java
new file mode 100644
index 000000000..6c608b2ac
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryPage.java
@@ -0,0 +1,60 @@
+/* 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 android.os.Parcel;
+import android.os.Parcelable;
+
+public class TabHistoryPage implements Parcelable {
+ private final String title;
+ private final String url;
+ private final boolean selected;
+
+ public TabHistoryPage(String title, String url, boolean selected) {
+ this.title = title;
+ this.url = url;
+ this.selected = selected;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public boolean isSelected() {
+ return selected;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(title);
+ dest.writeString(url);
+ dest.writeInt(selected ? 1 : 0);
+ }
+
+ public static final Parcelable.Creator<TabHistoryPage> CREATOR = new Parcelable.Creator<TabHistoryPage>() {
+ @Override
+ public TabHistoryPage createFromParcel(final Parcel source) {
+ final String title = source.readString();
+ final String url = source.readString();
+ final boolean selected = source.readByte() != 0;
+
+ final TabHistoryPage page = new TabHistoryPage(title, url, selected);
+ return page;
+ }
+
+ @Override
+ public TabHistoryPage[] newArray(int size) {
+ return new TabHistoryPage[size];
+ }
+ };
+}
diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabPanelBackButton.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabPanelBackButton.java
new file mode 100644
index 000000000..7ea02407e
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabPanelBackButton.java
@@ -0,0 +1,55 @@
+/* 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.R;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.ViewGroup;
+import android.widget.ImageButton;
+
+public class TabPanelBackButton extends ImageButton {
+
+ private int dividerWidth = 0;
+
+ private final Drawable divider;
+ private final int dividerPadding;
+
+ public TabPanelBackButton(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabPanelBackButton);
+ divider = a.getDrawable(R.styleable.TabPanelBackButton_rightDivider);
+ dividerPadding = (int) a.getDimension(R.styleable.TabPanelBackButton_dividerVerticalPadding, 0);
+ a.recycle();
+
+ if (divider != null) {
+ dividerWidth = divider.getIntrinsicWidth();
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ setMeasuredDimension(getMeasuredWidth() + dividerWidth, getMeasuredHeight());
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ if (divider != null) {
+ final ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) getLayoutParams();
+ final int left = getRight() - lp.rightMargin - dividerWidth;
+
+ divider.setBounds(left, getPaddingTop() + dividerPadding,
+ left + dividerWidth, getHeight() - getPaddingBottom() - dividerPadding);
+ divider.draw(canvas);
+ }
+ }
+}
diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabStrip.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabStrip.java
new file mode 100644
index 000000000..5d3719343
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabStrip.java
@@ -0,0 +1,170 @@
+/* -*- 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 android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.StateListDrawable;
+import android.graphics.Rect;
+import android.support.v4.content.ContextCompat;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.TouchDelegate;
+import android.view.View;
+import android.view.ViewTreeObserver;
+
+import org.mozilla.gecko.BrowserApp.TabStripInterface;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.Tab;
+import org.mozilla.gecko.Tabs;
+import org.mozilla.gecko.widget.themed.ThemedImageButton;
+import org.mozilla.gecko.widget.themed.ThemedLinearLayout;
+
+public class TabStrip extends ThemedLinearLayout
+ implements TabStripInterface {
+ private static final String LOGTAG = "GeckoTabStrip";
+
+ private final TabStripView tabStripView;
+ private final ThemedImageButton addTabButton;
+
+ private final TabsListener tabsListener;
+ private OnTabAddedOrRemovedListener tabChangedListener;
+
+ public TabStrip(Context context) {
+ this(context, null);
+ }
+
+ public TabStrip(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ setOrientation(HORIZONTAL);
+
+ LayoutInflater.from(context).inflate(R.layout.tab_strip_inner, this);
+ tabStripView = (TabStripView) findViewById(R.id.tab_strip);
+
+ addTabButton = (ThemedImageButton) findViewById(R.id.add_tab);
+ addTabButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ final Tabs tabs = Tabs.getInstance();
+ if (isPrivateMode()) {
+ tabs.addPrivateTab();
+ } else {
+ tabs.addTab();
+ }
+ }
+ });
+
+ getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
+ @Override
+ public boolean onPreDraw() {
+ getViewTreeObserver().removeOnPreDrawListener(this);
+
+ final Rect r = new Rect();
+ r.left = addTabButton.getRight();
+ r.right = getWidth();
+ r.top = 0;
+ r.bottom = getHeight();
+
+ // Redirect touch events between the 'new tab' button and the edge
+ // of the screen to the 'new tab' button.
+ setTouchDelegate(new TouchDelegate(r, addTabButton));
+
+ return true;
+ }
+ });
+
+ tabsListener = new TabsListener();
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ Tabs.registerOnTabsChangedListener(tabsListener);
+ tabStripView.refreshTabs();
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+
+ Tabs.unregisterOnTabsChangedListener(tabsListener);
+ tabStripView.clearTabs();
+ }
+
+ @Override
+ public void setPrivateMode(boolean isPrivate) {
+ super.setPrivateMode(isPrivate);
+ addTabButton.setPrivateMode(isPrivate);
+ }
+
+ public void setOnTabChangedListener(OnTabAddedOrRemovedListener listener) {
+ tabChangedListener = listener;
+ }
+
+ private class TabsListener implements Tabs.OnTabsChangedListener {
+ @Override
+ public void onTabChanged(Tab tab, Tabs.TabEvents msg, String data) {
+ switch (msg) {
+ case RESTORED:
+ tabStripView.restoreTabs();
+ break;
+
+ case ADDED:
+ tabStripView.addTab(tab);
+ if (tabChangedListener != null) {
+ tabChangedListener.onTabChanged();
+ }
+ break;
+
+ case CLOSED:
+ tabStripView.removeTab(tab);
+ if (tabChangedListener != null) {
+ tabChangedListener.onTabChanged();
+ }
+ break;
+
+ case SELECTED:
+ // Update the selected position, then fall through...
+ tabStripView.selectTab(tab);
+ setPrivateMode(tab.isPrivate());
+ case UNSELECTED:
+ // We just need to update the style for the unselected tab...
+ case TITLE:
+ case FAVICON:
+ case RECORDING_CHANGE:
+ case AUDIO_PLAYING_CHANGE:
+ tabStripView.updateTab(tab);
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void refresh() {
+ tabStripView.refresh();
+ }
+
+ @Override
+ public void onLightweightThemeChanged() {
+ final Drawable drawable = getTheme().getDrawable(this);
+ if (drawable == null) {
+ return;
+ }
+
+ final StateListDrawable stateList = new StateListDrawable();
+ stateList.addState(PRIVATE_STATE_SET, getColorDrawable(R.color.text_and_tabs_tray_grey));
+ stateList.addState(EMPTY_STATE_SET, drawable);
+
+ setBackgroundDrawable(stateList);
+ }
+
+ @Override
+ public void onLightweightThemeReset() {
+ final int defaultBackgroundColor = ContextCompat.getColor(getContext(), R.color.text_and_tabs_tray_grey);
+ setBackgroundColor(defaultBackgroundColor);
+ }
+}
diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabStripAdapter.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabStripAdapter.java
new file mode 100644
index 000000000..8778aac31
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabStripAdapter.java
@@ -0,0 +1,98 @@
+/* -*- 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 android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.Tab;
+import org.mozilla.gecko.Tabs;
+
+class TabStripAdapter extends BaseAdapter {
+ private static final String LOGTAG = "GeckoTabStripAdapter";
+
+ private final Context context;
+ private List<Tab> tabs;
+
+ public TabStripAdapter(Context context) {
+ this.context = context;
+ }
+
+ @Override
+ public Tab getItem(int position) {
+ return (tabs != null &&
+ position >= 0 &&
+ position < tabs.size() ? tabs.get(position) : null);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ final Tab tab = getItem(position);
+ return (tab != null ? tab.getId() : -1);
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ final TabStripItemView item;
+ if (convertView == null) {
+ item = (TabStripItemView)
+ LayoutInflater.from(context).inflate(R.layout.tab_strip_item, parent, false);
+ } else {
+ item = (TabStripItemView) convertView;
+ }
+
+ final Tab tab = tabs.get(position);
+ item.updateFromTab(tab);
+
+ return item;
+ }
+
+ @Override
+ public int getCount() {
+ return (tabs != null ? tabs.size() : 0);
+ }
+
+ int getPositionForTab(Tab tab) {
+ if (tabs == null || tab == null) {
+ return -1;
+ }
+
+ return tabs.indexOf(tab);
+ }
+
+ void removeTab(Tab tab) {
+ if (tabs == null) {
+ return;
+ }
+
+ tabs.remove(tab);
+ notifyDataSetChanged();
+ }
+
+ void refresh(List<Tab> tabs) {
+ // The list of tabs is guaranteed to be non-null.
+ // See TabStripView.refreshTabs().
+ this.tabs = tabs;
+ notifyDataSetChanged();
+ }
+
+ void clear() {
+ tabs = null;
+ notifyDataSetInvalidated();
+ }
+} \ No newline at end of file
diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabStripItemView.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabStripItemView.java
new file mode 100644
index 000000000..27eaed125
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabStripItemView.java
@@ -0,0 +1,254 @@
+/* -*- 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.AboutPages;
+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.ResizablePathDrawable;
+import org.mozilla.gecko.widget.ResizablePathDrawable.NonScaledPathShape;
+import org.mozilla.gecko.widget.themed.ThemedImageButton;
+import org.mozilla.gecko.widget.themed.ThemedLinearLayout;
+import org.mozilla.gecko.widget.themed.ThemedTextView;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Path;
+import android.graphics.Region;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.Checkable;
+import android.widget.ImageView;
+
+public class TabStripItemView extends ThemedLinearLayout
+ implements Checkable {
+ private static final String LOGTAG = "GeckoTabStripItem";
+
+ private static final int[] STATE_CHECKED = {
+ android.R.attr.state_checked
+ };
+
+ private int id = -1;
+ private boolean checked;
+
+ private final ImageView faviconView;
+ private final ThemedTextView titleView;
+ private final ThemedImageButton closeView;
+
+ private final ResizablePathDrawable backgroundDrawable;
+ private final Region tabRegion;
+ private final Region tabClipRegion;
+ private boolean tabRegionNeedsUpdate;
+
+ private final int faviconSize;
+ private Bitmap lastFavicon;
+
+ public TabStripItemView(Context context) {
+ this(context, null);
+ }
+
+ public TabStripItemView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ setOrientation(HORIZONTAL);
+
+ tabRegion = new Region();
+ tabClipRegion = new Region();
+
+ final Resources res = context.getResources();
+
+ final ColorStateList tabColors =
+ res.getColorStateList(R.color.tab_strip_item_bg);
+ backgroundDrawable = new ResizablePathDrawable(new TabCurveShape(), tabColors);
+ setBackgroundDrawable(backgroundDrawable);
+
+ faviconSize = res.getDimensionPixelSize(R.dimen.browser_toolbar_favicon_size);
+
+ LayoutInflater.from(context).inflate(R.layout.tab_strip_item_view, this);
+ setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (id < 0) {
+ throw new IllegalStateException("Invalid tab id:" + id);
+ }
+
+ Tabs.getInstance().selectTab(id);
+ }
+ });
+
+ faviconView = (ImageView) findViewById(R.id.favicon);
+ titleView = (ThemedTextView) findViewById(R.id.title);
+
+ closeView = (ThemedImageButton) findViewById(R.id.close);
+ closeView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (id < 0) {
+ throw new IllegalStateException("Invalid tab id:" + id);
+ }
+
+ final Tabs tabs = Tabs.getInstance();
+ tabs.closeTab(tabs.getTab(id), true);
+ }
+ });
+ }
+
+ @Override
+ protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
+ super.onSizeChanged(width, height, oldWidth, oldHeight);
+
+ // Queue a tab region update in the next draw() call. We don't
+ // update it immediately here because we need the new path from
+ // the background drawable to be updated first.
+ tabRegionNeedsUpdate = true;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ final int action = event.getActionMasked();
+ final int x = (int) event.getX();
+ final int y = (int) event.getY();
+
+ // Let motion events through if they're off the tab shape bounds.
+ if (action == MotionEvent.ACTION_DOWN && !tabRegion.contains(x, y)) {
+ return false;
+ }
+
+ return super.onTouchEvent(event);
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ super.draw(canvas);
+
+ if (tabRegionNeedsUpdate) {
+ final Path path = backgroundDrawable.getPath();
+ tabClipRegion.set(0, 0, getWidth(), getHeight());
+ tabRegion.setPath(path, tabClipRegion);
+ tabRegionNeedsUpdate = false;
+ }
+ }
+
+ @Override
+ public int[] onCreateDrawableState(int extraSpace) {
+ final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
+
+ if (checked) {
+ mergeDrawableStates(drawableState, STATE_CHECKED);
+ }
+
+ return drawableState;
+ }
+
+ @Override
+ public boolean isChecked() {
+ return checked;
+ }
+
+ @Override
+ public void setChecked(boolean checked) {
+ if (this.checked == checked) {
+ return;
+ }
+
+ this.checked = checked;
+ refreshDrawableState();
+ }
+
+ @Override
+ public void toggle() {
+ setChecked(!checked);
+ }
+
+ @Override
+ public void setPressed(boolean pressed) {
+ super.setPressed(pressed);
+
+ // The surrounding tab strip dividers need to be hidden
+ // when a tab item enters pressed state.
+ View parent = (View) getParent();
+ if (parent != null) {
+ parent.invalidate();
+ }
+ }
+
+ void updateFromTab(Tab tab) {
+ if (tab == null) {
+ return;
+ }
+
+ id = tab.getId();
+
+ updateTitle(tab);
+ updateFavicon(tab.getFavicon());
+ setPrivateMode(tab.isPrivate());
+ }
+
+ private void updateTitle(Tab tab) {
+ final String title;
+
+ // Avoid flickering the about:home URL on every load given how often
+ // this page is used in the UI.
+ if (AboutPages.isAboutHome(tab.getURL())) {
+ titleView.setText(R.string.home_title);
+ } else {
+ titleView.setText(tab.getDisplayTitle());
+ }
+
+ // TODO: Set content description to indicate audio is playing.
+ if (tab.isAudioPlaying()) {
+ titleView.setCompoundDrawablesWithIntrinsicBounds(R.drawable.tab_audio_playing, 0, 0, 0);
+ } else {
+ titleView.setCompoundDrawables(null, null, null, null);
+ }
+ }
+
+ private void updateFavicon(final Bitmap favicon) {
+ if (favicon == null) {
+ lastFavicon = null;
+ faviconView.setImageResource(R.drawable.toolbar_favicon_default);
+ return;
+ }
+ if (favicon == lastFavicon) {
+ return;
+ }
+
+ // Cache the original so we can debounce without scaling.
+ lastFavicon = favicon;
+
+ final Bitmap scaledFavicon =
+ Bitmap.createScaledBitmap(favicon, faviconSize, faviconSize, false);
+ faviconView.setImageBitmap(scaledFavicon);
+ }
+
+ private static class TabCurveShape extends NonScaledPathShape {
+ @Override
+ protected void onResize(float width, float height) {
+ final Path path = getPath();
+
+ path.reset();
+
+ final float curveWidth = TabCurve.getWidthForHeight(height);
+
+ path.moveTo(0, height);
+ TabCurve.drawFromBottom(path, 0, height, TabCurve.Direction.RIGHT);
+ path.lineTo(width - curveWidth, 0);
+
+ TabCurve.drawFromTop(path, width - curveWidth, height, TabCurve.Direction.RIGHT);
+ path.lineTo(0, height);
+ }
+ }
+}
diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabStripView.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabStripView.java
new file mode 100644
index 000000000..f3ec19cef
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabStripView.java
@@ -0,0 +1,449 @@
+/* -*- 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 android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.LinearGradient;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.Shader;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.animation.DecelerateInterpolator;
+import android.view.View;
+import android.view.ViewTreeObserver.OnPreDrawListener;
+
+import android.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.Tab;
+import org.mozilla.gecko.Tabs;
+import org.mozilla.gecko.widget.TwoWayView;
+
+public class TabStripView extends TwoWayView {
+ private static final String LOGTAG = "GeckoTabStrip";
+
+ private static final int ANIM_TIME_MS = 200;
+ private static final DecelerateInterpolator ANIM_INTERPOLATOR =
+ new DecelerateInterpolator();
+
+ private final TabStripAdapter adapter;
+ private final Drawable divider;
+
+ private final TabAnimatorListener animatorListener;
+
+ private boolean isRestoringTabs;
+
+ // Filled by calls to ShapeDrawable.getPadding();
+ // saved to prevent allocation in draw().
+ private final Rect dividerPadding = new Rect();
+
+ private boolean isPrivate;
+
+ private final Paint fadingEdgePaint;
+ private final int fadingEdgeSize;
+
+ public TabStripView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ setOrientation(Orientation.HORIZONTAL);
+ setChoiceMode(ChoiceMode.SINGLE);
+ setItemsCanFocus(true);
+ setChildrenDrawingOrderEnabled(true);
+ setWillNotDraw(false);
+
+ final Resources resources = getResources();
+
+ divider = resources.getDrawable(R.drawable.tab_strip_divider);
+ divider.getPadding(dividerPadding);
+
+ final int itemMargin =
+ resources.getDimensionPixelSize(R.dimen.tablet_tab_strip_item_margin);
+ setItemMargin(itemMargin);
+
+ animatorListener = new TabAnimatorListener();
+
+ fadingEdgePaint = new Paint();
+ fadingEdgeSize =
+ resources.getDimensionPixelOffset(R.dimen.tablet_tab_strip_fading_edge_size);
+
+ adapter = new TabStripAdapter(context);
+ setAdapter(adapter);
+ }
+
+ private View getViewForTab(Tab tab) {
+ final int position = adapter.getPositionForTab(tab);
+ return getChildAt(position - getFirstVisiblePosition());
+ }
+
+ private int getPositionForSelectedTab() {
+ return adapter.getPositionForTab(Tabs.getInstance().getSelectedTab());
+ }
+
+ private void updateSelectedStyle(int selected) {
+ setItemChecked(selected, true);
+ }
+
+ private void updateSelectedPosition(boolean ensureVisible) {
+ final int selected = getPositionForSelectedTab();
+ if (selected != -1) {
+ updateSelectedStyle(selected);
+
+ if (ensureVisible) {
+ ensurePositionIsVisible(selected, true);
+ }
+ }
+ }
+
+ private void animateRemoveTab(Tab removedTab) {
+ final int removedPosition = adapter.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;
+ }
+
+ // We don't animate the removed child view (it just disappears)
+ // but we still need its size of animate all affected children
+ // within the visible viewport.
+ final int removedSize = removedView.getWidth() + getItemMargin();
+
+ getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() {
+ @Override
+ public boolean onPreDraw() {
+ getViewTreeObserver().removeOnPreDrawListener(this);
+
+ final int firstPosition = getFirstVisiblePosition();
+ final List<Animator> childAnimators = new ArrayList<Animator>();
+
+ final int childCount = getChildCount();
+ for (int i = removedPosition - firstPosition; i < childCount; i++) {
+ final View child = getChildAt(i);
+
+ final ObjectAnimator animator =
+ ObjectAnimator.ofFloat(child, "translationX", removedSize, 0);
+ childAnimators.add(animator);
+ }
+
+ final AnimatorSet animatorSet = new AnimatorSet();
+ animatorSet.playTogether(childAnimators);
+ animatorSet.setDuration(ANIM_TIME_MS);
+ animatorSet.setInterpolator(ANIM_INTERPOLATOR);
+ animatorSet.addListener(animatorListener);
+
+ animatorSet.start();
+
+ return true;
+ }
+ });
+ }
+
+ private void animateNewTab(Tab newTab) {
+ final int newPosition = adapter.getPositionForTab(newTab);
+ if (newPosition < 0) {
+ return;
+ }
+
+ getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() {
+ @Override
+ public boolean onPreDraw() {
+ getViewTreeObserver().removeOnPreDrawListener(this);
+
+ final int firstPosition = getFirstVisiblePosition();
+
+ final View newChild = getChildAt(newPosition - firstPosition);
+ if (newChild == null) {
+ return true;
+ }
+
+ final List<Animator> childAnimators = new ArrayList<Animator>();
+ childAnimators.add(
+ ObjectAnimator.ofFloat(newChild, "translationY", newChild.getHeight(), 0));
+
+ // This will momentaneously add a gap on the right side
+ // because TwoWayView doesn't provide APIs to control
+ // view recycling programatically to handle these transitory
+ // states in the container during animations.
+
+ final int tabSize = newChild.getWidth();
+ final int newIndex = newPosition - firstPosition;
+ final int childCount = getChildCount();
+ for (int i = newIndex + 1; i < childCount; i++) {
+ final View child = getChildAt(i);
+
+ childAnimators.add(
+ ObjectAnimator.ofFloat(child, "translationX", -tabSize, 0));
+ }
+
+ final AnimatorSet animatorSet = new AnimatorSet();
+ animatorSet.playTogether(childAnimators);
+ animatorSet.setDuration(ANIM_TIME_MS);
+ animatorSet.setInterpolator(ANIM_INTERPOLATOR);
+ animatorSet.addListener(animatorListener);
+
+ animatorSet.start();
+
+ return true;
+ }
+ });
+ }
+
+ private void animateRestoredTabs() {
+ getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() {
+ @Override
+ public boolean onPreDraw() {
+ getViewTreeObserver().removeOnPreDrawListener(this);
+
+ final List<Animator> childAnimators = new ArrayList<Animator>();
+
+ final int tabHeight = getHeight() - getPaddingTop() - getPaddingBottom();
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+
+ childAnimators.add(
+ ObjectAnimator.ofFloat(child, "translationY", tabHeight, 0));
+ }
+
+ final AnimatorSet animatorSet = new AnimatorSet();
+ animatorSet.playTogether(childAnimators);
+ animatorSet.setDuration(ANIM_TIME_MS);
+ animatorSet.setInterpolator(ANIM_INTERPOLATOR);
+ animatorSet.addListener(animatorListener);
+
+ animatorSet.start();
+
+ return true;
+ }
+ });
+ }
+
+ /**
+ * Ensures the tab at the given position is visible. If we are not restoring tabs and
+ * shouldAnimate == true, the tab will animate to be visible, if it is not already visible.
+ */
+ private void ensurePositionIsVisible(final int position, final boolean shouldAnimate) {
+ // We just want to move the strip to the right position
+ // when restoring tabs on startup.
+ if (isRestoringTabs || !shouldAnimate) {
+ setSelection(position);
+ return;
+ }
+
+ getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() {
+ @Override
+ public boolean onPreDraw() {
+ getViewTreeObserver().removeOnPreDrawListener(this);
+ smoothScrollToPosition(position);
+ return true;
+ }
+ });
+ }
+
+ private int getCheckedIndex(int childCount) {
+ final int checkedIndex = getCheckedItemPosition() - getFirstVisiblePosition();
+ if (checkedIndex < 0 || checkedIndex > childCount - 1) {
+ return INVALID_POSITION;
+ }
+
+ return checkedIndex;
+ }
+
+ void refreshTabs() {
+ // Store a different copy of the tabs, so that we don't have
+ // to worry about accidentally updating it on the wrong thread.
+ final List<Tab> tabs = new ArrayList<Tab>();
+
+ for (Tab tab : Tabs.getInstance().getTabsInOrder()) {
+ if (tab.isPrivate() == isPrivate) {
+ tabs.add(tab);
+ }
+ }
+
+ adapter.refresh(tabs);
+ updateSelectedPosition(true);
+ }
+
+ void clearTabs() {
+ adapter.clear();
+ }
+
+ void restoreTabs() {
+ isRestoringTabs = true;
+ refreshTabs();
+ animateRestoredTabs();
+ isRestoringTabs = false;
+ }
+
+ void addTab(Tab tab) {
+ // Refresh the list to make sure the new tab is
+ // added in the right position.
+ refreshTabs();
+ animateNewTab(tab);
+ }
+
+ void removeTab(Tab tab) {
+ animateRemoveTab(tab);
+ adapter.removeTab(tab);
+ updateSelectedPosition(false);
+ }
+
+ void selectTab(Tab tab) {
+ if (tab.isPrivate() != isPrivate) {
+ isPrivate = tab.isPrivate();
+ refreshTabs();
+ } else {
+ updateSelectedPosition(true);
+ }
+ }
+
+ void updateTab(Tab tab) {
+ final TabStripItemView item = (TabStripItemView) getViewForTab(tab);
+ if (item != null) {
+ item.updateFromTab(tab);
+ }
+ }
+
+ private float getFadingEdgeStrength() {
+ final int childCount = getChildCount();
+ if (childCount == 0) {
+ return 0.0f;
+ } else {
+ if (getFirstVisiblePosition() + childCount - 1 < adapter.getCount() - 1) {
+ return 1.0f;
+ }
+
+ final int right = getChildAt(childCount - 1).getRight();
+ final int paddingRight = getPaddingRight();
+ final int width = getWidth();
+
+ final float strength = (right > width - paddingRight ?
+ (float) (right - width + paddingRight) / fadingEdgeSize : 0.0f);
+
+ return Math.max(0.0f, Math.min(strength, 1.0f));
+ }
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+
+ fadingEdgePaint.setShader(new LinearGradient(w - fadingEdgeSize, 0, w, 0,
+ new int[] { 0x0, 0x11292C29, 0xDD292C29 },
+ new float[] { 0, 0.4f, 1.0f }, Shader.TileMode.CLAMP));
+ }
+
+ @Override
+ protected int getChildDrawingOrder(int childCount, int i) {
+ final int checkedIndex = getCheckedIndex(childCount);
+ if (checkedIndex == INVALID_POSITION) {
+ return i;
+ }
+
+ // Always draw the currently selected tab on top of all
+ // other child views so that its curve is fully visible.
+ if (i == childCount - 1) {
+ return checkedIndex;
+ } else if (checkedIndex <= i) {
+ return i + 1;
+ } else {
+ return i;
+ }
+ }
+
+ private void drawDividers(Canvas canvas) {
+ final int bottom = getHeight() - getPaddingBottom() - dividerPadding.bottom;
+ final int top = bottom - divider.getIntrinsicHeight();
+
+ final int dividerWidth = divider.getIntrinsicWidth();
+ final int itemMargin = getItemMargin();
+
+ final int childCount = getChildCount();
+ final int checkedIndex = getCheckedIndex(childCount);
+
+ for (int i = 1; i < childCount; i++) {
+ final View child = getChildAt(i);
+
+ final boolean pressed = (child.isPressed() || getChildAt(i - 1).isPressed());
+ final boolean checked = (i == checkedIndex || i == checkedIndex + 1);
+
+ // Don't draw dividers for around checked or pressed items
+ // so that they are not drawn on top of the tab curves.
+ if (pressed || checked) {
+ continue;
+ }
+
+ final int left = child.getLeft() - (itemMargin / 2) - dividerWidth;
+ final int right = left + dividerWidth;
+
+ divider.setBounds(left, top, right, bottom);
+ divider.draw(canvas);
+ }
+ }
+
+ private void drawFadingEdge(Canvas canvas) {
+ final float strength = getFadingEdgeStrength();
+ if (strength > 0.0f) {
+ final int r = getRight();
+ canvas.drawRect(r - fadingEdgeSize, getTop(), r, getBottom(), fadingEdgePaint);
+ fadingEdgePaint.setAlpha((int) (strength * 255));
+ }
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ super.draw(canvas);
+ drawDividers(canvas);
+ drawFadingEdge(canvas);
+ }
+
+ public void refresh() {
+ final int selectedPosition = getPositionForSelectedTab();
+ if (selectedPosition != -1) {
+ ensurePositionIsVisible(selectedPosition, false);
+ }
+ }
+
+ private class TabAnimatorListener implements AnimatorListener {
+ private void setLayerType(int layerType) {
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ getChildAt(i).setLayerType(layerType, null);
+ }
+ }
+
+ @Override
+ public void onAnimationStart(Animator animation) {
+ setLayerType(View.LAYER_TYPE_HARDWARE);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ // This method is called even if the animator is canceled.
+ setLayerType(View.LAYER_TYPE_NONE);
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ }
+
+ }
+} \ No newline at end of file
diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsGridLayout.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsGridLayout.java
new file mode 100644
index 000000000..ead7db9fe
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsGridLayout.java
@@ -0,0 +1,712 @@
+/* -*- 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.AppConstants;
+import org.mozilla.gecko.GeckoAppShell;
+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.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.os.Build;
+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;
+import java.util.List;
+
+/**
+ * A tabs layout implementation for the tablet redesign (bug 1014156) and later ported to mobile (bug 1193745).
+ */
+
+class TabsGridLayout extends GridView
+ implements TabsLayout,
+ Tabs.OnTabsChangedListener {
+
+ private static final String LOGTAG = "Gecko" + TabsGridLayout.class.getSimpleName();
+
+ public static final int ANIM_DELAY_MULTIPLE_MS = 20;
+ private static final int ANIM_TIME_MS = 200;
+ private static final DecelerateInterpolator ANIM_INTERPOLATOR = new DecelerateInterpolator();
+
+ private final SparseArray<PointF> tabLocations = new SparseArray<PointF>();
+ 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);
+
+ setVerticalFadingEdgeEnabled(false);
+
+ final Resources resources = getResources();
+ 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
+ public void closeAll() {
+
+ autoHidePanel();
+
+ if (getChildCount() == 0) {
+ return;
+ }
+
+ final Iterable<Tab> tabs = Tabs.getInstance().getTabsInOrder();
+ for (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 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;
+ }
+ final int removedHeight = removedView.getMeasuredHeight();
+
+ 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);
+ }
+ }
+ }
+ }
+}
diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayout.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayout.java
new file mode 100644
index 000000000..d5362f1f1
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayout.java
@@ -0,0 +1,216 @@
+/* -*- 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,
+ TabsTouchHelperCallback.DismissListener {
+
+ 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:
+ final int tabIndex = Integer.parseInt(data);
+ tabsAdapter.notifyTabInserted(tab, tabIndex);
+ if (addAtIndexRequiresScroll(tabIndex)) {
+ // (The current Tabs implementation updates the SELECTED tab *after* this
+ // call to ADDED, so don't just call updateSelectedPosition().)
+ scrollToPosition(tabIndex);
+ }
+ 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;
+ }
+ }
+
+ // 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.
+ abstract protected boolean addAtIndexRequiresScroll(int index);
+
+ @Override
+ public void onItemClicked(RecyclerView recyclerView, int position, View v) {
+ final TabsLayoutItemView item = (TabsLayoutItemView) v;
+ final int tabId = item.getTabId();
+ final Tab tab = Tabs.getInstance().selectTab(tabId);
+ if (tab == null) {
+ // The tab that was clicked no longer exists in the tabs list (which can happen if you
+ // tap on a tab while its remove animation is running), so ignore the click.
+ return;
+ }
+
+ autoHidePanel();
+ 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.
+ 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);
+ if (tab == null) {
+ // We can be null here if this is the second closeTab call resulting from a sufficiently
+ // fast double tap on the close tab button.
+ return;
+ }
+
+ 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);
+ }
+ }
+ }
+
+ @Override
+ public void onItemDismiss(View view) {
+ closeTab(view);
+ }
+
+ 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();
+}
diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayoutAdapter.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayoutAdapter.java
new file mode 100644
index 000000000..367da640f
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayoutAdapter.java
@@ -0,0 +1,100 @@
+/* -*- 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.R;
+import org.mozilla.gecko.Tab;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+
+import java.util.ArrayList;
+
+// Adapter to bind tabs into a list
+public class TabsLayoutAdapter extends BaseAdapter {
+ public static final String LOGTAG = "Gecko" + TabsLayoutAdapter.class.getSimpleName();
+
+ private final Context mContext;
+ private final int mTabLayoutId;
+ private ArrayList<Tab> mTabs;
+ private final LayoutInflater mInflater;
+
+ public TabsLayoutAdapter (Context context, int tabLayoutId) {
+ mContext = context;
+ mInflater = LayoutInflater.from(mContext);
+ mTabLayoutId = tabLayoutId;
+ }
+
+ final void setTabs (ArrayList<Tab> tabs) {
+ mTabs = tabs;
+ notifyDataSetChanged(); // Be sure to call this whenever mTabs changes.
+ }
+
+ 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() {
+ mTabs = null;
+
+ notifyDataSetChanged(); // Be sure to call this whenever mTabs changes.
+ }
+
+ @Override
+ public int getCount() {
+ return (mTabs == null ? 0 : mTabs.size());
+ }
+
+ @Override
+ public Tab getItem(int position) {
+ return mTabs.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ final int getPositionForTab(Tab tab) {
+ if (mTabs == null || tab == null)
+ return -1;
+
+ return mTabs.indexOf(tab);
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ return true;
+ }
+
+ @Override
+ final public TabsLayoutItemView getView(int position, View convertView, ViewGroup parent) {
+ final TabsLayoutItemView view;
+ if (convertView == null) {
+ view = newView(position, parent);
+ } else {
+ view = (TabsLayoutItemView) convertView;
+ }
+ final Tab tab = mTabs.get(position);
+ bindView(view, tab);
+ return view;
+ }
+
+ TabsLayoutItemView newView(int position, ViewGroup parent) {
+ return (TabsLayoutItemView) mInflater.inflate(mTabLayoutId, parent, false);
+ }
+
+ void bindView(TabsLayoutItemView view, Tab tab) {
+ view.assignValues(tab);
+ }
+} \ No newline at end of file
diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayoutItemView.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayoutItemView.java
new file mode 100644
index 000000000..975e779d6
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayoutItemView.java
@@ -0,0 +1,172 @@
+/* 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.R;
+import org.mozilla.gecko.Tab;
+import org.mozilla.gecko.Tabs;
+import org.mozilla.gecko.widget.TabThumbnailWrapper;
+import org.mozilla.gecko.widget.TouchDelegateWithReset;
+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.View;
+import android.view.ViewTreeObserver;
+import android.widget.Checkable;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+public class TabsLayoutItemView extends LinearLayout
+ implements Checkable {
+ private static final String LOGTAG = "Gecko" + TabsLayoutItemView.class.getSimpleName();
+ private static final int[] STATE_CHECKED = { android.R.attr.state_checked };
+ private boolean mChecked;
+
+ private int mTabId;
+ private TextView mTitle;
+ private TabsPanelThumbnailView mThumbnail;
+ private ImageView mCloseButton;
+ private TabThumbnailWrapper mThumbnailWrapper;
+
+ public TabsLayoutItemView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public int[] onCreateDrawableState(int extraSpace) {
+ final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
+
+ if (mChecked) {
+ mergeDrawableStates(drawableState, STATE_CHECKED);
+ }
+
+ return drawableState;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return true;
+ }
+
+ @Override
+ public boolean isChecked() {
+ return mChecked;
+ }
+
+ @Override
+ public void setChecked(boolean checked) {
+ if (mChecked == checked) {
+ return;
+ }
+
+ mChecked = checked;
+ refreshDrawableState();
+
+ int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child instanceof Checkable) {
+ ((Checkable) child).setChecked(checked);
+ }
+ }
+ }
+
+ @Override
+ public void toggle() {
+ mChecked = !mChecked;
+ }
+
+ public void setCloseOnClickListener(OnClickListener mOnClickListener) {
+ mCloseButton.setOnClickListener(mOnClickListener);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mTitle = (TextView) findViewById(R.id.title);
+ mThumbnail = (TabsPanelThumbnailView) findViewById(R.id.thumbnail);
+ mCloseButton = (ImageView) findViewById(R.id.close);
+ mThumbnailWrapper = (TabThumbnailWrapper) findViewById(R.id.wrapper);
+
+ growCloseButtonHitArea();
+ }
+
+ private void growCloseButtonHitArea() {
+ getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
+ @Override
+ public boolean onPreDraw() {
+ getViewTreeObserver().removeOnPreDrawListener(this);
+
+ // Ideally we want the close button hit area to be 40x40dp but we are constrained by the height of the parent, so
+ // we make it as tall as the parent view and 40dp across.
+ final int targetHitArea = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 40, getResources().getDisplayMetrics());;
+
+ final Rect hitRect = new Rect();
+ hitRect.top = 0;
+ hitRect.right = getWidth();
+ hitRect.left = getWidth() - targetHitArea;
+ hitRect.bottom = targetHitArea;
+
+ setTouchDelegate(new TouchDelegateWithReset(hitRect, mCloseButton));
+
+ return true;
+ }
+ });
+ }
+
+ protected void assignValues(Tab tab) {
+ if (tab == null) {
+ return;
+ }
+
+ mTabId = tab.getId();
+
+ setChecked(Tabs.getInstance().isSelectedTab(tab));
+
+ Drawable thumbnailImage = tab.getThumbnail();
+ mThumbnail.setImageDrawable(thumbnailImage);
+
+ mThumbnail.setPrivateMode(tab.isPrivate());
+
+ if (mThumbnailWrapper != null) {
+ mThumbnailWrapper.setRecording(tab.isRecording());
+ }
+
+ final String tabTitle = tab.getDisplayTitle();
+ mTitle.setText(tabTitle);
+ mCloseButton.setTag(this);
+
+ if (tab.isAudioPlaying()) {
+ mTitle.setCompoundDrawablesWithIntrinsicBounds(R.drawable.tab_audio_playing, 0, 0, 0);
+ final String tabTitleWithAudio =
+ getResources().getString(R.string.tab_title_prefix_is_playing_audio, tabTitle);
+ mTitle.setContentDescription(tabTitleWithAudio);
+ } else {
+ mTitle.setCompoundDrawables(null, null, null, null);
+ mTitle.setContentDescription(tabTitle);
+ }
+ }
+
+ public int getTabId() {
+ return mTabId;
+ }
+
+ 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);
+ }
+}
diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayoutRecyclerAdapter.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayoutRecyclerAdapter.java
new file mode 100644
index 000000000..090d74f9d
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayoutRecyclerAdapter.java
@@ -0,0 +1,124 @@
+/* -*- 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.util.Log;
+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 static final String LOGTAG = "Gecko" + TabsLayoutRecyclerAdapter.class.getSimpleName();
+
+ 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));
+ }
+
+ /* package */ void notifyTabInserted(Tab tab, int index) {
+ 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);
+ }
+}
diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsListLayout.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsListLayout.java
new file mode 100644
index 000000000..8cf2f8ede
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsListLayout.java
@@ -0,0 +1,118 @@
+/* -*- 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.R;
+import org.mozilla.gecko.animation.PropertyAnimator;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.content.Context;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.helper.ItemTouchHelper;
+import android.util.AttributeSet;
+import android.view.View;
+
+public class TabsListLayout extends TabsLayout {
+ // Time to animate non-flinged tabs of screen, in milliseconds
+ private static final int ANIMATION_DURATION = 250;
+
+ // Time between starting successive tab animations in closeAllTabs.
+ private static final int ANIMATION_CASCADE_DELAY = 75;
+
+ private int closeAllAnimationCount;
+
+ public TabsListLayout(Context context, AttributeSet attrs) {
+ super(context, attrs, R.layout.tabs_list_item_view);
+
+ setHasFixedSize(true);
+
+ setLayoutManager(new LinearLayoutManager(context));
+
+ // A TouchHelper handler for swipe to close.
+ final TabsTouchHelperCallback callback = new TabsTouchHelperCallback(this);
+ final ItemTouchHelper touchHelper = new ItemTouchHelper(callback);
+ touchHelper.attachToRecyclerView(this);
+
+ setItemAnimator(new TabsListLayoutAnimator(ANIMATION_DURATION));
+ }
+
+ @Override
+ public void closeAll() {
+ final int childCount = getChildCount();
+
+ // Just close the panel if there are no tabs to close.
+ if (childCount == 0) {
+ autoHidePanel();
+ return;
+ }
+
+ // Disable the view so that gestures won't interfere wth the tab close animation.
+ setEnabled(false);
+
+ // Delay starting each successive animation to create a cascade effect.
+ int cascadeDelay = 0;
+ closeAllAnimationCount = 0;
+ for (int i = childCount - 1; i >= 0; i--) {
+ final View view = getChildAt(i);
+ if (view == null) {
+ continue;
+ }
+
+ final PropertyAnimator animator = new PropertyAnimator(ANIMATION_DURATION);
+ animator.attach(view, PropertyAnimator.Property.ALPHA, 0);
+
+ animator.attach(view, PropertyAnimator.Property.TRANSLATION_X, view.getWidth());
+
+ closeAllAnimationCount++;
+
+ animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
+ @Override
+ public void onPropertyAnimationStart() {
+ }
+
+ @Override
+ public void onPropertyAnimationEnd() {
+ closeAllAnimationCount--;
+ if (closeAllAnimationCount > 0) {
+ return;
+ }
+
+ // Hide the panel after the animation is done.
+ autoHidePanel();
+
+ // Re-enable the view after the animation is done.
+ TabsListLayout.this.setEnabled(true);
+
+ // Then actually close all the tabs.
+ closeAllTabs();
+ }
+ });
+
+ ThreadUtils.postDelayedToUiThread(new Runnable() {
+ @Override
+ public void run() {
+ animator.start();
+ }
+ }, cascadeDelay);
+
+ cascadeDelay += ANIMATION_CASCADE_DELAY;
+ }
+ }
+
+ @Override
+ protected boolean addAtIndexRequiresScroll(int index) {
+ 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);
+ }
+}
diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsListLayoutAnimator.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsListLayoutAnimator.java
new file mode 100644
index 000000000..471abf883
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsListLayoutAnimator.java
@@ -0,0 +1,65 @@
+/* -*- 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.v4.view.ViewCompat;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+
+class TabsListLayoutAnimator extends DefaultItemAnimatorBase {
+ public TabsListLayoutAnimator(int animationDuration) {
+ setRemoveDuration(animationDuration);
+ setAddDuration(animationDuration);
+ // A fade in/out each time the title/thumbnail/etc. gets updated isn't helpful, so disable
+ // the change animation.
+ setSupportsChangeAnimations(false);
+ }
+
+ @Override
+ protected boolean preAnimateRemoveImpl(final RecyclerView.ViewHolder holder) {
+ // If the view isn't at full alpha then we were closed by a swipe which an
+ // ItemTouchHelper is animating for us, so just return without animating the remove and
+ // let runPendingAnimations pick up the rest.
+ if (holder.itemView.getAlpha() < 1) {
+ return false;
+ }
+ resetAnimation(holder);
+ return true;
+ }
+
+ @Override
+ protected void animateRemoveImpl(final RecyclerView.ViewHolder holder) {
+ final View itemView = holder.itemView;
+ ViewCompat.animate(itemView)
+ .setDuration(getRemoveDuration())
+ .translationX(itemView.getWidth())
+ .alpha(0)
+ .setListener(new DefaultRemoveVpaListener(holder))
+ .start();
+ }
+
+ @Override
+ protected boolean preAnimateAddImpl(RecyclerView.ViewHolder holder) {
+ resetAnimation(holder);
+ final View itemView = holder.itemView;
+ itemView.setTranslationX(itemView.getWidth());
+ itemView.setAlpha(0);
+ return true;
+ }
+
+ @Override
+ protected void animateAddImpl(final RecyclerView.ViewHolder holder) {
+ final View itemView = holder.itemView;
+ ViewCompat.animate(itemView)
+ .setDuration(getAddDuration())
+ .translationX(0)
+ .alpha(1)
+ .setListener(new DefaultAddVpaListener(holder))
+ .start();
+ }
+}
diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsPanel.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsPanel.java
new file mode 100644
index 000000000..2be127010
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsPanel.java
@@ -0,0 +1,456 @@
+/* -*- 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 android.support.v4.content.ContextCompat;
+import org.mozilla.gecko.AppConstants.Versions;
+import org.mozilla.gecko.GeckoApp;
+import org.mozilla.gecko.GeckoApplication;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.Telemetry;
+import org.mozilla.gecko.TelemetryContract;
+import org.mozilla.gecko.animation.PropertyAnimator;
+import org.mozilla.gecko.animation.ViewHelper;
+import org.mozilla.gecko.lwt.LightweightTheme;
+import org.mozilla.gecko.lwt.LightweightThemeDrawable;
+import org.mozilla.gecko.restrictions.Restrictable;
+import org.mozilla.gecko.restrictions.Restrictions;
+import org.mozilla.gecko.util.HardwareUtils;
+import org.mozilla.gecko.widget.GeckoPopupMenu;
+import org.mozilla.gecko.widget.IconTabWidget;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
+import org.mozilla.gecko.widget.themed.ThemedImageButton;
+
+public class TabsPanel extends LinearLayout
+ implements GeckoPopupMenu.OnMenuItemClickListener,
+ LightweightTheme.OnChangeListener,
+ IconTabWidget.OnTabChangedListener {
+ private static final String LOGTAG = "Gecko" + TabsPanel.class.getSimpleName();
+
+ public enum Panel {
+ NORMAL_TABS,
+ PRIVATE_TABS,
+ }
+
+ public interface PanelView {
+ void setTabsPanel(TabsPanel panel);
+ void show();
+ void hide();
+ boolean shouldExpand();
+ }
+
+ public interface CloseAllPanelView extends PanelView {
+ void closeAll();
+ }
+
+ public interface TabsLayout extends CloseAllPanelView {
+ void setEmptyView(View view);
+ }
+
+ public interface TabsLayoutChangeListener {
+ void onTabsLayoutChange(int width, int height);
+ }
+
+ public static View createTabsLayout(final Context context, final AttributeSet attrs) {
+ final boolean isLandscape = context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
+
+ if (HardwareUtils.isTablet() || isLandscape) {
+ return new TabsGridLayout(context, attrs);
+ } else {
+ return new TabsListLayout(context, attrs);
+ }
+ }
+
+ private final Context mContext;
+ private final GeckoApp mActivity;
+ private final LightweightTheme mTheme;
+ private RelativeLayout mHeader;
+ private FrameLayout mTabsContainer;
+ private PanelView mPanel;
+ private PanelView mPanelNormal;
+ private PanelView mPanelPrivate;
+ private TabsLayoutChangeListener mLayoutChangeListener;
+
+ private IconTabWidget mTabWidget;
+ private View mMenuButton;
+ private ImageButton mAddTab;
+ private ImageButton mNavBackButton;
+
+ private Panel mCurrentPanel;
+ private boolean mVisible;
+ private boolean mHeaderVisible;
+
+ private final GeckoPopupMenu mPopupMenu;
+
+ public TabsPanel(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mContext = context;
+ mActivity = (GeckoApp) context;
+ mTheme = ((GeckoApplication) context.getApplicationContext()).getLightweightTheme();
+
+ mCurrentPanel = Panel.NORMAL_TABS;
+
+ mPopupMenu = new GeckoPopupMenu(context);
+ mPopupMenu.inflate(R.menu.tabs_menu);
+ mPopupMenu.setOnMenuItemClickListener(this);
+
+ inflateLayout(context);
+ initialize();
+ }
+
+ private void inflateLayout(Context context) {
+ LayoutInflater.from(context).inflate(R.layout.tabs_panel_default, this);
+ }
+
+ private void initialize() {
+ mHeader = (RelativeLayout) findViewById(R.id.tabs_panel_header);
+ mTabsContainer = (FrameLayout) findViewById(R.id.tabs_container);
+
+ mPanelNormal = (PanelView) findViewById(R.id.normal_tabs);
+ mPanelNormal.setTabsPanel(this);
+
+ mPanelPrivate = (PanelView) findViewById(R.id.private_tabs_panel);
+ mPanelPrivate.setTabsPanel(this);
+
+ mAddTab = (ImageButton) findViewById(R.id.add_tab);
+ mAddTab.setOnClickListener(new Button.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ TabsPanel.this.addTab();
+ }
+ });
+
+ mTabWidget = (IconTabWidget) findViewById(R.id.tab_widget);
+
+ mTabWidget.addTab(R.drawable.tabs_normal, R.string.tabs_normal);
+ final ThemedImageButton privateTabsPanel =
+ (ThemedImageButton) mTabWidget.addTab(R.drawable.tabs_private, R.string.tabs_private);
+ privateTabsPanel.setPrivateMode(true);
+
+ if (!Restrictions.isAllowed(mContext, Restrictable.PRIVATE_BROWSING)) {
+ mTabWidget.setVisibility(View.GONE);
+ }
+
+ mTabWidget.setTabSelectionListener(this);
+
+ mMenuButton = findViewById(R.id.menu);
+ mMenuButton.setOnClickListener(new Button.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ showMenu();
+ }
+ });
+
+ mNavBackButton = (ImageButton) findViewById(R.id.nav_back);
+ mNavBackButton.setOnClickListener(new Button.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ mActivity.onBackPressed();
+ }
+ });
+ }
+
+ public void showMenu() {
+ final Menu menu = mPopupMenu.getMenu();
+
+ // Each panel has a "+" shortcut button, so don't show it for that panel.
+ menu.findItem(R.id.new_tab).setVisible(mCurrentPanel != Panel.NORMAL_TABS);
+ menu.findItem(R.id.new_private_tab).setVisible(mCurrentPanel != Panel.PRIVATE_TABS
+ && Restrictions.isAllowed(mContext, Restrictable.PRIVATE_BROWSING));
+
+ // Only show "Clear * tabs" for current panel.
+ menu.findItem(R.id.close_all_tabs).setVisible(mCurrentPanel == Panel.NORMAL_TABS);
+ menu.findItem(R.id.close_private_tabs).setVisible(mCurrentPanel == Panel.PRIVATE_TABS);
+
+ mPopupMenu.show();
+ }
+
+ private void addTab() {
+ Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.ACTIONBAR, "new_tab");
+
+ if (mCurrentPanel == Panel.NORMAL_TABS) {
+ mActivity.addTab();
+ } else {
+ mActivity.addPrivateTab();
+ }
+
+ mActivity.autoHideTabs();
+ }
+
+ @Override
+ public void onTabChanged(int index) {
+ if (index == 0) {
+ show(Panel.NORMAL_TABS);
+ } else {
+ show(Panel.PRIVATE_TABS);
+ }
+ }
+
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ final int itemId = item.getItemId();
+
+ if (itemId == R.id.close_all_tabs) {
+ if (mCurrentPanel == Panel.NORMAL_TABS) {
+ Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.MENU, "close_all_tabs");
+
+ // Disable the menu button so that the menu won't interfere with the tab close animation.
+ mMenuButton.setEnabled(false);
+ ((CloseAllPanelView) mPanelNormal).closeAll();
+ } else {
+ Log.e(LOGTAG, "Close all tabs menu item should only be visible for normal tabs panel");
+ }
+ return true;
+ }
+
+ if (itemId == R.id.close_private_tabs) {
+ if (mCurrentPanel == Panel.PRIVATE_TABS) {
+ // Mask private browsing
+ Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.MENU, "close_all_tabs");
+
+ ((CloseAllPanelView) mPanelPrivate).closeAll();
+ } else {
+ Log.e(LOGTAG, "Close private tabs menu item should only be visible for private tabs panel");
+ }
+ return true;
+ }
+
+ if (itemId == R.id.new_tab || itemId == R.id.new_private_tab) {
+ hide();
+ }
+
+ return mActivity.onOptionsItemSelected(item);
+ }
+
+ private static int getTabContainerHeight(FrameLayout tabsContainer) {
+ final Resources resources = tabsContainer.getContext().getResources();
+
+ final int screenHeight = resources.getDisplayMetrics().heightPixels;
+ final int actionBarHeight = resources.getDimensionPixelSize(R.dimen.browser_toolbar_height);
+
+ return screenHeight - actionBarHeight;
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ mTheme.addListener(this);
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ mTheme.removeListener(this);
+ }
+
+ @Override
+ @SuppressWarnings("deprecation") // setBackgroundDrawable deprecated by API level 16
+ public void onLightweightThemeChanged() {
+ final int background = ContextCompat.getColor(getContext(), R.color.text_and_tabs_tray_grey);
+ final LightweightThemeDrawable drawable = mTheme.getColorDrawable(this, background, true);
+ if (drawable == null)
+ return;
+
+ drawable.setAlpha(34, 0);
+ setBackgroundDrawable(drawable);
+ }
+
+ @Override
+ public void onLightweightThemeReset() {
+ setBackgroundColor(ContextCompat.getColor(getContext(), R.color.text_and_tabs_tray_grey));
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ onLightweightThemeChanged();
+ }
+
+ // Tabs Panel Toolbar contains the Buttons
+ static class TabsPanelToolbar extends LinearLayout
+ implements LightweightTheme.OnChangeListener {
+ private final LightweightTheme mTheme;
+
+ public TabsPanelToolbar(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mTheme = ((GeckoApplication) context.getApplicationContext()).getLightweightTheme();
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ mTheme.addListener(this);
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ mTheme.removeListener(this);
+ }
+
+ @Override
+ @SuppressWarnings("deprecation") // setBackgroundDrawable deprecated by API level 16
+ public void onLightweightThemeChanged() {
+ final int background = ContextCompat.getColor(getContext(), R.color.text_and_tabs_tray_grey);
+ final LightweightThemeDrawable drawable = mTheme.getColorDrawable(this, background);
+ if (drawable == null)
+ return;
+
+ drawable.setAlpha(34, 34);
+ setBackgroundDrawable(drawable);
+ }
+
+ @Override
+ public void onLightweightThemeReset() {
+ setBackgroundColor(ContextCompat.getColor(getContext(), R.color.text_and_tabs_tray_grey));
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ onLightweightThemeChanged();
+ }
+ }
+
+ public void show(Panel panelToShow) {
+ prepareToShow(panelToShow);
+ int height = getVerticalPanelHeight();
+ dispatchLayoutChange(getWidth(), height);
+ mHeaderVisible = true;
+ }
+
+ public void prepareToShow(Panel panelToShow) {
+ if (!isShown()) {
+ setVisibility(View.VISIBLE);
+ }
+
+ if (mPanel != null) {
+ // Hide the old panel.
+ mPanel.hide();
+ }
+
+ mVisible = true;
+ mCurrentPanel = panelToShow;
+
+ int index = panelToShow.ordinal();
+ mTabWidget.setCurrentTab(index);
+
+ switch (panelToShow) {
+ case NORMAL_TABS:
+ mPanel = mPanelNormal;
+ break;
+ case PRIVATE_TABS:
+ mPanel = mPanelPrivate;
+ break;
+
+ default:
+ throw new IllegalArgumentException("Unknown panel type " + panelToShow);
+ }
+ mPanel.show();
+
+ mAddTab.setVisibility(View.VISIBLE);
+
+ mMenuButton.setEnabled(true);
+ mPopupMenu.setAnchor(mMenuButton);
+ }
+
+ public int getVerticalPanelHeight() {
+ final int actionBarHeight = mContext.getResources().getDimensionPixelSize(R.dimen.browser_toolbar_height);
+ final int height = actionBarHeight + getTabContainerHeight(mTabsContainer);
+ return height;
+ }
+
+ public void hide() {
+ mHeaderVisible = false;
+
+ if (mVisible) {
+ mVisible = false;
+ mPopupMenu.dismiss();
+ dispatchLayoutChange(0, 0);
+ }
+ }
+
+ public void refresh() {
+ removeAllViews();
+
+ inflateLayout(mContext);
+ initialize();
+
+ if (mVisible)
+ show(mCurrentPanel);
+ }
+
+ public void autoHidePanel() {
+ mActivity.autoHideTabs();
+ }
+
+ @Override
+ public boolean isShown() {
+ return mVisible;
+ }
+
+ public void setHWLayerEnabled(boolean enabled) {
+ if (enabled) {
+ mHeader.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+ mTabsContainer.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+ } else {
+ mHeader.setLayerType(View.LAYER_TYPE_NONE, null);
+ mTabsContainer.setLayerType(View.LAYER_TYPE_NONE, null);
+ }
+ }
+
+ public void prepareTabsAnimation(PropertyAnimator animator) {
+ if (!mHeaderVisible) {
+ final Resources resources = getContext().getResources();
+ final int toolbarHeight = resources.getDimensionPixelSize(R.dimen.browser_toolbar_height);
+ final int translationY = (mVisible ? 0 : -toolbarHeight);
+ if (mVisible) {
+ ViewHelper.setTranslationY(mHeader, -toolbarHeight);
+ ViewHelper.setTranslationY(mTabsContainer, -toolbarHeight);
+ ViewHelper.setAlpha(mTabsContainer, 0.0f);
+ }
+ animator.attach(mTabsContainer, PropertyAnimator.Property.ALPHA, mVisible ? 1.0f : 0.0f);
+ animator.attach(mTabsContainer, PropertyAnimator.Property.TRANSLATION_Y, translationY);
+ animator.attach(mHeader, PropertyAnimator.Property.TRANSLATION_Y, translationY);
+ }
+
+ setHWLayerEnabled(true);
+ }
+
+ public void finishTabsAnimation() {
+ setHWLayerEnabled(false);
+
+ // If the tray is now hidden, call hide() on current panel and unset it as the current panel
+ // to avoid hide() being called again when the layout is opened next.
+ if (!mVisible && mPanel != null) {
+ mPanel.hide();
+ mPanel = null;
+ }
+ }
+
+ public void setTabsLayoutChangeListener(TabsLayoutChangeListener listener) {
+ mLayoutChangeListener = listener;
+ }
+
+ private void dispatchLayoutChange(int width, int height) {
+ if (mLayoutChangeListener != null)
+ mLayoutChangeListener.onTabsLayoutChange(width, height);
+ }
+}
diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsPanelThumbnailView.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsPanelThumbnailView.java
new file mode 100644
index 000000000..09254bf76
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsPanelThumbnailView.java
@@ -0,0 +1,52 @@
+/* -*- 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.R;
+import org.mozilla.gecko.ThumbnailHelper;
+import org.mozilla.gecko.widget.CropImageView;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+
+/**
+ * A width constrained ImageView to show thumbnails of open tabs in the tabs panel.
+ */
+public class TabsPanelThumbnailView extends CropImageView {
+ public static final String LOGTAG = "Gecko" + TabsPanelThumbnailView.class.getSimpleName();
+
+
+ public TabsPanelThumbnailView(final Context context) {
+ this(context, null);
+ }
+
+ public TabsPanelThumbnailView(final Context context, final AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public TabsPanelThumbnailView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ protected float getAspectRatio() {
+ return ThumbnailHelper.TABS_PANEL_THUMBNAIL_ASPECT_RATIO;
+ }
+
+ @Override
+ public void setImageDrawable(Drawable drawable) {
+ boolean resize = true;
+
+ if (drawable == null) {
+ drawable = getResources().getDrawable(R.drawable.tab_panel_tab_background);
+ resize = false;
+ setScaleType(ScaleType.FIT_XY);
+ }
+
+ super.setImageDrawable(drawable, resize);
+ }
+}
diff --git a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsTouchHelperCallback.java b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsTouchHelperCallback.java
new file mode 100644
index 000000000..36e9e4739
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsTouchHelperCallback.java
@@ -0,0 +1,69 @@
+/* -*- 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 android.graphics.Canvas;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.helper.ItemTouchHelper;
+import android.view.View;
+
+class TabsTouchHelperCallback extends ItemTouchHelper.Callback {
+ private final DismissListener dismissListener;
+
+ interface DismissListener {
+ void onItemDismiss(View view);
+ }
+
+ public TabsTouchHelperCallback(DismissListener dismissListener) {
+ this.dismissListener = dismissListener;
+ }
+
+ @Override
+ public boolean isItemViewSwipeEnabled() {
+ return true;
+ }
+
+ @Override
+ public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
+ return makeFlag(ItemTouchHelper.ACTION_STATE_SWIPE, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT);
+ }
+
+ @Override
+ public void onSwiped(RecyclerView.ViewHolder viewHolder, int i) {
+ dismissListener.onItemDismiss(viewHolder.itemView);
+ }
+
+ @Override
+ public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
+ RecyclerView.ViewHolder target) {
+ return false;
+ }
+
+ // Alpha on an itemView being swiped should decrease to a min over a distance equal to the
+ // width of the item being swiped.
+ @Override
+ public void onChildDraw(Canvas c,
+ RecyclerView recyclerView,
+ RecyclerView.ViewHolder viewHolder,
+ float dX,
+ float dY,
+ int actionState,
+ boolean isCurrentlyActive) {
+ if (actionState != ItemTouchHelper.ACTION_STATE_SWIPE) {
+ return;
+ }
+
+ super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
+
+ viewHolder.itemView.setAlpha(Math.max(0.1f,
+ Math.min(1f, 1f - 2f * Math.abs(dX) / viewHolder.itemView.getWidth())));
+ }
+
+ public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
+ super.clearView(recyclerView, viewHolder);
+ viewHolder.itemView.setAlpha(1);
+ }
+}