I created the app drawer by using the following library:
http://developer.android.com/training/implementing-navigation/nav-drawer.html
I want to show the Navigation Drawer with animation when opening the app.
How can I do that?
Predraw listener, aka the safeway
Here is the predraw listener example. It will literally start the animation as soon as it can which maybe a little too fast. You might want to do a combination of this with a runnable shown second. I will not show the two combined, only separate.
#Override
protected void onCreate(Bundle savedInstanceState) {
...
// Building NavDrawer logic here. Just a method call would be best.
...
ViewTreeObserver vto = drawer.getViewTreeObserver();
if (vto != null) vto.addOnPreDrawListener(new ShouldShowListener(drawer));
}
private static class ShouldShowListener implements OnPreDrawListener {
private final DrawerLayout drawerLayout;
private ShouldShowListener(DrawerLayout drawerLayout) {
this.drawerLayout= drawerLayout;
}
#Override
public boolean onPreDraw() {
if (view != null) {
ViewTreeObserver vto = view.getViewTreeObserver();
if (vto != null) {
vto.removeOnPreDrawListener(this);
}
}
drawerLayout.openDrawer(Gravity.LEFT);
return true;
}
}
PostDelay Runnable, aka living dangerous
// 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);
}
}
}
WARNING
If they rotate on the start of the app for the first time BOOM! Read this blog post for more information http://corner.squareup.com/2013/12/android-main-thread-2.html. Best thing to do would be to use the predraw listener or remove your runnable in onPause.
You can call openDrawer(int gravity) on the DrawerLayout to make it open the drawer with an animation.
You need to call drawerLayout.openDrawer(Gravity.LEFT) to animate the drawer opening. The drawer won't animate if you make the call too early in the Activity lifecycle.
The simplest solution is to just set a flag in onCreate() and act on it in onResume().
You want to make sure that you only set the flag when savedInstanceState is null indicating that the Activity isn't being resumed from the background. You don't want the drawer sliding out every time you change orientation or switch applications.
public class MainActivity extends ActionBarActivity {
private DrawerLayout drawerLayout;
private boolean firstResume = false;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
drawerLayout = (DrawerLayout)findViewById(R.id.drawer);
if(savedInstanceState == null){
firstResume = true;
}
}
#Override
protected void onResume() {
super.onResume();
if(firstResume) {
drawerLayout.openDrawer(Gravity.LEFT);
}
firstResume = false;
}
}
You could also use an OnPreDrawListener but I feel it's a bit unnecessarily complicated as onPreDraw is called multiple times so you need to remove the listener after opening the drawer. You're also assuming that preDraw is a suitable time to activate the drawer which is an internal implementation of the drawer layout. A future implementation might not animate properly until after onDraw for example.
Delaying the drawer opening by an arbitrary number of milliseconds is a dangerous way to solve this problem. In the worst case the call to open the drawer could happen after onDestroy if the user navigates away quickly.
Related
I'm toggling a DrawerLayout's state from a button's onClick, and disabling its swipe.
That works OK, but when the Activity changes its orientation, the Drawer doesn't retain its state; if it was opened, it will get closed. It even happens adding android:configChanges="keyboardHidden|orientation" .
Code in my Activity:
private DrawerLayout drawer;
private int drawerLayoutGravity = Gravity.RIGHT;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
disableDrawer();
View btnOpenDrawer = findViewById(R.id.btn_open_drawer);
btnOpenDrawer.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
toggleDrawerState();
}
});
}
private void toggleDrawerState() {
if (drawer.isDrawerOpen(drawerLayoutGravity)) {
drawer.closeDrawer(drawerLayoutGravity);
} else {
drawer.openDrawer(drawerLayoutGravity);
}
}
/**
* doesn't let the user swipe to open the drawer
*/
private void disableDrawer() {
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
}
A possible solution is that I re open the DrawerLayout on the Activity's onConfigurationChanged, but I need to avoid showing the DrawerLayout re opening when the configuration changes.
You're setting a closed lock mode on the drawer to disable swiping. Even though you've disabled Activity re-creation, the orientation change will trigger a layout event on your Views, and the DrawerLayout will set the drawer state according to the lock mode when laying itself out.
You need to update the lock mode whenever you programmatically open/close the drawer.
private void toggleDrawerState() {
if (drawer.isDrawerOpen(drawerLayoutGravity)) {
drawer.closeDrawer(drawerLayoutGravity);
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
} else {
drawer.openDrawer(drawerLayoutGravity);
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_OPEN);
}
}
I have an Activity that displays various fragments using the supportFragmentManager. When I attempt to get a view in the fragment or the parent activity for that matter, and attempt to measure it's position on the screen it only seems to be available for measurement sometime after onResume in the fragment lifecycle or after onActivityCreated/onResume/onAttachedToWindow in the Activity. Typically it is available after about 100-200ms. Is there any lifecycle event documented/undocumented or solid method of knowing when this has occurred, like maybe a canvas drawing event. The fragment in question needs to measure a parent activity view, but it isn't always available in onResume right away. I really hate having to do some kind of hack like having a handler wait 200ms.
You can use ViewTreeObserver.addOnGlobalLayoutListener().
#Override
public void onCreate(Bundle savedInstanceState) {
//...
someView.getViewTreeObserver().addOnGlobalLayoutListener(getOnLayoutListener(someView));
//...
}
private ViewTreeObserver.OnGlobalLayoutListener getOnLayoutListener(final View unHookView) {
return new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN)
unHookView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
else
unHookView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
//YOUR CODE HERE
}
};
}
I have 10 different fragments in my application. I need to hide Navigation drawer (Drawer Layout) in few fragments, how can I access Drawer Layout from a fragment and hide it? I know we need to use in activity mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED); but how to do it in fragments?
You could do something like this in your Fragment:
private MainActivity main;
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
main = (MainActivity) activity;
}
You definitely should avoid this!
A mutch better solution would be to use an Interface to communicate between your Main and the Fragment. You will end up with something like this:
public interface MyInterface {
public void lockDrawer();
public void unlockDrawer();
}
Main:
public class DetailViewActivity extends AppCompatActivity implements MyInterface {
#Override
public void lockDrawer() {
mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
}
#Override
public void unlockDrawer() {
mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
}
}
Fragment:
public class MyFragment extends Fragment {
private MyInterface myInterface;
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
myInterface = (MyInterface) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString() + " must implement MyInterface");
}
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) {
// Inflate the layout for this fragment
myInterface.lockDrawer();
return inflater.inflate(R.layout.example_fragment, container, false);
}
#Override
public void onDestroyView() {
super.onDestroyView();
myInterface.unlockDrawer();
}
}
Why this is the best solution: If you do something like ((HomeActivity) mActivity) you will not be able to reuse your Fragment.
There will be a ClassCastException. In order to reuse your Fragment you should use an Interface instead of casting you MainActivity. So every Activity which will use
your Frament can simply implement this Interface. Even if there's no DrawerLayout you can use it. So the big effort is reusability.
KOTLIN SOLUTION WITH NAVIGATION COMPONENT:
If you use a navigation component (one main activity with multiple fragment destinations) then you need to use addOnDestinationChangedListener to handle which fragment will you show and on which will hide your navigation view inside drawer layout.
Here you can see how to start with the navigation component and here is about the setDrawerLockMode method.
So your code will look something like this:
val drawerLayout: DrawerLayout = findViewById(R.id.drawer_layout)
val navController = findNavController(R.id.nav_host_fragment)
navController.addOnDestinationChangedListener{_, destination, _ ->
if (destination.id == R.id.nav_fragment1) {
drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
//DRAWER LOCKED IN fragment1
} else if (destination.id == R.id.nav_fragment2) {
drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
//DRAWER UNLOCKED IN fragment2
} else {.....
}
}
This part of the code you can put in the onCreate() method in your MainActivity.
The easiest way to make a Navigation Drawer Activity is automatically in your android studio. Just follow File -> New -> Activity -> Navigation Drawer Activity.
You can do this by following way -
Write one public method inside your activity as follows -
public void enableDisableDrawer(int mode) {
if (mDrawerLayout != null) {
mDrawerLayout.setDrawerLockMode(mode);
}
}
and then inside fragment's onResume you can call this and change Drawer lock mode as required -
((HomeActivity) mActivity).enableDisableDrawer(DrawerLayout.LOCK_MODE_UNLOCKED);
OR
((HomeActivity) mActivity).enableDisableDrawer(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
where mActivity is my activity reference.
This way is working for me.
You can use this method to lock or unlock the drawer: DrawerLayout.setDrawerLockMode(...). (There are also two other versions of this method to specify a lock mode for specific drawers.) To lock, use DrawerLayout.LOCK_MODE_LOCKED_CLOSED; to unlock, use DrawerLayout.LOCK_MODE_UNLOCKED.
If you are using the ActionBarDrawerToggle, you need to add some extra code to prevent the drawer from opening when they click the ActionBarDrawerToggle if you've locked the drawer.
#Override
public boolean onOptionsItemSelected(MenuItem item) {
// check lock mode before passing to ActionBarDrawerToggle
// I assume your drawer is on the left; if not, use Gravity.RIGHT
int lockMode = mDrawer.getDrawerLockMode(Gravity.LEFT);
if (lockMode == DrawerLayout.LOCK_MODE_UNLOCKED &&
mDrawerToggle.onOptionsItemSelected(item)) {
return true;
}
// Handle your other action bar items...
return super.onOptionsItemSelected(item);
}
refer link for more infolink here
Create two methods in your activity. One for opening the drawer and other for closing it. See below code.
public class MainActivity extends AppCompatActivity {
private DrawerLayout mDrawerLayout;
.......
#Override
protected void onCreate(Bundle savedInstanceState) {
.........
mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
.........
}
public void openNavigationDrawer() {
mDrawerLayout.openDrawer(Gravity.LEFT);
}
public void closeNavigationDrawer() {
mDrawerLayout.closeDrawer(Gravity.LEFT);
}
public void lockNavigationDrawer() {
mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
}
public void unLockNavigationDrawer() {
mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
}
}
Now call the above methods from your fragment like below
((MainActivity)getActivity()).closeNavigationDrawer(); // to close drawer
((MainActivity)getActivity()).openNavigationDrawer(); // to open drawer
((MainActivity)getActivity()).lockNavigationDrawer(); // to lock drawer
((MainActivity)getActivity()).unLockNavigationDrawer(); // to unlock drawer
You can use this method to lock or unlock the drawer: DrawerLayout.setDrawerLockMode(...). (There are also two other versions of this method to specify a lock mode for specific drawers.) To lock, use DrawerLayout.LOCK_MODE_LOCKED_CLOSED; to unlock, use DrawerLayout.LOCK_MODE_UNLOCKED.
If you are using the ActionBarDrawerToggle, you need to add some extra code to prevent the drawer from opening when they click the ActionBarDrawerToggle if you've locked the drawer.
enter code here
#Override
public boolean onOptionsItemSelected(MenuItem item) {
// check lock mode before passing to ActionBarDrawerToggle
// I assume your drawer is on the left; if not, use Gravity.RIGHT
int lockMode = mDrawer.getDrawerLockMode(Gravity.LEFT);
if (lockMode == DrawerLayout.LOCK_MODE_UNLOCKED &&
mDrawerToggle.onOptionsItemSelected(item)) {
return true;
}
// Handle your other action bar items...
return super.onOptionsItemSelected(item);
}
I struggled with it for hours: setting DrawerLayout to lock mode simply didn't work for me. Even following your examples.
Finally I came up with this post
DrawerLayout.setDrawerLockMode won't work if you set layout_gravity e.g. to start|bottom in the NavigationView. Just set it to start or end
Hope this helps someone
I want my app to show the side navigation drawer as soon as the main activity is created.
My code works fine - user launches app and gets the open drawer - but I'd like to actually see the side drawer sliding from the left; instead, I find the drawer fully opened.
At what point should I call openDrawer()?
Have tried calling from:
main activity OnCreate;
similar points in the fragment hosted by the drawer.
I could try OnPrepareOptionsMenu, but I think it gets called more than once during the activity lifecycle. I also tried OnStart() and I fear my options are over.
Any idea? I'm sure this is pretty simple but I can't figure out.
Edit: I realize I wasn't so clear with my first exposition of the question (#Biu). I'm talking about a purely graphical issue here. The point is:
I have something to happen at startup; in my case we're speaking about the nav drawer sliding into the main screen, but it could be any animation I think;
In my case, one could just call:
protected void OnCreate(Bundle b) {
...
DrawerLayout.openDrawer()
}
The above solution works well. The issue I'm talking about is graphical; with the above code you launch the app and find the main activity covered with an already-opened drawer. Instead, I'd like the user to have clue of what is happing, to see where the panel came from; in other words, to see the opening animation.
So my question is: when should I call openDrawer()? The main Activity onCreate isn't quite right, because the animation ends before the user gets to see something on screen.
I thought that the wish of having something start when all is loaded would be more common.
#benjosantony suggests that you should open your drawer at onResume, however it's not guaranteed that the activity will be visible at that time:
onResume is not the best indicator that your
activity is visible to the user; Use onWindowFocusChanged(boolean) to know for certain
that your activity is visible to the user
You'd think that you can just use onWindowFocusChanged and be done, but you can't. There's still the transition animation which breaks (at least for me) the drawer's animation..
For API 21+:
There's onEnterAnimationComplete where you can open your drawer and see the animation properly. However 21+ is a requirement that's just too big..
For lower APIs:
The only possible way I can think of is removing the activity's animation with a theme adjustment:
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="android:windowAnimationStyle">#null</item>
</style>
And opening the drawer like so:
private static final String DRAWER_STATE = "mDrawerOpened";
private DrawerLayout mDrawer;
private ListView mDrawerList;
private boolean mDrawerOpened;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mDrawer = (DrawerLayout) findViewById(R.id.drawer_layout);
mDrawerList = (ListView) findViewById(R.id.left_drawer);
}
#Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (!mDrawerOpened && hasFocus) {
mDrawer.openDrawer(mDrawerList);
mDrawerOpened = true;
}
}
#Override
protected void onSaveInstanceState(#NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(DRAWER_STATE, mDrawerOpened);
}
#Override
protected void onRestoreInstanceState(#NonNull Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
mDrawerOpened = savedInstanceState.getBoolean(DRAWER_STATE);
}
This will animate the drawer only when the activity is started.
The boolean value is saved when your activity is destroyed abnormally, e.g. rotation or need for system resources.
If you don't like setting the instanceState you can use SharedPreferences as #Biu suggested, however IMO that wouldn't be the proper solution as android already provides tools for that, there's no need to re-invent the bike.
You could use this hack by using SharedPreferences
boolean firstTime = true;
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(this);
public void onCreate(Bundle bundle)
{
if (pref.getBoolean("firstTime", true) == true)
{
drawer.openDrawer(yourDrawer);
firstTime = false;
pref.editor().putBoolean("firstTime", firstTime).apply();
}
}
Activity
The foreground lifetime of an activity happens between a call to onResume() until a corresponding call to onPause(). During this time the activity is in front of all other activities and interacting with the user.
Thus I think onResume is the best place to open your drawer.
I'm developing an application which uses the navigation drawer pattern (With DrawerLayout).
Each click on a drawer's item, replaces the fragment in the main container.
However, I'm not sure when is the right time to do the fragment transaction?
When the drawer starts closing? Or after it is closed?
In google's documentaion example, you can see that they are doing the transaction
right after the item click, and then close the drawer.
As a result, the drawer seems laggy and not smooth, and it looks very bad (It happens in my application too).
In Gmail and Google Drive applications, on the other way, It seems like they are doing the transaction after the drawer closed (Am I Right?).
As a result, the drawer is not laggy and very smooth, BUT it takes about 1 second (the time it takes to the drawer get closed) at least, to see the next fragment.
It seems like there is no way the drawer will be smooth when immediately doing fragment transaction.
What do you think about that?
Thanks in advance!
Yup, couldn't agree more, performing a fragment (with view) transaction results in a layout pass which causes janky animations on views being animated, citing DrawerLayout docs:
DrawerLayout.DrawerListener can be used to monitor the state and motion of drawer views. Avoid performing expensive operations such as layout during animation as it can cause stuttering; try to perform expensive operations during the STATE_IDLE state.
So please perform your fragment transactions after the drawer is closed or somebody patches the support library to somehow fix that :)
Another solution is to create a Handler and post a delayed Runnable after you close the drawer, as shown here: https://stackoverflow.com/a/18483633/769501. The benefit with this approach is that your fragments will be replaced much sooner than they would be if you waited for DrawerListener#onDrawerClosed(), but of course the arbitrary delay doesn't 100% guarantee the drawer animation will be finished in time.
That said, I use a 200ms delay and it works wonderfully.
private class DrawerItemClickListener implements OnItemClickListener {
#Override
public void onItemClick(AdapterView<?> parent, View view, final int position, long id) {
drawerLayout.closeDrawer(drawerList);
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
switchFragments(position); // your fragment transactions go here
}
}, 200);
}
}
This is what I do to achieve an smooth transaction animation similar to Gmail app:
activity_drawer.xml
<android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<!-- The main content view -->
<FrameLayout
android:id="#+id/content_frame"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<!-- The navigation drawer -->
<ListView
android:id="#+id/left_drawer"
android:layout_width="280dp"
android:layout_height="match_parent"
android:layout_gravity="left"
android:choiceMode="singleChoice" />
</android.support.v4.widget.DrawerLayout>
DrawerActivity.java
private Fragment mContentFragment;
private Fragment mNextContentFragment;
private boolean mChangeContentFragment = false;
private Handler mHandler = new Handler();
...
#Override
public void onCreate(Bundle savedInstanceState) {
...
mDrawerLayout.setDrawerListener(new DrawerListener());
mDrawerList.setOnItemClickListener(new DrawerItemClickListener());
...
}
....
private class DrawerItemClickListener implements ListView.OnItemClickListener {
#Override
public void onItemClick(AdapterView parent, View view, int position, long id) {
getSupportFragmentManager().beginTransaction().remove(mContentFragment).commit();
switch (position) {
case 0:
mNextContentFragment = new Fragment1();
break;
case 1:
mNextContentFragment = new Fragment2();
break;
case 2:
mNextContentFragment = new Fragment3();
break;
}
mChangeContentFragment = true;
mDrawerList.setItemChecked(position, true);
mHandler.postDelayed(new Runnable() {
#Override
public void run() {
mDrawerLayout.closeDrawer(mDrawerList);
}
}, 150);
}
}
private class DrawerListener implements android.support.v4.widget.DrawerLayout.DrawerListener {
#Override
public void onDrawerClosed(View view) {
if (mChangeContentFragment) {
getSupportFragmentManager().beginTransaction().setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN).replace(R.id.content_frame, mNextContentFragment).commit();
mContentFragment = mNextContentFragment;
mNextContentFragment = null;
mChangeContentFragment = false;
}
}
}
Hope that helps you! :-)
I know this question is old but I ran into the same problem and figured I would post my solution as I think it is a better implementation than adding a hardcoded delay time. What I did was use the onDrawerClosed function to verify that the drawer IS closed before doing my task.
//on button click...
private void displayView(int position) {
switch (position) {
//if item 1 is selected, update a global variable `"int itemPosition"` to be 1
case 1:
itemPosition = 1;
//();
break;
default:
break;
}
// update selected item and title, then close the drawer
mDrawerList.setItemChecked(position, true);
mDrawerList.setSelection(position);
mDrawerLayout.closeDrawer(mDrawerList); //close drawer
}
and then in onDrawerClosed, open the corresponding activity.
public void onDrawerClosed(View view) {
getSupportActionBar().setTitle(mTitle);
// calling onPrepareOptionsMenu() to show action bar icons
supportInvalidateOptionsMenu();
if (itemPosition == 1) {
Intent intent = new Intent(BaseActivity.this, SecondActivity.class);
startActivity(intent);
}
}
Just write your code in a handler and put 200 ms delay.
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
openSelectionDrawerItem(position);
}
}, 200);
Instead of delaying your item clicks which may make your app feel slow. I would just delay the closing of the mDrawerLayout. I would not use the DrawerLayout.OnDrawerListener onClose(...) either because those callbacks are so slow to be called.
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
mDrawerLayout.closeDrawer(GravityCompat.START);
}
}, 200);
If you want it smooth and without any delay, leave the drawer open and close it afterwards when returning (in the onRestart() method).
#Override
protected void onRestart() {
// TODO Auto-generated method stub
super.onRestart();
mDrawerLayout.closeDrawer(mDrawerList);
}
The side effect is an (speedy) animation when returning, but this might be acceptable.