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; 18 19 import android.content.Context; 20 import android.content.res.TypedArray; 21 import android.net.Uri; 22 import android.os.Bundle; 23 import android.support.annotation.IdRes; 24 import android.support.annotation.NonNull; 25 import android.support.annotation.Nullable; 26 import android.support.v4.util.Pair; 27 import android.support.v4.util.SparseArrayCompat; 28 import android.util.AttributeSet; 29 30 import androidx.navigation.common.R; 31 32 import java.util.Collection; 33 import java.util.Iterator; 34 import java.util.NoSuchElementException; 35 36 /** 37 * NavGraph is a collection of {@link NavDestination} nodes fetchable by ID. 38 * 39 * <p>A NavGraph serves as a 'virtual' destination: while the NavGraph itself will not appear 40 * on the back stack, navigating to the NavGraph will cause the 41 * {@link #getStartDestination starting destination} to be added to the back stack.</p> 42 */ 43 public class NavGraph extends NavDestination implements Iterable<NavDestination> { 44 private final SparseArrayCompat<NavDestination> mNodes = new SparseArrayCompat<>(); 45 private int mStartDestId; 46 47 /** 48 * Construct a new NavGraph. This NavGraph is not valid until you 49 * {@link #addDestination(NavDestination) add a destination} and 50 * {@link #setStartDestination(int) set the starting destination}. 51 * 52 * @param navigatorProvider The {@link NavController} which this NavGraph 53 * will be associated with. 54 */ 55 public NavGraph(@NonNull NavigatorProvider navigatorProvider) { 56 this(navigatorProvider.getNavigator(NavGraphNavigator.class)); 57 } 58 59 /** 60 * Construct a new NavGraph. This NavGraph is not valid until you 61 * {@link #addDestination(NavDestination) add a destination} and 62 * {@link #setStartDestination(int) set the starting destination}. 63 * 64 * @param navGraphNavigator The {@link NavGraphNavigator} which this destination 65 * will be associated with. Generally retrieved via a 66 * {@link NavController}'s 67 * {@link NavigatorProvider#getNavigator(Class)} method. 68 */ 69 public NavGraph(@NonNull Navigator<? extends NavGraph> navGraphNavigator) { 70 super(navGraphNavigator); 71 } 72 73 @Override 74 public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs) { 75 super.onInflate(context, attrs); 76 TypedArray a = context.getResources().obtainAttributes(attrs, 77 R.styleable.NavGraphNavigator); 78 setStartDestination( 79 a.getResourceId(R.styleable.NavGraphNavigator_startDestination, 0)); 80 a.recycle(); 81 } 82 83 @Override 84 @Nullable 85 Pair<NavDestination, Bundle> matchDeepLink(@NonNull Uri uri) { 86 // First search through any deep links directly added to this NavGraph 87 Pair<NavDestination, Bundle> result = super.matchDeepLink(uri); 88 if (result != null) { 89 return result; 90 } 91 // Then search through all child destinations for a matching deep link 92 for (NavDestination child : this) { 93 Pair<NavDestination, Bundle> childResult = child.matchDeepLink(uri); 94 if (childResult != null) { 95 return childResult; 96 } 97 } 98 return null; 99 } 100 101 /** 102 * Adds a destination to this NavGraph. The destination must have an 103 * {@link NavDestination#getId()} id} set. 104 * 105 * <p>The destination must not have a {@link NavDestination#getParent() parent} set. If 106 * the destination is already part of a {@link NavGraph navigation graph}, call 107 * {@link #remove(NavDestination)} before calling this method.</p> 108 * 109 * @param node destination to add 110 */ 111 public void addDestination(@NonNull NavDestination node) { 112 if (node.getId() == 0) { 113 throw new IllegalArgumentException("Destinations must have an id." 114 + " Call setId() or include an android:id in your navigation XML."); 115 } 116 NavDestination existingDestination = mNodes.get(node.getId()); 117 if (existingDestination == node) { 118 return; 119 } 120 if (node.getParent() != null) { 121 throw new IllegalStateException("Destination already has a parent set." 122 + " Call NavGraph.remove() to remove the previous parent."); 123 } 124 if (existingDestination != null) { 125 existingDestination.setParent(null); 126 } 127 node.setParent(this); 128 mNodes.put(node.getId(), node); 129 } 130 131 /** 132 * Adds multiple destinations to this NavGraph. Each destination must have an 133 * {@link NavDestination#getId()} id} set. 134 * 135 * <p> Each destination must not have a {@link NavDestination#getParent() parent} set. If 136 * any destination is already part of a {@link NavGraph navigation graph}, call 137 * {@link #remove(NavDestination)} before calling this method.</p> 138 * 139 * @param nodes destinations to add 140 */ 141 public void addDestinations(@NonNull Collection<NavDestination> nodes) { 142 for (NavDestination node : nodes) { 143 if (node == null) { 144 continue; 145 } 146 addDestination(node); 147 } 148 } 149 150 /** 151 * Adds multiple destinations to this NavGraph. Each destination must have an 152 * {@link NavDestination#getId()} id} set. 153 * 154 * <p> Each destination must not have a {@link NavDestination#getParent() parent} set. If 155 * any destination is already part of a {@link NavGraph navigation graph}, call 156 * {@link #remove(NavDestination)} before calling this method.</p> 157 * 158 * @param nodes destinations to add 159 */ 160 public void addDestinations(@NonNull NavDestination... nodes) { 161 for (NavDestination node : nodes) { 162 if (node == null) { 163 continue; 164 } 165 addDestination(node); 166 } 167 } 168 169 /** 170 * Finds a destination in the collection by ID. This will recursively check the 171 * {@link #getParent() parent} of this navigation graph if node is not found in 172 * this navigation graph. 173 * 174 * @param resid ID to locate 175 * @return the node with ID resid 176 */ 177 public NavDestination findNode(@IdRes int resid) { 178 return findNode(resid, true); 179 } 180 181 NavDestination findNode(@IdRes int resid, boolean searchParents) { 182 NavDestination destination = mNodes.get(resid); 183 // Search the parent for the NavDestination if it is not a child of this navigation graph 184 // and searchParents is true 185 return destination != null 186 ? destination 187 : searchParents && getParent() != null ? getParent().findNode(resid) : null; 188 } 189 190 @NonNull 191 @Override 192 public Iterator<NavDestination> iterator() { 193 return new Iterator<NavDestination>() { 194 private int mIndex = -1; 195 private boolean mWentToNext = false; 196 197 @Override 198 public boolean hasNext() { 199 return mIndex + 1 < mNodes.size(); 200 } 201 202 @Override 203 public NavDestination next() { 204 if (!hasNext()) { 205 throw new NoSuchElementException(); 206 } 207 mWentToNext = true; 208 return mNodes.valueAt(++mIndex); 209 } 210 211 @Override 212 public void remove() { 213 if (!mWentToNext) { 214 throw new IllegalStateException( 215 "You must call next() before you can remove an element"); 216 } 217 mNodes.valueAt(mIndex).setParent(null); 218 mNodes.removeAt(mIndex); 219 mIndex--; 220 mWentToNext = false; 221 } 222 }; 223 } 224 225 /** 226 * Add all destinations from another collection to this one. As each destination has at most 227 * one parent, the destinations will be removed from the given NavGraph. 228 * 229 * @param other collection of destinations to add. All destinations will be removed from this 230 * graph after being added to this graph. 231 */ 232 public void addAll(@NonNull NavGraph other) { 233 Iterator<NavDestination> iterator = other.iterator(); 234 while (iterator.hasNext()) { 235 NavDestination destination = iterator.next(); 236 iterator.remove(); 237 addDestination(destination); 238 } 239 } 240 241 /** 242 * Remove a given destination from this NavGraph 243 * 244 * @param node the destination to remove. 245 */ 246 public void remove(@NonNull NavDestination node) { 247 int index = mNodes.indexOfKey(node.getId()); 248 if (index >= 0) { 249 mNodes.valueAt(index).setParent(null); 250 mNodes.removeAt(index); 251 } 252 } 253 254 /** 255 * Clear all destinations from this navigation graph. 256 */ 257 public void clear() { 258 Iterator<NavDestination> iterator = iterator(); 259 while (iterator.hasNext()) { 260 iterator.next(); 261 iterator.remove(); 262 } 263 } 264 265 /** 266 * Returns the starting destination for this NavGraph. When navigating to the NavGraph, this 267 * destination is the one the user will initially see. 268 * @return 269 */ 270 @IdRes 271 public int getStartDestination() { 272 return mStartDestId; 273 } 274 275 /** 276 * Sets the starting destination for this NavGraph. 277 * 278 * @param startDestId The id of the destination to be shown when navigating to this NavGraph. 279 */ 280 public void setStartDestination(@IdRes int startDestId) { 281 mStartDestId = startDestId; 282 } 283 } 284