mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-05 23:38:38 +09:00
import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo
This commit is contained in:
commit
dcd9973243
150858 changed files with 23884658 additions and 0 deletions
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
170
mobile/android/base/java/org/mozilla/gecko/tabs/TabStrip.java
Normal file
170
mobile/android/base/java/org/mozilla/gecko/tabs/TabStrip.java
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
216
mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayout.java
Normal file
216
mobile/android/base/java/org/mozilla/gecko/tabs/TabsLayout.java
Normal 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();
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
456
mobile/android/base/java/org/mozilla/gecko/tabs/TabsPanel.java
Normal file
456
mobile/android/base/java/org/mozilla/gecko/tabs/TabsPanel.java
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue