way2geek way2geek - 4 months ago 16
Android Question

How to slide the ActionBar along with the NavigationDrawer

What I want to do is slide the

ActionBar
along with the
NavigationDrawer
when the drawer is opened. I am currently not using any third party libraries and if at all possible I want to keep it that way. All i need is an implementation of method like:
getActionBarView.slide(dp);


This is the code I currently use to create the
NavigationDrawer
:

mDrawerToggle = new ActionBarDrawerToggle(this, drawerLayout, R.drawable.ic_drawer, R.string.drawer_open, R.string.drawer_close) {

public void onDrawerClosed(View view) {
invalidateOptionsMenu();

// calling onPrepareOptionsMenu() to hide action bar icons
}

@Override
public void onDrawerSlide(View drawerView, float slideOffset) {
if (getDeviceType(getApplicationContext()) == DEVICE_TYPE_PHONE) {
drawerLayout.setScrimColor(Color.parseColor("#00FFFFFF"));
float moveFactor = (listView.getWidth() * slideOffset);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
all_menu_container_parent.setTranslationX(moveFactor);
} else {
TranslateAnimation anim = new TranslateAnimation(lastTranslate, moveFactor, 0.0f, 0.0f);
anim.setDuration(0);
anim.setFillAfter(true);
all_menu_container_parent.startAnimation(anim);

lastTranslate = moveFactor;
}
}
}

public void onDrawerOpened(View drawerView) {
// calling onPrepareOptionsMenu() to hide action bar icons
}
};
drawerLayout.setDrawerListener(mDrawerToggle);


But it doesn't do what I want, it produces this:

I am currently stuck with this

What I want to achieve is this:

current screen shot from app

Answer

PLEASE NOTE: This answer was originally written when Android 4.4 (KitKat) was still pretty new. Since Android 5.0 and especially because of the introduction of the ToolBar this answer cannot be considered up-to-date anymore! But from a technical perspective and for those of you who want to learn about the inner workings of Android this answer might still hold a lot of value!

The NavigationDrawer was specifically designed to be situated below the ActionBar and there is no way to implement the NavigationDrawer to make the ActionBar move with it - unless maybe looking for the View which makes up the ActionBar and animating it alongside the NavigationDrawer, but I would never recommend something like this as it would be difficult and error prone. In my opinion you only have two options:

  1. Using a library like the SlidingMenu
  2. Implementing a custom sliding menu

Since you said that you don't want to use a library implementing a custom sliding menu is your only option, fortunately this is really not that hard once you know how to do it.


1) Basic Explanation

You can move the whole content of the Activity - I mean everything including the ActionBar - by putting a margin or a padding on the View which makes up the Activity. This View is the parent of the View with the id android.R.id.content:

View content = (View) activity.findViewById(android.R.id.content).getParent();

On Honeycomb (Android version 3.0 - API level 11) or above - in other words after the ActionBar was introduced - you need to use margins to change the Activities position and on previous versions you need to use a padding. To simplify this I recommend creating helper methods which perform the correct action for each API level. Let's first look at how to set the position of the Activity:

public void setActivityPosition(int x, int y) {
    // With this if statement we can check if the devices API level is above Honeycomb or below
    if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
        // On Honeycomb or abvoe we set a margin
        FrameLayout.LayoutParams contentParams = (FrameLayout.LayoutParams) this.content.getLayoutParams();
        contentParams.setMargins(x, y, -x, -y);
        this.content.setLayoutParams(contentParams);
    } else {
        // And on devices below Honeycomb we set a padding
        this.content.setPadding(x, y, -x, -y);
    }
}

Notice that in both cases there is either a negative margin or a negative padding on the opposite sides. This is to essentially increase the size of the Activity beyond its normal bounds. This prevents the actual size of the Activity to change when we slide it somewhere.

We additionally need two methods to get the current position of the Activity. One for the x position, one for the y position:

public int getActivityPositionX() {
    if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
        // On Honeycomb or above we return the left margin
        FrameLayout.LayoutParams contentParams = (FrameLayout.LayoutParams) this.content.getLayoutParams();
        return contentParams.leftMargin;
    } else {
        // On devices below Honeycomb we return the left padding
        return this.content.getPaddingLeft();
    }
}

public int getActivityPositionY() {
    if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
        // On Honeycomb or above we return the top margin
        FrameLayout.LayoutParams contentParams = (FrameLayout.LayoutParams) this.content.getLayoutParams();
        return contentParams.topMargin;
    } else {
        // On devices below Honeycomb we return the top padding
        return this.content.getPaddingTop();
    }
} 

It is also very simple to add animations. The only important thing here is a bit of math to animate it from its previous position to its new position

// We get the current position of the Activity
final int currentX = getActivityPositionX();
final int currentY = getActivityPositionY();

// The new position is set
setActivityPosition(x, y);

// We animate the Activity to slide from its previous position to its new position
TranslateAnimation animation = new TranslateAnimation(currentX - x, 0, currentY - y, 0);
animation.setDuration(500);
this.content.startAnimation(animation);

You can display a View at the location which is revealed by sliding away the Activity by adding it to the parent of the View:

final int currentX = getActivityPositionX();

FrameLayout menuContainer = new FrameLayout(context);

// The width of the menu is equal to the x position of the `Activity`
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(currentX, ViewGroup.LayoutParams.MATCH_PARENT);
menuContainer.setLayoutParams(params);

ViewGroup parent = (ViewGroup) content.getParent();
parent.addView(menuContainer);

And that is pretty much all you need to create a basic sliding menu that works on most if not all devices above Eclair (Android 2.1 - API level 7).


2) Animating the Activity

The first part of creating a sliding menu is making the Activity move out of the way. As such we should first try to move the Activity around like this:
enter image description here

To create this we just have to put the code above together:

import android.os.Build;
import android.support.v4.app.FragmentActivity;
import android.view.View;
import android.view.animation.TranslateAnimation;
import android.widget.FrameLayout;

public class ActivitySlider {

    private final FragmentActivity activity;
    private final View content;

    public ActivitySlider(FragmentActivity activity) {
        this.activity = activity;

        // Here we get the content View from the Activity.
        this.content = (View) activity.findViewById(android.R.id.content).getParent();
    }

    public void slideTo(int x, int y) {

        // We get the current position of the Activity
        final int currentX = getActivityPositionX();
        final int currentY = getActivityPositionY();

        // The new position is set
        setActivityPosition(x, y);

        // We animate the Activity to slide from its previous position to its new position
        TranslateAnimation animation = new TranslateAnimation(currentX - x, 0, currentY - y, 0);
        animation.setDuration(500);
        this.content.startAnimation(animation);
    }

    public void setActivityPosition(int x, int y) {
        // With this if statement we can check if the devices API level is above Honeycomb or below
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            // On Honeycomb or above we set a margin
            FrameLayout.LayoutParams contentParams = (FrameLayout.LayoutParams) this.content.getLayoutParams();
            contentParams.setMargins(x, y, -x, -y);
            this.content.setLayoutParams(contentParams);
        } else {
            // And on devices below Honeycomb we set a padding
            this.content.setPadding(x, y, -x, -y);
        }
    }

    public int getActivityPositionX() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            // On Honeycomb or above we return the left margin
            FrameLayout.LayoutParams contentParams = (FrameLayout.LayoutParams) this.content.getLayoutParams();
            return contentParams.leftMargin;
        } else {
            // On devices below Honeycomb we return the left padding
            return this.content.getPaddingLeft();
        }
    }

    public int getActivityPositionY() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            // On Honeycomb or above we return the top margin
            FrameLayout.LayoutParams contentParams = (FrameLayout.LayoutParams) this.content.getLayoutParams();
            return contentParams.topMargin;
        } else {
            // On devices below Honeycomb we return the top padding
            return this.content.getPaddingTop();
        }
    }
}

You can use the ActivitySlider class like this:

ActivitySlider slider = new ActivitySlider(activity);

// This would move the Activity 400 pixel to the right and 100 pixel down
slider.slideTo(400, 100);

3) Adding the sliding menu

Now we want to reveal a menu when the Activity moves out of the way like this: enter image description here
As you can see it also pushes the ActionBar to the side.

The ActivitySlider class does not need to be modified that much to create a sliding menu, basically we just add two methods, showMenu() and hideMenu(). I will stick to best practices and use a Fragment as the sliding menu. The first thing we need need is a View - for example a FrameLayout - as a container for our Fragment. We need to add this View to the parent of the View of the Activity:

// We get the View of the Activity
View content = (View) activity.findViewById(android.R.id.content).getParent();

// And its parent
ViewGroup parent = (ViewGroup)  content.getParent();

// The container for the menu Fragment is a FrameLayout
// We set an id so we can perform FragmentTransactions later on
FrameLayout menuContainer = new FrameLayout(this.activity);
menuContainer.setId(R.id.flMenuContainer);

// The visibility is set to GONE because the menu is initially hidden
menuContainer.setVisibility(View.GONE);

// The container for the menu Fragment is added to the parent
parent.addView(menuContainer);

Since we set the visibility of the container View to VISIBLE only when the sliding menu is actually open we can use the following method to check if the menu is open or closed:

public boolean isMenuVisible() {
    return this.menuContainer.getVisibility() == View.VISIBLE;
}

To set the menu Fragment we add a setter method that performs a FragmentTransaction and adds the menu Fragment to the FrameLayout:

public void setMenuFragment(Fragment fragment) {
    FragmentManager manager = this.activity.getSupportFragmentManager();
    FragmentTransaction transaction = manager.beginTransaction();
    transaction.replace(R.id.flMenuContainer, fragment);
    transaction.commit();
}

I also tend to add a second setter which instantiates the Fragment from a Class for convenience:

public <T extends Fragment> void setMenuFragment(Class<T> cls) {
    Fragment fragment = Fragment.instantiate(this.activity, cls.getName());
    setMenuFragment(fragment);
}

There is one additional important thing to consider when it comes to the menu Fragment. We are operating much further up in the View hierarchy than normally. As such we have to take things like the height of the status bar into account. If we didn't account for this the top of the menu Fragment would we be hidden behind the status bar. You can get the height of the status bar like this:

Rect rectangle = new Rect();
Window window = this.activity.getWindow();
window.getDecorView().getWindowVisibleDisplayFrame(rectangle);
final int statusBarHeight = rectangle.top;

We have to put a top margin on the container View of the menu Fragment like this:

// These are the LayoutParams for the menu Fragment
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(width, ViewGroup.LayoutParams.MATCH_PARENT);

// We put a top margin on the menu Fragment container which is equal to the status bar height
params.setMargins(0, statusBarHeight, 0, 0);
menuContainer.setLayoutParams(fragmentParams);

Finally we can put all this together:

import android.graphics.Rect;
import android.os.Build;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.animation.Animation;
import android.view.animation.TranslateAnimation;
import android.widget.FrameLayout;
import at.test.app.R;
import at.test.app.helper.LayoutHelper;

public class ActivitySlider {

    private final FragmentActivity activity;
    private final View content;
    private final FrameLayout menuContainer;

    public ActivitySlider(FragmentActivity activity) {
        this.activity = activity;

        // We get the View of the Activity
        this.content = (View) activity.findViewById(android.R.id.content).getParent();

        // And its parent
        ViewGroup parent = (ViewGroup) this.content.getParent();

        // The container for the menu Fragment is added to the parent. We set an id so we can perform FragmentTransactions later on
        this.menuContainer = new FrameLayout(this.activity);
        this.menuContainer.setId(R.id.flMenuContainer);

        // We set visibility to GONE because the menu is initially hidden
        this.menuContainer.setVisibility(View.GONE);
        parent.addView(this.menuContainer);
    }

    public <T extends Fragment> void setMenuFragment(Class<T> cls) {
        Fragment fragment = Fragment.instantiate(this.activity, cls.getName());
        setMenuFragment(fragment);
    }

    public void setMenuFragment(Fragment fragment) {
        FragmentManager manager = this.activity.getSupportFragmentManager();
        FragmentTransaction transaction = manager.beginTransaction();
        transaction.replace(R.id.flMenuContainer, fragment);
        transaction.commit();
    }

    public boolean isMenuVisible() {
        return this.menuContainer.getVisibility() == View.VISIBLE;
    }

    // We pass the width of the menu in dip to showMenu()
    public void showMenu(int dpWidth) {

        // We convert the width from dip into pixels
        final int menuWidth = LayoutHelper.dpToPixel(this.activity, dpWidth);

        // We move the Activity out of the way
        slideTo(menuWidth, 0);

        // We have to take the height of the status bar at the top into account!
        Rect rectangle = new Rect();
        Window window = this.activity.getWindow();
        window.getDecorView().getWindowVisibleDisplayFrame(rectangle);
        final int statusBarHeight = rectangle.top;

        // These are the LayoutParams for the menu Fragment
        FrameLayout.LayoutParams fragmentParams = new FrameLayout.LayoutParams(menuWidth, ViewGroup.LayoutParams.MATCH_PARENT);

        // We put a top margin on the menu Fragment container which is equal to the status bar height
        fragmentParams.setMargins(0, statusBarHeight, 0, 0);
        this.menuContainer.setLayoutParams(fragmentParams);

        // Perform the animation only if the menu is not visible
        if(!isMenuVisible()) {

            // Visibility of the menu container View is set to VISIBLE
            this.menuContainer.setVisibility(View.VISIBLE);

            // The menu slides in from the right
            TranslateAnimation animation = new TranslateAnimation(-menuWidth, 0, 0, 0);
            animation.setDuration(500);
            this.menuContainer.startAnimation(animation);
        }
    }

    public void hideMenu() {

        // We can only hide the menu if it is visible
        if(isMenuVisible()) {

            // We slide the Activity back to its original position
            slideTo(0, 0);

            // We need the width of the menu to properly animate it
            final int menuWidth = this.menuContainer.getWidth();

            // Now we need an extra animation for the menu fragment container
            TranslateAnimation menuAnimation = new TranslateAnimation(0, -menuWidth, 0, 0);
            menuAnimation.setDuration(500);
            menuAnimation.setAnimationListener(new Animation.AnimationListener() {
                @Override
                public void onAnimationStart(Animation animation) {

                }

                @Override
                public void onAnimationEnd(Animation animation) {
                    // As soon as the hide animation is finished we set the visibility of the fragment container back to GONE
                    menuContainer.setVisibility(View.GONE);
                }

                @Override
                public void onAnimationRepeat(Animation animation) {

                }
            });
            this.menuContainer.startAnimation(menuAnimation);
        }
    }

    public void slideTo(int x, int y) {

        // We get the current position of the Activity
        final int currentX = getActivityPositionX();
        final int currentY = getActivityPositionY();

        // The new position is set
        setActivityPosition(x, y);

        // We animate the Activity to slide from its previous position to its new position
        TranslateAnimation animation = new TranslateAnimation(currentX - x, 0, currentY - y, 0);
        animation.setDuration(500);
        this.content.startAnimation(animation);
    }

    public void setActivityPosition(int x, int y) {
        // With this if statement we can check if the devices API level is above Honeycomb or below
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            // On Honeycomb or above we set a margin
            FrameLayout.LayoutParams contentParams = (FrameLayout.LayoutParams) this.content.getLayoutParams();
            contentParams.setMargins(x, y, -x, -y);
            this.content.setLayoutParams(contentParams);
        } else {
            // And on devices below Honeycomb we set a padding
            this.content.setPadding(x, y, -x, -y);
        }
    }

    public int getActivityPositionX() {
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            // On Honeycomb or above we return the left margin
            FrameLayout.LayoutParams contentParams = (FrameLayout.LayoutParams) this.content.getLayoutParams();
            return contentParams.leftMargin;
        } else {
            // On devices below Honeycomb we return the left padding
            return this.content.getPaddingLeft();
        }
    }

    public int getActivityPositionY() {
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            // On Honeycomb or above we return the top margin
            FrameLayout.LayoutParams contentParams = (FrameLayout.LayoutParams) this.content.getLayoutParams();
            return contentParams.topMargin;
        } else {
            // On devices below Honeycomb we return the top padding
            return this.content.getPaddingTop();
        }
    }
}

I use a static helper method in showMenu() to convert dip to pixels. Here is the code of this method:

public static int dpToPixel(Context context, int dp) {
    float scale = getDisplayDensityFactor(context);
    return (int) (dp * scale + 0.5f);
}

private static float getDisplayDensityFactor(Context context) {
    if (context != null) {
        Resources res = context.getResources();
        if (res != null) {
            DisplayMetrics metrics = res.getDisplayMetrics();
            if(metrics != null) {
                return metrics.density;
            }
        }
    }
    return 1.0f;
}

You can use this new version of the ActivitySlider class like this:

ActivitySlider slider = new ActivitySlider(activity);
slider.setMenuFragment(MenuFragment.class);

// The menu is shown with a width of 200 dip
slider.showMenu(200);

...

// Hide the menu again
slider.hideMenu();

4) Conclusion & Testing

Doing something like this is surprisingly easy when you know that you can simply put a margin or a padding on the View of the Activity. But the difficulty is in making it work on a lot of different devices. Implementations can change a lot across multiple API Levels and that can have considerable influence on how this behaves. Having said that any code I posted here should work on most if not all devices above Eclair (Android 2.1 - API level 7) without any problems.
Of course the solution I posted here is not complete, it could use a little extra polishing and cleaning up, so feel free to improve the code to suit your needs!

I have tested everything on the following devices:

HTC

  • One M8 (Android 4.4.2 - KitKat): Working
  • Sensation (Android 4.0.3 - Ice Cream Sandwich): Working
  • Desire (Android 2.3.3 - Gingerbread): Working
  • One (Android 4.4.2 - KitKat): Working

Samsung

  • Galaxy S3 Mini (Android 4.1.2 - Jelly Bean): Working
  • Galaxy S4 Mini (Android 4.2.2 - Jelly Bean): Working
  • Galaxy S4 (Android 4.4.2 - KitKat): Working
  • Galaxy S5 (Android 4.4.2 - KitKat): Working
  • Galaxy S Plus (Android 2.3.3 - Gingerbread): Working
  • Galaxy Ace (Android 2.3.6 - Gingerbread): Working
  • Galaxy S2 (Android 4.1.2 - Jelly Bean): Working
  • Galaxy S3 (Android 4.3 - Jelly Bean): Working
  • Galaxy Note 2 (Android 4.3 - Jelly Bean): Working
  • Galaxy Nexus (Android 4.2.1 - Jelly Bean): Working

Motorola

  • Moto G (Android 4.4.2 - KitKat): Working

LG

  • Nexus 5 (Android 4.4.2 - KitKat): Working

ZTE

  • Blade (Android 2.1 - Eclair): Working

I hope I could help you and if you have any further questions or anything else is unclear please feel free to ask!

Comments