Home | History | Annotate | Download | only in fragment
      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