Bug 863828 - New tab increment animation. r=lucasr
authorWes Johnston <wjohnston@mozilla.com>
Fri, 26 Apr 2013 16:17:34 -0700
changeset 130194 2fbb0094f6f4b69705bb29b2289bcc92ca50459d
parent 130193 576ff91b64d7dc816b7fccce6b1686fad09b402f
child 130195 e116c3a3476406f91bff7b62c26a05e6829a7636
push idunknown
push userunknown
push dateunknown
reviewerslucasr
bugs863828
milestone23.0a1
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;