Как добавить анимацию, когда панель действий меняет свой контент с помощью Appcompat?

Я смотрю рекомендации Google Material Design и хочу добавить анимированную панель действий. Моя цель - сделать что-то вроде этого:

enter image description here

Как добавить переход для содержимого панели действий? Я использую Appcompat для обеспечения обратной совместимости.

Ответы

Ответ 1

Обновление:

Я создал библиотеку с открытым исходным кодом, которая обеспечивает поддержку перехода/анимации как для View, так и для MenuItem:

MenuItem transition Переключение MenuItem

View transition Просмотреть переход

Инструкция:

В Android Studio добавьте код ниже в Gradle зависимости:

compile 'com.github.kaichunlin.transition:core:0.8.1'

Пример кода с пояснениями:

  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);
  }

Источник реализации этой функции здесь и демонстрационное приложение .


Первоначально принятый ответ:

Здесь более универсальное решение, а именно то, как работает MenuItem из Google Диска, Документов Google, Google Таблиц и Google Презентаций.

Преимущество заключается в том, что когда пользователь скользит по левому краю экрана, чтобы открыть ящик вручную или сдвигаться вправо, когда ящик открыт, чтобы закрыть его, состояние анимации интегрировано с тем, как ящик открыто/закрыто.


ProgressAnimator.java. Это мясо реализации, оно преобразует значение прогрессии на основе float (0f ~ 1f) в значение, которое понимает Android Animator.

public class ProgressAnimator implements TimeAnimator.TimeListener {
    private final List<AnimationControl> 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: управляет прогрессией перехода.

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: он прослушивает обновление состояния DrawerLayout и настраивает необходимую анимацию.

public class ProgressDrawerListener implements DrawerLayout.DrawerListener {
private final List<ProgressAnimator> 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);
}

}

Настроить в действии: приведенный ниже примерный код переключает между двумя различными опциями меню между открытыми и закрытыми состояниями. При необходимости добавьте offsetDrawerListener.setDrawerListener(DrawerListener), если вам нужно иметь свой собственный 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. Это означает, что пользовательский ActionView имеет тот же макет, что и представление, используемое MenuIem

<?xml version="1.0" encoding="utf-8"?>
    <ImageView xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/menu_animation"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:minWidth="@*android:dimen/action_button_min_width"
        android:padding="8dp"
        style="@style/Widget.AppCompat.ActionBar" />

Ответ 2

Я думаю, что наконец нашел ваш ответ. Это было труднее найти, чем я думал. Если вы посмотрите на эту ссылку: http://suhan.in/material-design-toolbar-animation/

В первом объясняется, как это делается.

Ниже вы найдете мой собственный фрагмент кода о том, как это можно сделать только с элементами меню:

for(int i = 0; i < toolbarView.getChildCount(); i++) 
{
    final View v = toolbarView.getChildAt(i);

    if(v instanceof ActionMenuView) 
    {
        for(int j = 0; j < ((ActionMenuView)v).getChildCount(); j++) 
        {
            final View innerView = ((ActionMenuView)v).getChildAt(j);

            if(innerView instanceof ActionMenuItemView) 
            {
                innerView.setTranslationY(-30);
                innerView.animate().setStartDelay(100 + (j * 10)).setDuration(200).translationY(0);
            }
        }
    }
}

Это анимация для оси Y. Вы также можете добавить анимацию для размера, которую я думаю, что они делают в руководстве по дизайну. Также, если вы не хотите, чтобы они запускались одновременно, вы можете установить startDelay и добавить дополнительные как это: setStartDelay(i * 10). Таким образом, каждый элемент запускает анимацию немного позже. Я уже помещал это в фрагмент кода, но настраивал его так, как вам бы хотелось.