Bug 1097121 - Animate items being removed from the tabs panel grid (r=lucasr)
authorMartyn Haigh <martyn.haigh@gmail.com>
Thu, 27 Nov 2014 16:37:42 +0000
changeset 242225 f2312ecb6fd0502e4cbc39bbcd32ac08bb343a52
parent 242224 198a4bd1f9c6e93497241f5c212c15729830d689
child 242226 2ab7d3193c8762068e0ccefa5a3a3195ef49f178
push id4311
push userraliiev@mozilla.com
push dateMon, 12 Jan 2015 19:37:41 +0000
treeherdermozilla-beta@150c9fed433b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerslucasr
bugs1097121
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 1097121 - Animate items being removed from the tabs panel grid (r=lucasr)
mobile/android/base/tabs/TabsGridLayout.java
--- a/mobile/android/base/tabs/TabsGridLayout.java
+++ b/mobile/android/base/tabs/TabsGridLayout.java
@@ -1,52 +1,68 @@
 /* -*- 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 java.util.ArrayList;
+import java.util.List;
 
 import org.mozilla.gecko.animation.ViewHelper;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoEvent;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Tab;
 import org.mozilla.gecko.tabs.TabsPanel.TabsLayout;
 import org.mozilla.gecko.Tabs;
 
 import android.content.Context;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
+import android.graphics.PointF;
 import android.util.AttributeSet;
-import android.util.TypedValue;
+import android.util.SparseArray;
 import android.view.Gravity;
 import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.Button;
 import android.widget.GridView;
-import android.view.ViewGroup;
-import android.widget.Button;
+import com.nineoldandroids.animation.Animator;
+import com.nineoldandroids.animation.AnimatorSet;
+import com.nineoldandroids.animation.ObjectAnimator;
+import com.nineoldandroids.animation.PropertyValuesHolder;
+import com.nineoldandroids.animation.ValueAnimator;
+
 
 /**
  * A tabs layout implementation for the tablet redesign (bug 1014156).
  * Expected to replace TabsListLayout once complete.
  */
 
 class TabsGridLayout extends GridView
                      implements TabsLayout,
                                 Tabs.OnTabsChangedListener {
     private static final String LOGTAG = "Gecko" + TabsGridLayout.class.getSimpleName();
 
+    private static final int ANIM_TIME_MS = 200;
+    public static final int ANIM_DELAY_MULTIPLE_MS = 20;
+    private static final DecelerateInterpolator ANIM_INTERPOLATOR = new DecelerateInterpolator();
+
     private final Context mContext;
     private TabsPanel mTabsPanel;
+    private final SparseArray<PointF> mTabLocations = new SparseArray<PointF>();
 
     final private boolean mIsPrivate;
 
     private final TabsLayoutAdapter mTabsAdapter;
+    private final int mColumnWidth;
 
     public TabsGridLayout(Context context, AttributeSet attrs) {
         super(context, attrs, R.attr.tabGridLayoutViewStyle);
         mContext = context;
 
         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabsLayout);
         mIsPrivate = (a.getInt(R.styleable.TabsLayout_tabs, 0x0) == 1);
         a.recycle();
@@ -62,19 +78,23 @@ class TabsGridLayout extends GridView
             }
         });
 
         setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY);
         setStretchMode(GridView.STRETCH_SPACING);
         setGravity(Gravity.CENTER);
         setNumColumns(GridView.AUTO_FIT);
 
+        // The clipToPadding setting in the styles.xml doesn't seem to be working (bug 1101784)
+        // so lets set it manually in code for the moment as it's needed for the padding animation
+        setClipToPadding(false);
+
         final Resources resources = getResources();
-        final int columnWidth = resources.getDimensionPixelSize(R.dimen.new_tablet_tab_panel_column_width);
-        setColumnWidth(columnWidth);
+        mColumnWidth = resources.getDimensionPixelSize(R.dimen.new_tablet_tab_panel_column_width);
+        setColumnWidth(mColumnWidth);
 
         final int padding = resources.getDimensionPixelSize(R.dimen.new_tablet_tab_panel_grid_padding);
         final int paddingTop = resources.getDimensionPixelSize(R.dimen.new_tablet_tab_panel_grid_padding_top);
         setPadding(padding, paddingTop, padding, padding);
     }
 
     private class TabsGridLayoutAdapter extends TabsLayoutAdapter {
 
@@ -82,19 +102,17 @@ class TabsGridLayout extends GridView
         final private View.OnClickListener mSelectClickListener;
 
         public TabsGridLayoutAdapter (Context context) {
             super(context, R.layout.new_tablet_tabs_item_cell);
 
             mCloseClickListener = new Button.OnClickListener() {
                 @Override
                 public void onClick(View v) {
-                    TabsLayoutItemView itemView = (TabsLayoutItemView) v.getTag();
-                    Tab tab = Tabs.getInstance().getTab(itemView.getTabId());
-                    Tabs.getInstance().closeTab(tab);
+                    closeTab(v);
                 }
             };
 
             mSelectClickListener = new View.OnClickListener() {
                 @Override
                 public void onClick(View v) {
                     TabsLayoutItemView tab = (TabsLayoutItemView) v;
                     Tabs.getInstance().selectTab(tab.getTabId());
@@ -116,16 +134,57 @@ class TabsGridLayout extends GridView
             super.bindView(view, tab);
 
             // If we're recycling this view, there's a chance it was transformed during
             // the close animation. Remove any of those properties.
             resetTransforms(view);
         }
     }
 
+    private void populateTabLocations(final Tab removedTab) {
+        mTabLocations.clear();
+
+        final int firstPosition = getFirstVisiblePosition();
+        final int lastPosition = getLastVisiblePosition();
+        final int numberOfColumns = getNumColumns();
+        final int childCount = getChildCount();
+        final int removedPosition = mTabsAdapter.getPositionForTab(removedTab);
+
+        for (int x = 1, i = (removedPosition - firstPosition) + 1; i < childCount; i++, x++) {
+            final View child = getChildAt(i);
+            if (child != null) {
+                mTabLocations.append(x, new PointF(child.getX(), child.getY()));
+            }
+        }
+
+        final boolean firstChildOffScreen = ((firstPosition > 0) || getChildAt(0).getY() < 0);
+        final boolean lastChildVisible = (lastPosition - childCount == firstPosition - 1);
+        final boolean oneItemOnLastRow = (lastPosition % numberOfColumns == 0);
+        if (firstChildOffScreen && lastChildVisible && oneItemOnLastRow) {
+            // We need to set the view's bottom padding to prevent a sudden jump as the
+            // last item in the row is being removed. We then need to remove the padding
+            // via a sweet animation
+
+            final int removedHeight = getChildAt(0).getMeasuredHeight();
+            final int verticalSpacing = getVerticalSpacing();
+
+            ValueAnimator paddingAnimator = ValueAnimator.ofInt(getPaddingBottom() + removedHeight + verticalSpacing, getPaddingBottom());
+            paddingAnimator.setDuration(ANIM_TIME_MS * 2);
+
+            paddingAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+
+                @Override
+                public void onAnimationUpdate(ValueAnimator animation) {
+                    setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), (Integer) animation.getAnimatedValue());
+                }
+            });
+            paddingAnimator.start();
+        }
+    }
+
     @Override
     public void setTabsPanel(TabsPanel panel) {
         mTabsPanel = panel;
     }
 
     @Override
     public void show() {
         setVisibility(View.VISIBLE);
@@ -155,16 +214,19 @@ class TabsGridLayout extends GridView
     public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) {
         switch (msg) {
             case ADDED:
                 // Refresh the list to make sure the new tab is added in the right position.
                 refreshTabsData();
                 break;
 
             case CLOSED:
+                if(mTabsAdapter.getCount() > 0) {
+                    animateRemoveTab(tab);
+                }
                if (tab.isPrivate() == mIsPrivate && mTabsAdapter.getCount() > 0) {
                    if (mTabsAdapter.removeTab(tab)) {
                        int selected = mTabsAdapter.getPositionForTab(Tabs.getInstance().getSelectedTab());
                        updateSelectedStyle(selected);
                    }
                }
                break;
 
@@ -239,9 +301,95 @@ class TabsGridLayout extends GridView
         for (Tab tab : tabs) {
             // In the normal panel we want to close all tabs (both private and normal),
             // but in the private panel we only want to close private tabs.
             if (!mIsPrivate || tab.isPrivate()) {
                 Tabs.getInstance().closeTab(tab, false);
             }
         }
     }
+
+    private View getViewForTab(Tab tab) {
+        final int position = mTabsAdapter.getPositionForTab(tab);
+        return getChildAt(position - getFirstVisiblePosition());
+    }
+
+    void closeTab(View v) {
+        TabsLayoutItemView itemView = (TabsLayoutItemView) v.getTag();
+        Tab tab = Tabs.getInstance().getTab(itemView.getTabId());
+
+        Tabs.getInstance().closeTab(tab);
+        updateSelectedPosition();
+    }
+
+    private void animateRemoveTab(final Tab removedTab) {
+        final int removedPosition = mTabsAdapter.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;
+        }
+        final int removedHeight = removedView.getMeasuredHeight();
+
+        populateTabLocations(removedTab);
+
+        getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
+            @Override
+            public boolean onPreDraw() {
+                getViewTreeObserver().removeOnPreDrawListener(this);
+                // We don't animate the removed child view (it just disappears)
+                // but we still need its size to animate all affected children
+                // within the visible viewport.
+                final int childCount = getChildCount();
+                final int firstPosition = getFirstVisiblePosition();
+                final int numberOfColumns = getNumColumns();
+
+                final List<Animator> childAnimators = new ArrayList<>();
+
+                PropertyValuesHolder translateX, translateY;
+                for (int x = 0, i = removedPosition - firstPosition ; i < childCount; i++, x++) {
+                    final View child = getChildAt(i);
+                    ObjectAnimator animator;
+
+                    if (i % numberOfColumns == numberOfColumns - 1) {
+                        // Animate X & Y
+                        translateX = PropertyValuesHolder.ofFloat("translationX", -(mColumnWidth * numberOfColumns), 0);
+                        translateY = PropertyValuesHolder.ofFloat("translationY", removedHeight, 0);
+                        animator = ObjectAnimator.ofPropertyValuesHolder(child, translateX, translateY);
+                    } else {
+                        // Just animate X
+                        translateX = PropertyValuesHolder.ofFloat("translationX", mColumnWidth, 0);
+                        animator = ObjectAnimator.ofPropertyValuesHolder(child, translateX);
+                    }
+                    animator.setStartDelay(x * ANIM_DELAY_MULTIPLE_MS);
+                    childAnimators.add(animator);
+                }
+
+                final AnimatorSet animatorSet = new AnimatorSet();
+                animatorSet.playTogether(childAnimators);
+                animatorSet.setDuration(ANIM_TIME_MS);
+                animatorSet.setInterpolator(ANIM_INTERPOLATOR);
+                animatorSet.start();
+
+                // Set the starting position of the child views - because we are delaying the start
+                // of the animation, we need to prevent the items being drawn in their final position
+                // prior to the animation starting
+                for (int x = 1, i = (removedPosition - firstPosition) + 1; i < childCount; i++, x++) {
+                    final View child = getChildAt(i);
+
+                    final PointF targetLocation = mTabLocations.get(x+1);
+                    if (targetLocation == null) {
+                        continue;
+                    }
+
+                    child.setX(targetLocation.x);
+                    child.setY(targetLocation.y);
+                }
+
+                return true;
+            }
+        });
+    }
+
 }