From bb787d958f7796921ed4baa546f2515f2795b6f0 Mon Sep 17 00:00:00 2001 From: Lucas Rocha Date: Tue, 4 Nov 2014 15:49:06 +0000 Subject: [PATCH] Bug 1055606 - Animate tab strip when tabs added added or removed (r=mcomella) --- mobile/android/base/tabs/TabStrip.java | 4 +- mobile/android/base/tabs/TabStripView.java | 123 ++++++++++++++++++++- 2 files changed, 119 insertions(+), 8 deletions(-) diff --git a/mobile/android/base/tabs/TabStrip.java b/mobile/android/base/tabs/TabStrip.java index 4c835ac615b..3d449faf16a 100644 --- a/mobile/android/base/tabs/TabStrip.java +++ b/mobile/android/base/tabs/TabStrip.java @@ -82,9 +82,7 @@ public class TabStrip extends ThemedLinearLayout { 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: diff --git a/mobile/android/base/tabs/TabStripView.java b/mobile/android/base/tabs/TabStripView.java index b87f97f4d82..5716012ebb6 100644 --- a/mobile/android/base/tabs/TabStripView.java +++ b/mobile/android/base/tabs/TabStripView.java @@ -11,11 +11,16 @@ 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; @@ -27,6 +32,10 @@ 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; @@ -71,14 +80,110 @@ public class TabStripView extends TwoWayView { 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 childAnimators = new ArrayList(); + + 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 childAnimators = new ArrayList(); + 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 @@ -111,16 +216,24 @@ public class TabStripView extends TwoWayView { } 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) { @@ -128,7 +241,7 @@ public class TabStripView extends TwoWayView { isPrivate = tab.isPrivate(); refreshTabs(); } else { - updateSelectedPosition(); + updateSelectedPosition(true); } }