How to add animation while actionbar are changing their content with Appcompat?

前端 未结 2 1835
栀梦
栀梦 2021-02-05 15:04

I\'m looking at Google\'s Material Design guidelines and I want add animated action bar. My goal is do something like this:

2条回答
  •  囚心锁ツ
    2021-02-05 15:52

    Update:

    I've created an open source library that provides transition/animation support to both View and MenuItem:

    MenuItem transition MenuItem transition

    View 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.


    Originally Accepted Answer:

    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

    
        
    

提交回复
热议问题