I\'m looking at Google\'s Material Design guidelines and I want add animated action bar. My goal is do something like this:
I've created an open source library that provides transition/animation support to both View and MenuItem:
MenuItem transition
View transition
Instructions:
On Android Studio, add the code below to Gradle dependencies:
compile 'com.github.kaichunlin.transition:core:0.8.1'
Sample code with explanations:
protected void onCreate(Bundle savedInstanceState) {
//...
//standard onCreate() stuff that creates set configs toolbar, mDrawerLayout & mDrawerToggle
//Use the appropriate adapter that extends MenuBaseAdapter:
DrawerListenerAdapter mDrawerListenerAdapter = new DrawerListenerAdapter(mDrawerToggle, R.id.drawerList);
mDrawerListenerAdapter.setDrawerLayout(mDrawerLayout);
//Add desired transition to the adapter, MenuItemTransitionBuilder is used to build the transition:
//Creates a shared configuration that: applies alpha, the transition effect is applied in a cascading manner (v.s. simultaneously), MenuItems will resets to enabled when transiting, and invalidates menu on transition completion
MenuItemTransitionBuilder builder = MenuItemTransitionBuilder.transit(toolbar).alpha(1f, 0.5f).scale(1f, 0f).cascade(0.3f).visibleOnStartAnimation(true).invalidateOptionOnStopTransition(this, true);
MenuItemTransition mShrinkClose = builder.translationX(0, 30).build();
MenuItemTransition mShrinkOpen = builder.reverse().translationX(0, 30).build();
mDrawerListenerAdapter.setupOptions(this, new MenuOptionConfiguration(mShrinkOpen, R.menu.drawer), new MenuOptionConfiguration(mShrinkClose, R.menu.main));
}
//Let the adapter manage the creation of options menu:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
mDrawerListenerAdapter.onCreateOptionsMenu(this, menu);
return super.onCreateOptionsMenu(menu);
}
Source of the activity implementing the above is here, and a demo app here.
Here's a solution that's more versatile and is exactly how the MenuItem fade-out of Google Drive, Google Docs, Google Sheets, and Google Slides work.
The advantage is that when the user slide in from the left edge of the screen to open the drawer manually, or slide right when the drawer is opened to close it, the animation state is integrated with how the drawer is being opened/closed.
ProgressAnimator.java: This is the meat of the implementation, it translates a float based progression value (0f~1f) into a value that Android Animator
understands.
public class ProgressAnimator implements TimeAnimator.TimeListener {
private final List animationControls = new ArrayList<>();
private final MenuItem mMenuItem; //TODO shouldn't be here, add animation end listener
private final ImageView mImageView;
private final TimeAnimator mTimeAnim;
private final AnimatorSet mInternalAnimSet;
public ProgressAnimator(Context context, MenuItem mMenuItem) {
if (mMenuItem == null) {
mImageView = null;
} else {
mImageView = (ImageView) LayoutInflater.from(context).inflate(R.layout.menu_animation, null).findViewById(R.id.menu_animation);
mImageView.setImageDrawable(mMenuItem.getIcon());
}
this.mMenuItem = mMenuItem;
this.mInternalAnimSet = new AnimatorSet();
mTimeAnim = new TimeAnimator();
mTimeAnim.setTimeListener(this);
}
public void addAnimatorSet(AnimatorSet mAnimSet, float start, float end) {
animationControls.add(new AnimationControl(mImageView, mAnimSet, start, end));
}
public void addAnimatorSet(Object target, AnimatorSet mAnimSet, float start, float end) {
animationControls.add(new AnimationControl(target, mAnimSet, start, end));
}
public void start() {
ValueAnimator colorAnim = ObjectAnimator.ofInt(new Object() {
private int dummy;
public int getDummy() {
return dummy;
}
public void setDummy(int dummy) {
this.dummy = dummy;
}
}, "dummy", 0, 1);
colorAnim.setDuration(Integer.MAX_VALUE);
mInternalAnimSet.play(colorAnim).with(mTimeAnim);
mInternalAnimSet.start();
if (mMenuItem != null) {
mMenuItem.setActionView(mImageView);
}
for (AnimationControl ctrl : animationControls) {
ctrl.start();
}
}
public void end() {
mTimeAnim.end();
if (mMenuItem != null) {
mMenuItem.setActionView(null);
}
}
public void updateProgress(float progress) {
for (AnimationControl ctrl : animationControls) {
ctrl.updateProgress(progress);
}
}
@Override
public void onTimeUpdate(TimeAnimator animation, long totalTime, long deltaTime) {
for (AnimationControl ctrl : animationControls) {
ctrl.updateState();
}
}
}
AnimationControl.java: Controls the progression of transition.
public class AnimationControl {
private AnimatorSet mAnimSet;
private Object target;
private float start;
private float end = 1.0f;
private float progressWidth;
private long time;
private boolean started;
private long mStartDelay;
private long mDuration;
private long mTotalDuration;
public AnimationControl(AnimatorSet mAnimSet, float start, float end) {
this(null, mAnimSet, start, end);
}
public AnimationControl(Object target, AnimatorSet mAnimSet, float start, float end) {
for (Animator animator : mAnimSet.getChildAnimations()) {
if (!(animator instanceof ValueAnimator)) {
throw new UnsupportedOperationException("Only ValueAnimator and its subclasses are supported");
}
}
this.target = target;
this.mAnimSet = mAnimSet;
mStartDelay = mAnimSet.getStartDelay();
mDuration = mAnimSet.getDuration();
if (mAnimSet.getDuration() >= 0) {
long duration = mAnimSet.getDuration();
for (Animator animator : mAnimSet.getChildAnimations()) {
animator.setDuration(duration);
}
} else {
for (Animator animator : mAnimSet.getChildAnimations()) {
long endTime = animator.getStartDelay() + animator.getDuration();
if (mDuration < endTime) {
mDuration = endTime;
}
}
}
mTotalDuration = mStartDelay + mDuration;
this.start = start;
this.end = end;
progressWidth = Math.abs(end - start);
}
public void start() {
if (target != null) {
for (Animator animator : mAnimSet.getChildAnimations()) {
animator.setTarget(target);
}
}
}
public void updateProgress(float progress) {
if (start < end && progress >= start && progress <= end || start > end && progress >= end && progress <= start) {
if (start < end) {
time = (long) (mTotalDuration * (progress - start) / progressWidth);
} else {
time = (long) (mTotalDuration - mTotalDuration * (progress - end) / progressWidth);
}
time -= mStartDelay;
if (time > 0) {
started = true;
}
Log.e(getClass().getSimpleName(), "updateState: " + mTotalDuration + ";" + time+"/"+start+"/"+end);
} else {
//forward
if (start < end) {
if (progress < start) {
time = 0;
} else if (progress > end) {
time = mTotalDuration;
}
//backward
} else if (start > end) {
if (progress > start) {
time = 0;
} else if (progress > end) {
time = mTotalDuration;
}
}
started = false;
}
}
public void updateState() {
if (started) {
for (Animator animator : mAnimSet.getChildAnimations()) {
ValueAnimator va = (ValueAnimator) animator;
long absTime = time - va.getStartDelay();
if (absTime > 0) {
va.setCurrentPlayTime(absTime);
}
}
}
}
}
ProgressDrawerListener.java: This listens for DrawerLayout
state update and setup the required animation.
public class ProgressDrawerListener implements DrawerLayout.DrawerListener {
private final List mAnimatingMenuItems = new ArrayList<>();
private final Toolbar mToolbar;
private final ActionBarDrawerToggle mDrawerToggle;
private DrawerLayout.DrawerListener mDrawerListener;
private MenuItemAnimation mMenuItemAnimation;
private Animation mAnimation;
private boolean started;
private boolean mOpened;
private Activity mActivity;
private boolean mInvalidateOptionOnOpenClose;
public ProgressDrawerListener(Toolbar mToolbar, ActionBarDrawerToggle mDrawerToggle) {
this.mToolbar = mToolbar;
this.mDrawerToggle = mDrawerToggle;
}
@Override
public void onDrawerOpened(View view) {
mDrawerToggle.onDrawerOpened(view);
clearAnimation();
started = false;
if (mDrawerListener != null) {
mDrawerListener.onDrawerOpened(view);
}
mToolbar.getMenu().setGroupVisible(0, false); //TODO not always needed
mOpened=true;
mActivity.invalidateOptionsMenu();
}
@Override
public void onDrawerClosed(View view) {
mDrawerToggle.onDrawerClosed(view);
clearAnimation();
started = false;
if (mDrawerListener != null) {
mDrawerListener.onDrawerClosed(view);
}
mOpened=false;
mActivity.invalidateOptionsMenu();
}
@Override
public void onDrawerStateChanged(int state) {
mDrawerToggle.onDrawerStateChanged(state);
switch (state) {
case DrawerLayout.STATE_DRAGGING:
case DrawerLayout.STATE_SETTLING:
if (mAnimatingMenuItems.size() > 0 || started) {
break;
}
started = true;
setupAnimation();
break;
case DrawerLayout.STATE_IDLE:
clearAnimation();
started = false;
break;
}
if (mDrawerListener != null) {
mDrawerListener.onDrawerStateChanged(state);
}
}
private void setupAnimation() {
mToolbar.getMenu().setGroupVisible(0, true); //TODO not always needed
mAnimatingMenuItems.clear();
for (int i = 0; i < mToolbar.getChildCount(); i++) {
final View v = mToolbar.getChildAt(i);
if (v instanceof ActionMenuView) {
int menuItemCount = 0;
int childCount = ((ActionMenuView) v).getChildCount();
for (int j = 0; j < childCount; j++) {
if (((ActionMenuView) v).getChildAt(j) instanceof ActionMenuItemView) {
menuItemCount++;
}
}
for (int j = 0; j < childCount; j++) {
final View innerView = ((ActionMenuView) v).getChildAt(j);
if (innerView instanceof ActionMenuItemView) {
MenuItem mMenuItem = ((ActionMenuItemView) innerView).getItemData();
ProgressAnimator offsetAnimator = new ProgressAnimator(mToolbar.getContext(), mMenuItem);
if(mMenuItemAnimation!=null) {
mMenuItemAnimation.setupAnimation(mMenuItem, offsetAnimator, j, menuItemCount);
}
if(mAnimation!=null) {
mAnimation.setupAnimation(offsetAnimator);
}
offsetAnimator.start();
mAnimatingMenuItems.add(offsetAnimator);
}
}
}
}
onDrawerSlide(null, mOpened ? 1f : 0f);
Log.e(getClass().getSimpleName(), "setupAnimation: "+mAnimatingMenuItems.size()); //TODO
}
@Override
public void onDrawerSlide(View view, float slideOffset) {
for (ProgressAnimator ani : mAnimatingMenuItems) {
ani.updateProgress(slideOffset);
}
if(view==null) {
return;
}
mDrawerToggle.onDrawerSlide(view, slideOffset);
if (mDrawerListener != null) {
mDrawerListener.onDrawerSlide(view, slideOffset);
}
}
private void clearAnimation() {
for (ProgressAnimator ani : mAnimatingMenuItems) {
ani.end();
}
mAnimatingMenuItems.clear();
}
public void setDrawerListener(DrawerLayout.DrawerListener mDrawerListener) {
this.mDrawerListener = mDrawerListener;
}
public MenuItemAnimation getMenuItemAnimation() {
return mMenuItemAnimation;
}
public void setMenuItemAnimation(MenuItemAnimation mMenuItemAnimation) {
this.mMenuItemAnimation = mMenuItemAnimation;
}
public Animation getAnimation() {
return mAnimation;
}
public void setAnimation(Animation mAnimation) {
this.mAnimation = mAnimation;
}
public void setmInvalidateOptionOnOpenClose(Activity activity, boolean invalidateOptionOnOpenClose) {
mActivity=activity;
mInvalidateOptionOnOpenClose = invalidateOptionOnOpenClose;
}
public interface MenuItemAnimation {
public void setupAnimation(MenuItem mMenuItem, ProgressAnimator offsetAnimator, int itemIndex, int menuCount);
}
public interface Animation {
public void setupAnimation(ProgressAnimator offsetAnimator);
}
}
Set up in Activity: The example code below switches between two different menu options between opened and closed state. Optionally add offsetDrawerListener.setDrawerListener(DrawerListener)
if you need to have your own DrawerListener
.:
@Override
protected void onCreate(Bundle savedInstanceState) {
//other init
mProgressDrawerListener =new ProgressDrawerListener(toolbar, mDrawerToggle);
mProgressDrawerListener.setmInvalidateOptionOnOpenClose(this, true);
mOpenAnimation = new ProgressDrawerListener.MenuItemAnimation() {
@Override
public void setupAnimation(MenuItem mMenuItem, ProgressAnimator offsetAnimator, int itemIndex, int menuCount) {
MainActivity.this.setupAnimation(true, offsetAnimator, itemIndex);
}
};
mCloseAnimation = new ProgressDrawerListener.MenuItemAnimation() {
@Override
public void setupAnimation(MenuItem mMenuItem, ProgressAnimator offsetAnimator, int itemIndex, int menuCount) {
MainActivity.this.setupAnimation(false, offsetAnimator, itemIndex);
}
};
mDrawerLayout.setDrawerListener(mProgressDrawerListener);
}
//customize your animation here
private void setupAnimation(boolean open, ProgressAnimator offsetAnimator, int itemIndex) {
AnimatorSet set = new AnimatorSet();
set.playTogether(
ObjectAnimator.ofFloat(null, "alpha", 1.0f, 0f),
ObjectAnimator.ofFloat(null, "scaleX", 1.0f, 0f)
);
set.setStartDelay(itemIndex * 200);
set.setDuration(1000 - itemIndex * 200); //not the actual time the animation will be played
if(open) {
offsetAnimator.addAnimatorSet(set, 0, 1);
} else {
offsetAnimator.addAnimatorSet(set, 1, 0);
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Only show items in the action bar relevant to this screen
// if the drawer is not showing. Otherwise, let the drawer
// decide what to show in the action bar.
if(mDrawerLayout.isDrawerOpen(findViewById(R.id.drawerList))) {
getMenuInflater().inflate(R.menu.drawer, menu);
mProgressDrawerListener.setMenuItemAnimation(
mCloseAnimation);
} else {
getMenuInflater().inflate(R.menu.main, menu);
mProgressDrawerListener.setMenuItemAnimation(
mOpenAnimation);
mDrawerLayout.setDrawerListener(mProgressDrawerListener);
}
return super.onCreateOptionsMenu(menu);
}
menu_animation.xml: This is to get the custom ActionView
to have the same layout as the view used by MenuIem