Bug 863828 - New tab increment animation. r=lucasr
authorWes Johnston <wjohnston@mozilla.com>
Fri, 26 Apr 2013 16:17:34 -0700
changeset 141141 2fbb0094f6f4b69705bb29b2289bcc92ca50459d
parent 141140 576ff91b64d7dc816b7fccce6b1686fad09b402f
child 141142 e116c3a3476406f91bff7b62c26a05e6829a7636
push id2579
push userakeybl@mozilla.com
push dateMon, 24 Jun 2013 18:52:47 +0000
treeherdermozilla-beta@b69b7de8a05a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerslucasr
bugs863828
milestone23.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 863828 - New tab increment animation. r=lucasr
mobile/android/base/BrowserToolbar.java
mobile/android/base/Makefile.in
mobile/android/base/Rotate3DAnimation.java
mobile/android/base/TabCounter.java
mobile/android/base/resources/drawable-hdpi/tabs_count.png
mobile/android/base/resources/drawable-hdpi/tabs_count_foreground.png
mobile/android/base/resources/drawable-mdpi/tabs_count_foreground.png
mobile/android/base/resources/drawable-xhdpi/tabs_count.png
mobile/android/base/resources/drawable-xhdpi/tabs_count_foreground.png
mobile/android/base/resources/layout-large-v11/browser_toolbar.xml
mobile/android/base/resources/layout/browser_toolbar.xml
mobile/android/base/resources/layout/tabs_counter.xml
mobile/android/base/resources/values/styles.xml
mobile/android/base/tests/BaseTest.java.in
mobile/android/base/tests/testNewTab.java.in
--- a/mobile/android/base/BrowserToolbar.java
+++ b/mobile/android/base/BrowserToolbar.java
@@ -28,42 +28,40 @@ import android.os.Handler;
 import android.os.SystemClock;
 import android.text.style.ForegroundColorSpan;
 import android.text.Spannable;
 import android.text.SpannableStringBuilder;
 import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.ContextMenu;
-import android.view.LayoutInflater;
 import android.view.MenuInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewGroup.MarginLayoutParams;
 import android.view.Window;
 import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.animation.Animation;
 import android.view.animation.AlphaAnimation;
-import android.view.animation.Animation;
 import android.view.animation.TranslateAnimation;
 import android.view.inputmethod.InputMethodManager;
 import android.widget.Button;
 import android.widget.ImageButton;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.PopupWindow;
 import android.widget.RelativeLayout;
 import android.widget.RelativeLayout.LayoutParams;
 import android.widget.ViewSwitcher;
 
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 
-public class BrowserToolbar implements ViewSwitcher.ViewFactory,
-                                       Tabs.OnTabsChangedListener,
+public class BrowserToolbar implements Tabs.OnTabsChangedListener,
                                        GeckoMenu.ActionItemBarPresenter,
                                        Animation.AnimationListener,
                                        SharedPreferences.OnSharedPreferenceChangeListener {
     private static final String LOGTAG = "GeckoToolbar";
     public static final String PREFS_NAME = "BrowserToolbar";
     public static final String PREFS_SHOW_URL = "ShowUrl";
     private GeckoRelativeLayout mLayout;
     private LayoutParams mAwesomeBarParams;
@@ -79,50 +77,42 @@ public class BrowserToolbar implements V
     private int mTabsPaneWidth;
     private ImageButton mBack;
     private ImageButton mForward;
     public ImageButton mFavicon;
     public ImageButton mStop;
     public ImageButton mSiteSecurity;
     public ImageButton mReader;
     private AnimationDrawable mProgressSpinner;
-    private GeckoTextSwitcher mTabsCount;
+    private TabCounter mTabsCounter;
     private ImageView mShadow;
     private GeckoImageButton mMenu;
     private LinearLayout mActionItemBar;
     private MenuPopup mMenuPopup;
     private List<View> mFocusOrder;
 
     final private BrowserApp mActivity;
-    private LayoutInflater mInflater;
     private Handler mHandler;
     private boolean mHasSoftMenuButton;
 
     private boolean mShowSiteSecurity;
     private boolean mShowReader;
 
     private static List<View> sActionItems;
 
     private boolean mAnimatingEntry;
 
-    private int mDuration;
-    private TranslateAnimation mSlideUpIn;
-    private TranslateAnimation mSlideUpOut;
-    private TranslateAnimation mSlideDownIn;
-    private TranslateAnimation mSlideDownOut;
-
     private AlphaAnimation mLockFadeIn;
     private TranslateAnimation mTitleSlideLeft;
     private TranslateAnimation mTitleSlideRight;
 
     private int mAddressBarViewOffset;
     private int mDefaultForwardMargin;
     private PropertyAnimator mForwardAnim = null;
 
-    private int mCount;
     private int mFaviconSize;
 
     private PropertyAnimator mVisibilityAnimator;
 
     private static final int TABS_CONTRACTED = 1;
     private static final int TABS_EXPANDED = 2;
 
     private static final int FORWARD_ANIMATION_DURATION = 450;
@@ -130,17 +120,16 @@ public class BrowserToolbar implements V
     private final ForegroundColorSpan mDomainColor;
     private final ForegroundColorSpan mPrivateDomainColor;
 
     private boolean mShowUrl;
 
     public BrowserToolbar(BrowserApp activity) {
         // BrowserToolbar is attached to BrowserApp only.
         mActivity = activity;
-        mInflater = LayoutInflater.from(activity);
 
         sActionItems = new ArrayList<View>();
         Tabs.registerOnTabsChangedListener(this);
         mAnimateSiteSecurity = true;
 
         mAnimatingEntry = false;
         mShowUrl = false;
 
@@ -246,33 +235,17 @@ public class BrowserToolbar implements V
         mTabs.setOnClickListener(new Button.OnClickListener() {
             @Override
             public void onClick(View v) {
                 toggleTabs();
             }
         });
         mTabs.setImageLevel(0);
 
-        mTabsCount = (GeckoTextSwitcher) mLayout.findViewById(R.id.tabs_count);
-        mTabsCount.removeAllViews();
-        mTabsCount.setFactory(this);
-        mTabsCount.setText("");
-        mCount = 0;
-        if (Build.VERSION.SDK_INT >= 16) {
-            // This adds the TextSwitcher to the a11y node tree, where we in turn
-            // could make it return an empty info node. If we don't do this the
-            // TextSwitcher's child TextViews get picked up, and we don't want
-            // that since the tabs ImageButton is already properly labeled for
-            // accessibility.
-            mTabsCount.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
-            mTabsCount.setAccessibilityDelegate(new View.AccessibilityDelegate() {
-                    @Override
-                    public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {}
-                });
-        }
+        mTabsCounter = (TabCounter) mLayout.findViewById(R.id.tabs_counter);
 
         mBack = (ImageButton) mLayout.findViewById(R.id.back);
         mBack.setOnClickListener(new Button.OnClickListener() {
             @Override
             public void onClick(View view) {
                 Tabs.getInstance().getSelectedTab().doBack();
             }
         });
@@ -361,26 +334,16 @@ public class BrowserToolbar implements V
         mShadow = (ImageView) mLayout.findViewById(R.id.shadow);
         mShadow.setOnClickListener(new Button.OnClickListener() {
             @Override
             public void onClick(View v) {
             }
         });
 
         mHandler = new Handler();
-        mSlideUpIn = new TranslateAnimation(0, 0, 40, 0);
-        mSlideUpOut = new TranslateAnimation(0, 0, 0, -40);
-        mSlideDownIn = new TranslateAnimation(0, 0, -40, 0);
-        mSlideDownOut = new TranslateAnimation(0, 0, 0, 40);
-
-        mDuration = 750;
-        mSlideUpIn.setDuration(mDuration);
-        mSlideUpOut.setDuration(mDuration);
-        mSlideDownIn.setDuration(mDuration);
-        mSlideDownOut.setDuration(mDuration);
 
         float slideWidth = mActivity.getResources().getDimension(R.dimen.browser_toolbar_lock_width);
 
         LinearLayout.LayoutParams siteSecParams = (LinearLayout.LayoutParams) mSiteSecurity.getLayoutParams();
         final float scale = mActivity.getResources().getDisplayMetrics().density;
         slideWidth += (siteSecParams.leftMargin + siteSecParams.rightMargin) * scale + 0.5f;
 
         mLockFadeIn = new AlphaAnimation(0.0f, 1.0f);
@@ -574,22 +537,16 @@ public class BrowserToolbar implements V
 
     @Override
     public void onAnimationEnd(Animation animation) {
         if (animation.equals(mTitleSlideRight)) {
             mSiteSecurity.startAnimation(mLockFadeIn);
         }
     }
 
-    @Override
-    public View makeView() {
-        // This returns a TextView for the TextSwitcher.
-        return mInflater.inflate(R.layout.tabs_counter, null);
-    }
-
     private int getAwesomeBarAnimTranslation() {
         return mLayout.getWidth() - mAwesomeBarEntry.getRight();
     }
 
     public void fromAwesomeBarSearch(String url) {
         // Update the title with the url that was just entered. Don't update the title if
         // the AwesomeBar activity was cancelled, or if the user entered an empty string.
         if (url != null && url.length() > 0) {
@@ -612,17 +569,17 @@ public class BrowserToolbar implements V
             mLayout.setSelected(true);
 
             final int translation = getAwesomeBarAnimTranslation();
 
             proxy = AnimatorProxy.create(mAwesomeBarRightEdge);
             proxy.setTranslationX(translation);
             proxy = AnimatorProxy.create(mTabs);
             proxy.setTranslationX(translation);
-            proxy = AnimatorProxy.create(mTabsCount);
+            proxy = AnimatorProxy.create(mTabsCounter);
             proxy.setTranslationX(translation);
             proxy = AnimatorProxy.create(mActionItemBar);
             proxy.setTranslationX(translation);
 
             if (mHasSoftMenuButton) {
                 proxy = AnimatorProxy.create(mMenu);
                 proxy.setTranslationX(translation);
             }
@@ -647,17 +604,17 @@ public class BrowserToolbar implements V
 
         // Shrink the awesome entry back to its original size
         contentAnimator.attach(mAwesomeBarRightEdge,
                                PropertyAnimator.Property.TRANSLATION_X,
                                0);
         contentAnimator.attach(mTabs,
                                PropertyAnimator.Property.TRANSLATION_X,
                                0);
-        contentAnimator.attach(mTabsCount,
+        contentAnimator.attach(mTabsCounter,
                                PropertyAnimator.Property.TRANSLATION_X,
                                0);
         contentAnimator.attach(mActionItemBar,
                                PropertyAnimator.Property.TRANSLATION_X,
                                0);
 
         if (mHasSoftMenuButton)
             contentAnimator.attach(mMenu,
@@ -748,17 +705,17 @@ public class BrowserToolbar implements V
 
         // Slide the right side elements of the toolbar
         contentAnimator.attach(mAwesomeBarRightEdge,
                                PropertyAnimator.Property.TRANSLATION_X,
                                translation);
         contentAnimator.attach(mTabs,
                                PropertyAnimator.Property.TRANSLATION_X,
                                translation);
-        contentAnimator.attach(mTabsCount,
+        contentAnimator.attach(mTabsCounter,
                                PropertyAnimator.Property.TRANSLATION_X,
                                translation);
         contentAnimator.attach(mActionItemBar,
                                PropertyAnimator.Property.TRANSLATION_X,
                                translation);
 
         if (mHasSoftMenuButton)
             contentAnimator.attach(mMenu,
@@ -810,53 +767,42 @@ public class BrowserToolbar implements V
 
     public void updateTabCountAndAnimate(int count) {
         // Don't animate if the toolbar is hidden.
         if (!isVisible()) {
             updateTabCount(count);
             return;
         }
 
-        if (mCount > count) {
-            mTabsCount.setInAnimation(mSlideDownIn);
-            mTabsCount.setOutAnimation(mSlideDownOut);
-        } else if (mCount < count) {
-            mTabsCount.setInAnimation(mSlideUpIn);
-            mTabsCount.setOutAnimation(mSlideUpOut);
-        } else {
-            return;
-        }
+        mTabsCounter.setCount(count);
 
-        mTabsCount.setText(String.valueOf(count));
         mTabs.setContentDescription((count > 1) ?
                                     mActivity.getString(R.string.num_tabs, count) :
                                     mActivity.getString(R.string.one_tab));
-        mCount = count;
     }
 
     public void updateTabCount(int count) {
-        mTabsCount.setCurrentText(String.valueOf(count));
+        mTabsCounter.setCurrentText(String.valueOf(count));
         mTabs.setContentDescription((count > 1) ?
                                     mActivity.getString(R.string.num_tabs, count) :
                                     mActivity.getString(R.string.one_tab));
-        mCount = count;
         updateTabs(mActivity.areTabsShown());
     }
 
     public void prepareTabsAnimation(PropertyAnimator animator, int width) {
         animator.attach(mAwesomeBarEntry,
                         PropertyAnimator.Property.TRANSLATION_X,
                         width);
         animator.attach(mAddressBarBg,
                         PropertyAnimator.Property.TRANSLATION_X,
                         width);
         animator.attach(mTabs,
                         PropertyAnimator.Property.TRANSLATION_X,
                         width);
-        animator.attach(mTabsCount,
+        animator.attach(mTabsCounter,
                         PropertyAnimator.Property.TRANSLATION_X,
                         width);
         animator.attach(mBack,
                         PropertyAnimator.Property.TRANSLATION_X,
                         width);
         animator.attach(mForward,
                         PropertyAnimator.Property.TRANSLATION_X,
                         width);
@@ -882,17 +828,17 @@ public class BrowserToolbar implements V
             setPageActionVisibility(mStop.getVisibility() == View.VISIBLE);
     }
 
     public void adjustTabsAnimation(boolean reset) {
         int width = reset ? 0 : mTabsPaneWidth;
         mAwesomeBarEntry.setTranslationX(width);
         mAddressBarBg.setTranslationX(width);
         mTabs.setTranslationX(width);
-        mTabsCount.setTranslationX(width);
+        mTabsCounter.setTranslationX(width);
         mBack.setTranslationX(width);
         mForward.setTranslationX(width);
         mTitle.setTranslationX(width);
         mFavicon.setTranslationX(width);
         mSiteSecurity.setTranslationX(width);
 
         ((ViewGroup.MarginLayoutParams) mLayout.getLayoutParams()).leftMargin = reset ? mTabsPaneWidth : 0;
     }
--- a/mobile/android/base/Makefile.in
+++ b/mobile/android/base/Makefile.in
@@ -138,25 +138,27 @@ FENNEC_JAVA_FILES = \
   Restarter.java \
   sqlite/ByteBufferInputStream.java \
   sqlite/MatrixBlobCursor.java \
   sqlite/SQLiteBridge.java \
   sqlite/SQLiteBridgeException.java \
   ReaderModeUtils.java \
   RemoteTabs.java \
   RobocopAPI.java \
+  Rotate3DAnimation.java \
   ServiceNotificationClient.java \
   SessionParser.java \
   SetupScreen.java \
   ShapedButton.java \
   SiteIdentityPopup.java \
   SmsManager.java \
   SuggestClient.java \
   SurfaceBits.java \
   Tab.java \
+  TabCounter.java \
   Tabs.java \
   TabsPanel.java \
   TabsTray.java \
   TabsAccessor.java \
   TailTouchDelegate.java \
   Telemetry.java \
   TextSelection.java \
   TextSelectionHandle.java \
@@ -594,16 +596,17 @@ RES_DRAWABLE_MDPI = \
   res/drawable-mdpi/spinner_focused.9.png \
   res/drawable-mdpi/spinner_pressed.9.png \
   res/drawable-mdpi/tab_new.png \
   res/drawable-mdpi/tab_new_pb.png \
   res/drawable-mdpi/tab_close.png \
   res/drawable-mdpi/tab_thumbnail_default.png \
   res/drawable-mdpi/tab_thumbnail_shadow.png \
   res/drawable-mdpi/tabs_count.png \
+  res/drawable-mdpi/tabs_count_foreground.png \
   res/drawable-mdpi/address_bar_url_default.9.png \
   res/drawable-mdpi/address_bar_url_default_pb.9.png \
   res/drawable-mdpi/address_bar_url_pressed.9.png \
   res/drawable-mdpi/address_bar_url_pressed_pb.9.png \
   res/drawable-mdpi/doorhanger_popup_bg.9.png \
   res/drawable-mdpi/find_close.png \
   res/drawable-mdpi/find_next.png \
   res/drawable-mdpi/find_prev.png \
@@ -692,16 +695,17 @@ RES_DRAWABLE_HDPI = \
   res/drawable-hdpi/spinner_focused.9.png \
   res/drawable-hdpi/spinner_pressed.9.png \
   res/drawable-hdpi/tab_new.png \
   res/drawable-hdpi/tab_new_pb.png \
   res/drawable-hdpi/tab_close.png \
   res/drawable-hdpi/tab_thumbnail_default.png \
   res/drawable-hdpi/tab_thumbnail_shadow.png \
   res/drawable-hdpi/tabs_count.png \
+  res/drawable-hdpi/tabs_count_foreground.png \
   res/drawable-hdpi/address_bar_url_default.9.png \
   res/drawable-hdpi/address_bar_url_default_pb.9.png \
   res/drawable-hdpi/address_bar_url_pressed.9.png \
   res/drawable-hdpi/address_bar_url_pressed_pb.9.png \
   res/drawable-hdpi/doorhanger_popup_bg.9.png \
   res/drawable-hdpi/find_close.png \
   res/drawable-hdpi/find_next.png \
   res/drawable-hdpi/find_prev.png \
@@ -779,16 +783,17 @@ RES_DRAWABLE_XHDPI = \
   res/drawable-xhdpi/spinner_focused.9.png \
   res/drawable-xhdpi/spinner_pressed.9.png \
   res/drawable-xhdpi/tab_new.png \
   res/drawable-xhdpi/tab_new_pb.png \
   res/drawable-xhdpi/tab_close.png \
   res/drawable-xhdpi/tab_thumbnail_default.png \
   res/drawable-xhdpi/tab_thumbnail_shadow.png \
   res/drawable-xhdpi/tabs_count.png \
+  res/drawable-xhdpi/tabs_count_foreground.png \
   res/drawable-xhdpi/doorhanger_popup_bg.9.png \
   res/drawable-xhdpi/find_close.png \
   res/drawable-xhdpi/find_next.png \
   res/drawable-xhdpi/find_prev.png \
   res/drawable-xhdpi/urlbar_stop.png \
   res/drawable-xhdpi/reader.png \
   res/drawable-xhdpi/reader_active.png \
   res/drawable-xhdpi/reading_list.png \
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/Rotate3DAnimation.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.mozilla.gecko;
+
+import android.view.animation.Animation;
+import android.view.animation.Transformation;
+
+import android.graphics.Camera;
+import android.graphics.Matrix;
+
+/**
+ * An animation that rotates the view on the Y axis between two specified angles.
+ * This animation also adds a translation on the Z axis (depth) to improve the effect.
+ */
+public class Rotate3DAnimation extends Animation {
+    private final float mFromDegrees;
+    private final float mToDegrees;
+
+    private final float mCenterX;
+    private final float mCenterY;
+
+    private final float mDepthZ;
+    private final boolean mReverse;
+    private Camera mCamera;
+
+    private int mWidth = 1;
+    private int mHeight = 1;
+
+    /**
+     * Creates a new 3D rotation on the Y axis. The rotation is defined by its
+     * start angle and its end angle. Both angles are in degrees. The rotation
+     * is performed around a center point on the 2D space, definied by a pair
+     * of X and Y coordinates, called centerX and centerY. When the animation
+     * starts, a translation on the Z axis (depth) is performed. The length
+     * of the translation can be specified, as well as whether the translation
+     * should be reversed in time.
+     *
+     * @param fromDegrees the start angle of the 3D rotation
+     * @param toDegrees the end angle of the 3D rotation
+     * @param centerX the X center of the 3D rotation
+     * @param centerY the Y center of the 3D rotation
+     * @param reverse true if the translation should be reversed, false otherwise
+     */
+    public Rotate3DAnimation(float fromDegrees, float toDegrees,
+            float centerX, float centerY, float depthZ, boolean reverse) {
+        mFromDegrees = fromDegrees;
+        mToDegrees = toDegrees;
+        mCenterX = centerX;
+        mCenterY = centerY;
+        mDepthZ = depthZ;
+        mReverse = reverse;
+    }
+
+   @Override
+    public void initialize(int width, int height, int parentWidth, int parentHeight) {
+        super.initialize(width, height, parentWidth, parentHeight);
+        mCamera = new Camera();
+        mWidth = width;
+        mHeight = height;
+    }
+
+    @Override
+    protected void applyTransformation(float interpolatedTime, Transformation t) {
+        final float fromDegrees = mFromDegrees;
+        float degrees = fromDegrees + ((mToDegrees - fromDegrees) * interpolatedTime);
+
+        final Camera camera = mCamera;
+        final Matrix matrix = t.getMatrix();
+
+        camera.save();
+        if (mReverse) {
+            camera.translate(0.0f, 0.0f, mDepthZ * interpolatedTime);
+        } else {
+            camera.translate(0.0f, 0.0f, mDepthZ * (1.0f - interpolatedTime));
+        }
+        camera.rotateX(degrees);
+        camera.getMatrix(matrix);
+        camera.restore();
+
+        matrix.preTranslate(-mCenterX * mWidth, -mCenterY * mHeight);
+        matrix.postTranslate(mCenterX * mWidth, mCenterY * mHeight);
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/TabCounter.java
@@ -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;
+
+import android.content.Context;
+import android.os.Build;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.animation.Animation;
+import android.view.animation.AnimationSet;
+import android.view.animation.AlphaAnimation;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.util.AttributeSet;
+import android.widget.ViewSwitcher;
+
+public class TabCounter extends GeckoTextSwitcher
+                        implements ViewSwitcher.ViewFactory {
+
+    private static final float CENTER_X = 0.5f;
+    private static final float CENTER_Y = 1.25f;
+    private static final int DURATION = 500;
+    private static final float Z_DISTANCE = 200;
+
+    private final AnimationSet mFlipInForward;
+    private final AnimationSet mFlipInBackward;
+    private final AnimationSet mFlipOutForward;
+    private final AnimationSet mFlipOutBackward;
+    private final LayoutInflater mInflater;
+
+    private int mCount = 0;
+
+    private enum FadeMode {
+        FADE_IN,
+        FADE_OUT
+    }
+
+    public TabCounter(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mInflater = LayoutInflater.from(context);
+
+        mFlipInForward = createAnimation(-90, 0, FadeMode.FADE_IN, -1 * Z_DISTANCE, false);
+        mFlipInBackward = createAnimation(90, 0, FadeMode.FADE_IN, Z_DISTANCE, false);
+        mFlipOutForward = createAnimation(0, -90, FadeMode.FADE_OUT, -1 * Z_DISTANCE, true);
+        mFlipOutBackward = createAnimation(0, 90, FadeMode.FADE_OUT, Z_DISTANCE, true);
+
+        removeAllViews();
+        setFactory(this);
+        setCount(0);
+
+        if (Build.VERSION.SDK_INT >= 16) {
+            // This adds the TextSwitcher to the a11y node tree, where we in turn
+            // could make it return an empty info node. If we don't do this the
+            // TextSwitcher's child TextViews get picked up, and we don't want
+            // that since the tabs ImageButton is already properly labeled for
+            // accessibility.
+            setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
+            setAccessibilityDelegate(new View.AccessibilityDelegate() {
+                    @Override
+                    public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {}
+                });
+        }
+    }
+
+    public void setCount(int count) {
+        if (mCount > count) {
+            setInAnimation(mFlipInBackward);
+            setOutAnimation(mFlipOutForward);
+        } else if (mCount < count) {
+            setInAnimation(mFlipInForward);
+            setOutAnimation(mFlipOutBackward);
+        } else {
+            return;
+        }
+
+        setText(String.valueOf(count));
+        mCount = count;
+    }
+
+    private AnimationSet createAnimation(float startAngle, float endAngle,
+                                         FadeMode fadeMode,
+                                         float zEnd, boolean reverse) {
+        final Context context = getContext();
+        AnimationSet set = new AnimationSet(context, null);
+        set.addAnimation(new Rotate3DAnimation(startAngle, endAngle, CENTER_X, CENTER_Y, zEnd, reverse));
+        set.addAnimation(fadeMode == FadeMode.FADE_IN ? new AlphaAnimation(0.0f, 1.0f) :
+                                                        new AlphaAnimation(1.0f, 0.0f));
+        set.setDuration(DURATION);
+        set.setInterpolator(context, android.R.anim.accelerate_interpolator);
+        return set;
+    }
+
+    @Override
+    public View makeView() {
+        return mInflater.inflate(R.layout.tabs_counter, null);
+    }
+
+}
index c81712cec96ae28ce1eeea8e240ebf5aa4222e2f..f81e047e34ae3d97d477ff030a106e879b60cce0
GIT binary patch
literal 288
zc%17D@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$NbB7>k44ofy`glX(f`uqAoByD<C*
z!3BGlPX>x`7I;J!Gca%qgD@k*tT_@uLG}_)Usv|KEE3|{j8g6_uYp1fJY5_^A`ZX3
zt|-)OAkg~IUN(kjom9K-^#=|yi_%v-<*?9eIQ~VfbjFfpN+kla3sTd6h2<JeJ~RDh
zZ-jXAk)SySV$U3I9-7CVwScMUK<<wJj9(nSzhwM=%~>v^;qy8E{Y>9YRaftCVAQxa
z?aibo<_QdkCi>1*|9RffdqIipm$$4!9tw>tLLLk3S$wZKEZ%CkqE_+rVh6v)TO61;
cRW8`f<FpsM-f9%X19U5cr>mdKI;Vst07KPfl>h($
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..8b20b5eb3cd56eb7d6c135522cc08c1f37b1bb42
GIT binary patch
literal 325
zc%17D@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$NbB7>k44ofy`glX(f`uqAoByD<C*
z!3BGlPX>x`7I;J!Gca%qgD@k*tT_@uLG}_)Usv|KEE3|{s%kC|tbjt2C9V-A&iT2y
zsd*&~&PAz-C8;S2<(VZJ3hti10pX2&;y^_UJY5_^A`ZX3zL)oqfk4~C>tR+>yUs95
zHk(GYaT;}A_jtBI$@i8_Z}*gmM>W+y#%7E6|M#0ed8Xh>qk~=wjV!;c1Ii2T2}{&%
zVPF<ZVB*^Fj{WjMKU=G}&!j8f2L61q_-&*67VDC3wq0(_sb3WvS%f@*y0{o_$?#rd
zijr}hFLvP#$E;|FrEEq06XK)lWErMQop^6rw#<0HW7W6BSKOz4m1f))-Mt*>Rt8U3
KKbLh*2~7ZtJ#Od#
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..b3c15996628758c4f490087502864e47b3a43398
GIT binary patch
literal 290
zc%17D@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjjKx9jP7LeL$-D$|*pj^6T^Rm@
z;DWu&Cj&(|3p^r=85p>QL70(Y)*O%l>?NMQuIzVNB*b-8<e1#DfkKict`Q~9`MJ5N
zc_j?aMX8A;sVNHOnI#zt?w-B@;f;LaKt;8lE{-7{$KOsh<a2NkaJ}Ev<d)CBFmPA<
zYYYBz$445omPYl?O6~1<ldt~yzKXr^;x$`6w45UTvpn%<no@G&qvKk}FRu2?JPzDz
z(j{L=GcR{wy>dxW{8EFCP80*n0e6PY?duB}edHQ0K8-1FVDeHZG>L1ddtdwfVMqHO
Z#{4!>L2LO#4nT)6c)I$ztaD0e0svR>U3LHf
index 763d0eec4dd4011a05c0a7337d3f491738263c77..74eef827b1a74854637437ff9e34ef63efb063b5
GIT binary patch
literal 330
zc%17D@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTC#^NA%Cx&(BWL^R}Y)RhkE)4%c
zaKYZ?lYt_f1s;*b3=G`DAk4@xYmNj^kiEpy*OmP)i-fo?r_(X*c%aZJPZ!6KjC*fy
z9?WEN6k&Ui->gu<?cy(%s3oxGc-tjME{?`06CAiUw9D+d(ZF)>>%Il-k<)4))^AVb
z`@HYrUdb7mx3+~^%8B%UYB<@uQ_`&Bn9SvQ*X<rR$k#kv7nQf}%K@Qi8@??Q*Z+98
zx+RTSo&U96b%Jrhrw>=s`PW!I_TTe3Vri%N+>g@#v}70Gy_~k?<Z~r%g$4#D4rHR9
z%_5Zj&1z=5J3oVL#18Q3|5zK~=eutPLe;Bdtk241_nw!IJ`D5~gQu&X%Q~loCIGtU
Bcd`Hg
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..bad3253310d1e4c3c7350a81803a359e147261ce
GIT binary patch
literal 385
zc%17D@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTC#^NA%Cx&(BWL^R}Y)RhkE)4%c
zaKYZ?lYt_f1s;*b3=G`DAk4@xYmNj^kiEpy*OmP)i-fqY3cF;v7EnmC#5JPCIX^cy
zHLrxhxhOTUBsE2$JhLQ2!QIn0AiR-J9H{7yr;B4q#=W;U4f&cJ1X>@;Cz~w1)K*-<
zru)U<-qAx9GCP}O4zcN)h^bsy!+JwQRA<fPJ>kw}`%=`r+fG=vKlL$?V`LF<U|?i1
zn48&NzU=^C2m89UEbk7mdNeM2yKu$f?H3YT9_|0k(s$shJNHpWt5n7}x6f@eVfIeQ
zt?dZE#@JbUAVz#90~3cr0}{c=7<!;eTfV_YLE$^&2__~Z`38{+7L9*b=iHSJU{HV6
z`a0sLPI<lAmB4=cuZ9~I#(h^kkkin(*EF~I*yF0$??7Yz1m1h+rD2tMao!%FKN&n-
L{an^LB{Ts54&sG&
--- a/mobile/android/base/resources/layout-large-v11/browser_toolbar.xml
+++ b/mobile/android/base/resources/layout-large-v11/browser_toolbar.xml
@@ -26,25 +26,24 @@
                         android:paddingRight="38dip"/>
 
     <!-- The TextSwitcher should be shifted 28dp on the right, to avoid
          the curve. On a 56dp space, centering 24dp image will leave
          16dp on all sides. However this image has a perception of
          2 layers. Hence to center this, an additional 4dp is added to the right.
          The margins will be 12dp on left, 48dp on right, instead of ideal 16dp
          and 44dp. -->
-    <Gecko.TextSwitcher android:id="@+id/tabs_count"
-                        style="@style/AddressBar.ImageButton"
+    <org.mozilla.gecko.TabCounter android:id="@+id/tabs_counter"
+                        style="@style/AddressBar.ImageButton.TabCount"
                         android:layout_width="24dip"
                         android:layout_height="24dip"
                         android:layout_marginLeft="12dip"
                         android:layout_marginRight="48dip"
                         android:layout_marginTop="16dp"
-                        android:layout_alignLeft="@id/tabs"
-                        android:gravity="center_horizontal"/>
+                        android:layout_alignLeft="@id/tabs"/>
 
     <FrameLayout android:layout_width="fill_parent"
                  android:layout_height="fill_parent"
                  android:layout_toRightOf="@id/tabs"
                  android:layout_toLeftOf="@id/menu_items"
                  android:layout_marginLeft="-18dp"
                  android:paddingRight="18dp"
                  android:layout_marginTop="7dp"
--- a/mobile/android/base/resources/layout/browser_toolbar.xml
+++ b/mobile/android/base/resources/layout/browser_toolbar.xml
@@ -3,16 +3,18 @@
    - 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.BrowserToolbarLayout xmlns:android="http://schemas.android.com/apk/res/android"
               xmlns:gecko="http://schemas.android.com/apk/res-auto"
               android:id="@+id/browser_toolbar"
               style="@style/BrowserToolbar"
               android:layout_centerVertical="true"
+              android:clipChildren="false"
+              android:clipToPadding="false"
               android:clickable="true"
               android:focusable="true">
 
     <ImageButton android:id="@+id/back"
                  style="@style/AddressBar.ImageButton.Back"/>
 
     <ImageButton android:id="@+id/forward"
                  style="@style/AddressBar.ImageButton.Forward"/>
@@ -75,25 +77,24 @@
                         android:paddingRight="11dip"/>
 
     <!-- The TextSwitcher should be shifted 24dp on the left, to avoid
          the curve. On a 48dp space, centering 24dp image will leave
          12dp on all sides. However this image has a perception of
          2 layers. Hence to center this, an additional 4dp is added to the left.
          The margins will be 40dp on left, 8dp on right, instead of ideal 30dp
          and 12dp. -->
-    <Gecko.TextSwitcher android:id="@+id/tabs_count"
-                        style="@style/AddressBar.ImageButton"
+    <org.mozilla.gecko.TabCounter android:id="@+id/tabs_counter"
+                        style="@style/AddressBar.ImageButton.TabCount"
                         android:layout_width="24dip"
                         android:layout_height="24dip"
                         android:layout_marginLeft="40dip"
                         android:layout_marginRight="8dip"
                         android:layout_marginTop="12dip"
-                        android:layout_alignRight="@id/tabs"
-                        android:gravity="center_horizontal"/>
+                        android:layout_alignRight="@id/tabs"/>
 
     <LinearLayout android:id="@+id/awesome_bar_content"
                   style="@style/AddressBar.Button"
                   android:layout_toLeftOf="@id/tabs"
                   android:layout_marginRight="-24dp"
                   android:orientation="horizontal">
 
         <ImageButton android:id="@+id/favicon"
--- a/mobile/android/base/resources/layout/tabs_counter.xml
+++ b/mobile/android/base/resources/layout/tabs_counter.xml
@@ -4,14 +4,14 @@
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 <Gecko.TextView xmlns:android="http://schemas.android.com/apk/res/android"
                 android:layout_width="24dip"
                 android:layout_height="24dip"
                 android:layout_margin="12dip"
                 android:paddingTop="2dip"
                 android:paddingLeft="4dip"
-                android:background="@drawable/tabs_count"
+                android:background="@drawable/tabs_count_foreground"
                 android:textAppearance="@style/TextAppearance.Micro"
                 android:textColor="#FF43484E"
                 android:textStyle="bold"
                 android:duplicateParentState="true"
                 android:gravity="center"/>
--- a/mobile/android/base/resources/values/styles.xml
+++ b/mobile/android/base/resources/values/styles.xml
@@ -156,16 +156,23 @@
 
     <!-- BrowserToolbar -->
     <style name="BrowserToolbar">
         <item name="android:layout_width">fill_parent</item>
         <item name="android:layout_height">@dimen/browser_toolbar_height</item>
         <item name="android:orientation">horizontal</item>
     </style>
 
+    <style name="AddressBar.ImageButton.TabCount">
+        <item name="android:background">@drawable/tabs_count</item>
+        <item name="android:gravity">center_horizontal</item>
+        <item name="android:clipChildren">false</item>
+        <item name="android:clipToPadding">false</item>
+    </style>
+
     <!-- Address bar -->
     <style name="AddressBar">
         <item name="android:layout_width">fill_parent</item>
         <item name="android:layout_height">fill_parent</item>
         <item name="android:orientation">horizontal</item>
     </style>
 
     <!-- Address bar - Button -->
--- a/mobile/android/base/tests/BaseTest.java.in
+++ b/mobile/android/base/tests/BaseTest.java.in
@@ -402,17 +402,17 @@ abstract class BaseTest extends Activity
                 return true;
             }
             return false;
         }
     }
 
     public final void verifyTabCount(int expectedTabCount) {
         Activity activity = getActivity();
-        Element tabCount = mDriver.findElement(activity, "tabs_count");
+        Element tabCount = mDriver.findElement(activity, "tabs_counter");
         String tabCountText = tabCount.getText();
         int tabCountInt = Integer.parseInt(tabCountText);
         mAsserter.is(tabCountInt, expectedTabCount, "The correct number of tabs are opened");
     }
 
     private ListView getAwesomeList(String waitText, int expectedChildCount,
          String clickText, String tagName, String callerName) {
         ArrayList<ListView> views;
--- a/mobile/android/base/tests/testNewTab.java.in
+++ b/mobile/android/base/tests/testNewTab.java.in
@@ -20,17 +20,17 @@ public class testNewTab extends BaseTest
 
     public void testNewTab() {
         String url = getAbsoluteUrl("/robocop/robocop_blank_01.html");
         String url2 = getAbsoluteUrl("/robocop/robocop_blank_02.html");
 
         blockForGeckoReady();
 
         Activity activity = getActivity();
-        tabCount = mDriver.findElement(activity, "tabs_count");
+        tabCount = mDriver.findElement(activity, "tabs_counter");
         tabs = mDriver.findElement(activity, "tabs");
         addTab = mDriver.findElement(activity, "add_tab");
         mAsserter.ok(tabCount != null &&
                      tabs != null &&
                      addTab != null, 
                      "Checking elements", "all elements present");
 
         int expectedTabCount = 1;