Bug 1014987 - Introduce the new tab strip views (r=mcomella)
authorLucas Rocha <lucasr@mozilla.com>
Tue, 09 Sep 2014 13:45:18 +0100
changeset 227947 c8cbe8cb8febfd67104c07c3e5900425edc20ea9
parent 227946 a17eec723de53662955cf717f7dd6e94a6c01fdc
child 227948 0356dcf5da53bee070e0a099ae09c9f46804e062
push id4187
push userbhearsum@mozilla.com
push dateFri, 28 Nov 2014 15:29:12 +0000
treeherdermozilla-beta@f23cc6a30c11 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmcomella
bugs1014987
milestone35.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1014987 - Introduce the new tab strip views (r=mcomella)
mobile/android/base/moz.build
mobile/android/base/resources/color/new_tablet_tab_strip_item_title.xml
mobile/android/base/resources/drawable/new_tablet_tab_strip_divider.xml
mobile/android/base/resources/drawable/new_tablet_tab_strip_item_bg.xml
mobile/android/base/resources/layout/tab_strip_item.xml
mobile/android/base/resources/layout/tab_strip_item_view.xml
mobile/android/base/resources/values/dimens.xml
mobile/android/base/tabs/TabStripAdapter.java
mobile/android/base/tabs/TabStripItemView.java
mobile/android/base/tabs/TabStripView.java
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -392,16 +392,19 @@ gbjar.sources += [
     'tabs/RemoteTabsPanel.java',
     'tabs/RemoteTabsSetupPanel.java',
     'tabs/RemoteTabsVerificationPanel.java',
     'tabs/TabCurve.java',
     'tabs/TabsLayoutAdapter.java',
     'tabs/TabsLayoutItemView.java',
     'tabs/TabsListLayout.java',
     'tabs/TabsPanel.java',
+    'tabs/TabStripAdapter.java',
+    'tabs/TabStripItemView.java',
+    'tabs/TabStripView.java',
     'TabsAccessor.java',
     'Telemetry.java',
     'TelemetryContract.java',
     'TextSelection.java',
     'TextSelectionHandle.java',
     'ThumbnailHelper.java',
     'toolbar/AutocompleteHandler.java',
     'toolbar/BackButton.java',
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/color/new_tablet_tab_strip_item_title.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+	      xmlns:gecko="http://schemas.android.com/apk/res-auto">
+
+    <item android:state_checked="true"
+          gecko:state_private="true"
+          android:color="@color/text_color_primary_inverse"/>
+
+    <item android:state_checked="true"
+          android:color="@color/text_color_primary"/>
+
+    <item android:color="@color/text_color_tertiary_inverse"/>
+
+</selector>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/drawable/new_tablet_tab_strip_divider.xml
@@ -0,0 +1,17 @@
+<?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/. -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:shape="rectangle">
+
+    <gradient
+        android:startColor="#D1D5DA"
+        android:endColor="@android:color/transparent"
+        android:angle="90"/>
+
+    <size android:width="1dp"
+          android:height="32dp"/>
+
+</shape>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/drawable/new_tablet_tab_strip_item_bg.xml
@@ -0,0 +1,52 @@
+<?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/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+	      xmlns:gecko="http://schemas.android.com/apk/res-auto">
+
+    <item android:state_pressed="true"
+          android:state_checked="true"
+          gecko:state_private="true">
+        <shape>
+            <solid android:color="@color/highlight_nav_pb" />
+        </shape>
+    </item>
+
+    <item android:state_checked="true"
+          gecko:state_private="true">
+        <shape>
+            <solid android:color="@color/background_private" />
+        </shape>
+    </item>
+
+    <item android:state_pressed="true"
+          gecko:state_private="true">
+        <shape>
+            <solid android:color="@color/highlight_dark_focused" />
+        </shape>
+    </item>
+
+    <item android:state_pressed="true"
+          android:state_checked="true">
+        <shape>
+            <solid android:color="@color/highlight_nav" />
+        </shape>
+    </item>
+
+    <item android:state_checked="true">
+        <shape>
+            <solid android:color="@color/background_normal" />
+        </shape>
+    </item>
+
+    <item android:state_pressed="true">
+        <shape>
+            <solid android:color="@color/highlight_dark" />
+        </shape>
+    </item>
+
+    <item android:drawable="@android:color/transparent"/>
+
+</selector>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/layout/tab_strip_item.xml
@@ -0,0 +1,12 @@
+<?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.TabStripItemView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="@dimen/new_tablet_tab_strip_item_width"
+    android:layout_height="match_parent"
+    android:background="@drawable/new_tablet_tab_strip_item_bg"
+    android:paddingLeft="28dp"
+    android:paddingRight="15dp"/>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/layout/tab_strip_item_view.xml
@@ -0,0 +1,31 @@
+<?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/. -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <org.mozilla.gecko.widget.ThemedTextView
+        android:id="@+id/title"
+        android:layout_width="0dip"
+        android:layout_height="match_parent"
+        android:layout_weight="1.0"
+        android:layout_marginRight="-5dp"
+        android:gravity="center_vertical"
+        android:textSize="14sp"
+        android:ellipsize="end"
+        android:textColor="@color/new_tablet_tab_strip_item_title"
+        android:maxLines="1"
+        android:duplicateParentState="true"/>
+
+    <org.mozilla.gecko.widget.ThemedImageButton
+        android:id="@+id/close"
+        android:layout_width="40dip"
+        android:layout_height="match_parent"
+        android:background="@android:color/transparent"
+        android:scaleType="center"
+        android:contentDescription="@string/close_tab"
+        android:src="@drawable/tab_close"
+        android:duplicateParentState="true"/>
+
+</merge>
--- a/mobile/android/base/resources/values/dimens.xml
+++ b/mobile/android/base/resources/values/dimens.xml
@@ -10,16 +10,19 @@
 
     <dimen name="browser_toolbar_height">48dp</dimen>
     <dimen name="browser_toolbar_button_padding">12dp</dimen>
     <dimen name="browser_toolbar_icon_width">48dp</dimen>
     <dimen name="browser_toolbar_lock_width">20dp</dimen>
     <dimen name="browser_toolbar_favicon_size">25.33dip</dimen>
     <dimen name="browser_toolbar_shadow_size">2dp</dimen>
 
+    <dimen name="new_tablet_tab_strip_item_width">250dp</dimen>
+    <dimen name="new_tablet_tab_strip_item_margin">-30dp</dimen>
+
     <!-- Dimensions used by Favicons and FaviconView -->
     <dimen name="favicon_size_small">16dp</dimen>
     <dimen name="favicon_size_large">32dp</dimen>
     <dimen name="favicon_bg">32dp</dimen>
     <dimen name="favicon_bg_radius">1dp</dimen>
     <!-- Set the upper limit on the size of favicon that will be processed. Favicons larger than
          this will be downscaled to this value. If you need to use larger Favicons (Due to a UI
          redesign sometime after this is written) you should increase this value to the largest
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/tabs/TabStripAdapter.java
@@ -0,0 +1,96 @@
+/* -*- 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 ? tabs.get(position) : null);
+    }
+
+    @Override
+    public long getItemId(int position) {
+        final Tab tab = getItem(position);
+        return (tab != null ? tab.getId() : -1);
+    }
+
+    @Override
+    public boolean hasStableIds() {
+        return true;
+    }
+
+    @Override
+    public View getView(int position, View convertView, ViewGroup parent) {
+        final TabStripItemView item;
+        if (convertView == null) {
+            item = (TabStripItemView)
+                    LayoutInflater.from(context).inflate(R.layout.tab_strip_item, parent, false);
+        } else {
+            item = (TabStripItemView) convertView;
+        }
+
+        final Tab tab = tabs.get(position);
+        item.updateFromTab(tab);
+
+        return item;
+    }
+
+    @Override
+    public int getCount() {
+        return (tabs != null ? tabs.size() : 0);
+    }
+
+    int getPositionForTab(Tab tab) {
+        if (tabs == null || tab == null) {
+            return -1;
+        }
+
+        return tabs.indexOf(tab);
+    }
+
+    void removeTab(Tab tab) {
+        if (tabs == null) {
+            return;
+        }
+
+        tabs.remove(tab);
+        notifyDataSetChanged();
+    }
+
+    void refresh(List<Tab> tabs) {
+        // The list of tabs is guaranteed to be non-null.
+        // See TabStripView.refreshTabs().
+        this.tabs = tabs;
+        notifyDataSetChanged();
+    }
+
+    void clear() {
+        tabs = null;
+        notifyDataSetInvalidated();
+    }
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/tabs/TabStripItemView.java
@@ -0,0 +1,193 @@
+/* -*- 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.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PorterDuff.Mode;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Region;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.Checkable;
+
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.Tab;
+import org.mozilla.gecko.Tabs;
+import org.mozilla.gecko.widget.ThemedImageButton;
+import org.mozilla.gecko.widget.ThemedLinearLayout;
+import org.mozilla.gecko.widget.ThemedTextView;
+
+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 ThemedTextView titleView;
+    private final ThemedImageButton closeView;
+
+    private final Paint tabPaint;
+    private final Path tabShape;
+    private final Region tabRegion;
+    private final Region tabClipRegion;
+
+    public TabStripItemView(Context context) {
+        this(context, null);
+    }
+
+    public TabStripItemView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        setOrientation(HORIZONTAL);
+
+        tabShape = new Path();
+        tabRegion = new Region();
+        tabClipRegion = new Region();
+
+        tabPaint = new Paint();
+        tabPaint.setAntiAlias(true);
+        tabPaint.setColor(0xFFFF0000);
+        tabPaint.setStrokeWidth(0.0f);
+        tabPaint.setXfermode(new PorterDuffXfermode(Mode.DST_IN));
+
+        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);
+            }
+        });
+
+        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);
+
+        tabShape.reset();
+
+        final int curveWidth = TabCurve.getWidthForHeight(height);
+
+        tabShape.moveTo(0, height);
+        TabCurve.drawFromBottom(tabShape, 0, height, TabCurve.Direction.RIGHT);
+        tabShape.lineTo(width - curveWidth, 0);
+
+        TabCurve.drawFromTop(tabShape, width - curveWidth, height, TabCurve.Direction.RIGHT);
+        tabShape.lineTo(0, height);
+
+        tabClipRegion.set(0, 0, width, height);
+        tabRegion.setPath(tabShape, tabClipRegion);
+    }
+
+    @Override
+    public void draw(Canvas canvas) {
+        final int saveCount = canvas.saveLayer(0, 0,
+                                               getWidth(), getHeight(), null,
+                                               Canvas.MATRIX_SAVE_FLAG |
+                                               Canvas.CLIP_SAVE_FLAG |
+                                               Canvas.HAS_ALPHA_LAYER_SAVE_FLAG |
+                                               Canvas.FULL_COLOR_LAYER_SAVE_FLAG |
+                                               Canvas.CLIP_TO_LAYER_SAVE_FLAG);
+
+        super.draw(canvas);
+
+        canvas.drawPath(tabShape, tabPaint);
+
+        canvas.restoreToCount(saveCount);
+    }
+
+    @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 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();
+        parent.invalidate();
+    }
+
+    void updateFromTab(Tab tab) {
+        if (tab == null) {
+            return;
+        }
+
+        id = tab.getId();
+        titleView.setText(tab.getDisplayTitle());
+        setPrivateMode(tab.isPrivate());
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/tabs/TabStripView.java
@@ -0,0 +1,208 @@
+/* -*- 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.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+
+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 final TabStripAdapter adapter;
+    private final TabsListener tabsListener;
+
+    private final Drawable divider;
+
+    private boolean isPrivate;
+
+    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.new_tablet_tab_strip_divider);
+
+        final int itemMargin =
+                resources.getDimensionPixelSize(R.dimen.new_tablet_tab_strip_item_margin);
+        setItemMargin(itemMargin);
+
+        tabsListener = new TabsListener();
+
+        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() {
+        final int selected = getPositionForSelectedTab();
+        if (selected != -1) {
+            updateSelectedStyle(selected);
+        }
+    }
+
+    private int getCheckedIndex(int childCount) {
+        final int checkedIndex = getCheckedItemPosition() - getFirstVisiblePosition();
+        if (checkedIndex < 0 || checkedIndex > childCount - 1) {
+            return INVALID_POSITION;
+        }
+
+        return checkedIndex;
+    }
+
+    private 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();
+    }
+
+    private void removeTab(Tab tab) {
+        adapter.removeTab(tab);
+        updateSelectedPosition();
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+
+        Tabs.registerOnTabsChangedListener(tabsListener);
+        refreshTabs();
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+
+        Tabs.unregisterOnTabsChangedListener(tabsListener);
+        adapter.clear();
+    }
+
+    @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;
+        }
+    }
+
+    @Override
+    public void draw(Canvas canvas) {
+        super.draw(canvas);
+
+        final int bottom = getHeight() - getPaddingBottom();
+        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 class TabsListener implements Tabs.OnTabsChangedListener {
+        @Override
+        public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) {
+            switch (msg) {
+                case RESTORED:
+                case ADDED:
+                    // Refresh the list to make sure the new tab is
+                    // added in the right position.
+                    refreshTabs();
+                    break;
+
+                case CLOSED:
+                    removeTab(tab);
+                    break;
+
+                case SELECTED:
+                    // Update the selected position, then fall through...
+                    if (tab.isPrivate() != isPrivate) {
+                        isPrivate = tab.isPrivate();
+                        refreshTabs();
+                    } else {
+                        updateSelectedPosition();
+                    }
+                case UNSELECTED:
+                    // We just need to update the style for the unselected tab...
+                case TITLE:
+                case RECORDING_CHANGE:
+                    final TabStripItemView item = (TabStripItemView) getViewForTab(tab);
+                    if (item != null) {
+                        item.updateFromTab(tab);
+                    }
+                    break;
+            }
+        }
+    }
+}
\ No newline at end of file