mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-01 14:45:29 +00:00
642 lines
22 KiB
Java
642 lines
22 KiB
Java
/* -*- 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;
|
|
|
|
import org.mozilla.gecko.PropertyAnimator.Property;
|
|
import org.mozilla.gecko.widget.TwoWayView;
|
|
|
|
import android.content.Context;
|
|
import android.content.res.TypedArray;
|
|
import android.graphics.Rect;
|
|
import android.graphics.drawable.Drawable;
|
|
import android.text.TextUtils;
|
|
import android.util.AttributeSet;
|
|
import android.view.LayoutInflater;
|
|
import android.view.MotionEvent;
|
|
import android.view.VelocityTracker;
|
|
import android.view.View;
|
|
import android.view.ViewConfiguration;
|
|
import android.view.ViewGroup;
|
|
import android.widget.BaseAdapter;
|
|
import android.widget.Button;
|
|
import android.widget.ImageButton;
|
|
import android.widget.ImageView;
|
|
import android.widget.TextView;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
|
|
public class TabsTray extends TwoWayView
|
|
implements TabsPanel.PanelView {
|
|
private static final String LOGTAG = "GeckoTabsTray";
|
|
|
|
private Context mContext;
|
|
private TabsPanel mTabsPanel;
|
|
|
|
private TabsAdapter mTabsAdapter;
|
|
|
|
private List<View> mPendingClosedTabs;
|
|
private int mCloseAnimationCount;
|
|
|
|
private TabSwipeGestureListener mSwipeListener;
|
|
|
|
// Time to animate non-flinged tabs of screen, in milliseconds
|
|
private static final int ANIMATION_DURATION = 250;
|
|
|
|
private static final String ABOUT_HOME = "about:home";
|
|
|
|
public TabsTray(Context context, AttributeSet attrs) {
|
|
super(context, attrs);
|
|
mContext = context;
|
|
|
|
mCloseAnimationCount = 0;
|
|
mPendingClosedTabs = new ArrayList<View>();
|
|
|
|
setItemsCanFocus(true);
|
|
|
|
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabsTray);
|
|
boolean isPrivate = (a.getInt(R.styleable.TabsTray_tabs, 0x0) == 1);
|
|
a.recycle();
|
|
|
|
mTabsAdapter = new TabsAdapter(mContext, isPrivate);
|
|
setAdapter(mTabsAdapter);
|
|
|
|
mSwipeListener = new TabSwipeGestureListener();
|
|
setOnTouchListener(mSwipeListener);
|
|
setOnScrollListener(mSwipeListener.makeScrollListener());
|
|
|
|
setRecyclerListener(new RecyclerListener() {
|
|
@Override
|
|
public void onMovedToScrapHeap(View view) {
|
|
TabRow row = (TabRow) view.getTag();
|
|
row.thumbnail.setImageDrawable(null);
|
|
row.close.setVisibility(View.VISIBLE);
|
|
}
|
|
});
|
|
}
|
|
|
|
@Override
|
|
public ViewGroup getLayout() {
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public void setTabsPanel(TabsPanel panel) {
|
|
mTabsPanel = panel;
|
|
}
|
|
|
|
@Override
|
|
public void show() {
|
|
setVisibility(View.VISIBLE);
|
|
Tabs.getInstance().refreshThumbnails();
|
|
Tabs.registerOnTabsChangedListener(mTabsAdapter);
|
|
mTabsAdapter.refreshTabsData();
|
|
}
|
|
|
|
@Override
|
|
public void hide() {
|
|
setVisibility(View.GONE);
|
|
Tabs.unregisterOnTabsChangedListener(mTabsAdapter);
|
|
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Tab:Screenshot:Cancel",""));
|
|
mTabsAdapter.clear();
|
|
}
|
|
|
|
@Override
|
|
public boolean shouldExpand() {
|
|
return isVertical();
|
|
}
|
|
|
|
private void autoHidePanel() {
|
|
mTabsPanel.autoHidePanel();
|
|
}
|
|
|
|
// ViewHolder for a row in the list
|
|
private class TabRow {
|
|
int id;
|
|
TextView title;
|
|
ImageView thumbnail;
|
|
ImageButton close;
|
|
ViewGroup info;
|
|
|
|
public TabRow(View view) {
|
|
info = (ViewGroup) view;
|
|
title = (TextView) view.findViewById(R.id.title);
|
|
thumbnail = (ImageView) view.findViewById(R.id.thumbnail);
|
|
close = (ImageButton) view.findViewById(R.id.close);
|
|
}
|
|
}
|
|
|
|
// Adapter to bind tabs into a list
|
|
private class TabsAdapter extends BaseAdapter implements Tabs.OnTabsChangedListener {
|
|
private Context mContext;
|
|
private boolean mIsPrivate;
|
|
private ArrayList<Tab> mTabs;
|
|
private LayoutInflater mInflater;
|
|
private Button.OnClickListener mOnCloseClickListener;
|
|
|
|
public TabsAdapter(Context context, boolean isPrivate) {
|
|
mContext = context;
|
|
mInflater = LayoutInflater.from(mContext);
|
|
mIsPrivate = isPrivate;
|
|
|
|
mOnCloseClickListener = new Button.OnClickListener() {
|
|
@Override
|
|
public void onClick(View v) {
|
|
TabRow tab = (TabRow) v.getTag();
|
|
final int pos = (isVertical() ? tab.info.getWidth() : tab.info.getHeight());
|
|
animateClose(tab.info, pos);
|
|
}
|
|
};
|
|
}
|
|
|
|
@Override
|
|
public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) {
|
|
switch (msg) {
|
|
case ADDED:
|
|
// Refresh the list to make sure the new tab is added in the right position.
|
|
refreshTabsData();
|
|
break;
|
|
|
|
case CLOSED:
|
|
removeTab(tab);
|
|
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:
|
|
View view = TabsTray.this.getChildAt(getPositionForTab(tab) - TabsTray.this.getFirstVisiblePosition());
|
|
if (view == null)
|
|
return;
|
|
|
|
TabRow row = (TabRow) view.getTag();
|
|
assignValues(row, tab);
|
|
break;
|
|
}
|
|
}
|
|
|
|
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.
|
|
mTabs = new ArrayList<Tab>();
|
|
|
|
Iterable<Tab> tabs = Tabs.getInstance().getTabsInOrder();
|
|
for (Tab tab : tabs) {
|
|
if (tab.isPrivate() == mIsPrivate)
|
|
mTabs.add(tab);
|
|
}
|
|
|
|
notifyDataSetChanged(); // Be sure to call this whenever mTabs changes.
|
|
updateSelectedPosition();
|
|
}
|
|
|
|
// Updates the selected position in the list so that it will be scrolled to the right place.
|
|
private void updateSelectedPosition() {
|
|
int selected = getPositionForTab(Tabs.getInstance().getSelectedTab());
|
|
for (int i=0; i < getCount(); i++)
|
|
TabsTray.this.setItemChecked(i, (i == selected));
|
|
|
|
if (selected != -1)
|
|
TabsTray.this.setSelection(selected);
|
|
}
|
|
|
|
public 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;
|
|
}
|
|
|
|
private int getPositionForTab(Tab tab) {
|
|
if (mTabs == null || tab == null)
|
|
return -1;
|
|
|
|
return mTabs.indexOf(tab);
|
|
}
|
|
|
|
private void removeTab(Tab tab) {
|
|
if (tab.isPrivate() == mIsPrivate && mTabs != null) {
|
|
mTabs.remove(tab);
|
|
notifyDataSetChanged(); // Be sure to call this whenever mTabs changes.
|
|
updateSelectedPosition();
|
|
}
|
|
}
|
|
|
|
private void assignValues(TabRow row, Tab tab) {
|
|
if (row == null || tab == null)
|
|
return;
|
|
|
|
row.id = tab.getId();
|
|
|
|
Drawable thumbnailImage = tab.getThumbnail();
|
|
if (thumbnailImage != null)
|
|
row.thumbnail.setImageDrawable(thumbnailImage);
|
|
else if (TextUtils.equals(tab.getURL(), ABOUT_HOME))
|
|
row.thumbnail.setImageResource(R.drawable.abouthome_thumbnail);
|
|
else
|
|
row.thumbnail.setImageResource(R.drawable.tab_thumbnail_default);
|
|
|
|
row.title.setText(tab.getDisplayTitle());
|
|
row.close.setTag(row);
|
|
}
|
|
|
|
@Override
|
|
public View getView(int position, View convertView, ViewGroup parent) {
|
|
TabRow row;
|
|
|
|
if (convertView == null) {
|
|
convertView = mInflater.inflate(R.layout.tabs_row, null);
|
|
row = new TabRow(convertView);
|
|
row.close.setOnClickListener(mOnCloseClickListener);
|
|
convertView.setTag(row);
|
|
} else {
|
|
row = (TabRow) convertView.getTag();
|
|
}
|
|
|
|
Tab tab = mTabs.get(position);
|
|
assignValues(row, tab);
|
|
|
|
return convertView;
|
|
}
|
|
}
|
|
|
|
private boolean isVertical() {
|
|
return (getOrientation().compareTo(TwoWayView.Orientation.VERTICAL) == 0);
|
|
}
|
|
|
|
private void animateClose(final View view, int pos) {
|
|
PropertyAnimator animator = new PropertyAnimator(ANIMATION_DURATION);
|
|
animator.attach(view, Property.ALPHA, 0);
|
|
|
|
if (isVertical())
|
|
animator.attach(view, Property.TRANSLATION_X, pos);
|
|
else
|
|
animator.attach(view, Property.TRANSLATION_Y, pos);
|
|
|
|
mCloseAnimationCount++;
|
|
mPendingClosedTabs.add(view);
|
|
|
|
animator.setPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
|
|
@Override
|
|
public void onPropertyAnimationStart() { }
|
|
@Override
|
|
public void onPropertyAnimationEnd() {
|
|
mCloseAnimationCount--;
|
|
if (mCloseAnimationCount > 0)
|
|
return;
|
|
|
|
for (View pendingView : mPendingClosedTabs) {
|
|
animateFinishClose(pendingView);
|
|
}
|
|
|
|
mPendingClosedTabs.clear();
|
|
}
|
|
});
|
|
|
|
if (mTabsAdapter.getCount() == 1)
|
|
autoHidePanel();
|
|
|
|
animator.start();
|
|
}
|
|
|
|
private void animateFinishClose(final View view) {
|
|
PropertyAnimator animator = new PropertyAnimator(ANIMATION_DURATION);
|
|
|
|
final boolean isVertical = isVertical();
|
|
if (isVertical)
|
|
animator.attach(view, Property.HEIGHT, 1);
|
|
else
|
|
animator.attach(view, Property.WIDTH, 1);
|
|
|
|
TabRow tab = (TabRow)view.getTag();
|
|
final int tabId = tab.id;
|
|
final int originalSize = (isVertical ? view.getHeight() : view.getWidth());
|
|
|
|
animator.setPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
|
|
@Override
|
|
public void onPropertyAnimationStart() { }
|
|
@Override
|
|
public void onPropertyAnimationEnd() {
|
|
// Reset view presentation as it will be recycled in the
|
|
// list view by the adapter.
|
|
AnimatorProxy proxy = AnimatorProxy.create(view);
|
|
proxy.setAlpha(1);
|
|
|
|
if (isVertical) {
|
|
proxy.setHeight(originalSize);
|
|
proxy.setTranslationX(0);
|
|
} else {
|
|
proxy.setWidth(originalSize);
|
|
proxy.setTranslationY(0);
|
|
}
|
|
|
|
Tabs tabs = Tabs.getInstance();
|
|
Tab tab = tabs.getTab(tabId);
|
|
tabs.closeTab(tab);
|
|
}
|
|
});
|
|
|
|
animator.start();
|
|
}
|
|
|
|
private void animateCancel(final View view) {
|
|
PropertyAnimator animator = new PropertyAnimator(ANIMATION_DURATION);
|
|
animator.attach(view, Property.ALPHA, 1);
|
|
|
|
if (isVertical())
|
|
animator.attach(view, Property.TRANSLATION_X, 0);
|
|
else
|
|
animator.attach(view, Property.TRANSLATION_Y, 0);
|
|
|
|
|
|
animator.setPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
|
|
@Override
|
|
public void onPropertyAnimationStart() { }
|
|
@Override
|
|
public void onPropertyAnimationEnd() {
|
|
TabRow tab = (TabRow) view.getTag();
|
|
tab.close.setVisibility(View.VISIBLE);
|
|
}
|
|
});
|
|
|
|
animator.start();
|
|
}
|
|
|
|
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 int mSwipeThreshold;
|
|
private int mMinFlingVelocity;
|
|
|
|
private int mMaxFlingVelocity;
|
|
private VelocityTracker mVelocityTracker;
|
|
|
|
private int mListWidth = 1;
|
|
private int mListHeight = 1;
|
|
|
|
private View mSwipeView;
|
|
private AnimatorProxy mSwipeProxy;
|
|
private int mSwipeViewPosition;
|
|
private Runnable mPendingCheckForTap;
|
|
|
|
private float mSwipeStartX;
|
|
private float mSwipeStartY;
|
|
private boolean mSwiping;
|
|
private boolean mEnabled;
|
|
|
|
public TabSwipeGestureListener() {
|
|
mSwipeView = null;
|
|
mSwipeProxy = null;
|
|
mSwipeViewPosition = TwoWayView.INVALID_POSITION;
|
|
mSwiping = false;
|
|
mEnabled = true;
|
|
|
|
ViewConfiguration vc = ViewConfiguration.get(TabsTray.this.getContext());
|
|
mSwipeThreshold = vc.getScaledTouchSlop();
|
|
mMinFlingVelocity = (int) (getContext().getResources().getDisplayMetrics().density * MIN_VELOCITY);
|
|
mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();
|
|
}
|
|
|
|
public void setEnabled(boolean enabled) {
|
|
mEnabled = enabled;
|
|
}
|
|
|
|
public TwoWayView.OnScrollListener makeScrollListener() {
|
|
return new TwoWayView.OnScrollListener() {
|
|
@Override
|
|
public void onScrollStateChanged(TwoWayView twoWayView, int scrollState) {
|
|
setEnabled(scrollState != TwoWayView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
|
|
}
|
|
|
|
@Override
|
|
public void onScroll(TwoWayView twoWayView, int i, int i1, int i2) {
|
|
}
|
|
};
|
|
}
|
|
|
|
@Override
|
|
public boolean onTouch(View view, MotionEvent e) {
|
|
if (!mEnabled)
|
|
return false;
|
|
|
|
if (mListWidth < 2 || mListHeight < 2) {
|
|
mListWidth = TabsTray.this.getWidth();
|
|
mListHeight = TabsTray.this.getHeight();
|
|
}
|
|
|
|
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) {
|
|
mSwipeStartX = e.getRawX();
|
|
mSwipeStartY = e.getRawY();
|
|
mSwipeViewPosition = TabsTray.this.getPositionForView(mSwipeView);
|
|
|
|
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) {
|
|
TabRow tab = (TabRow) mSwipeView.getTag();
|
|
Tabs.getInstance().selectTab(tab.id);
|
|
autoHidePanel();
|
|
break;
|
|
}
|
|
|
|
mVelocityTracker.addMovement(e);
|
|
mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
|
|
|
|
float velocityX = Math.abs(mVelocityTracker.getXVelocity());
|
|
float velocityY = Math.abs(mVelocityTracker.getYVelocity());
|
|
|
|
boolean dismiss = false;
|
|
boolean dismissDirection = false;
|
|
int dismissTranslation = 0;
|
|
|
|
if (isVertical()) {
|
|
float deltaX = mSwipeProxy.getTranslationX();
|
|
|
|
if (Math.abs(deltaX) > mListWidth / 2) {
|
|
dismiss = true;
|
|
dismissDirection = (deltaX > 0);
|
|
} else if (mMinFlingVelocity <= velocityX && velocityX <= mMaxFlingVelocity
|
|
&& velocityY < velocityX) {
|
|
dismiss = mSwiping && (deltaX * mVelocityTracker.getXVelocity() > 0);
|
|
dismissDirection = (mVelocityTracker.getXVelocity() > 0);
|
|
}
|
|
|
|
dismissTranslation = (dismissDirection ? mListWidth : -mListWidth);
|
|
} else {
|
|
float deltaY = mSwipeProxy.getTranslationY();
|
|
|
|
if (Math.abs(deltaY) > mListHeight / 2) {
|
|
dismiss = true;
|
|
dismissDirection = (deltaY > 0);
|
|
} else if (mMinFlingVelocity <= velocityY && velocityY <= mMaxFlingVelocity
|
|
&& velocityX < velocityY) {
|
|
dismiss = mSwiping && (deltaY * mVelocityTracker.getYVelocity() > 0);
|
|
dismissDirection = (mVelocityTracker.getYVelocity() > 0);
|
|
}
|
|
|
|
dismissTranslation = (dismissDirection ? mListHeight : -mListHeight);
|
|
}
|
|
|
|
if (dismiss)
|
|
animateClose(mSwipeView, dismissTranslation);
|
|
else
|
|
animateCancel(mSwipeView);
|
|
|
|
mVelocityTracker = null;
|
|
mSwipeView = null;
|
|
mSwipeViewPosition = TwoWayView.INVALID_POSITION;
|
|
mSwipeProxy = null;
|
|
|
|
mSwipeStartX = 0;
|
|
mSwipeStartY = 0;
|
|
mSwiping = false;
|
|
|
|
break;
|
|
}
|
|
|
|
case MotionEvent.ACTION_MOVE: {
|
|
if (mSwipeView == null)
|
|
break;
|
|
|
|
mVelocityTracker.addMovement(e);
|
|
|
|
final boolean isVertical = isVertical();
|
|
|
|
float deltaX = e.getRawX() - mSwipeStartX;
|
|
float deltaY = e.getRawY() - mSwipeStartY;
|
|
float delta = (isVertical ? deltaX : deltaY);
|
|
|
|
boolean isScrollingX = Math.abs(deltaX) > mSwipeThreshold;
|
|
boolean isScrollingY = Math.abs(deltaY) > mSwipeThreshold;
|
|
boolean isSwipingToClose = (isVertical ? isScrollingX : isScrollingY);
|
|
|
|
// If we're actually swiping, make sure we don't
|
|
// set pressed state on the swiped view.
|
|
if (isScrollingX || isScrollingY)
|
|
cancelCheckForTap();
|
|
|
|
if (isSwipingToClose) {
|
|
mSwiping = true;
|
|
TabsTray.this.requestDisallowInterceptTouchEvent(true);
|
|
|
|
TabRow tab = (TabRow) mSwipeView.getTag();
|
|
tab.close.setVisibility(View.INVISIBLE);
|
|
|
|
// 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));
|
|
TabsTray.this.onTouchEvent(cancelEvent);
|
|
|
|
mSwipeProxy = AnimatorProxy.create(mSwipeView);
|
|
}
|
|
|
|
if (mSwiping) {
|
|
if (isVertical)
|
|
mSwipeProxy.setTranslationX(delta);
|
|
else
|
|
mSwipeProxy.setTranslationY(delta);
|
|
|
|
mSwipeProxy.setAlpha(Math.max(0.1f, Math.min(1f,
|
|
1f - 2f * Math.abs(delta) / (isVertical ? mListWidth : mListHeight))));
|
|
|
|
return true;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private View findViewAt(float rawX, float rawY) {
|
|
Rect rect = new Rect();
|
|
|
|
int[] listViewCoords = new int[2];
|
|
TabsTray.this.getLocationOnScreen(listViewCoords);
|
|
|
|
int x = (int) rawX - listViewCoords[0];
|
|
int y = (int) rawY - listViewCoords[1];
|
|
|
|
for (int i = 0; i < TabsTray.this.getChildCount(); i++) {
|
|
View child = TabsTray.this.getChildAt(i);
|
|
child.getHitRect(rect);
|
|
|
|
if (rect.contains(x, y))
|
|
return child;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private void triggerCheckForTap() {
|
|
if (mPendingCheckForTap == null)
|
|
mPendingCheckForTap = new CheckForTap();
|
|
|
|
TabsTray.this.postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
|
|
}
|
|
|
|
private void cancelCheckForTap() {
|
|
if (mPendingCheckForTap == null)
|
|
return;
|
|
|
|
TabsTray.this.removeCallbacks(mPendingCheckForTap);
|
|
}
|
|
|
|
private class CheckForTap implements Runnable {
|
|
@Override
|
|
public void run() {
|
|
if (!mSwiping && mSwipeView != null && mEnabled)
|
|
mSwipeView.setPressed(true);
|
|
}
|
|
}
|
|
}
|
|
}
|