如何在使用Appcompat时添加动画效果来改变ActionBar的内容?

14
我正在查看Google的材料设计指南,并想要添加动画操作栏。我的目标是做出像这样的东西:

enter image description here

如何为操作栏的内容添加过渡效果?我正在使用Appcompat保持向后兼容。

所以你想让操作栏图标在导航抽屉打开和关闭之间变化,是吗? - Xcihnegn
不要直接使用导航抽屉。我只想要菜单的动画更改(例如,当主活动的片段更新时)。 - Siper
@Siper 我更新了我的答案,实现了相同的结果,但更加强大且模块化更好,请查看并告诉我。 - Kai
@Kai 这看起来太棒了! :D - Siper
2个回答

4
我想我终于找到了你的答案。这比我想象的要困难得多。如果您查看此链接: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)。这样每个项目都会稍后开始动画效果。我已经将此代码段放入代码片段中,但请按照您的喜好进行微调。


是的,这个可以用,但还需要很多改进。@Siper只需要将操作项进行动画处理,而不需要标题和标志。 - Lamorak
是的,它可以用来动画化菜单项,但它仍然没有解决交叉淡入淡出两个菜单的主要问题。 - Lamorak
@Lamorak 另一个已经被批准,所以你可以使用那个。如果你想使用我的,我可以为你找出如何进行交叉淡入淡出的方法。让我知道。 - Kevin van Mierlo

4

更新:

我创建了一个开源库,为View和MenuItem提供了过渡/动画支持:

菜单项转换 菜单项转换

视图转换 视图转换

使用方法:

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

以上实现的活动来源可以在这里找到,演示应用程序可以在这里找到。


原始答案:

以下是一种更通用的解决方案,正是Google Drive、Google Docs、Google Sheets和Google Slides中的菜单项淡出效果的实现方式。

优点在于,当用户从屏幕左边缘滑入手动打开抽屉或在打开抽屉时向右滑动以关闭它时,动画状态与抽屉的打开/关闭方式集成在一起


ProgressAnimator.java:这是实现的主要部分,它将基于浮点数的进度值(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);
}

在Activity中设置: 下面的示例代码在打开和关闭状态之间切换两个不同的菜单选项。如果需要自己的DrawerListener,可以选择添加offsetDrawerListener.setDrawerListener(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" />

你能指导我在哪里查找如何扩展 MenuItem Transition 以便在没有 Navigation Drawer 的情况下使用它吗?例如通过调用函数启动转换。 - Siper
@Siper 最简单的方法是使用提供的适配器(抽屉式、ViewPager、滑动面板)之一,或者编写自己的适配器。您有什么场景需要考虑? - Kai
我想通过更新主活动的片段来更改菜单项(在我的应用程序中,这是带有ListItem的活动,当我点击项目时,片段会更新并显示有关所选项目的详细信息。我的目标也是更改片段菜单:在列表项片段上显示设置等,在详细信息上显示例如共享按钮、删除...)。我现在正在查看MenuItemTransition类。 - Siper
1
@Siper 在这种情况下,最好的方法是创建一个“适配器”,将所需的动画时间转换为[0..1]浮点值,以传递给ITransition。虽然在内部,[0..1]浮点值会再次转换为整数值,但这是一种绕路的方式。但是,在不同情况下具有一致的View/MenuItem动画/过渡方法的背景下,这确实是有意义的。我今天或明天应该能够轻松地创建一个适配器,听起来很有用 :P - Kai
@Siper 这是一个新的适配器,在我的(有限的)测试中运行良好,请尝试并告诉我它的效果如何。设置方法如下:mAnimationAdapter=new AnimationAdapter(); mAnimationAdapter.setupOpenOption(this, new MenuOptionConfiguration(mShrinkOpen, R.menu.drawer)); 运行方法如下:mAnimationAdapter.startAnimation(duration); - Kai
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接