import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo

This commit is contained in:
Roy Tam 2018-01-19 03:59:58 +08:00
commit dcd9973243
150858 changed files with 23884658 additions and 0 deletions

View file

@ -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();
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}
}

View file

@ -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());
}
}

View file

@ -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];
}
};
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}

View file

@ -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();
}
}

View file

@ -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);
}
}
}

View file

@ -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) {
}
}
}

View file

@ -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);
}
}
}
}
}

View file

@ -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();
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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();
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}