Hide navigation drawer when user presses back button - android

I've followed Google's official developer tutorials here to create a navigation drawer.
At the moment, everything works fine, except for when the user uses the native back button Android provides at the bottom of the screen (along with the home and recent app buttons). If the user navigates back using this native back button, the navigation drawer will still be open. If the user instead navigates back using the ActionBar, the navigation drawer will be closed like I want it to be.
My code is nearly identical to the official tutorials, except for how I handle the user selecting an item on the drawer:
mDrawerList.setOnItemClickListener(new ListView.OnItemClickListener()
{
#Override
public void onItemClick(AdapterView parent, View view, int position, long id)
{
switch(position)
{
case 0:
{
Intent intent = new Intent(MainActivity.this, NextActivity.class);
startActivity(intent);
}
}
}
});
How can I have the navigation drawer be closed when the user navigates back using the native back button? Any advice appreciated. Thanks!

You have to override onBackPressed(). From the docs :
Called when the activity has detected the user's press of the back
key. The default implementation simply finishes the current activity,
but you can override this to do whatever you want.
So you can have code like this :
#Override
public void onBackPressed() {
if (this.drawerLayout.isDrawerOpen(GravityCompat.START)) {
this.drawerLayout.closeDrawer(GravityCompat.START);
} else {
super.onBackPressed();
}
}
If is open this method closes it, else falls back to the default behavior.

You need to override onBackPressed() in your activity and check for the condition where the navigation drawer is open. If it is open, then close it, else do a normal back pressed method. Here is some code mixed with some pseudocode to help you:
#Override
public void onBackPressed(){
if(drawer.isDrawerOpen()){ //replace this with actual function which returns if the drawer is open
drawer.close(); // replace this with actual function which closes drawer
}
else{
super.onBackPressed();
}
}
To replace the pseudocode look in the documentation for the drawer. I know both those methods exist.

Here is an alternative solution to your problem.
#Override
public void onBackPressed(){
if(drawerLayout.isDrawerOpen(navigationView)){
drawerLayout.closeDrawer(navigationView);
}else {
finish();
}
}

UPDATE:
As of support library 24.0.0 this is possible without any workarounds. Two new openDrawer and closeDrawer methods have been added to DrawerLayout that allow the drawer to be opened or closed with no animation.
You can now use openDrawer(drawerView, false) and closeDrawer(drawerView, false) to open and close the drawer with no delay.
If you call startActivity() without calling closeDrawer(), the drawer will be left open in that instance of the activity when you navigate back to it using the back button. Calling closeDrawer() when you call startActivity() has several issues, ranging from choppy animation to a long perceptual delay, depending on which workaround you use. So I agree the best approach is to just call startActivity() and then close the drawer upon return.
To make this work nicely, you need a way to close the drawer without a close animation when navigating back to the activity with the back button. (A relatively wasteful workaround would be to just force the activity to recreate() when navigating back, but it's possible to solve this without doing that.)
You also need to make sure you only close the drawer if you're returning after navigating, and not after an orientation change, but that's easy.
Details
(You can skip past this explanation if you just want to see the code.)
Although calling closeDrawer() from onCreate() will make the drawer start out closed without any animation, the same is not true from onResume(). Calling closeDrawer() from onResume() will close the drawer with an animation that is momentarily visible to the user. DrawerLayout doesn't provide any method to close the drawer without that animation, but it's possible to extend it in order to add one.
Closing the drawer actually just slides it off the screen, so you can effectively skip the animation by moving the drawer directly to its "closed" position. The translation direction will vary according to the gravity (whether it's a left or right drawer), and the exact position depends on the size of the drawer once it's laid out with all its children.
However, simply moving it isn't quite enough, as DrawerLayout keeps some internal state in extended LayoutParams that it uses to know whether the drawer is open. If you just move the drawer off screen, it won't know that it's closed, and that will cause other problems. (For example, the drawer will reappear on the next orientation change.)
Since you're compiling the support library into your app, you can create a class in the android.support.v4.widget package to gain access to its default (package-private) parts, or extend DrawerLayout without copying over any of the other classes it needs. This will also reduce the burden of updating your code with future changes to the support library. (It's always best to insulate your code from implementation details as much as possible.) You can use moveDrawerToOffset() to move the drawer, and set the LayoutParams so it will know that the drawer is closed.
Code
This is the code that'll skip the animation:
// move drawer directly to the closed position
moveDrawerToOffset(drawerView, 0.f);
// set internal state so DrawerLayout knows it's closed
final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams();
lp.onScreen = 0.f;
lp.knownOpen = false;
invalidate();
Note: if you just call moveDrawerToOffset() without changing the LayoutParams, the drawer will move back to its open position on the next orientation change.
Option 1 (use existing DrawerLayout)
This approach adds a utility class to the support.v4 package to gain access to the package-private parts we need inside DrawerLayout.
Place this class into /src/android/support/v4/widget/:
package android.support.v4.widget;
import android.support.annotation.IntDef;
import android.support.v4.view.GravityCompat;
import android.view.Gravity;
import android.view.View;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
public class Support4Widget {
/** #hide */
#IntDef({Gravity.LEFT, Gravity.RIGHT, GravityCompat.START, GravityCompat.END})
#Retention(RetentionPolicy.SOURCE)
private #interface EdgeGravity {}
public static void setDrawerClosed(DrawerLayout drawerLayout, #EdgeGravity int gravity) {
final View drawerView = drawerLayout.findDrawerWithGravity(gravity);
if (drawerView == null) {
throw new IllegalArgumentException("No drawer view found with gravity " +
DrawerLayout.gravityToString(gravity));
}
// move drawer directly to the closed position
drawerLayout.moveDrawerToOffset(drawerView, 0.f);
// set internal state so DrawerLayout knows it's closed
final DrawerLayout.LayoutParams lp = (DrawerLayout.LayoutParams) drawerView.getLayoutParams();
lp.onScreen = 0.f;
lp.knownOpen = false;
drawerLayout.invalidate();
}
}
Set a boolean in your activity when you navigate away, indicating the drawer should be closed:
public static final String CLOSE_NAV_DRAWER = "CLOSE_NAV_DRAWER";
private boolean mCloseNavDrawer;
#Override
public void onCreate(Bundle savedInstanceState) {
// ...
if (savedInstanceState != null) {
mCloseNavDrawer = savedInstanceState.getBoolean(CLOSE_NAV_DRAWER);
}
}
#Override
public boolean onNavigationItemSelected(MenuItem menuItem) {
// ...
startActivity(intent);
mCloseNavDrawer = true;
}
#Override
public void onSaveInstanceState(Bundle savedInstanceState) {
savedInstanceState.putBoolean(CLOSE_NAV_DRAWER, mCloseNavDrawer);
super.onSaveInstanceState(savedInstanceState);
}
...and use the setDrawerClosed() method to shut the drawer in onResume() with no animation:
#Overrid6e
protected void onResume() {
super.onResume();
if(mCloseNavDrawer && mDrawerLayout != null && mDrawerLayout.isDrawerOpen(GravityCompat.START)) {
Support4Widget.setDrawerClosed(mDrawerLayout, GravityCompat.START);
mCloseNavDrawer = false;
}
}
Option 2 (extend from DrawerLayout)
This approach extends DrawerLayout to add a setDrawerClosed() method.
Place this class into /src/android/support/v4/widget/:
package android.support.v4.widget;
import android.content.Context;
import android.support.annotation.IntDef;
import android.support.v4.view.GravityCompat;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
public class CustomDrawerLayout extends DrawerLayout {
/** #hide */
#IntDef({Gravity.LEFT, Gravity.RIGHT, GravityCompat.START, GravityCompat.END})
#Retention(RetentionPolicy.SOURCE)
private #interface EdgeGravity {}
public CustomDrawerLayout(Context context) {
super(context);
}
public CustomDrawerLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomDrawerLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public void setDrawerClosed(View drawerView) {
if (!isDrawerView(drawerView)) {
throw new IllegalArgumentException("View " + drawerView + " is not a sliding drawer");
}
// move drawer directly to the closed position
moveDrawerToOffset(drawerView, 0.f);
// set internal state so DrawerLayout knows it's closed
final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams();
lp.onScreen = 0.f;
lp.knownOpen = false;
invalidate();
}
public void setDrawerClosed(#EdgeGravity int gravity) {
final View drawerView = findDrawerWithGravity(gravity);
if (drawerView == null) {
throw new IllegalArgumentException("No drawer view found with gravity " +
gravityToString(gravity));
}
// move drawer directly to the closed position
moveDrawerToOffset(drawerView, 0.f);
// set internal state so DrawerLayout knows it's closed
final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams();
lp.onScreen = 0.f;
lp.knownOpen = false;
invalidate();
}
}
Use CustomDrawerLayout instead of DrawerLayout in your activity layouts:
<android.support.v4.widget.CustomDrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
>
...and set a boolean in your activity when you navigate away, indicating the drawer should be closed:
public static final String CLOSE_NAV_DRAWER = "CLOSE_NAV_DRAWER";
private boolean mCloseNavDrawer;
#Override
public void onCreate(Bundle savedInstanceState) {
// ...
if (savedInstanceState != null) {
mCloseNavDrawer = savedInstanceState.getBoolean(CLOSE_NAV_DRAWER);
}
}
#Override
public boolean onNavigationItemSelected(MenuItem menuItem) {
// ...
startActivity(intent);
mCloseNavDrawer = true;
}
#Override
public void onSaveInstanceState(Bundle savedInstanceState) {
savedInstanceState.putBoolean(CLOSE_NAV_DRAWER, mCloseNavDrawer);
super.onSaveInstanceState(savedInstanceState);
}
...and use the setDrawerClosed() method to shut the drawer in onResume() with no animation:
#Overrid6e
protected void onResume() {
super.onResume();
if(mCloseNavDrawer && mDrawerLayout != null && mDrawerLayout.isDrawerOpen(GravityCompat.START)) {
mDrawerLayout.setDrawerClosed(GravityCompat.START);
mCloseNavDrawer = false;
}
}

Using an implementation of the answer provided by #James Cross worked, but the animation to close the drawer was undesirable and unfixable without much hassle, example.
#Override
public void onResume()
{
super.onResume();
mDrawerLayout.closeDrawers();
}
A work-around is to restart the activity when the device back button is pressed. It does not seem ideal to me, but it works. Overriding onBackPressed(), as suggested by #mt0s and #Qazi Ahmed and passing an extra to determine the calling activity:
mDrawerList.setOnItemClickListener(new ListView.OnItemClickListener()
{
#Override
public void onItemClick(AdapterView parent, View view, int position, long id)
{
switch(position)
{
case 0:
{
Intent intent = new Intent(MainActivity.this, NextActivity.class);
//pass int extra to determine calling activity
intent.putExtra(EXTRA_CALLING_ACTIVITY, CallingActivityInterface.MAIN_ACTIVITY);
startActivity(intent);
}
}
}
});
In NextActivity.class, check for the calling activity:
#Override
public void onBackPressed()
{
int callingActivity = getIntent().getIntExtra(EXTRA_CALLING_ACTIVITY, CallingActivityInterface.MAIN_ACTIVITY);
switch(callingActivity)
{
case CallingActivityInterface.MAIN_ACTIVITY:
{
Intent intent = new Intent(this, MainActivity.class);
startActivity(intent);
finish();
}
...
}
}
This way the drawer is closed with no animation when I return to MainActivity regardless of whether I use the up button or the back button. There are probably better ways to do this. My app is relatively simple at the moment and this works, but I await a more effective method if anyone has one.

Why the hassle? Simply close the Drawer when clicking a drawer item. That's how it's done in the official Google Play app.
private class DrawerItemClickListener implements ListView.OnItemClickListener {
#Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
drawerLayout.closeDrawer(GravityCompat.START, false);
selectItem(position);
}
}

You will probably want to make sure the navigation draw is always closed when the activity is opened. Use this to do that:
#Override
public void onResume(){
mDrawerList.closeDrawer(Gravity.LEFT);
}

simple sample:
Drawer resultDrawer;
public void onBackPressed(){
if (this.resultDrawer.isDrawerOpen()) {
this.resultDrawer.closeDrawer();
} else {
super.onBackPressed();
}
}

With androidx.drawerlayout:drawerlayout:1.1.0 or higher, you can keep it simple using isOpen and close().
// YourActivity.kt
override fun onBackPressed() {
if (drawerLayout.isOpen) {
drawerLayout.close()
} else {
super.onBackPressed()
}
}

This how i did it:
#Override
public void onBackPressed() {
if(drawerLayout.isDrawerOpen(navigationView)){
drawerLayout.closeDrawer(Gravity.LEFT);
}else{
super.onBackPressed();
}
}

JETPACK COMPOSE
For someone that using jetpack compose.
use this code in your scaffold:
BackHandler(enabled = drawerState.isOpen) {
scope.launch { drawerState.close() }
}
complete version:
val scope = rememberCoroutineScope()
val drawerState = rememberDrawerState(DrawerValue.Closed)
Scaffold(
topBar = {},
bottomBar = {},
snackbarHost = {},
content = {
...
BackHandler(enabled = drawerState.isOpen) {
scope.launch { drawerState.close() }
}
},
...
)

Related

Drawer Toggle button transitions with fragment navigation

I'm trying to write an app with a toolbar navigation using fragments exactly like the Gmail app: You have a drawer toggle shown as the "hamburger" button, when you click on a mail, the hamburger makes a transition to the back button and vice-versa.
As of now, I've been able to achieve something very close to what I want, except for the toggle button that is not "animating" from Hamburger to back arrow.
What I did is to bind a Listener for the BackStack in the mainActivity:
SupportFragmentManager.AddOnBackStackChangedListener(this);
Then from Fragment A, I can load fragment B adding it to the Back Stack:
ResultFragment fragment = new ResultFragment();
this.Activity.SupportFragmentManager.BeginTransaction()
.Replace(Resource.Id.main_fragment, fragment)
.AddToBackStack("results")
.Commit();
In the Main Activity the Listener checks for Backstack, if is not empty it switches the hamburger to the backbutton:
bool canGoBack = SupportFragmentManager.BackStackEntryCount > 0;
if (canGoBack)
{
//Showing Back Button
if (!_toolbarNavigationListererSet)
{
drawerToggle.DrawerIndicatorEnabled = false;
SupportActionBar.SetDisplayHomeAsUpEnabled(true);
BackHandler backHandler = new BackHandler(this);
drawerToggle.ToolbarNavigationClickListener = backHandler;
_toolbarNavigationListererSet = true;
}
}
else
{
//Show the hamburger
SupportActionBar.SetDisplayHomeAsUpEnabled(false);
drawerToggle.DrawerIndicatorEnabled = true;
drawerToggle.ToolbarNavigationClickListener = null;
_toolbarNavigationListererSet = false;
}
The drawerToggle.ToolbarNavigationClickListener changes the behaviour of the back button to call the back button press event, like:
Activity.OnBackPressed();
nothing more.
I understand that by doing this, the hamburger is hidden and the back button is shown, and viceversa, so I'm sure that's the reason why I cannot see the animation.
What am I missing? Thank you for your help.
P.s. The code is written in C# as I'm using Xamarin.Android but Java code and/or Android Native Code is well accepted as a suggestion.
You could add a animation when you press the back button, like this :
ValueAnimator anim = ValueAnimator.OfFloat(0f, 1.0f);
anim.AddUpdateListener(new AnimatorUpdateListener(this));
anim.SetInterpolator(new DecelerateInterpolator());
anim.SetDuration(500);
anim.Start();
public class AnimatorUpdateListener : Java.Lang.Object, ValueAnimator.IAnimatorUpdateListener
{
private MainActivity mContext;
public AnimatorUpdateListener(MainActivity context)
{
mContext = context;
}
public void OnAnimationUpdate(ValueAnimator valueAnimator)
{
var slideOffset = (System.Single)valueAnimator.AnimatedValue;
mContext.drawerToggle.OnDrawerSlide(mContext.drawerLayout, slideOffset);
}
}

Does SimpleExoPlayerView have controller visibility changed events?

I'm trying to implement a full screen mode with SimpleExoPlayerView. I've got this mostly working using setSystemUiVisibility.
During onCreate i add a OnSystemUiVisibilityChange listener to sync hiding the player controls with the actionbar.
#Override
public void onCreate(Bundle savedInstanceState) {
View decorView = getWindow().getDecorView();
decorView.setOnSystemUiVisibilityChangeListener
(onSystemUiChange());
hideSystemUI();
}
In the OnSystemUiVisibilityChangeListener i'm also setting a timeout that matches the simpleExoplayerViews timeout so the controls and action bar are hidden at the same time.
#NonNull
private View.OnSystemUiVisibilityChangeListener onSystemUiChange() {
return new View.OnSystemUiVisibilityChangeListener() {
#Override
public void onSystemUiVisibilityChange(int visibility) {
if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
mSimpleExoPlayerView.showController();
final Handler handler = new Handler();
handler.postDelayed(new Runnable() {
#Override
public void run() {
//sync the hide system ui with
//simpleExoPlayerView's auto hide timeout
hideSystemUI();
}
}, mSimpleExoPlayerView.getControllerShowTimeoutMs());
} else {
mSimpleExoPlayerView.hideController();
}
}
};
}
private void hideSystemUI() {
View rootView = findViewById(R.id.root);
rootView.setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // hide nav bar
| View.SYSTEM_UI_FLAG_FULLSCREEN // hide status bar
);
}
This works pretty well except in one case. If you tap the screen and then tap it again before the SimpleExoPlayerView controls timeout the SimpleExoPlayerView are hidden but the system ui do not get set until the timeout. Is there any events i can hook into instead?
I've tried setting a onClick and onTouch listener for my root layout but these events are not fired, i suspect SimpleExoPlayerView might be swallowing them?
ExoPlayer 2.10.4 has it.
exoplayer PlayerView has a method called
public void setControllerVisibilityListener(PlayerControlView.VisibilityListener listener) {
}
As of 2.6.1, SimpleExoPlayerView doesn't seem to have any visibility change listeners for the controls, but PlaybackControlView has. However, it's stored in a private field in SimpleExoPlayerView and there's no builtin way to a access it. To set your own listener, you'll either have to:
copy SimpleExoPlayerView.java to your project and make the required changes,
use reflection (don't forget to add proguard rules, if needed),
override exo_simple_player_view.xml and make sure it contains a PlaybackControlView, then find it using findViewById,
find it manually by traversing the view hierarchy.
In my opinion, the first and third options are the nicest, but the last one requires the least amount of changes, and it also works very well. Here is an example:
import com.google.android.exoplayer2.ui.PlaybackControlView;
import com.google.android.exoplayer2.ui.SimpleExoPlayerView;
public SomeActivity extends Activity implements PlaybackControlView.VisibilityListener {
private initExoPlayer() {
// ...
addPlaybackControlVisibilityListener(mSimpleExoPlayerView, this);
}
#Override
public void onVisibilityChange(int visibility) {
// show/hide system ui here
}
private static void addPlaybackControlVisibilityListener(SimpleExoPlayerView playerView, PlaybackControlView.VisibilityListener listener) {
PlaybackControlView playbackControlView = findPlaybackControlView(playerView);
if (playbackControlView != null)
playbackControlView.setVisibilityListener(listener);
}
private static PlaybackControlView findPlaybackControlView(ViewGroup viewGroup) {
for (int i = 0; i < viewGroup.getChildCount(); i++) {
View child = viewGroup.getChildAt(i);
if (child instanceof PlaybackControlView)
return (PlaybackControlView) child;
if (child instanceof ViewGroup) {
PlaybackControlView result = findPlaybackControlView((ViewGroup) child);
if (result != null)
return result;
}
}
return null;
}
}
With Exoplayer 2.16.1 you can use setControllerVisibilityListener like this:
viewBinding.playerView.setControllerVisibilityListener { visibility ->
if (visibility == View.VISIBLE) {
// controller is visible
} else {
// controller is not visible
}
}
There are two classes we have 1. PlayerView 2. StyledPlayerView.
I am answering here for StyledPlayerView since PlayerView is deprecated now.
First create a class which extends StyledPlayerView and also your Class do implement this interface class CustomPlayerView extends StyledPlayerView implements StyledPlayerView.ControllerVisibilityListener So you need to override onVisibilityChanged Method:
#Override
public void onVisibilityChanged(int visibility) {
isControllerVisible = visibility == View.VISIBLE;
}
Now you can call this method on some other class where all your playerView methods present
binding.playerView.setControllerVisibilityListener(customPlayerView)
So on Visibility change of your controls you will get callbacks.

Drawer item animation when opened

I've created a drawer using a DrawerLayout which contains a RecyclerView with the items. I've also attached a layoutAnimation to the RecyclerView to have the items come in from the side when opening the drawer. This works peachy the first time but when opening the drawer the second time everything is already in place. I would like the layoutAnimation to run every time the drawer is opened.
What I've tried so far is to have a custom ActionBarDrawerToggle (I need that one anyway), and add the following:
#Override
public void onDrawerOpened(final View drawerView) {
super.onDrawerOpened(drawerView);
final RecyclerView recyclerView =
(RecyclerView) drawerView.findViewById(R.id.drawer_content);
if (recyclerView != null) {
recyclerView.startLayoutAnimation();
}
}
It works sort of, because it re-runs the animation, however all the items are there when opening the drawer, then they disappear and then the animations starts.
Anyone have a solution how to "reset" the drawer item views every time the drawer is closed?
Not sure these are needed but I'll include them anyway
<--! layout_animation.xml -->
<layoutAnimation
xmlns:android="http://schemas.android.com/apk/res/android"
android:animation="#anim/slide_from_right"
android:delay="15%"
android:animationOrder="normal"
/>
<--! slide_from_right.xml -->
<translate
xmlns:android="http://schemas.android.com/apk/res/android"
android:fromXDelta="100%p"
android:interpolator="#android:anim/decelerate_interpolator"
android:toXDelta="0"
/>
I found the solution after some more testing, perhaps not the prettiest solution but it works. By hiding the content when the drawer is closed and then making it visible again just before starting the animation solves the issue I was having:
private boolean mFirstDrawerOpen = true;
private boolean mAnimationScheduled;
#Override
public void onDrawerSlide(View drawerView, float slideOffset) {
super.onDrawerSlide(drawerView, slideOffset);
// The framework handles the first animation
if (mIsFirstDrawerOpen) {
mIsFirstDrawerOpen = false;
return;
}
final RecyclerView recyclerView =
(RecyclerView) drawerView.findViewById(R.id.drawer_content);
if (mAnimationScheduled && recyclerView != null) {
recyclerView.setVisibility(View.VISIBLE);
recyclerView.startLayoutAnimation();
mAnimationScheduled = false;
} else if (slideOffset == 0f) {
// Handles the case when the drawer is not completly opened and then closed,
// which does not trigger onDrawerClosed()
mAnimationScheduled = true;
}
}
#Override
public void onDrawerOpened(final View drawerView) {
super.onDrawerOpened(drawerView);
mAnimationScheduled = false;
}
#Override
public void onDrawerClosed(View drawerView) {
super.onDrawerClosed(drawerView);
mAnimationScheduled = true;
final RecyclerView recyclerView =
(RecyclerView) drawerView.findViewById(R.id.drawer_content);
if (recyclerView != null) {
recyclerView.setVisibility(View.INVISIBLE);
}
}
Update:
The previous answer did not handle the case where the drawer is dragged halfway open and then closed, since onDrawerClosed is not called if the drawer haven't been fully opened. To solve that, I moved most of the code from onDrawerOpen to onDrawerSlide() and modify it a bit.
I was having the same problem as #patrick-iv and was wondering how other people solved it. I came up with adding the below code to the onDrawerStateChanged listener.
boolean drawerOpen = drawer.isDrawerOpen(GravityCompat.START);
_drawerTopMenu.setVisibility(drawerOpen ? View.VISIBLE : View.INVISIBLE);
if (newState == DrawerLayout.STATE_SETTLING && !drawerOpen)
_drawerTopMenu.startLayoutAnimation();

Custom animation for navigation drawer

I've successfully implemented the Navigation Drawer as explained here
Now, I'd like to override the default animation for opening/closing. I'd like to use a custom animation. Is it feasible? If so, how can I do it? Thank you very much
You can call openDrawer(int gravity) on the DrawerLayout to make it open the drawer with an animation.
Example:
// Delay is in milliseconds
static final int DRAWER_DELAY = 200;
#Override
protected void onCreate(Bundle savedInstanceState) {
...
// Building NavDrawer logic here. Just a method call would be best.
...
new Handler().postDelayed(openDrawerRunnable(), DRAWER_DELAY);
}
private Runnable openDrawerRunnable() {
return new Runnable() {
#Override
public void run() {
drawerLayout.openDrawer(Gravity.LEFT);
}
}
}

How to add Action Bar from support library into PreferenceActivity?

Action Bar compatibility has been added into support library, revision 18. It now has ActionBarActivity class for creating activities with Action Bar on older versions of Android.
Is there any way to add Action Bar from support library into PreferenceActivity?
Previously I used ActionBarSherlock and it has SherlockPreferenceActivity.
EDIT: In appcompat-v7 22.1.0 Google added the AppCompatDelegate abstract class as a delegate you can use to extend AppCompat's support to any activity.
Use it like this:
...
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatDelegate;
import android.support.v7.widget.Toolbar;
...
public class SettingsActivity extends PreferenceActivity {
private AppCompatDelegate mDelegate;
#Override
protected void onCreate(Bundle savedInstanceState) {
getDelegate().installViewFactory();
getDelegate().onCreate(savedInstanceState);
super.onCreate(savedInstanceState);
}
#Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
getDelegate().onPostCreate(savedInstanceState);
}
public ActionBar getSupportActionBar() {
return getDelegate().getSupportActionBar();
}
public void setSupportActionBar(#Nullable Toolbar toolbar) {
getDelegate().setSupportActionBar(toolbar);
}
#Override
public MenuInflater getMenuInflater() {
return getDelegate().getMenuInflater();
}
#Override
public void setContentView(#LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
#Override
public void setContentView(View view) {
getDelegate().setContentView(view);
}
#Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
getDelegate().setContentView(view, params);
}
#Override
public void addContentView(View view, ViewGroup.LayoutParams params) {
getDelegate().addContentView(view, params);
}
#Override
protected void onPostResume() {
super.onPostResume();
getDelegate().onPostResume();
}
#Override
protected void onTitleChanged(CharSequence title, int color) {
super.onTitleChanged(title, color);
getDelegate().setTitle(title);
}
#Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
getDelegate().onConfigurationChanged(newConfig);
}
#Override
protected void onStop() {
super.onStop();
getDelegate().onStop();
}
#Override
protected void onDestroy() {
super.onDestroy();
getDelegate().onDestroy();
}
public void invalidateOptionsMenu() {
getDelegate().invalidateOptionsMenu();
}
private AppCompatDelegate getDelegate() {
if (mDelegate == null) {
mDelegate = AppCompatDelegate.create(this, null);
}
return mDelegate;
}
}
No more hacking. Code taken from AppCompatPreferenceActivity.java.
There is currently no way to achieve with AppCompat. I've opened a bug internally.
I have managed to create a workaround similar to what the Google Play Store uses. Link to Original Answer
Please find the GitHub Repo: Here
Very Similar to your own code but added xml to allow for set title:
Continuing to use PreferenceActivity:
settings_toolbar.xml :
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.Toolbar
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/toolbar"
app:theme="#style/ThemeOverlay.AppCompat.Dark.ActionBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
app:navigationContentDescription="#string/abc_action_bar_up_description"
android:background="?attr/colorPrimary"
app:navigationIcon="?attr/homeAsUpIndicator"
app:title="#string/action_settings"
/>
SettingsActivity.java :
public class SettingsActivity extends PreferenceActivity {
#Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
LinearLayout root = (LinearLayout)findViewById(android.R.id.list).getParent().getParent().getParent();
Toolbar bar = (Toolbar) LayoutInflater.from(this).inflate(R.layout.settings_toolbar, root, false);
root.addView(bar, 0); // insert at top
bar.setNavigationOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
finish();
}
});
}
}
Result :
UPDATE (Gingerbread Compatibility) :
As pointed out here, Gingerbread Devices are returning NullPointerException on this line:
LinearLayout root = (LinearLayout)findViewById(android.R.id.list).getParent().getParent().getParent();
FIX:
SettingsActivity.java :
public class SettingsActivity extends PreferenceActivity {
#Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
Toolbar bar;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
LinearLayout root = (LinearLayout) findViewById(android.R.id.list).getParent().getParent().getParent();
bar = (Toolbar) LayoutInflater.from(this).inflate(R.layout.settings_toolbar, root, false);
root.addView(bar, 0); // insert at top
} else {
ViewGroup root = (ViewGroup) findViewById(android.R.id.content);
ListView content = (ListView) root.getChildAt(0);
root.removeAllViews();
bar = (Toolbar) LayoutInflater.from(this).inflate(R.layout.settings_toolbar, root, false);
int height;
TypedValue tv = new TypedValue();
if (getTheme().resolveAttribute(R.attr.actionBarSize, tv, true)) {
height = TypedValue.complexToDimensionPixelSize(tv.data, getResources().getDisplayMetrics());
}else{
height = bar.getHeight();
}
content.setPadding(0, height, 0, 0);
root.addView(content);
root.addView(bar);
}
bar.setNavigationOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
finish();
}
});
}
}
Any issues with the above let me know!
UPDATE 2: TINTING WORKAROUND
As pointed out in many dev notes PreferenceActivity does not support tinting of elements, however by utilising a few internal classes you CAN achieve this. That is until these classes are removed. (Works using appCompat support-v7 v21.0.3).
Add the following imports:
import android.support.v7.internal.widget.TintCheckBox;
import android.support.v7.internal.widget.TintCheckedTextView;
import android.support.v7.internal.widget.TintEditText;
import android.support.v7.internal.widget.TintRadioButton;
import android.support.v7.internal.widget.TintSpinner;
Then override the onCreateView method:
#Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
// Allow super to try and create a view first
final View result = super.onCreateView(name, context, attrs);
if (result != null) {
return result;
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
// If we're running pre-L, we need to 'inject' our tint aware Views in place of the
// standard framework versions
switch (name) {
case "EditText":
return new TintEditText(this, attrs);
case "Spinner":
return new TintSpinner(this, attrs);
case "CheckBox":
return new TintCheckBox(this, attrs);
case "RadioButton":
return new TintRadioButton(this, attrs);
case "CheckedTextView":
return new TintCheckedTextView(this, attrs);
}
}
return null;
}
Result:
AppCompat 22.1
AppCompat 22.1 introduced new tinted elements, meaning that there is no longer a need to utilise the internal classes to achieve the same effect as the last update. Instead follow this (still overriding onCreateView):
#Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
// Allow super to try and create a view first
final View result = super.onCreateView(name, context, attrs);
if (result != null) {
return result;
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
// If we're running pre-L, we need to 'inject' our tint aware Views in place of the
// standard framework versions
switch (name) {
case "EditText":
return new AppCompatEditText(this, attrs);
case "Spinner":
return new AppCompatSpinner(this, attrs);
case "CheckBox":
return new AppCompatCheckBox(this, attrs);
case "RadioButton":
return new AppCompatRadioButton(this, attrs);
case "CheckedTextView":
return new AppCompatCheckedTextView(this, attrs);
}
}
return null;
}
NESTED PREFERENCE SCREENS
A lot of people are experiencing issues with including the Toolbar in nested <PreferenceScreen />s however, I have found a solution!! - After a lot of trial and error!
Add the following to your SettingsActivity:
#SuppressWarnings("deprecation")
#Override
public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) {
super.onPreferenceTreeClick(preferenceScreen, preference);
// If the user has clicked on a preference screen, set up the screen
if (preference instanceof PreferenceScreen) {
setUpNestedScreen((PreferenceScreen) preference);
}
return false;
}
public void setUpNestedScreen(PreferenceScreen preferenceScreen) {
final Dialog dialog = preferenceScreen.getDialog();
Toolbar bar;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
LinearLayout root = (LinearLayout) dialog.findViewById(android.R.id.list).getParent();
bar = (Toolbar) LayoutInflater.from(this).inflate(R.layout.settings_toolbar, root, false);
root.addView(bar, 0); // insert at top
} else {
ViewGroup root = (ViewGroup) dialog.findViewById(android.R.id.content);
ListView content = (ListView) root.getChildAt(0);
root.removeAllViews();
bar = (Toolbar) LayoutInflater.from(this).inflate(R.layout.settings_toolbar, root, false);
int height;
TypedValue tv = new TypedValue();
if (getTheme().resolveAttribute(R.attr.actionBarSize, tv, true)) {
height = TypedValue.complexToDimensionPixelSize(tv.data, getResources().getDisplayMetrics());
}else{
height = bar.getHeight();
}
content.setPadding(0, height, 0, 0);
root.addView(content);
root.addView(bar);
}
bar.setTitle(preferenceScreen.getTitle());
bar.setNavigationOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
dialog.dismiss();
}
});
}
The reason that PreferenceScreen's are such a pain is because they are based as a wrapper dialog, so we need to capture the dialog layout to add the toolbar to it.
Toolbar Shadow
By design importing the Toolbar does not allow for elevation and shadowing in pre-v21 devices, so if you would like to have elevation on your Toolbar you need to wrap it in a AppBarLayout:
`settings_toolbar.xml :
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.Toolbar
.../>
</android.support.design.widget.AppBarLayout>
Not forgetting to add the add the Design Support library as a dependency in build.gradle file:
compile 'com.android.support:support-v4:22.2.0'
compile 'com.android.support:appcompat-v7:22.2.0'
compile 'com.android.support:design:22.2.0'
Android 6.0
I have investigated the reported overlapping issue and I cannot reproduce the issue.
The full code in use as above produces the following:
If I am missing something please let me know via this repo and I will investigate.
Found a PreferenceFragment implementation based on support-v4 Fragment:
https://github.com/kolavar/android-support-v4-preferencefragment
Edit: I just tested it and its working great!
Integrating PreferenceActivity with ABC is not possible, at least for me. I tried the two possibilities I could find but none worked:
Option 1:
ActionBarPreferenceActivity extends PreferenceActivity. When you do this you get restricted by ActionBarActivityDelegate.createDelegate(ActionBarActivity activity). Also you need to implement ActionBar.Callbacks which is not accessible
Option 2:
ActionBarPreferenceActivity extends ActionBarActivity. This approach requires rewriting a whole new PreferenceActivity, PreferenceManager and may be PreferenceFragment which means you need access to hidden classes like com.android.internal.util.XmlUtils
The solution to this can only come from Google devs implementing an ActionBarWrapper that can be added to any activity.
If you really need a preference activity, my advice for now is ActionBarSherlock.
However, I managed to implement it here.
Problem Background:
The OP wants to know how can we put MenuItems in the ActionBar of PreferenceActivity for pre-Honeycomb because Android's support library has a bug which doesn't allow this to happen.
My Solution:
I've found a much cleaner way, than already proposed, to achieve the target (and found it in the Android Docs):
android:parentActivityName
The class name of the logical parent of the
activity. The name here must match the class name given to the
corresponding element's android:name attribute.
The system reads this attribute to determine which activity should be
started when the use presses the Up button in the action bar. The
system can also use this information to synthesize a back stack of
activities with TaskStackBuilder.
To support API levels 4 - 16, you can also declare the parent activity
with a element that specifies a value for
"android.support.PARENT_ACTIVITY". For example:
<activity
android:name="com.example.app.ChildActivity"
android:label="#string/title_child_activity"
android:parentActivityName="com.example.myfirstapp.MainActivity" >
<!-- Parent activity meta-data to support API level 4+ -->
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.example.app.MainActivity" />
</activity>
Now do what you would normally do in your onOptionsItemSelected(). Since it's a part of Android Docs, it has no side-affects.
Happy coding. :)
Update:
This solution no longer works if you're targeting Lollipop. If you're using AppCompat, this answer is what you should be looking for.
I was able to get android.app.Actionbar by using getActionBar(). It returned a null value at first... then I went to the manifest and changed the theme to:
android:theme="#style/Theme.AppCompat"
Then I was able to have the actionbar again. I'm assuming this will only work for certain build levels. So you might want to do a check for the build number or check if the value returned is null.
It'll be fine for me because the app I'm working on is for ICS/4.0+.
Now the official answer for this problem has been released. It is the v7/v14 Preference Support library.
See How to use the v7/v14 Preference Support library? for the discussion how to use it.

Categories

Resources