Monday, 28 September 2015

Morphing animation for Toolbar back button and Navigation drawer icon (Android)


It is common to use one of the third-party libs to create a morphing animation for icon, I have used the popular material-menu library. With this libray it is easy to use to morph animation for the navigation drawer icon, or any other icon in ui. However, the problem came up when I wanted to animate the transition from the Navigation drawer burger icon to Toolbar back button. The scenario is as follows. There is a Navigation drawer icon on the left side of Toolbar, also there is a collapsed SearchView in Toolbar. When the user clicks on search icon on the left side of Actionbar, SearchView expands and the burger icons turns into the back icon. Pressing this back button hides SearchView. The desired effect is to morph the back button into the hamburger icon and vice versa.

This is easier said then done because the burger button and back button are actually two different buttons. But, looking into Android source for android.support.v7.widget.Toolbar it is obvious that the two buttons are actually mNavButtonView and mCollapseButtonView, and they luckily have the same layout parameters and positioning (see ensureNavButtonView() and ensureCollapseButtonView() methods).

Now, the basic idea is to setup the same morphing drawable on both buttons, and play animation on them simultaneously - and hence animation will always be seen irrespective of the current visible button. For the Navigation drawer button drawable can be set simply with the call to setHomeAsUpIndicator(). To set the back button drawable we must use reflection to get to the mCollapseButtonView field and ensureCollapseButtonView() method. We need to call the ensureCollapseButtonView() method first to make sure that the mCollapseButtonView is created.


import com.balysv.materialmenu.MaterialMenuDrawable;

...


private MaterialMenuDrawable mToolbarMorphDrawable;
private MaterialMenuDrawable mSearchViewMorphDrawable;

... 

private void setupActionBar(final Toolbar toolbar) {
  
mToolbarMorphDrawable = new MaterialMenuDrawable(this, Color.BLACK,
                                           MaterialMenuDrawable.Stroke.THIN);
    
mToolbarMorphDrawable.setIconState(MaterialMenuDrawable.IconState.BURGER);
 
mSearchViewMorphDrawable = new MaterialMenuDrawable(this, Color.BLACK, 
                                           MaterialMenuDrawable.Stroke.THIN);

mSearchViewMorphDrawable.setIconState(MaterialMenuDrawable.IconState.BURGER);

Toolbar toolbar = setupActionBar((Toolbar) findViewById(R.id.upperToolbar));
setSupportActionBar(toolbar);
final ActionBar ab = getSupportActionBar();
if (ab != null) {
    ab.setHomeAsUpIndicator(mToolbarMorphDrawable);
    ab.setDisplayHomeAsUpEnabled(true);
}

try {
    Method ensureCollapseButtonView = android.support.v7.widget.Toolbar.class
                        .getDeclaredMethod("ensureCollapseButtonView", null);

    ensureCollapseButtonView.setAccessible(true);
    ensureCollapseButtonView.invoke(toolbar, null);

    Field collapseButtonViewField = android.support.v7.widget.Toolbar.class.
                                    getDeclaredField("mCollapseButtonView");

    collapseButtonViewField.setAccessible(true);

    ImageButton imageButtonCollapse = (ImageButton) collapseButtonViewField
                                                             .get(toolbar);

    imageButtonCollapse.setImageDrawable(mSearchViewMorphDrawable);
}
catch (Exception e) {
    // Something went wrong, let the app work without morphing the buttons :)
    e.printStackTrace(); 
}
}


That is the OnActionExpandListener that starts morphing animation when the expanded action view (SearchView in our case) expands and collapses.

MenuItemCompat.setOnActionExpandListener(searchMenuItem, 
                            new MenuItemCompat.OnActionExpandListener() {
            @Override
            public boolean onMenuItemActionCollapse(MenuItem item) {
                mToolbarMorphDrawable.animateIconState(MaterialMenuDrawable
                                                         .IconState.BURGER);
                mSearchViewMorphDrawable.animateIconState(MaterialMenuDrawable
                                                         .IconState.BURGER);
                return true;
            }

            @Override
            public boolean onMenuItemActionExpand(MenuItem item) {
                mToolbarMorphDrawable.animateIconState(MaterialMenuDrawable
                                                          .IconState.ARROW);
                mSearchViewMorphDrawable.animateIconState(MaterialMenuDrawable
                                                          .IconState.ARROW);
                return true;
            }
        });


You might ask, why there are two separate drawables for these two buttons. Yes, we might have designated one drawable instance but then the animation wouldn't work as expected because one of the buttons "locks" the drawable and this makes the animation to freeze when another button becomes invisible.

Note that the reflection is used on the android.support.v7.widget.Toolbar.class, but if you use android.widget.Toolbar then change this line. Other than that android.widget.Toolbar seems to have the same fields and methods names so that you can try the same technique for it.