diff options
Diffstat (limited to 'core/java')
| -rw-r--r-- | core/java/android/widget/ActionMenuPresenter.java | 260 |
1 files changed, 247 insertions, 13 deletions
diff --git a/core/java/android/widget/ActionMenuPresenter.java b/core/java/android/widget/ActionMenuPresenter.java index 4fadc190f540..710d59eba635 100644 --- a/core/java/android/widget/ActionMenuPresenter.java +++ b/core/java/android/widget/ActionMenuPresenter.java @@ -16,6 +16,10 @@ package android.widget; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.Configuration; @@ -24,6 +28,7 @@ import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.os.Parcel; import android.os.Parcelable; +import android.util.SparseArray; import android.util.SparseBooleanArray; import android.view.ActionProvider; import android.view.Gravity; @@ -32,6 +37,7 @@ import android.view.SoundEffectConstants; import android.view.View; import android.view.View.MeasureSpec; import android.view.ViewGroup; +import android.view.ViewTreeObserver; import android.view.accessibility.AccessibilityNodeInfo; import android.widget.ListPopupWindow.ForwardingListener; import com.android.internal.transition.ActionBarTransition; @@ -45,6 +51,7 @@ import com.android.internal.view.menu.MenuView; import com.android.internal.view.menu.SubMenuBuilder; import java.util.ArrayList; +import java.util.List; /** * MenuPresenter for building action menus as seen in the action bar and action modes. @@ -54,6 +61,7 @@ import java.util.ArrayList; public class ActionMenuPresenter extends BaseMenuPresenter implements ActionProvider.SubUiVisibilityListener { private static final String TAG = "ActionMenuPresenter"; + private static final int ITEM_ANIMATION_DURATION = 150; private OverflowMenuButton mOverflowButton; private boolean mReserveOverflow; @@ -71,8 +79,6 @@ public class ActionMenuPresenter extends BaseMenuPresenter // Group IDs that have been added as actions - used temporarily, allocated here for reuse. private final SparseBooleanArray mActionButtonGroups = new SparseBooleanArray(); - private View mScrapActionButtonView; - private OverflowPopup mOverflowPopup; private ActionButtonSubmenu mActionButtonPopup; @@ -84,6 +90,18 @@ public class ActionMenuPresenter extends BaseMenuPresenter final PopupPresenterCallback mPopupPresenterCallback = new PopupPresenterCallback(); int mOpenSubMenuId; + // These collections are used to store pre- and post-layout information for menu items, + // which is used to determine appropriate animations to run for changed items. + private SparseArray<MenuItemLayoutInfo> mPreLayoutItems = + new SparseArray<MenuItemLayoutInfo>(); + private SparseArray<MenuItemLayoutInfo> mPostLayoutItems = + new SparseArray<MenuItemLayoutInfo>(); + + // The list of currently running animations on menu items. + private List<ItemAnimationInfo> mRunningItemAnimations = new ArrayList<ItemAnimationInfo>(); + + + public ActionMenuPresenter(Context context) { super(context, com.android.internal.R.layout.action_menu_layout, com.android.internal.R.layout.action_menu_item_layout); @@ -125,9 +143,6 @@ public class ActionMenuPresenter extends BaseMenuPresenter mActionItemWidthLimit = width; mMinCellSize = (int) (ActionMenuView.MIN_CELL_SIZE * res.getDisplayMetrics().density); - - // Drop a scrap view as it may no longer reflect the proper context/config. - mScrapActionButtonView = null; } public void onConfigurationChanged(Configuration newConfig) { @@ -202,10 +217,186 @@ public class ActionMenuPresenter extends BaseMenuPresenter return item.isActionButton(); } + /** + * Store layout information about current items in the menu. This is stored for + * both pre- and post-layout phases and compared in runItemAnimations() to determine + * the animations that need to be run on any item changes. + * + * @param preLayout Whether this is being called in the pre-layout phase. This is passed + * into the MenuItemLayoutInfo structure to store the appropriate position values. + */ + private void computeMenuItemAnimationInfo(boolean preLayout) { + final ViewGroup menuViewParent = (ViewGroup) mMenuView; + final int count = menuViewParent.getChildCount(); + SparseArray items = preLayout ? mPreLayoutItems : mPostLayoutItems; + for (int i = 0; i < count; ++i) { + View child = menuViewParent.getChildAt(i); + final int id = child.getId(); + if (id > 0 && child.getWidth() != 0 && child.getHeight() != 0) { + MenuItemLayoutInfo info = new MenuItemLayoutInfo(child, preLayout); + items.put(id, info); + } + } + } + + /** + * This method is called once both the pre-layout and post-layout steps have + * happened. It figures out which views are new (didn't exist prior to layout), + * gone (existed pre-layout, but are now gone), or changed (exist in both, + * but in a different location) and runs appropriate animations on those views. + * Items are tracked by ids, since the underlying views that represent items + * pre- and post-layout may be different. + */ + private void runItemAnimations() { + for (int i = 0; i < mPreLayoutItems.size(); ++i) { + int id = mPreLayoutItems.keyAt(i); + final MenuItemLayoutInfo menuItemLayoutInfoPre = mPreLayoutItems.get(id); + final int postLayoutIndex = mPostLayoutItems.indexOfKey(id); + if (postLayoutIndex >= 0) { + // item exists pre and post: see if it's changed + final MenuItemLayoutInfo menuItemLayoutInfoPost = + mPostLayoutItems.valueAt(postLayoutIndex); + PropertyValuesHolder pvhX = null; + PropertyValuesHolder pvhY = null; + if (menuItemLayoutInfoPre.left != menuItemLayoutInfoPost.left) { + pvhX = PropertyValuesHolder.ofFloat(View.TRANSLATION_X, + (menuItemLayoutInfoPre.left - menuItemLayoutInfoPost.left), 0); + } + if (menuItemLayoutInfoPre.top != menuItemLayoutInfoPost.top) { + pvhY = PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, + menuItemLayoutInfoPre.top - menuItemLayoutInfoPost.top, 0); + } + if (pvhX != null || pvhY != null) { + for (int j = 0; j < mRunningItemAnimations.size(); ++j) { + ItemAnimationInfo oldInfo = mRunningItemAnimations.get(j); + if (oldInfo.id == id && oldInfo.animType == ItemAnimationInfo.MOVE) { + oldInfo.animator.cancel(); + } + } + ObjectAnimator anim; + if (pvhX != null) { + if (pvhY != null) { + anim = ObjectAnimator.ofPropertyValuesHolder(menuItemLayoutInfoPost.view, + pvhX, pvhY); + } else { + anim = ObjectAnimator.ofPropertyValuesHolder(menuItemLayoutInfoPost.view, pvhX); + } + } else { + anim = ObjectAnimator.ofPropertyValuesHolder(menuItemLayoutInfoPost.view, pvhY); + } + anim.setDuration(ITEM_ANIMATION_DURATION); + anim.start(); + ItemAnimationInfo info = new ItemAnimationInfo(id, menuItemLayoutInfoPost, anim, + ItemAnimationInfo.MOVE); + mRunningItemAnimations.add(info); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + for (int j = 0; j < mRunningItemAnimations.size(); ++j) { + if (mRunningItemAnimations.get(j).animator == animation) { + mRunningItemAnimations.remove(j); + break; + } + } + } + }); + } + mPostLayoutItems.remove(id); + } else { + // item used to be there, is now gone + float oldAlpha = 1; + for (int j = 0; j < mRunningItemAnimations.size(); ++j) { + ItemAnimationInfo oldInfo = mRunningItemAnimations.get(j); + if (oldInfo.id == id && oldInfo.animType == ItemAnimationInfo.FADE_IN) { + oldAlpha = oldInfo.menuItemLayoutInfo.view.getAlpha(); + oldInfo.animator.cancel(); + } + } + ObjectAnimator anim = ObjectAnimator.ofFloat(menuItemLayoutInfoPre.view, View.ALPHA, + oldAlpha, 0); + // Re-using the view from pre-layout assumes no view recycling + ((ViewGroup) mMenuView).getOverlay().add(menuItemLayoutInfoPre.view); + anim.setDuration(ITEM_ANIMATION_DURATION); + anim.start(); + ItemAnimationInfo info = new ItemAnimationInfo(id, menuItemLayoutInfoPre, anim, ItemAnimationInfo.FADE_OUT); + mRunningItemAnimations.add(info); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + for (int j = 0; j < mRunningItemAnimations.size(); ++j) { + if (mRunningItemAnimations.get(j).animator == animation) { + mRunningItemAnimations.remove(j); + break; + } + } + ((ViewGroup) mMenuView).getOverlay().remove(menuItemLayoutInfoPre.view); + } + }); + } + } + for (int i = 0; i < mPostLayoutItems.size(); ++i) { + int id = mPostLayoutItems.keyAt(i); + final int postLayoutIndex = mPostLayoutItems.indexOfKey(id); + if (postLayoutIndex >= 0) { + // item is new + final MenuItemLayoutInfo menuItemLayoutInfo = + mPostLayoutItems.valueAt(postLayoutIndex); + float oldAlpha = 0; + for (int j = 0; j < mRunningItemAnimations.size(); ++j) { + ItemAnimationInfo oldInfo = mRunningItemAnimations.get(j); + if (oldInfo.id == id && oldInfo.animType == ItemAnimationInfo.FADE_OUT) { + oldAlpha = oldInfo.menuItemLayoutInfo.view.getAlpha(); + oldInfo.animator.cancel(); + } + } + ObjectAnimator anim = ObjectAnimator.ofFloat(menuItemLayoutInfo.view, View.ALPHA, + oldAlpha, 1); + anim.start(); + anim.setDuration(ITEM_ANIMATION_DURATION); + ItemAnimationInfo info = new ItemAnimationInfo(id, menuItemLayoutInfo, anim, ItemAnimationInfo.FADE_IN); + mRunningItemAnimations.add(info); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + for (int j = 0; j < mRunningItemAnimations.size(); ++j) { + if (mRunningItemAnimations.get(j).animator == animation) { + mRunningItemAnimations.remove(j); + break; + } + } + } + }); + } + } + mPreLayoutItems.clear(); + mPostLayoutItems.clear(); + } + + /** + * Gets position/existence information on menu items before and after layout, + * which is then fed into runItemAnimations() + */ + private void setupItemAnimations() { + final ViewGroup menuViewParent = (ViewGroup) mMenuView; + computeMenuItemAnimationInfo(true); + final ViewTreeObserver observer = menuViewParent.getViewTreeObserver(); + if (observer != null) { + observer.addOnDrawListener(new ViewTreeObserver.OnDrawListener() { + @Override + public void onDraw() { + computeMenuItemAnimationInfo(false); + observer.removeOnDrawListener(this); + runItemAnimations(); + } + }); + } + } + @Override public void updateMenuView(boolean cleared) { final ViewGroup menuViewParent = (ViewGroup) ((View) mMenuView).getParent(); if (menuViewParent != null) { + setupItemAnimations(); ActionBarTransition.beginDelayedTransition(menuViewParent); } super.updateMenuView(cleared); @@ -431,10 +622,7 @@ public class ActionMenuPresenter extends BaseMenuPresenter MenuItemImpl item = visibleItems.get(i); if (item.requiresActionButton()) { - View v = getItemView(item, mScrapActionButtonView, parent); - if (mScrapActionButtonView == null) { - mScrapActionButtonView = v; - } + View v = getItemView(item, null, parent); if (mStrictWidthLimit) { cellsRemaining -= ActionMenuView.measureChildForCells(v, cellSize, cellsRemaining, querySpec, 0); @@ -460,10 +648,7 @@ public class ActionMenuPresenter extends BaseMenuPresenter (!mStrictWidthLimit || cellsRemaining > 0); if (isAction) { - View v = getItemView(item, mScrapActionButtonView, parent); - if (mScrapActionButtonView == null) { - mScrapActionButtonView = v; - } + View v = getItemView(item, null, parent); if (mStrictWidthLimit) { final int cells = ActionMenuView.measureChildForCells(v, cellSize, cellsRemaining, querySpec, 0); @@ -819,4 +1004,53 @@ public class ActionMenuPresenter extends BaseMenuPresenter boolean mHasTintMode; boolean mHasTintList; } + + /** + * This class holds layout information for a menu item. This is used to determine + * pre- and post-layout information about menu items, which will then be used to + * determine appropriate item animations. + */ + private static class MenuItemLayoutInfo { + View view; + int left; + int top; + + MenuItemLayoutInfo(View view, boolean preLayout) { + left = view.getLeft(); + top = view.getTop(); + if (preLayout) { + // We track translation for pre-layout because a view might be mid-animation + // and we need this information to know where to animate from + left += view.getTranslationX(); + top += view.getTranslationY(); + } + this.view = view; + } + } + + /** + * This class is used to store information about currently-running item animations. + * This is used when new animations are scheduled to determine whether any existing + * animations need to be canceled, based on whether the running animations overlap + * with any new animations. For example, if an item is currently animating from + * location A to B and another change dictates that it be animated to C, then the current + * A-B animation will be canceled and a new animation to C will be started. + */ + private static class ItemAnimationInfo { + int id; + MenuItemLayoutInfo menuItemLayoutInfo; + Animator animator; + int animType; + static final int MOVE = 0; + static final int FADE_IN = 1; + static final int FADE_OUT = 2; + + ItemAnimationInfo(int id, MenuItemLayoutInfo info, Animator anim, int animType) { + this.id = id; + menuItemLayoutInfo = info; + animator = anim; + this.animType = animType; + } + } + } |
