你們好,我叫小琪;java
本人16年畢業於中南林業科技大學軟件工程專業,畢業後在教育行業作安卓開發,後來於19年10月加入37手遊安卓團隊;node
目前主要負責國內發行安卓相關開發,同時兼顧內部幾款App開發。android
navigation——進階篇(本章講解)markdown
navigation——實戰篇 (敬請期待...)app
上篇對Navigation的一些概念進行了介紹,並在前言中提到了app中經常使用的一個場景,就是app的首頁,通常都會由一個activity+多個子tab組成,這種場景有不少種實現方式,好比可使用RadioGroup、FrgmentTabHost、TabLayout或者自定義view等方式,但這些都離不開經典的FragmentManager來管理fragment之間的切換。ide
如今,咱們有了新的實現方式,Navigation+BottomNavigationView,廢話很少說,先看最終要實現的效果函數
先確保引入了navigation相關依賴oop
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.1'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.1'
複製代碼
很簡單,包含三個頁面,首頁、發現、個人,點擊底部能夠切換頁面,有了上一篇的基礎,先新建一個nav_graph的導航資源文件,包含三個framgent子節點源碼分析
<?xml version="1.0" encoding="utf-8"?>
<navigation 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:id="@+id/nav_graph" app:startDestination="@id/FragmentHome">
<fragment android:id="@+id/FragmentHome" android:name="com.example.testnavigation.FragmentHome" android:label="fragment_home" tools:layout="@layout/fragment_home">
</fragment>
<fragment android:id="@+id/FragmentDicover" android:name="com.example.testnavigation.FragmentDicover" android:label="fragment_discover" tools:layout="@layout/fragment_discover">
</fragment>
<fragment android:id="@+id/FragmentMine" android:name="com.example.testnavigation.FragmentMine" android:label="fragment_mine" tools:layout="@layout/fragment_mine">
</fragment>
</navigation>
複製代碼
而後在activity的佈局中(這裏爲MainActivity的activity_main)中添加BottomNavigationView控件,
<?xml version="1.0" encoding="utf-8"?>
<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=".MainActivity">
<fragment android:id="@+id/fragment" android:name="androidx.navigation.fragment.NavHostFragment" android:layout_width="match_parent" android:layout_height="match_parent" app:defaultNavHost="false" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:navGraph="@navigation/nav_graph" />
<com.google.android.material.bottomnavigation.BottomNavigationView android:id="@+id/bottom_nav_view" android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:menu="@menu/bottom_nav_menu" />
</androidx.constraintlayout.widget.ConstraintLayout>
複製代碼
其中fragment節點在上面已經介紹過了,這篇再也不講解,BottomNavigationView是谷歌的一個實現底部導航的組件, app:menu屬性爲底部導航欄指定元素,新建一個bottom_nav_menu的menu資源文件
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/FragmentHome" android:icon="@mipmap/icon_tab_home" android:title="首頁" />
<item android:id="@+id/FragmentDicover" android:icon="@mipmap/icon_tab_find" android:title="發現" />
<item android:id="@+id/FragmentMine" android:icon="@mipmap/icon_tab_mine" android:title="個人" />
</menu>
複製代碼
注意:這裏item標籤的id和上面nav_graph中fragment標籤的id一致
資源準備好後,在MainActivity中
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//fragment的容器視圖,navHost的默認實現——NavHostFragment
val navHostFragment = supportFragmentManager.findFragmentById(R.id.fragment) as NavHostFragment
//管理應用導航的對象
val navController = navHostFragment.navController
//fragment與BottomNavigationView的交互交給NavigationUI
bottom_nav_view.setupWithNavController(navController)
}
}
複製代碼
經過NavigationUI庫,將BottomNavigationView和navigation關聯,就能實現上面的效果圖了,是否是so easy!
是否是很疑惑,這是怎麼作到的?,此時咱們進到源碼看看,進入setupWithNavController方法
fun BottomNavigationView.setupWithNavController(navController: NavController) {
NavigationUI.setupWithNavController(this, navController)
}
複製代碼
再進入
public static void setupWithNavController(
@NonNull final BottomNavigationView bottomNavigationView,
@NonNull final NavController navController) {
bottomNavigationView.setOnNavigationItemSelectedListener(
new BottomNavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
return onNavDestinationSelected(item, navController);
}
});
......
}
複製代碼
在這裏能夠看到,給bottomNavigationView設置了一個item點擊事件,進到onNavDestinationSelected方法,
public static boolean onNavDestinationSelected(@NonNull MenuItem item,
@NonNull NavController navController) {
NavOptions.Builder builder = new NavOptions.Builder()
.setLaunchSingleTop(true)
.setEnterAnim(R.animator.nav_default_enter_anim)
.setExitAnim(R.animator.nav_default_exit_anim)
.setPopEnterAnim(R.animator.nav_default_pop_enter_anim)
.setPopExitAnim(R.animator.nav_default_pop_exit_anim);
if ((item.getOrder() & Menu.CATEGORY_SECONDARY) == 0) {
builder.setPopUpTo(findStartDestination(navController.getGraph()).getId(), false);
}
NavOptions options = builder.build();
try {
//TODO provide proper API instead of using Exceptions as Control-Flow.
navController.navigate(item.getItemId(), null, options);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
複製代碼
還記得上篇介紹過的,怎麼從一個頁面跳轉到另外一個頁面的嗎,這裏也同樣,其實最終就是調用到了navController.navigate()方法進行頁面切換的。
使用Navigation+BottomNavigationView結合navigationUI擴展庫,這種方式是否是相比於以往的實現方式更簡單?可能你們火燒眉毛的想應用到本身的項目中去了,可卻不知還有坑在裏面。
分別在三個fragment中的主要生命週期中打印各自的log,運行程序,打開FragmentHome,能夠看到生命週期是正常執行的
而後點擊底部的發現切換到FragmentDiscover,FragmentDiscover生命週期也是正常的,但卻發現FragmentHome回調了onDestoryView()方法,
再次點擊首頁切回到FragmentHome,神奇的事情發生了,原來的FragmentHome銷燬了,卻又從新建立了一個新的FragmentHome實例,即fragment的重繪,而且從log日誌中也能夠看到,剛剛打開的FragmentDiscover也執行了onDestory一樣也銷燬了。
下面從源碼角度分析爲何會這樣。
從NavHostFragment入手,首先看到它的oncreate方法中,
@CallSuper
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
......
mNavController = new NavHostController(context);
......
onCreateNavController(mNavController);
......
}
複製代碼
去掉無關代碼,只看核心代碼,能夠看到,有一個NavHostController類型的mNavController成員變量,mNavController就是前篇文章中提到的管理導航的navController對象,只不過它是繼承自NavController的,戳進去構造方法,發現調用了父類的構造方法,再戳進去來到了NavController的構造方法,
public NavController(@NonNull Context context) {
mContext = context;
.......
mNavigatorProvider.addNavigator(new NavGraphNavigator(mNavigatorProvider));
mNavigatorProvider.addNavigator(new ActivityNavigator(mContext));
}
複製代碼
在構造方法中,mNavigatorProvider添加了兩個navigator,首先看看mNavigatorProvider是個什麼東東,
public class NavigatorProvider {
private static final HashMap<Class<?>, String> sAnnotationNames = new HashMap<>();
......
@NonNull
static String getNameForNavigator(@NonNull Class<? extends Navigator> navigatorClass) {
String name = sAnnotationNames.get(navigatorClass);
if (name == null) {
Navigator.Name annotation = navigatorClass.getAnnotation(Navigator.Name.class);
name = annotation != null ? annotation.value() : null;
if (!validateName(name)) {
throw new IllegalArgumentException("No @Navigator.Name annotation found for "
+ navigatorClass.getSimpleName());
}
sAnnotationNames.put(navigatorClass, name);
}
return name;
}
}
複製代碼
看核心的一個方法getNameForNavigator,該方法傳入一個繼承了Navigator的類,而後獲取其註解爲Navigator.Name的值,並經過sAnnotationNames緩存起來,這提及來好像有點抽象,咱們看具體的,前面有說到mNavigatorProvider添加了兩個navigator,分別是NavGraphNavigator和ActivityNavigator,咱們戳進去ActivityNavigator源碼,
getNameForNavigator方法對應到這裏,其實就是獲取到了Navigator.Name的註解值activity,由此能夠知道,mNavigatorProvider調用addNavigator方法,就會緩存key爲navigator的類,值爲這個類的Navigator.Name註解值。
回到前面的NavHostFragment的onCreate方法中,
@CallSuper
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
......
mNavController = new NavHostController(context);
......
onCreateNavController(mNavController);
......
}
複製代碼
看完了mNavController的構造函數,繼續onCreateNavController方法,
@CallSuper
protected void onCreateNavController(@NonNull NavController navController) {
navController.getNavigatorProvider().addNavigator(
new DialogFragmentNavigator(requireContext(), getChildFragmentManager()));
navController.getNavigatorProvider().addNavigator(createFragmentNavigator());
}
複製代碼
createFragmentNavigator方法
@Deprecated
@NonNull
protected Navigator<? extends FragmentNavigator.Destination> createFragmentNavigator() {
return new FragmentNavigator(requireContext(), getChildFragmentManager(),
getContainerId());
}
複製代碼
能夠看到,又繼續添加了DialogFragmentNavigator和FragmentNavigator兩個navigator,至此總共緩存了四個navigator。
回到NavHostFragment的oncreate方法,繼續看後面的代碼
@CallSuper
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
......
mNavController = new NavHostController(context);
......
onCreateNavController(mNavController);
......
if (mGraphId != 0) {
// Set from onInflate()
mNavController.setGraph(mGraphId);
} else {
......
}
}
複製代碼
在onInflate()方法中能夠看出,mGraphId就是在佈局文件中定義NavHostFragment時,經過app:navGraph屬性指定的導航資源文件,
跟進setGraph()方法,
public void setGraph(@NavigationRes int graphResId) {
setGraph(graphResId, null);
}
public void setGraph(@NavigationRes int graphResId, @Nullable Bundle startDestinationArgs) {
setGraph(getNavInflater().inflate(graphResId), startDestinationArgs);
}
public void setGraph(@NonNull NavGraph graph, @Nullable Bundle startDestinationArgs) {
if (mGraph != null) {
// Pop everything from the old graph off the back stack
popBackStackInternal(mGraph.getId(), true);
}
mGraph = graph;
onGraphCreated(startDestinationArgs);
}
複製代碼
在第二個重載方法中,經過getNavInflater().inflate方法建立出一個NavGraph對象,傳到第三個重載的方法中,並賦值給成員變量mGraph,最後在onGraphCreated方法中將第一個頁面顯示出來。
因而可知,導航資源文件nav_graph會被解析成一個NavGraph對象,看下NavGraph
public class NavGraph extends NavDestination implements Iterable<NavDestination> {
final SparseArrayCompat<NavDestination> mNodes = new SparseArrayCompat<>();
}
複製代碼
NavGraph繼承了NavDestination,NavDestination其實就是nav_graph.xml中navigation下的一個個節點,也就是一個個頁面,NavGraph內部有個集合mNodes,用來保存一組NavDestination。
至此咱們具體分析了兩個重要的步驟,一個是navigator的,一個是nav_graph.xml是如何被解析並關聯到navController,弄清楚這兩個步驟,對接下來的分析大有幫助。
還記得前面有分析到,BottomNavigationView是怎麼作到頁面切換的嗎,把上面代碼照樣搬過來,
public static boolean onNavDestinationSelected(@NonNull MenuItem item,
@NonNull NavController navController) {
NavOptions.Builder builder = new NavOptions.Builder()
.setLaunchSingleTop(true)
.setEnterAnim(R.animator.nav_default_enter_anim)
.setExitAnim(R.animator.nav_default_exit_anim)
.setPopEnterAnim(R.animator.nav_default_pop_enter_anim)
.setPopExitAnim(R.animator.nav_default_pop_exit_anim);
if ((item.getOrder() & Menu.CATEGORY_SECONDARY) == 0) {
builder.setPopUpTo(findStartDestination(navController.getGraph()).getId(), false);
}
NavOptions options = builder.build();
try {
//TODO provide proper API instead of using Exceptions as Control-Flow.
navController.navigate(item.getItemId(), null, options);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
複製代碼
沒錯,是經過 navController.navigate這個方法,傳入item.getItemId(),由此能夠知道,上面提到過的,定義BottomNavigationView時 app:menu屬性指定的menu資源文件中,item標籤的id和nav_graph中fragment標籤的id保持一致的緣由了吧,咱們繼續跟蹤,
public void navigate(@IdRes int resId, @Nullable Bundle args, @Nullable NavOptions navOptions) {
navigate(resId, args, navOptions, null);
}
public void navigate(@IdRes int resId, @Nullable Bundle args, @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
......
@IdRes int destId = resId;
.......
NavDestination node = findDestination(destId);
......
navigate(node, combinedArgs, navOptions, navigatorExtras);
}
private void navigate(@NonNull NavDestination node, @Nullable Bundle args, @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
......
Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator(
node.getNavigatorName());
Bundle finalArgs = node.addInDefaultArgs(args);
NavDestination newDest = navigator.navigate(node, finalArgs,
navOptions, navigatorExtras);
......
}
複製代碼
能夠看到,在第二個重載方法中,經過findDestination方法傳入導航到目標頁面的id,得到NavDestination對象node,在第三個重載方法中,經過mNavigatorProvider獲取navigator,那麼這個navigator是什麼呢,還記得上面分析的NavHostFragment通過oncreate方法以後,navigatorProvider總共緩存了四個navigator嗎, 因爲在nav.graph.xml中,定義的是標籤,因此這裏navigator最終拿到的是一個FragmentNavigator對象。進到FragmentNavigator的navigate方法
public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args, @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
if (mFragmentManager.isStateSaved()) {
Log.i(TAG, "Ignoring navigate() call: FragmentManager has already"
+ " saved its state");
return null;
}
String className = destination.getClassName();
if (className.charAt(0) == '.') {
className = mContext.getPackageName() + className;
}
final Fragment frag = instantiateFragment(mContext, mFragmentManager,
className, args);
frag.setArguments(args);
final FragmentTransaction ft = mFragmentManager.beginTransaction();
int enterAnim = navOptions != null ? navOptions.getEnterAnim() : -1;
int exitAnim = navOptions != null ? navOptions.getExitAnim() : -1;
int popEnterAnim = navOptions != null ? navOptions.getPopEnterAnim() : -1;
int popExitAnim = navOptions != null ? navOptions.getPopExitAnim() : -1;
if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
enterAnim = enterAnim != -1 ? enterAnim : 0;
exitAnim = exitAnim != -1 ? exitAnim : 0;
popEnterAnim = popEnterAnim != -1 ? popEnterAnim : 0;
popExitAnim = popExitAnim != -1 ? popExitAnim : 0;
ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim);
}
ft.replace(mContainerId, frag);
ft.setPrimaryNavigationFragment(frag);
final @IdRes int destId = destination.getId();
final boolean initialNavigation = mBackStack.isEmpty();
// TODO Build first class singleTop behavior for fragments
final boolean isSingleTopReplacement = navOptions != null && !initialNavigation
&& navOptions.shouldLaunchSingleTop()
&& mBackStack.peekLast() == destId;
boolean isAdded;
if (initialNavigation) {
isAdded = true;
} else if (isSingleTopReplacement) {
// Single Top means we only want one instance on the back stack
if (mBackStack.size() > 1) {
// If the Fragment to be replaced is on the FragmentManager's
// back stack, a simple replace() isn't enough so we
// remove it from the back stack and put our replacement
// on the back stack in its place
mFragmentManager.popBackStack(
generateBackStackName(mBackStack.size(), mBackStack.peekLast()),
FragmentManager.POP_BACK_STACK_INCLUSIVE);
ft.addToBackStack(generateBackStackName(mBackStack.size(), destId));
}
isAdded = false;
} else {
ft.addToBackStack(generateBackStackName(mBackStack.size() + 1, destId));
isAdded = true;
}
if (navigatorExtras instanceof Extras) {
Extras extras = (Extras) navigatorExtras;
for (Map.Entry<View, String> sharedElement : extras.getSharedElements().entrySet()) {
ft.addSharedElement(sharedElement.getKey(), sharedElement.getValue());
}
}
ft.setReorderingAllowed(true);
ft.commit();
// The commit succeeded, update our view of the world
if (isAdded) {
mBackStack.add(destId);
return destination;
} else {
return null;
}
}
複製代碼
經過Destination拿到ClassName,instantiateFragment方法經過內反射建立出對應的fragment,最後經過FragmentTransaction的replace方法建立fragment。
至此,終於真相大白了!咱們知道replace方法每次都會從新建立fragment,因此使用Navigation建立的底部導航頁面,每次點擊切換頁面當前fragment都會重建。
既然知道了fragment重繪的緣由,那就能夠對症下藥了,咱們知道,fragment的切換除了replace,還能夠經過hide和show,那怎麼作到呢,經過前面的分析,其實能夠自定義一個navigator繼承FragmentNavigator,重寫它的navigate方法,從而達到經過hide和show進行fragment切換的目的。
這裏新建一個FixFragmentNavigator類,咱們但願在nav_graph中經過fixFragment標籤來指定每一個導航頁面
@Navigator.Name("fixFragment")
class FixFragmentNavigator(context: Context, manager: FragmentManager, containerId: Int) :
FragmentNavigator(context, manager, containerId) {
private val mContext = context
private val mManager = manager
private val mContainerId = containerId
private val TAG = "FixFragmentNavigator"
override fun navigate( destination: Destination, args: Bundle?, navOptions: NavOptions?, navigatorExtras: Navigator.Extras? ): NavDestination? {
if (mManager.isStateSaved) {
Log.i(TAG, "Ignoring navigate() call: FragmentManager has already" + " saved its state")
return null
}
var className = destination.className
if (className[0] == '.') {
className = mContext.packageName + className
}
val ft = mManager.beginTransaction()
var enterAnim = navOptions?.enterAnim ?: -1
var exitAnim = navOptions?.exitAnim ?: -1
var popEnterAnim = navOptions?.popEnterAnim ?: -1
var popExitAnim = navOptions?.popExitAnim ?: -1
if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
enterAnim = if (enterAnim != -1) enterAnim else 0
exitAnim = if (exitAnim != -1) exitAnim else 0
popEnterAnim = if (popEnterAnim != -1) popEnterAnim else 0
popExitAnim = if (popExitAnim != -1) popExitAnim else 0
ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim)
}
/** * 一、先查詢當前顯示的fragment 不爲空則將其hide * 二、根據tag查詢當前添加的fragment是否不爲null,不爲null則將其直接show * 三、爲null則經過instantiateFragment方法建立fragment實例 * 四、將建立的實例添加在事務中 */
val fragment = mManager.primaryNavigationFragment //當前顯示的fragment
if (fragment != null) {
ft.hide(fragment)
ft.setMaxLifecycle(fragment, Lifecycle.State.STARTED);
}
var frag: Fragment?
val tag = destination.id.toString()
frag = mManager.findFragmentByTag(tag)
if (frag != null) {
ft.show(frag)
ft.setMaxLifecycle(frag, Lifecycle.State.RESUMED);
} else {
frag = instantiateFragment(mContext, mManager, className, args)
frag.arguments = args
ft.add(mContainerId, frag, tag)
}
ft.setPrimaryNavigationFragment(frag)
@IdRes val destId = destination.id
/** * 經過反射的方式獲取 mBackStack */
val mBackStack: ArrayDeque<Int>
val field = FragmentNavigator::class.java.getDeclaredField("mBackStack")
field.isAccessible = true
mBackStack = field.get(this) as ArrayDeque<Int>
val initialNavigation = mBackStack.isEmpty()
val isSingleTopReplacement = (navOptions != null && !initialNavigation
&& navOptions.shouldLaunchSingleTop()
&& mBackStack.peekLast() == destId)
val isAdded: Boolean
if (initialNavigation) {
isAdded = true
} else if (isSingleTopReplacement) {
// Single Top means we only want one instance on the back stack
if (mBackStack.size > 1) {
// If the Fragment to be replaced is on the FragmentManager's
// back stack, a simple replace() isn't enough so we
// remove it from the back stack and put our replacement
// on the back stack in its place
mManager.popBackStack(
zygoteBackStackName(mBackStack.size, mBackStack.peekLast()),
FragmentManager.POP_BACK_STACK_INCLUSIVE
)
ft.addToBackStack(zygoteBackStackName(mBackStack.size, destId))
}
isAdded = false
} else {
ft.addToBackStack(zygoteBackStackName(mBackStack.size + 1, destId))
isAdded = true
}
if (navigatorExtras is Extras) {
val extras = navigatorExtras as Extras?
for ((key, value) in extras!!.sharedElements) {
ft.addSharedElement(key, value)
}
}
ft.setReorderingAllowed(true)
ft.commit()
// The commit succeeded, update our view of the world
if (isAdded) {
mBackStack.add(destId)
return destination
} else {
return null
}
}
private fun zygoteBackStackName(backIndex: Int, destid: Int): String {
return "$backIndex - $destid"
}
}
複製代碼
新建一個導航資源文件fix_nav_graph.xml,將本來的fragment換成fixFragment
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemams.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/nav_graph" app:startDestination="@id/FragmentHome">
<fixFragment android:id="@+id/FragmentHome" android:name="com.example.testnavigation.FragmentHome" android:label="fragment_home" tools:layout="@layout/fragment_home">
</fixFragment>
<fixFragment android:id="@+id/FragmentDicover" android:name="com.example.testnavigation.FragmentDicover" android:label="fragment_discover" tools:layout="@layout/fragment_discover">
</fixFragment>
<fixFragment android:id="@+id/FragmentMine" android:name="com.example.testnavigation.FragmentMine" android:label="fragment_mine" tools:layout="@layout/fragment_mine">
</fixFragment>
</navigation>
複製代碼
而後把activity_main.xml中的app:navGraph屬性值替換爲fix_nav_graph,
「修復版的」FragmentNavigator寫好後,在MainActivity中,經過navController把它添加到fragmentNavigator中,
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val navController = Navigation.findNavController(this, R.id.fragment)
val fragment =
supportFragmentManager.findFragmentById(R.id.fragment) as NavHostFragment
val fragmentNavigator =
FixFragmentNavigator(this, supportFragmentManager, fragment.id)
//添加自定義的FixFragmentNavigator
navController.navigatorProvider.addNavigator(fragmentNavigator)
bottom_nav_view.setupWithNavController(navController)
}
複製代碼
滿心歡喜的覺得大功告成了,運行程序發現崩了,報錯以下:
報錯信息很明顯,找不到fixFragment對應的navigator,必須經過addNavigator方法進行添加,這怎麼回事呢?明明已經調用addNavigator方法添加自定義的FixFragmentNavigator了。別急,仍是回到NavHostFragment的onCreate()方法中,
@CallSuper
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
......
if (mGraphId != 0) {
// Set from onInflate()
mNavController.setGraph(mGraphId);
} else {
// See if it was set by NavHostFragment.create()
final Bundle args = getArguments();
final int graphId = args != null ? args.getInt(KEY_GRAPH_ID) : 0;
final Bundle startDestinationArgs = args != null
? args.getBundle(KEY_START_DESTINATION_ARGS)
: null;
if (graphId != 0) {
mNavController.setGraph(graphId, startDestinationArgs);
}
}
}
複製代碼
上面已經說過了mGraphId就是經過app:navGraph指定的導航資源文件,那麼mGraphId此時不等於0,走到if語句中,
@CallSuper
public void setGraph(@NavigationRes int graphResId) {
setGraph(graphResId, null);
}
複製代碼
@CallSuper
public void setGraph(@NavigationRes int graphResId, @Nullable Bundle startDestinationArgs) {
setGraph(getNavInflater().inflate(graphResId), startDestinationArgs);
}
複製代碼
進到getNavInflater().inflate
@SuppressLint("ResourceType")
@NonNull
public NavGraph inflate(@NavigationRes int graphResId) {
......
try {
......
NavDestination destination = inflate(res, parser, attrs, graphResId);
if (!(destination instanceof NavGraph)) {
throw new IllegalArgumentException("Root element <" + rootElement + ">"
+ " did not inflate into a NavGraph");
}
return (NavGraph) destination;
} catch (Exception e) {
throw new RuntimeException("Exception inflating "
+ res.getResourceName(graphResId) + " line "
+ parser.getLineNumber(), e);
} finally {
parser.close();
}
}
複製代碼
進到inflate方法,
@NonNull
private NavDestination inflate(@NonNull Resources res, @NonNull XmlResourceParser parser, @NonNull AttributeSet attrs, int graphResId) throws XmlPullParserException, IOException {
Navigator<?> navigator = mNavigatorProvider.getNavigator(parser.getName());
......
}
複製代碼
進到getNavigator方法
@CallSuper
@NonNull
public <T extends Navigator<?>> T getNavigator(@NonNull String name) {
if (!validateName(name)) {
throw new IllegalArgumentException("navigator name cannot be an empty string");
}
Navigator<? extends NavDestination> navigator = mNavigators.get(name);
if (navigator == null) {
throw new IllegalStateException("Could not find Navigator with name \"" + name
+ "\". You must call NavController.addNavigator() for each navigation type.");
}
return (T) navigator;
}
複製代碼
原來報錯的信息在這裏,這裏其實就是經過標籤獲取對應的navigator,然而在NavHostFragmen執行oncreate後,默認只添加了本來的四個navigator,而此時在解析fixFragment節點時,咱們自定義的FixFragmentNavigator還未添加進來,因此拋了這個異常。
那麼咱們是不能在佈局文件中經過app:navGraph屬性指定自定義的導航資源文件了,只能在佈局文件中去掉app:navGraph這個屬性,而後在添加FixFragmentNavigator的同時,經過代碼將導航資源文件設置進去。
最終代碼以下:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val navController = Navigation.findNavController(this, R.id.fragment)
val fragment =
supportFragmentManager.findFragmentById(R.id.fragment) as NavHostFragment
val fragmentNavigator =
FixFragmentNavigator(this, supportFragmentManager, fragment.id)
//添加自定義的FixFragmentNavigator
navController.navigatorProvider.addNavigator(fragmentNavigator)
//經過代碼將導航資源文件設置進去
navController.setGraph(R.navigation.fix_nav_graph)
bottom_nav_view.setupWithNavController(navController)
}
複製代碼
運行程序,觀察各fragment的生命週期,發現已經不會從新走生命週期了。
本篇在上篇的基礎上,結合BottomNavigationView實現了第一個底部導航切換的實例,而後介紹了這種方式引起的坑,進而經過源碼分析了發生這種現象的緣由,並給出瞭解決的思路。讀懂源碼纔是最重要的,如今再總結一下navigator進行頁面切換的原理:
首先須要一個承載頁面的容器NavHost,這個容器有個默認的實現NavHostFragment
NavHostFragment有個mNavController成員變量,它是一個NavController對象,最終頁面導航都是經過調用它的navigate方法實現的
mNavController內部經過NavigatorProvider管理navigator
NavHostFragment在oncreate方法中,mNavController添加了四個navigator,分別是FragmentNavigator、ActivityNavigator、DialogFragmentNavigator、NavGraphNavigator,分別實現各自的navigate方法,進行頁面切換
mNavController經過調用setGraph()方法,傳入導航資源文件,並進行解析,獲取導航資源文件中的節點,獲得NavDestination
FragmentNavigator的navigate方法中,是經過replace方法達到fragment的切換目的,所以會引發fragment的重繪