From 63f1621378ede9580ca38ac4e4e5b51021861dd8 Mon Sep 17 00:00:00 2001 From: Martyn Haigh Date: Thu, 27 Nov 2014 16:37:42 +0000 Subject: [PATCH] Bug 1097121 - Animate items being removed from the tabs panel grid (r=lucasr) --- mobile/android/base/tabs/TabsGridLayout.java | 162 ++++++++++++++++++- 1 file changed, 155 insertions(+), 7 deletions(-) diff --git a/mobile/android/base/tabs/TabsGridLayout.java b/mobile/android/base/tabs/TabsGridLayout.java index 505d00637dca..544577944c7f 100644 --- a/mobile/android/base/tabs/TabsGridLayout.java +++ b/mobile/android/base/tabs/TabsGridLayout.java @@ -6,6 +6,7 @@ package org.mozilla.gecko.tabs; import java.util.ArrayList; +import java.util.List; import org.mozilla.gecko.animation.ViewHelper; import org.mozilla.gecko.GeckoAppShell; @@ -18,13 +19,22 @@ 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.widget.GridView; import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.animation.DecelerateInterpolator; import android.widget.Button; +import android.widget.GridView; +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). @@ -36,12 +46,18 @@ class TabsGridLayout extends GridView 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 mTabLocations = new SparseArray(); final private boolean mIsPrivate; private final TabsLayoutAdapter mTabsAdapter; + private final int mColumnWidth; public TabsGridLayout(Context context, AttributeSet attrs) { super(context, attrs, R.attr.tabGridLayoutViewStyle); @@ -67,9 +83,13 @@ class TabsGridLayout extends GridView 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); @@ -87,9 +107,7 @@ class TabsGridLayout extends GridView 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); } }; @@ -121,6 +139,47 @@ class TabsGridLayout extends GridView } } + 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; @@ -160,6 +219,9 @@ class TabsGridLayout extends GridView 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()); @@ -244,4 +306,90 @@ class TabsGridLayout extends GridView } } } + + 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 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; + } + }); + } + }