Fixed
Status Update
Comments
il...@google.com <il...@google.com>
il...@google.com <il...@google.com> #2
How should this work "else if (parent instanceof BottomSheetDialogFragment)"?
sl...@czerwinski.info.pl <sl...@czerwinski.info.pl> #3
Yeah, I just realized that it's not as simple as in the case of the DrawerLayout. When using a DrawerLayout, people mostly have something like this:
<android.support.v4.widget.DrawerLayout 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">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/header_container"/>
<android.support.design.widget.NavigationView
android:id="@+id/nav_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
app:headerLayout="@layout/main_nav_header"
android:fitsSystemWindows="true"
app:menu="@menu/main_drawercontent"/>
</android.support.v4.widget.DrawerLayout>
Obviously here the NavigationView is a direct child of the DrawerLayout, which is why the implementation we already have for handling the closing of the navigation drawer works like it does, by checking the NavigationView's parent.
But when using a BottomSheetDialogFragment, we can't rely on the NavigationView being a direct child of the BottomSheetDialogFragment. Since it's mostly a regular Fragment, people may use a layout like this (simplified):
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android "
xmlns:app="http://schemas.android.com/apk/res-auto "
xmlns:tools="http://schemas.android.com/tools "
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".view.BottomDrawerFragment">
<!-- Here we'd have views typically found in the navigation drawer,
such as a user profile picture or their name -->
<com.google.android.material.navigation.NavigationView
android:id="@+id/bottom_drawer_navigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/divider"
app:menu="@menu/bottom_drawer_menu" />
</androidx.constraintlayout.widget.ConstraintLayout>
My suggestion wouldn't work with this, as the NavigationView's parent is not the BottomSheetDialogFragment, but the ConstraintLayout. So we can't use the parent to detect when we should dismiss a BottomSheetDialogFragment.
A potential solution would probably have to use a different approach. Maybe in setupWithNavController() you could pass a reference to the BottomSheetDialogFragment directly, in addition to the NavigationView and NavController. Something similar happens already, for example when using a Toolbar that should be kept in sync also, the method call is NavigationUI.setupWithNavController(collapsingToolbarLayout, toolbar, navController) for that.
So we could end up having an implementation of setupWithNavController just for our case with the BottomSheetDialogFragment that goes something like this:
public static void setupWithNavController(
@NonNull BottomSheetDialogFragment bottomSheetDialogFragment,
@NonNull final NavigationView navigationView,
@NonNull final NavController navController) {
navigationView.setNavigationItemSelectedListener(
new NavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
boolean handled = onNavDestinationSelected(item, navController, true);
if (handled) {
bottomSheetDialogFragment.dismiss();
}
return handled;
}
});
final WeakReference<NavigationView> weakReference = new WeakReference<>(navigationView);
navController.addOnNavigatedListener(new NavController.OnNavigatedListener() {
@Override
public void onNavigated(@NonNull NavController controller,
@NonNull NavDestination destination) {
NavigationView view = weakReference.get();
if (view == null) {
controller.removeOnNavigatedListener(this);
return;
}
Menu menu = view.getMenu();
for (int h = 0, size = menu.size(); h < size; h++) {
MenuItem item = menu.getItem(h);
item.setChecked(matchDestination(destination, item.getItemId()));
}
}
});
}
And we'd use it in our example like so:
public class BottomDrawerFragment extends BottomSheetDialogFragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_bottom_drawer, container, false);
NavigationView navigationView = view.findViewById(R.id.bottom_drawer_navigation);
NavigationUI.setupWithNavController(this, navigationView, Navigation.findNavController(getActivity(), R.id.nav_host_fragment));
return view;
}
}
<android.support.v4.widget.DrawerLayout xmlns:android="
xmlns:app="
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/header_container"/>
<android.support.design.widget.NavigationView
android:id="@+id/nav_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
app:headerLayout="@layout/main_nav_header"
android:fitsSystemWindows="true"
app:menu="@menu/main_drawercontent"/>
</android.support.v4.widget.DrawerLayout>
Obviously here the NavigationView is a direct child of the DrawerLayout, which is why the implementation we already have for handling the closing of the navigation drawer works like it does, by checking the NavigationView's parent.
But when using a BottomSheetDialogFragment, we can't rely on the NavigationView being a direct child of the BottomSheetDialogFragment. Since it's mostly a regular Fragment, people may use a layout like this (simplified):
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="
xmlns:app="
xmlns:tools="
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".view.BottomDrawerFragment">
<!-- Here we'd have views typically found in the navigation drawer,
such as a user profile picture or their name -->
<com.google.android.material.navigation.NavigationView
android:id="@+id/bottom_drawer_navigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/divider"
app:menu="@menu/bottom_drawer_menu" />
</androidx.constraintlayout.widget.ConstraintLayout>
My suggestion wouldn't work with this, as the NavigationView's parent is not the BottomSheetDialogFragment, but the ConstraintLayout. So we can't use the parent to detect when we should dismiss a BottomSheetDialogFragment.
A potential solution would probably have to use a different approach. Maybe in setupWithNavController() you could pass a reference to the BottomSheetDialogFragment directly, in addition to the NavigationView and NavController. Something similar happens already, for example when using a Toolbar that should be kept in sync also, the method call is NavigationUI.setupWithNavController(collapsingToolbarLayout, toolbar, navController) for that.
So we could end up having an implementation of setupWithNavController just for our case with the BottomSheetDialogFragment that goes something like this:
public static void setupWithNavController(
@NonNull BottomSheetDialogFragment bottomSheetDialogFragment,
@NonNull final NavigationView navigationView,
@NonNull final NavController navController) {
navigationView.setNavigationItemSelectedListener(
new NavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
boolean handled = onNavDestinationSelected(item, navController, true);
if (handled) {
bottomSheetDialogFragment.dismiss();
}
return handled;
}
});
final WeakReference<NavigationView> weakReference = new WeakReference<>(navigationView);
navController.addOnNavigatedListener(new NavController.OnNavigatedListener() {
@Override
public void onNavigated(@NonNull NavController controller,
@NonNull NavDestination destination) {
NavigationView view = weakReference.get();
if (view == null) {
controller.removeOnNavigatedListener(this);
return;
}
Menu menu = view.getMenu();
for (int h = 0, size = menu.size(); h < size; h++) {
MenuItem item = menu.getItem(h);
item.setChecked(matchDestination(destination, item.getItemId()));
}
}
});
}
And we'd use it in our example like so:
public class BottomDrawerFragment extends BottomSheetDialogFragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_bottom_drawer, container, false);
NavigationView navigationView = view.findViewById(R.id.bottom_drawer_navigation);
NavigationUI.setupWithNavController(this, navigationView, Navigation.findNavController(getActivity(), R.id.nav_host_fragment));
return view;
}
}
il...@google.com <il...@google.com> #4
You nailed it man. Thanks for your investigation!
il...@google.com <il...@google.com>
ap...@google.com <ap...@google.com> #5
Are there any cases where you'd have a NavigationView in a BottomSheet of any type and not expect the bottom sheet to be hidden when you tap on a NavigationView item?
If you'd always expect it to be hidden, we could use BottomSheetBehavior.from(navigationView) to find the bottom sheet, avoiding the need to create another setup method.
If you'd always expect it to be hidden, we could use BottomSheetBehavior.from(navigationView) to find the bottom sheet, avoiding the need to create another setup method.
il...@google.com <il...@google.com> #6
Not that I can think of. According to the Material Design spec, there are standard bottom sheets which remain open while the user interacts with the main UI region or the sheet itself, and modal bottom sheets which must be dismissed to interact with the underlying content.
Examples for standard bottom sheets are playback controls inside a music player or the location information in Google Maps, while modal bottom sheets provide a set of choices and are an alternative to inline menus.
This makes it pretty clear that modal bottom sheets should be used for any kind of menu, and modal bottom sheets are always dismissed by tapping a menu item or action. That being said, it's not unlikely that someone is going to use a NavigationView inside a standard bottom sheet at some point. While the spec doesn't explicitly prohibit that, it's clearly not the intention, so I wouldn't consider this case in the implementation.
With that in mind, I think using BottomSheetBehavior.from(navigationView) is a good solution.
Examples for standard bottom sheets are playback controls inside a music player or the location information in Google Maps, while modal bottom sheets provide a set of choices and are an alternative to inline menus.
This makes it pretty clear that modal bottom sheets should be used for any kind of menu, and modal bottom sheets are always dismissed by tapping a menu item or action. That being said, it's not unlikely that someone is going to use a NavigationView inside a standard bottom sheet at some point. While the spec doesn't explicitly prohibit that, it's clearly not the intention, so I wouldn't consider this case in the implementation.
With that in mind, I think using BottomSheetBehavior.from(navigationView) is a good solution.
Description
Version used: 1.0.0-alpha02
Devices/Android versions reproduced on: Emulator API 28, Samsung J5 API 25
Sample project attached.
I'm trying to create a custom Navigator and custom NavDestination that would animate ConstraintLayout (but any custom NavDestination would cause this issue).
Intended behaviour is shown in attached video: Layout_animations.webm
However, when I rotate the screen, while the navigation is on the custom destination, the application crashes.
The crash is caused by an unknown destination, which can only be added after custom navigator.
If I add custom destination in nav_main.xml, the app will also crash, because the navigator is not yet added.
This issue could be fixed by extending NavHostFragment, but I would need to override the whole onCreate() method.
I would need to add my custom navigator right after the line:
mNavController = new NavController(context);
I can think of 2 possible solutions in the library:
1. Delegate creating of NavController, so that a custom NavController can be provided in the XML layout file.
2. Call a new protected method "NavHostFragment.onNavControllerCreated(NavController)" right after the NavController is created, that could be overridden in a custom NavHostFragment, before the graph is created.
If there is any solution to my problem that I didn't think of, please provide a more exhaustive example in the documentation:
Stack trace:
2018-06-25 10:54:28.356 14543-14543/it.czerwinski.customnavigatorexample E/AndroidRuntime: FATAL EXCEPTION: main
Process: it.czerwinski.customnavigatorexample, PID: 14543
java.lang.RuntimeException: Unable to start activity ComponentInfo{it.czerwinski.customnavigatorexample/it.czerwinski.customnavigatorexample.MainActivity}: android.view.InflateException: Binary XML file line #10: Binary XML file line #10: Error inflating class fragment
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2925)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3060)
at android.app.ActivityThread.handleRelaunchActivityInner(ActivityThread.java:4792)
at android.app.ActivityThread.handleRelaunchActivity(ActivityThread.java:4711)
at android.app.servertransaction.ActivityRelaunchItem.execute(ActivityRelaunchItem.java:69)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:110)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:70)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1800)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:164)
at android.app.ActivityThread.main(ActivityThread.java:6649)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:826)
Caused by: android.view.InflateException: Binary XML file line #10: Binary XML file line #10: Error inflating class fragment
Caused by: android.view.InflateException: Binary XML file line #10: Error inflating class fragment
Caused by: java.lang.IllegalStateException: unknown destination during restore: it.czerwinski.customnavigatorexample:id/animatedLayoutDestination
at androidx.navigation.NavController.onGraphCreated(NavController.java:424)
at androidx.navigation.NavController.setGraph(NavController.java:386)
at androidx.navigation.NavController.restoreState(NavController.java:742)
at androidx.navigation.fragment.NavHostFragment.onCreate(NavHostFragment.java:215)
at androidx.fragment.app.Fragment.performCreate(Fragment.java:2400)
at androidx.fragment.app.FragmentManagerImpl.moveToState(FragmentManager.java:1418)
at androidx.fragment.app.FragmentManagerImpl.moveToState(FragmentManager.java:1684)
at androidx.fragment.app.FragmentManagerImpl.onCreateView(FragmentManager.java:3772)
at androidx.fragment.app.FragmentController.onCreateView(FragmentController.java:120)
at androidx.fragment.app.FragmentActivity.dispatchFragmentsOnCreateView(FragmentActivity.java:395)
at androidx.fragment.app.FragmentActivity.onCreateView(FragmentActivity.java:377)
at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:780)
at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:730)
at android.view.LayoutInflater.rInflate(LayoutInflater.java:863)
at android.view.LayoutInflater.rInflateChildren(LayoutInflater.java:824)
at android.view.LayoutInflater.inflate(LayoutInflater.java:515)
at android.view.LayoutInflater.inflate(LayoutInflater.java:423)
at android.view.LayoutInflater.inflate(LayoutInflater.java:374)
at androidx.appcompat.app.AppCompatDelegateImpl.setContentView(AppCompatDelegateImpl.java:467)
at androidx.appcompat.app.AppCompatActivity.setContentView(AppCompatActivity.java:140)
at it.czerwinski.customnavigatorexample.MainActivity.onCreate(MainActivity.kt:15)
at android.app.Activity.performCreate(Activity.java:7130)
at android.app.Activity.performCreate(Activity.java:7121)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1262)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2905)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3060)
at android.app.ActivityThread.handleRelaunchActivityInner(ActivityThread.java:4792)
at android.app.ActivityThread.handleRelaunchActivity(ActivityThread.java:4711)
2018-06-25 10:54:28.356 14543-14543/it.czerwinski.customnavigatorexample E/AndroidRuntime: at android.app.servertransaction.ActivityRelaunchItem.execute(ActivityRelaunchItem.java:69)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:110)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:70)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1800)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:164)
at android.app.ActivityThread.main(ActivityThread.java:6649)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:826)