1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.navigation.fragment; 18 19 import android.content.Context; 20 import android.content.res.TypedArray; 21 import android.os.Bundle; 22 import android.support.annotation.NavigationRes; 23 import android.support.annotation.NonNull; 24 import android.support.annotation.Nullable; 25 import android.support.v4.app.Fragment; 26 import android.util.AttributeSet; 27 import android.view.LayoutInflater; 28 import android.view.View; 29 import android.view.ViewGroup; 30 import android.widget.FrameLayout; 31 32 import androidx.navigation.NavController; 33 import androidx.navigation.NavGraph; 34 import androidx.navigation.NavHost; 35 import androidx.navigation.Navigation; 36 import androidx.navigation.Navigator; 37 38 /** 39 * NavHostFragment provides an area within your layout for self-contained navigation to occur. 40 * 41 * <p>NavHostFragment is intended to be used as the content area within a layout resource 42 * defining your app's chrome around it, e.g.:</p> 43 * 44 * <pre class="prettyprint"> 45 * <android.support.v4.widget.DrawerLayout 46 * xmlns:android="http://schemas.android.com/apk/res/android" 47 * xmlns:app="http://schemas.android.com/apk/res-auto" 48 * android:layout_width="match_parent" 49 * android:layout_height="match_parent"> 50 * <fragment 51 * android:layout_width="match_parent" 52 * android:layout_height="match_parent" 53 * android:id="@+id/my_nav_host_fragment" 54 * android:name="androidx.navigation.fragment.NavHostFragment" 55 * app:navGraph="@xml/nav_sample" 56 * app:defaultNavHost="true" /> 57 * <android.support.design.widget.NavigationView 58 * android:layout_width="wrap_content" 59 * android:layout_height="match_parent" 60 * android:layout_gravity="start"/> 61 * </android.support.v4.widget.DrawerLayout> 62 * </pre> 63 * 64 * <p>Each NavHostFragment has a {@link NavController} that defines valid navigation within 65 * the navigation host. This includes the {@link NavGraph navigation graph} as well as navigation 66 * state such as current location and back stack that will be saved and restored along with the 67 * NavHostFragment itself.</p> 68 * 69 * <p>NavHostFragments register their navigation controller at the root of their view subtree 70 * such that any descendant can obtain the controller instance through the {@link Navigation} 71 * helper class's methods such as {@link Navigation#findNavController(View)}. View event listener 72 * implementations such as {@link android.view.View.OnClickListener} within navigation destination 73 * fragments can use these helpers to navigate based on user interaction without creating a tight 74 * coupling to the navigation host.</p> 75 */ 76 public class NavHostFragment extends Fragment implements NavHost { 77 private static final String KEY_GRAPH_ID = "android-support-nav:fragment:graphId"; 78 private static final String KEY_NAV_CONTROLLER_STATE = 79 "android-support-nav:fragment:navControllerState"; 80 private static final String KEY_DEFAULT_NAV_HOST = "android-support-nav:fragment:defaultHost"; 81 82 /** 83 * Find a {@link NavController} given a local {@link Fragment}. 84 * 85 * <p>This method will locate the {@link NavController} associated with this Fragment, 86 * looking first for a {@link NavHostFragment} along the given Fragment's parent chain. 87 * If a {@link NavController} is not found, this method will look for one along this 88 * Fragment's {@link Fragment#getView() view hierarchy} as specified by 89 * {@link Navigation#findNavController(View)}.</p> 90 * 91 * @param fragment the locally scoped Fragment for navigation 92 * @return the locally scoped {@link NavController} for navigating from this {@link Fragment} 93 * @throws IllegalStateException if the given Fragment does not correspond with a 94 * {@link NavHost} or is not within a NavHost. 95 */ 96 @NonNull 97 public static NavController findNavController(@NonNull Fragment fragment) { 98 Fragment findFragment = fragment; 99 while (findFragment != null) { 100 if (findFragment instanceof NavHostFragment) { 101 return ((NavHostFragment) findFragment).getNavController(); 102 } 103 Fragment primaryNavFragment = findFragment.requireFragmentManager() 104 .getPrimaryNavigationFragment(); 105 if (primaryNavFragment instanceof NavHostFragment) { 106 return ((NavHostFragment) primaryNavFragment).getNavController(); 107 } 108 findFragment = findFragment.getParentFragment(); 109 } 110 111 // Try looking for one associated with the view instead, if applicable 112 View view = fragment.getView(); 113 if (view != null) { 114 return Navigation.findNavController(view); 115 } 116 throw new IllegalStateException("Fragment " + fragment 117 + " does not have a NavController set"); 118 } 119 120 private NavController mNavController; 121 122 // State that will be saved and restored 123 private boolean mDefaultNavHost; 124 125 /** 126 * Create a new NavHostFragment instance with an inflated {@link NavGraph} resource. 127 * 128 * @param graphResId resource id of the navigation graph to inflate 129 * @return a new NavHostFragment instance 130 */ 131 public static NavHostFragment create(@NavigationRes int graphResId) { 132 Bundle b = null; 133 if (graphResId != 0) { 134 b = new Bundle(); 135 b.putInt(KEY_GRAPH_ID, graphResId); 136 } 137 138 final NavHostFragment result = new NavHostFragment(); 139 if (b != null) { 140 result.setArguments(b); 141 } 142 return result; 143 } 144 145 /** 146 * Returns the {@link NavController navigation controller} for this navigation host. 147 * This method will return null until this host fragment's {@link #onCreate(Bundle)} 148 * has been called and it has had an opportunity to restore from a previous instance state. 149 * 150 * @return this host's navigation controller 151 * @throws IllegalStateException if called before {@link #onCreate(Bundle)} 152 */ 153 @NonNull 154 @Override 155 public NavController getNavController() { 156 if (mNavController == null) { 157 throw new IllegalStateException("NavController is not available before onCreate()"); 158 } 159 return mNavController; 160 } 161 162 /** 163 * Set a {@link NavGraph} for this navigation host's {@link NavController} by resource id. 164 * The existing graph will be replaced. 165 * 166 * @param graphResId resource id of the navigation graph to inflate 167 */ 168 public void setGraph(@NavigationRes int graphResId) { 169 if (mNavController == null) { 170 Bundle args = getArguments(); 171 if (args == null) { 172 args = new Bundle(); 173 } 174 args.putInt(KEY_GRAPH_ID, graphResId); 175 setArguments(args); 176 } else { 177 mNavController.setGraph(graphResId); 178 } 179 } 180 181 @Override 182 public void onAttach(Context context) { 183 super.onAttach(context); 184 // TODO This feature should probably be a first-class feature of the Fragment system, 185 // but it can stay here until we can add the necessary attr resources to 186 // the fragment lib. 187 if (mDefaultNavHost) { 188 requireFragmentManager().beginTransaction() 189 .setPrimaryNavigationFragment(this) 190 .commit(); 191 } 192 } 193 194 @Override 195 public void onCreate(@Nullable Bundle savedInstanceState) { 196 super.onCreate(savedInstanceState); 197 final Context context = requireContext(); 198 199 mNavController = new NavController(context); 200 mNavController.getNavigatorProvider().addNavigator(createFragmentNavigator()); 201 202 Bundle navState = null; 203 if (savedInstanceState != null) { 204 navState = savedInstanceState.getBundle(KEY_NAV_CONTROLLER_STATE); 205 if (savedInstanceState.getBoolean(KEY_DEFAULT_NAV_HOST, false)) { 206 mDefaultNavHost = true; 207 requireFragmentManager().beginTransaction() 208 .setPrimaryNavigationFragment(this) 209 .commit(); 210 } 211 } 212 213 if (navState != null) { 214 // Navigation controller state overrides arguments 215 mNavController.restoreState(navState); 216 } else { 217 final Bundle args = getArguments(); 218 final int graphId = args != null ? args.getInt(KEY_GRAPH_ID) : 0; 219 if (graphId != 0) { 220 mNavController.setGraph(graphId); 221 } else { 222 mNavController.setMetadataGraph(); 223 } 224 } 225 } 226 227 /** 228 * Create the FragmentNavigator that this NavHostFragment will use. By default, this uses 229 * {@link FragmentNavigator}, which replaces the entire contents of the NavHostFragment. 230 * <p> 231 * This is only called once in {@link #onCreate(Bundle)} and should not be called directly by 232 * subclasses. 233 * @return a new instance of a FragmentNavigator 234 */ 235 @NonNull 236 protected Navigator<? extends FragmentNavigator.Destination> createFragmentNavigator() { 237 return new FragmentNavigator(requireContext(), getChildFragmentManager(), getId()); 238 } 239 240 @Nullable 241 @Override 242 public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, 243 @Nullable Bundle savedInstanceState) { 244 FrameLayout frameLayout = new FrameLayout(inflater.getContext()); 245 // When added via XML, this has no effect (since this FrameLayout is given the ID 246 // automatically), but this ensures that the View exists as part of this Fragment's View 247 // hierarchy in cases where the NavHostFragment is added programmatically as is required 248 // for child fragment transactions 249 frameLayout.setId(getId()); 250 return frameLayout; 251 } 252 253 @Override 254 public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { 255 super.onViewCreated(view, savedInstanceState); 256 if (!(view instanceof ViewGroup)) { 257 throw new IllegalStateException("created host view " + view + " is not a ViewGroup"); 258 } 259 // When added via XML, the parent is null and our view is the root of the NavHostFragment 260 // but when added programmatically, we need to set the NavController on the parent - i.e., 261 // the View that has the ID matching this NavHostFragment. 262 View rootView = view.getParent() != null ? (View) view.getParent() : view; 263 Navigation.setViewNavController(rootView, mNavController); 264 } 265 266 @Override 267 public void onInflate(Context context, AttributeSet attrs, Bundle savedInstanceState) { 268 super.onInflate(context, attrs, savedInstanceState); 269 270 final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NavHostFragment); 271 final int graphId = a.getResourceId(R.styleable.NavHostFragment_navGraph, 0); 272 final boolean defaultHost = a.getBoolean(R.styleable.NavHostFragment_defaultNavHost, false); 273 274 if (graphId != 0) { 275 setGraph(graphId); 276 } 277 if (defaultHost) { 278 mDefaultNavHost = true; 279 } 280 a.recycle(); 281 } 282 283 @Override 284 public void onSaveInstanceState(@NonNull Bundle outState) { 285 super.onSaveInstanceState(outState); 286 Bundle navState = mNavController.saveState(); 287 if (navState != null) { 288 outState.putBundle(KEY_NAV_CONTROLLER_STATE, navState); 289 } 290 if (mDefaultNavHost) { 291 outState.putBoolean(KEY_DEFAULT_NAV_HOST, true); 292 } 293 } 294 } 295