Bug 1055606 - Animate tab strip when tabs added added or removed (r=mcomella)
authorLucas Rocha <lucasr@lucasr.org>
Tue, 04 Nov 2014 15:49:06 +0000
changeset 213968 9ba384a83788fe5d1a4d9c2b499783208d0477f9
parent 213967 a51796d31cf46a10f312da649b673522ad0f585d
child 213969 d283c18f5c9cd029a01c79d6f3015665bbf6a858
push id27769
push userkwierso@gmail.com
push dateWed, 05 Nov 2014 03:53:35 +0000
treeherdermozilla-central@62990ec7ad78 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmcomella
bugs1055606
milestone36.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 1055606 - Animate tab strip when tabs added added or removed (r=mcomella)
mobile/android/base/tabs/TabStrip.java
mobile/android/base/tabs/TabStripView.java
--- a/mobile/android/base/tabs/TabStrip.java
+++ b/mobile/android/base/tabs/TabStrip.java
@@ -77,19 +77,17 @@ public class TabStrip extends ThemedLine
     }
 
     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.
-                    tabStripView.refreshTabs();
+                    tabStripView.addTab(tab);
                     break;
 
                 case CLOSED:
                     tabStripView.removeTab(tab);
                     break;
 
                 case SELECTED:
                     // Update the selected position, then fall through...
--- a/mobile/android/base/tabs/TabStripView.java
+++ b/mobile/android/base/tabs/TabStripView.java
@@ -6,32 +6,41 @@
 package org.mozilla.gecko.tabs;
 
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Canvas;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
+import android.view.animation.AccelerateDecelerateInterpolator;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewTreeObserver;
 import android.view.ViewTreeObserver.OnPreDrawListener;
 
+import com.nineoldandroids.animation.Animator;
+import com.nineoldandroids.animation.AnimatorSet;
+import com.nineoldandroids.animation.ObjectAnimator;
+
 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 static final int ANIM_TIME_MS = 200;
+    private static final AccelerateDecelerateInterpolator ANIM_INTERPOLATOR =
+            new AccelerateDecelerateInterpolator();
+
     private final TabStripAdapter adapter;
     private final Drawable divider;
 
     // Filled by calls to ShapeDrawable.getPadding();
     // saved to prevent allocation in draw().
     private final Rect dividerPadding = new Rect();
 
     private boolean isPrivate;
@@ -66,22 +75,118 @@ public class TabStripView extends TwoWay
     private int getPositionForSelectedTab() {
         return adapter.getPositionForTab(Tabs.getInstance().getSelectedTab());
     }
 
     private void updateSelectedStyle(int selected) {
         setItemChecked(selected, true);
     }
 
-    private void updateSelectedPosition() {
+    private void updateSelectedPosition(boolean ensureVisible) {
         final int selected = getPositionForSelectedTab();
         if (selected != -1) {
             updateSelectedStyle(selected);
-            ensurePositionIsVisible(selected);
+
+            if (ensureVisible) {
+                ensurePositionIsVisible(selected);
+            }
+        }
+    }
+
+    private void animateRemoveTab(Tab removedTab) {
+        final int removedPosition = adapter.getPositionForTab(removedTab);
+
+        final View removedView = getViewForTab(removedTab);
+
+        // The removed position might not have a matching child view
+        // when it's not within the visible range of positions in the strip.
+        if (removedView == null) {
+            return;
         }
+
+        // We don't animate the removed child view (it just disappears)
+        // but we still need its size of animate all affected children
+        // within the visible viewport.
+        final int removedSize = removedView.getWidth() + getItemMargin();
+
+        getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() {
+            @Override
+            public boolean onPreDraw() {
+                getViewTreeObserver().removeOnPreDrawListener(this);
+
+                final int firstPosition = getFirstVisiblePosition();
+                final List<Animator> childAnimators = new ArrayList<Animator>();
+
+                final int childCount = getChildCount();
+                for (int i = removedPosition - firstPosition; i < childCount; i++) {
+                    final View child = getChildAt(i);
+
+                    // TODO: optimize with Valueresolver
+                    final ObjectAnimator animator =
+                            ObjectAnimator.ofFloat(child, "translationX", removedSize, 0);
+                    childAnimators.add(animator);
+                }
+
+                final AnimatorSet animatorSet = new AnimatorSet();
+                animatorSet.playTogether(childAnimators);
+                animatorSet.setDuration(ANIM_TIME_MS);
+                animatorSet.setInterpolator(ANIM_INTERPOLATOR);
+                animatorSet.start();
+
+                return true;
+            }
+        });
+    }
+
+    private void animateNewTab(Tab newTab) {
+        final int newPosition = adapter.getPositionForTab(newTab);
+        if (newPosition < 0) {
+            return;
+        }
+
+        getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() {
+            @Override
+            public boolean onPreDraw() {
+                getViewTreeObserver().removeOnPreDrawListener(this);
+
+                final int firstPosition = getFirstVisiblePosition();
+
+                final View newChild = getChildAt(newPosition - firstPosition);
+                if (newChild == null) {
+                    return true;
+                }
+
+                final List<Animator> childAnimators = new ArrayList<Animator>();
+                childAnimators.add(
+                        ObjectAnimator.ofFloat(newChild, "translationY", newChild.getHeight(), 0));
+
+                // This will momentaneously add a gap on the right side
+                // because TwoWayView doesn't provide APIs to control
+                // view recycling programatically to handle these transitory
+                // states in the container during animations.
+
+                final int tabSize = newChild.getWidth();
+                final int newIndex = newPosition - firstPosition;
+                final int childCount = getChildCount();
+                for (int i = newIndex + 1; i < childCount; i++) {
+                    final View child = getChildAt(i);
+
+                    childAnimators.add(
+                        ObjectAnimator.ofFloat(child, "translationX", -tabSize, 0));
+                }
+
+                final AnimatorSet animatorSet = new AnimatorSet();
+                animatorSet.playTogether(childAnimators);
+                animatorSet.setDuration(ANIM_TIME_MS);
+                animatorSet.setInterpolator(ANIM_INTERPOLATOR);
+                animatorSet.start();
+
+                return true;
+            }
+        });
     }
 
     private void ensurePositionIsVisible(final int position) {
         getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() {
             @Override
             public boolean onPreDraw() {
                 getViewTreeObserver().removeOnPreDrawListener(this);
                 smoothScrollToPosition(position);
@@ -106,34 +211,42 @@ public class TabStripView extends TwoWay
 
         for (Tab tab : Tabs.getInstance().getTabsInOrder()) {
             if (tab.isPrivate() == isPrivate) {
                 tabs.add(tab);
             }
         }
 
         adapter.refresh(tabs);
-        updateSelectedPosition();
+        updateSelectedPosition(true);
     }
 
     void clearTabs() {
         adapter.clear();
     }
 
+    void addTab(Tab tab) {
+        // Refresh the list to make sure the new tab is
+        // added in the right position.
+        refreshTabs();
+        animateNewTab(tab);
+    }
+
     void removeTab(Tab tab) {
+        animateRemoveTab(tab);
         adapter.removeTab(tab);
-        updateSelectedPosition();
+        updateSelectedPosition(false);
     }
 
     void selectTab(Tab tab) {
         if (tab.isPrivate() != isPrivate) {
             isPrivate = tab.isPrivate();
             refreshTabs();
         } else {
-            updateSelectedPosition();
+            updateSelectedPosition(true);
         }
     }
 
     void updateTab(Tab tab) {
         final TabStripItemView item = (TabStripItemView) getViewForTab(tab);
         if (item != null) {
             item.updateFromTab(tab);
         }