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
17package androidx.navigation.fragment;
18
19import android.content.Context;
20import android.content.res.TypedArray;
21import android.os.Bundle;
22import android.support.annotation.NavigationRes;
23import android.support.annotation.NonNull;
24import android.support.annotation.Nullable;
25import android.support.v4.app.Fragment;
26import android.util.AttributeSet;
27import android.view.LayoutInflater;
28import android.view.View;
29import android.view.ViewGroup;
30import android.widget.FrameLayout;
31
32import androidx.navigation.NavController;
33import androidx.navigation.NavGraph;
34import androidx.navigation.NavHost;
35import androidx.navigation.Navigation;
36import 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 */
76public 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