Bug 1203036 - Put tab tray improvements behind nightly flag; r=mcomella

This patch reintroduced changes behind a nightly flag removed by:

    1161638: Remove the chrome at the bottom of the screen in the Tabs Tray
    1164723: Inherit from Tablet UI on Mobile UI (aka compact tabs)
    1193745: Implement the tablet tabs tray grid view on mobile

I've also done a bit of work to allow the chrome to sit at the correct Y location in landscape on mobile devices when the tabs panel is shown to account for bug 1193374 which adjusts the aspect ratio of the tabs panel thumnbnails and didn't need to be hidden behind a nightly flag. Tablets remain unaffected by this change.
This commit is contained in:
Martyn Haigh 2015-09-10 10:51:54 +01:00
parent 35b1398f12
commit 23f7e684b7
13 changed files with 887 additions and 11 deletions

View File

@ -449,6 +449,8 @@ public class BrowserApp extends GeckoApp
final View view;
if (BrowserToolbar.class.getName().equals(name)) {
view = BrowserToolbar.create(context, attrs);
} else if (TabsPanel.TabsLayout.class.getName().equals(name)) {
view = TabsPanel.createTabsLayout(context, attrs);
} else {
view = super.onCreateView(name, context, attrs);
}

View File

@ -472,6 +472,7 @@ gbjar.sources += [
'tabs/TabsGridLayout.java',
'tabs/TabsLayoutAdapter.java',
'tabs/TabsLayoutItemView.java',
'tabs/TabsListLayout.java',
'tabs/TabsPanel.java',
'tabs/TabsPanelThumbnailView.java',
'Telemetry.java',

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<org.mozilla.gecko.tabs.TabsLayoutItemView xmlns:android="http://schemas.android.com/apk/res/android"
style="@style/TabsItem"
android:focusable="true"
android:id="@+id/info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="@dimen/tab_vertical_padding"
android:paddingBottom="@dimen/tab_vertical_padding"
android:paddingLeft="1dip"
android:paddingRight="1dip"
android:gravity="center">
<!-- We set state_private on this View dynamically in TabsListLayout. -->
<org.mozilla.gecko.widget.TabThumbnailWrapper
android:id="@+id/wrapper"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/tab_thumbnail_margin"
android:padding="@dimen/tab_thumbnail_padding"
android:background="@drawable/tab_thumbnail"
android:duplicateParentState="true">
<org.mozilla.gecko.tabs.TabsPanelThumbnailView android:id="@+id/thumbnail"
android:layout_width="@dimen/tab_thumbnail_width"
android:layout_height="@dimen/tab_thumbnail_height"
android:layout_above="@+id/title_bar"
android:layout_alignParentTop="true"/>
<LinearLayout android:id="@id/title_bar"
android:layout_alignParentBottom="true"
android:layout_width="@dimen/tab_thumbnail_width"
android:layout_height="@dimen/tab_title_height"
android:orientation="horizontal"
android:background="#EFFF"
android:duplicateParentState="true">
<TextView android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.0"
android:padding="4dip"
style="@style/TabLayoutItemTextAppearance"
android:textSize="12sp"
android:textColor="@color/placeholder_active_grey"
android:singleLine="true"
android:duplicateParentState="true"/>
<ImageButton android:id="@+id/close"
style="@style/TabsItemClose"
android:layout_width="32dip"
android:layout_height="match_parent"
android:background="@drawable/action_bar_button_inverse"
android:scaleType="center"
android:contentDescription="@string/close_tab"
android:src="@drawable/tab_close"/>
</LinearLayout>
</org.mozilla.gecko.widget.TabThumbnailWrapper>
</org.mozilla.gecko.tabs.TabsLayoutItemView>

View File

@ -14,7 +14,7 @@
<!-- Note: for an unknown reason, scrolling in the TabsLayout
does not work unless it is laid out after the empty view. -->
<org.mozilla.gecko.tabs.TabsGridLayout
<view class="org.mozilla.gecko.tabs.TabsPanel$TabsLayout"
android:id="@+id/private_tabs_layout"
style="@style/TabsLayout"
android:layout_width="match_parent"

View File

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<org.mozilla.gecko.tabs.TabsLayoutItemView xmlns:android="http://schemas.android.com/apk/res/android"
style="@style/TabsItem"
android:focusable="true"
android:id="@+id/info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="12dip"
android:paddingTop="6dip"
android:paddingBottom="6dip"
android:background="@drawable/tab_row">
<!-- We set state_private on this View dynamically in TabsListLayout. -->
<org.mozilla.gecko.widget.TabThumbnailWrapper
android:id="@+id/wrapper"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="4dip"
android:background="@drawable/tab_thumbnail"
android:duplicateParentState="true">
<org.mozilla.gecko.tabs.TabsPanelThumbnailView android:id="@+id/thumbnail"
android:layout_width="@dimen/tab_thumbnail_width"
android:layout_height="@dimen/tab_thumbnail_height"/>
</org.mozilla.gecko.widget.TabThumbnailWrapper>
<LinearLayout android:layout_width="0dip"
android:layout_height="match_parent"
android:orientation="vertical"
android:layout_weight="1.0"
android:paddingTop="4dip"
android:paddingLeft="8dip"
android:paddingRight="4dip">
<TextView android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="0dip"
android:layout_weight="1.0"
style="@style/TabLayoutItemTextAppearance"
android:textColor="#FFFFFFFF"
android:textSize="14sp"
android:singleLine="false"
android:maxLines="4"
android:duplicateParentState="true"/>
</LinearLayout>
<ImageButton android:id="@+id/close"
style="@style/TabsItemClose"
android:layout_width="34dip"
android:layout_height="match_parent"
android:background="@drawable/action_bar_button_inverse"
android:scaleType="center"
android:contentDescription="@string/close_tab"
android:src="@drawable/tab_close"/>
</org.mozilla.gecko.tabs.TabsLayoutItemView>

View File

@ -70,12 +70,12 @@
</RelativeLayout>
<FrameLayout
<view class="org.mozilla.gecko.tabs.TabsPanel$TabsLayoutContainer"
android:id="@+id/tabs_container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<org.mozilla.gecko.tabs.TabsGridLayout
<view class="org.mozilla.gecko.tabs.TabsPanel$TabsLayout"
android:id="@+id/normal_tabs"
style="@style/TabsLayout"
android:layout_width="match_parent"
@ -90,6 +90,6 @@
android:layout_height="match_parent"
android:visibility="gone"/>
</FrameLayout>
</view>
</merge>

View File

@ -137,7 +137,6 @@
<dimen name="tabs_strip_button_width">100dp</dimen>
<dimen name="tabs_strip_button_padding">18dp</dimen>
<dimen name="tabs_strip_shadow_size">1dp</dimen>
<dimen name="tabs_layout_horizontal_height">156dp</dimen>
<dimen name="text_selection_handle_width">47dp</dimen>
<dimen name="text_selection_handle_height">58dp</dimen>
<dimen name="text_selection_handle_shadow">11dp</dimen>
@ -152,6 +151,10 @@
<dimen name="tab_panel_grid_padding_top">19dp</dimen>
<dimen name="tab_highlight_stroke_width">4dp</dimen>
<dimen name="tab_title_height">22dp</dimen>
<dimen name="tab_vertical_padding">6dp</dimen>
<dimen name="tab_thumbnail_padding">4dp</dimen>
<dimen name="tab_thumbnail_margin">6dp</dimen>
<!-- PageActionButtons dimensions -->
<dimen name="page_action_button_width">32dp</dimen>

View File

@ -10,4 +10,4 @@
provide dummy values below. -->
<item type="layout" name="tab_strip">@null</item>
<item type="layout" name="tabs_panel_back_button">@null</item>
</resources>
</resources>

View File

@ -51,6 +51,11 @@ class PrivateTabsPanel extends FrameLayout implements CloseAllPanelView {
tabsLayout.hide();
}
@Override
public boolean shouldExpand() {
return tabsLayout.shouldExpand();
}
@Override
public void closeAll() {
tabsLayout.closeAll();

View File

@ -185,6 +185,11 @@ class TabsGridLayout extends GridView
tabsAdapter.clear();
}
@Override
public boolean shouldExpand() {
return true;
}
private void autoHidePanel() {
tabsPanel.autoHidePanel();
}

View File

@ -4,6 +4,7 @@
package org.mozilla.gecko.tabs;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.GeckoEvent;
import org.mozilla.gecko.R;
@ -100,7 +101,9 @@ public class TabsLayoutItemView extends LinearLayout
mCloseButton = (ImageView) findViewById(R.id.close);
mThumbnailWrapper = (TabThumbnailWrapper) findViewById(R.id.wrapper);
growCloseButtonHitArea();
if (HardwareUtils.isTablet() || AppConstants.NIGHTLY_BUILD) {
growCloseButtonHitArea();
}
}
private void growCloseButtonHitArea() {

View File

@ -0,0 +1,657 @@
/* -*- 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.GeckoEvent;
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.animation.PropertyAnimator.Property;
import org.mozilla.gecko.animation.ViewHelper;
import org.mozilla.gecko.tabs.TabsPanel.TabsLayout;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.widget.TwoWayView;
import org.mozilla.gecko.widget.themed.ThemedRelativeLayout;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.Button;
import java.util.ArrayList;
import java.util.List;
class TabsListLayout extends TwoWayView
implements TabsLayout,
Tabs.OnTabsChangedListener {
private static final String LOGTAG = "Gecko" + TabsListLayout.class.getSimpleName();
// 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 final boolean isPrivate;
private final TabsLayoutAdapter tabsAdapter;
private final List<View> pendingClosedTabs;
private TabsPanel tabsPanel;
private int closeAnimationCount;
private int closeAllAnimationCount;
private int originalSize;
public TabsListLayout(Context context, AttributeSet attrs) {
super(context, attrs);
pendingClosedTabs = new ArrayList<View>();
setItemsCanFocus(true);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabsLayout);
isPrivate = (a.getInt(R.styleable.TabsLayout_tabs, 0x0) == 1);
a.recycle();
tabsAdapter = new TabsListLayoutAdapter(context);
setAdapter(tabsAdapter);
final TabSwipeGestureListener swipeListener = new TabSwipeGestureListener();
setOnTouchListener(swipeListener);
setOnScrollListener(swipeListener.makeScrollListener());
setRecyclerListener(new RecyclerListener() {
@Override
public void onMovedToScrapHeap(View view) {
TabsLayoutItemView item = (TabsLayoutItemView) view;
item.setThumbnail(null);
item.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.sendEventToGecko(GeckoEvent.createBroadcastEvent("Tab:Screenshot:Cancel", ""));
tabsAdapter.clear();
}
@Override
public boolean shouldExpand() {
return isVertical();
}
private void autoHidePanel() {
tabsPanel.autoHidePanel();
}
@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:
if (tab.isPrivate() == isPrivate && tabsAdapter.getCount() > 0) {
if (tabsAdapter.removeTab(tab)) {
int selected = tabsAdapter.getPositionForTab(Tabs.getInstance().getSelectedTab());
updateSelectedStyle(selected);
}
}
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 item = (TabsLayoutItemView) view;
item.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(int selected) {
for (int i = 0; i < tabsAdapter.getCount(); i++) {
setItemChecked(i, (i == 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.
ArrayList<Tab> tabData = new ArrayList<Tab>();
Iterable<Tab> allTabs = Tabs.getInstance().getTabsInOrder();
for (Tab tab : allTabs) {
if (tab.isPrivate() == isPrivate) {
tabData.add(tab);
}
}
tabsAdapter.setTabs(tabData);
updateSelectedPosition();
}
public void resetTransforms(View view) {
ViewHelper.setAlpha(view, 1);
if (isVertical()) {
ViewHelper.setTranslationX(view, 0);
} else {
ViewHelper.setTranslationY(view, 0);
}
// We only need to reset the height or width after individual tab close animations.
if (originalSize != 0) {
if (isVertical()) {
ViewHelper.setHeight(view, originalSize);
} else {
ViewHelper.setWidth(view, originalSize);
}
}
}
private boolean isVertical() {
return (getOrientation().compareTo(TwoWayView.Orientation.VERTICAL) == 0);
}
@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;
for (int i = childCount - 1; i >= 0; i--) {
final View view = getChildAt(i);
final PropertyAnimator animator = new PropertyAnimator(ANIMATION_DURATION);
animator.attach(view, Property.ALPHA, 0);
if (isVertical()) {
animator.attach(view, Property.TRANSLATION_X, view.getWidth());
} else {
animator.attach(view, Property.TRANSLATION_Y, view.getHeight());
}
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.
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);
}
}
}
});
ThreadUtils.getUiHandler().postDelayed(new Runnable() {
@Override
public void run() {
animator.start();
}
}, cascadeDelay);
cascadeDelay += ANIMATION_CASCADE_DELAY;
}
}
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);
}
closeAnimationCount++;
pendingClosedTabs.add(view);
animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
@Override
public void onPropertyAnimationStart() {
}
@Override
public void onPropertyAnimationEnd() {
closeAnimationCount--;
if (closeAnimationCount > 0) {
return;
}
for (View pendingView : pendingClosedTabs) {
animateFinishClose(pendingView);
}
pendingClosedTabs.clear();
}
});
if (tabsAdapter.getCount() == 1) {
autoHidePanel();
}
animator.start();
}
private void animateFinishClose(final View view) {
final boolean isVertical = isVertical();
PropertyAnimator animator = new PropertyAnimator(ANIMATION_DURATION);
if (isVertical) {
animator.attach(view, Property.HEIGHT, 1);
} else {
animator.attach(view, Property.WIDTH, 1);
}
final int tabId = ((TabsLayoutItemView) view).getTabId();
// Caching this assumes that all rows are the same height
if (originalSize == 0) {
originalSize = (isVertical ? view.getHeight() : view.getWidth());
}
animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
@Override
public void onPropertyAnimationStart() {
}
@Override
public void onPropertyAnimationEnd() {
Tabs tabs = Tabs.getInstance();
Tab tab = tabs.getTab(tabId);
tabs.closeTab(tab, true);
}
});
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.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
@Override
public void onPropertyAnimationStart() {
}
@Override
public void onPropertyAnimationEnd() {
TabsLayoutItemView tab = (TabsLayoutItemView) view;
tab.setCloseVisible(true);
}
});
animator.start();
}
private class TabsListLayoutAdapter extends TabsLayoutAdapter {
private final Button.OnClickListener mCloseOnClickListener;
public TabsListLayoutAdapter(Context context) {
super(context, R.layout.tabs_list_item_view);
mCloseOnClickListener = 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 it's tag, hence the getTag() call
TabsLayoutItemView item = (TabsLayoutItemView) v.getTag();
final int pos = (isVertical() ? item.getWidth() : 0 - item.getHeight());
animateClose(item, pos);
}
};
}
@Override
public TabsLayoutItemView newView(int position, ViewGroup parent) {
TabsLayoutItemView item = super.newView(position, parent);
item.setCloseOnClickListener(mCloseOnClickListener);
((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 swipeThreshold;
private final int minFlingVelocity;
private final int maxFlingVelocity;
private VelocityTracker velocityTracker;
private int listWidth = 1;
private int listHeight = 1;
private View swipeView;
private Runnable pendingCheckForTap;
private float swipeStartX;
private float swipeStartY;
private boolean swiping;
private boolean enabled;
public TabSwipeGestureListener() {
enabled = true;
ViewConfiguration vc = ViewConfiguration.get(TabsListLayout.this.getContext());
swipeThreshold = vc.getScaledTouchSlop();
minFlingVelocity = (int) (getContext().getResources().getDisplayMetrics().density * MIN_VELOCITY);
maxFlingVelocity = vc.getScaledMaximumFlingVelocity();
}
public void setEnabled(boolean enabled) {
this.enabled = 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 (!enabled) {
return false;
}
if (listWidth < 2 || listHeight < 2) {
listWidth = TabsListLayout.this.getWidth();
listHeight = TabsListLayout.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
swipeView = findViewAt(x, y);
if (swipeView != null) {
swipeStartX = e.getRawX();
swipeStartY = e.getRawY();
velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(e);
}
view.onTouchEvent(e);
return true;
}
case MotionEvent.ACTION_UP: {
if (swipeView == null) {
break;
}
cancelCheckForTap();
swipeView.setPressed(false);
if (!swiping) {
TabsLayoutItemView item = (TabsLayoutItemView) swipeView;
Tabs.getInstance().selectTab(item.getTabId());
autoHidePanel();
velocityTracker.recycle();
velocityTracker = null;
break;
}
velocityTracker.addMovement(e);
velocityTracker.computeCurrentVelocity(1000, maxFlingVelocity);
float velocityX = Math.abs(velocityTracker.getXVelocity());
float velocityY = Math.abs(velocityTracker.getYVelocity());
boolean dismiss = false;
boolean dismissDirection = false;
int dismissTranslation;
if (isVertical()) {
float deltaX = ViewHelper.getTranslationX(swipeView);
if (Math.abs(deltaX) > listWidth / 2) {
dismiss = true;
dismissDirection = (deltaX > 0);
} else if (minFlingVelocity <= velocityX && velocityX <= maxFlingVelocity
&& velocityY < velocityX) {
dismiss = swiping && (deltaX * velocityTracker.getXVelocity() > 0);
dismissDirection = (velocityTracker.getXVelocity() > 0);
}
dismissTranslation = (dismissDirection ? listWidth : -listWidth);
} else {
float deltaY = ViewHelper.getTranslationY(swipeView);
if (Math.abs(deltaY) > listHeight / 2) {
dismiss = true;
dismissDirection = (deltaY > 0);
} else if (minFlingVelocity <= velocityY && velocityY <= maxFlingVelocity
&& velocityX < velocityY) {
dismiss = swiping && (deltaY * velocityTracker.getYVelocity() > 0);
dismissDirection = (velocityTracker.getYVelocity() > 0);
}
dismissTranslation = (dismissDirection ? listHeight : -listHeight);
}
if (dismiss) {
animateClose(swipeView, dismissTranslation);
} else {
animateCancel(swipeView);
}
velocityTracker.recycle();
velocityTracker = null;
swipeView = null;
swipeStartX = 0;
swipeStartY = 0;
swiping = false;
break;
}
case MotionEvent.ACTION_MOVE: {
if (swipeView == null || velocityTracker == null) {
break;
}
velocityTracker.addMovement(e);
final boolean isVertical = isVertical();
float deltaX = e.getRawX() - swipeStartX;
float deltaY = e.getRawY() - swipeStartY;
float delta = (isVertical ? deltaX : deltaY);
boolean isScrollingX = Math.abs(deltaX) > swipeThreshold;
boolean isScrollingY = Math.abs(deltaY) > swipeThreshold;
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) {
swiping = true;
TabsListLayout.this.requestDisallowInterceptTouchEvent(true);
((TabsLayoutItemView) swipeView).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));
TabsListLayout.this.onTouchEvent(cancelEvent);
cancelEvent.recycle();
}
if (swiping) {
if (isVertical) {
ViewHelper.setTranslationX(swipeView, delta);
} else {
ViewHelper.setTranslationY(swipeView, delta);
}
ViewHelper.setAlpha(swipeView, Math.max(0.1f, Math.min(1f,
1f - 2f * Math.abs(delta) / (isVertical ? listWidth : listHeight))));
return true;
}
break;
}
}
return false;
}
private View findViewAt(float rawX, float rawY) {
Rect rect = new Rect();
int[] listViewCoords = new int[2];
TabsListLayout.this.getLocationOnScreen(listViewCoords);
int x = (int) rawX - listViewCoords[0];
int y = (int) rawY - listViewCoords[1];
for (int i = 0; i < TabsListLayout.this.getChildCount(); i++) {
View child = TabsListLayout.this.getChildAt(i);
child.getHitRect(rect);
if (rect.contains(x, y)) {
return child;
}
}
return null;
}
private void triggerCheckForTap() {
if (pendingCheckForTap == null) {
pendingCheckForTap = new CheckForTap();
}
TabsListLayout.this.postDelayed(pendingCheckForTap, ViewConfiguration.getTapTimeout());
}
private void cancelCheckForTap() {
if (pendingCheckForTap == null) {
return;
}
TabsListLayout.this.removeCallbacks(pendingCheckForTap);
}
private class CheckForTap implements Runnable {
@Override
public void run() {
if (!swiping && swipeView != null && enabled) {
swipeView.setPressed(true);
}
}
}
}
}

View File

@ -5,6 +5,7 @@
package org.mozilla.gecko.tabs;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.AppConstants.Versions;
import org.mozilla.gecko.GeckoApp;
import org.mozilla.gecko.GeckoApplication;
@ -26,6 +27,7 @@ import org.mozilla.gecko.widget.IconTabWidget;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
@ -55,6 +57,7 @@ public class TabsPanel extends LinearLayout
void setTabsPanel(TabsPanel panel);
void show();
void hide();
boolean shouldExpand();
}
public interface CloseAllPanelView extends PanelView {
@ -69,11 +72,20 @@ public class TabsPanel extends LinearLayout
void onTabsLayoutChange(int width, int height);
}
public static View createTabsLayout(final Context context, final AttributeSet attrs) {
if (HardwareUtils.isTablet() || AppConstants.NIGHTLY_BUILD) {
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 TabsLayoutContainer mTabsContainer;
private PanelView mPanel;
private PanelView mPanelNormal;
private PanelView mPanelPrivate;
@ -112,7 +124,7 @@ public class TabsPanel extends LinearLayout
private void initialize() {
mHeader = (RelativeLayout) findViewById(R.id.tabs_panel_header);
mTabsContainer = (FrameLayout) findViewById(R.id.tabs_container);
mTabsContainer = (TabsLayoutContainer) findViewById(R.id.tabs_container);
mPanelNormal = (PanelView) findViewById(R.id.normal_tabs);
mPanelNormal.setTabsPanel(this);
@ -234,14 +246,40 @@ public class TabsPanel extends LinearLayout
return mActivity.onOptionsItemSelected(item);
}
private static int getTabContainerHeight(View tabsContainer) {
private static int getTabContainerHeight(TabsLayoutContainer tabsContainer) {
Resources resources = tabsContainer.getContext().getResources();
int screenHeight = resources.getDisplayMetrics().heightPixels;
int actionBarHeight = resources.getDimensionPixelSize(R.dimen.browser_toolbar_height);
return screenHeight - actionBarHeight;
if (HardwareUtils.isTablet() || AppConstants.NIGHTLY_BUILD) {
return screenHeight - actionBarHeight;
}
PanelView panelView = tabsContainer.getCurrentPanelView();
if (panelView != null && !panelView.shouldExpand()) {
// This allows us to accommodate varying height tab previews across different devices.
// We should be able to remove once we remove the list view and remove the chrome again
return resources.getDimensionPixelSize(R.dimen.tab_thumbnail_height
+ resources.getDimensionPixelSize(R.dimen.tab_title_height)
+ 2 * (resources.getDimensionPixelSize(R.dimen.tab_highlight_stroke_width)
+ resources.getDimensionPixelSize(R.dimen.tab_vertical_padding)
+ resources.getDimensionPixelSize(R.dimen.tab_thumbnail_padding)
+ resources.getDimensionPixelSize(R.dimen.tab_thumbnail_margin)));
}
Rect windowRect = new Rect();
tabsContainer.getWindowVisibleDisplayFrame(windowRect);
int windowHeight = windowRect.bottom - windowRect.top;
// The web content area should have at least 1.5x the height of the action bar.
// The tabs panel shouldn't take less than 50% of the screen height and can take
// up to 80% of the window height.
return (int) Math.max(screenHeight * 0.5f,
Math.min(windowHeight - 2.5f * actionBarHeight, windowHeight * 0.8f) - actionBarHeight);
}
@Override
@ -279,6 +317,35 @@ public class TabsPanel extends LinearLayout
onLightweightThemeChanged();
}
static class TabsLayoutContainer extends FrameLayout {
public TabsLayoutContainer(Context context, AttributeSet attrs) {
super(context, attrs);
}
public PanelView getCurrentPanelView() {
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (!(child instanceof PanelView)) {
continue;
}
if (child.getVisibility() == View.VISIBLE) {
return (PanelView) child;
}
}
return null;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int heightSpec = MeasureSpec.makeMeasureSpec(getTabContainerHeight(TabsLayoutContainer.this), MeasureSpec.EXACTLY);
super.onMeasure(widthMeasureSpec, heightSpec);
}
}
// Tabs Panel Toolbar contains the Buttons
static class TabsPanelToolbar extends LinearLayout
implements LightweightTheme.OnChangeListener {
@ -422,6 +489,10 @@ public class TabsPanel extends LinearLayout
return mVisible;
}
public Panel getCurrentPanel() {
return mCurrentPanel;
}
public void setHWLayerEnabled(boolean enabled) {
if (Versions.preHC) {
return;