介紹
導航Navigator是方便用戶在一個主Activity包含多個Fragment時,進行內容片斷的切換的交互。
優點:
1)將處理Fragment的事務進行了封裝處理,用戶不需要再處理該類事務
2)切換的動畫標準化了
3)只需要配置好導航視圖即可方便切換
4)片斷之間的數據傳遞需要使用Gradle插件(該點本文未涉及)
三個角色:
導航圖:該圖包含了導航需要的相關信息,全部保存在xml資源文件中
NavHost: 負責顯示目標內容的容器;
NavController: 管理導航的對象,根據需要來替換顯示容器中的內容;
Navigation使用
1、添加依賴
val nav_version = "2.3.5"
// Java language implementation
implementation("androidx.navigation:navigation-fragment:$nav_version")
implementation("androidx.navigation:navigation-ui:$nav_version")
2、創建導航圖
在res/navigation/nav_graph.xml 這里的文件名會在布局文件中使用;
navigation 是導航圖的根節點,
**app:startDestination 指定第一個目的地**
fragment標簽:指明了目的地的信息
**id 屬性 是唯一標識,用于在代碼中引用該目的地** **name 屬性 目的地的全類名** **Label 屬性 包含該目的地的用戶可讀名稱** **tool:layout: 屬性 指定對應的布局文件**
action標簽: 是目的地的子元素,例如有安裝點擊跳轉時,可以直接調用action的對應的id即可
<?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"
app:startDestination="@id/home">
<fragment
android:id="@+id/home"
android:name="com.leon.jetpack.navigator.FragmentFirst"
android:label="首頁"
tools:layout="@layout/fragment_navigator_first_layout">
<!--這里給第一個Fragment的按鈕添加點擊事件后的目的地是到第二個Fragment中-->
<action
android:id="@+id/gotoSectond"
app:destination="@id/square" />
</fragment>
<fragment
android:id="@+id/square"
android:name="com.leon.jetpack.navigator.FragmentSecond"
android:label="廣場"
tools:layout="@layout/fragment_navigator_second_layout">
<action
android:id="@+id/backtoFirst"
app:destination="@id/home" />
<action
android:id="@+id/gotothird"
app:destination="@+id/mine" />
</fragment>
<fragment
android:id="@+id/mine"
android:name="com.leon.jetpack.navigator.FragmentThird"
android:label="我的"
tools:layout="@layout/fragment_navigator_third_layout">
<action
android:id="@+id/backtoSecond"
app:destination="@+id/square" />
<action
android:id="@+id/gotofirst"
app:destination="@+id/home" />
</fragment>
</navigation>
3、布局文件XML添加NavHostFragment容器
name: 是指定的NavHost的實現類
app:navGraph 指向導航視圖文件,這樣導航控制器根據這個導航視圖就可以導航所有的目的地
app:defaultNavHost: 這個屬性時確保NavHostFragment會攔截系統的返回事件。
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_container"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="@+id/nav_view"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_graph" />
4、初始化導航控制器
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_navigator_main);
NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.fragment_container);
//這里就可以得到導航控制器
NavController navController = navHostFragment.getNavController();
}
5、操作導航控制器
Button btn = view.findViewById(R.id.btn1);
btn.setOnClickListener(v -> {
//這里的v 是當前操作的view ,navigate中的參數就是我們的導航視圖中寫好的action 的Id
Navigation.findNavController(v).navigate(R.id.gotoSectond);
});
6、結合BottomNavigationView 使用
1)引入依賴包
implementation 'com.google.android.material:material:1.1.0'
2)創建Menu
<!--item 的id 需要和nav_graph.xml 中fragment的id保持一致,否則不會tab切換會無效的。-->
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/home"
android:icon="@drawable/ic_home"
android:title="首頁" />
<item
android:id="@+id/square"
android:icon="@drawable/ic_square"
android:title="廣場" />
<item
android:id="@+id/mine"
android:icon="@drawable/ic_person"
android:title="我的" />
</menu>
3)添加布局
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/nav_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
app:itemTextColor="#ff0000"
app:layout_constraintBottom_toBottomOf="parent"
app:menu="@menu/menu"
tools:ignore="MissingConstraints" />
4)綁定NavController
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_navigator_main);
BottomNavigationView bottomNavigationView = findViewById(R.id.nav_view);
NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.fragment_container);
NavController navController = navHostFragment.getNavController();
//下面通過NavigationUI將bottomNavigationView 與 navController進行綁定
NavigationUI.setupWithNavController(bottomNavigationView, navController);
}
原理分析
當Activity 加載Fragment時,先從xml布局文件中解析導航視圖的ID,當Fragment創建的時候根據解析出來的導航視圖的ID找到導航視圖的第一個目的地視圖的ID ,然后找到對應該目的地ID的節點信息,通過類加載器加載找到的目的地ID對應的Fragment,最后通過FragmentSupportManager 來替換默認的顯示容器,從而顯示出來。
當在調用navigate導航新的目的地時,會根據傳入的actionID ,找到新的目的地ID ,然后重復上面的步驟通過類加載器來加載新目的地Fragment ,然后再次替換顯示容器。
以上就是Navigator的執行過程,其中核心的技術是:XMLPull解析,類加載 ,工廠模式來創建目的地實例
1、創建第一個目的地Fragment(從Fragment的生命周期分析)
1.1)NavHostFragment的onInflate()解析布局文件
解析布局文件中的defaultNavHost 、navGraph 的值,并保存下來
@CallSuper
@Override
public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs,
@Nullable Bundle savedInstanceState) {
super.onInflate(context, attrs, savedInstanceState);
final TypedArray navHost = context.obtainStyledAttributes(attrs,
androidx.navigation.R.styleable.NavHost);
final int graphId = navHost.getResourceId(
androidx.navigation.R.styleable.NavHost_navGraph, 0);
if (graphId != 0) {
mGraphId = graphId;
}
navHost.recycle();
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NavHostFragment);
final boolean defaultHost = a.getBoolean(R.styleable.NavHostFragment_defaultNavHost, false);
if (defaultHost) {
mDefaultNavHost = true;
}
a.recycle();
}
1.2) NavHostFragment的onCreate
1.2.1)、 創建NavHostController 并添加FragmentNavigator
public void onCreate(@Nullable Bundle savedInstanceState) {
final Context context = requireContext();
// 創建NavHostController
mNavController = new NavHostController(context);
mNavController.setLifecycleOwner(this);
mNavController.setOnBackPressedDispatcher(requireActivity().getOnBackPressedDispatcher());
// Set the default state - this will be updated whenever
// onPrimaryNavigationFragmentChanged() is called
mNavController.enableOnBackPressed(
mIsPrimaryBeforeOnCreate != null && mIsPrimaryBeforeOnCreate);
mIsPrimaryBeforeOnCreate = null;
mNavController.setViewModelStore(getViewModelStore());
// 給NavHostController 的provider 添加FragmentNavigator
onCreateNavController(mNavController);
Bundle navState = null;
if (savedInstanceState != null) {
navState = savedInstanceState.getBundle(KEY_NAV_CONTROLLER_STATE);
if (savedInstanceState.getBoolean(KEY_DEFAULT_NAV_HOST, false)) {
mDefaultNavHost = true;
getParentFragmentManager().beginTransaction()
.setPrimaryNavigationFragment(this)
.commit();
}
mGraphId = savedInstanceState.getInt(KEY_GRAPH_ID);
}
if (navState != null) {
// Navigation controller state overrides arguments
mNavController.restoreState(navState);
}
if (mGraphId != 0) {
// Set from onInflate()
//此處官方已經注釋是在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);
}
}
// We purposefully run this last as this will trigger the onCreate() of
// child fragments, which may be relying on having the NavController already
// created and having its state restored by that point.
super.onCreate(savedInstanceState);
}
1.2.2) NavController 設置Graph
1)、inflate 導航視圖
XMLPull 解析器解析導航視圖xml文件
創建NavDestination(NavGraph),
dest.onInflate(mContext, attrs); 這里需要注意,由于NavGraph 是NavDestination 的子類,所以這里會調入NavGraph 的onInflate()方法中
//NavInflater.java
@SuppressLint("ResourceType")
@NonNull
public NavGraph inflate(@NavigationRes int graphResId) {
Resources res = mContext.getResources();
XmlResourceParser parser = res.getXml(graphResId);
final AttributeSet attrs = Xml.asAttributeSet(parser);
try {
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG
&& type != XmlPullParser.END_DOCUMENT) {
// Empty loop
}
if (type != XmlPullParser.START_TAG) {
throw new XmlPullParserException("No start tag found");
}
String rootElement = parser.getName();
//解析NavDestination
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();
}
}
@NonNull
private NavDestination inflate(@NonNull Resources res, @NonNull XmlResourceParser parser,
@NonNull AttributeSet attrs, int graphResId)
throws XmlPullParserException, IOException {
Navigator<?> navigator = mNavigatorProvider.getNavigator(parser.getName());
//這里創建一個NavDestination
final NavDestination dest = navigator.createDestination();
//進入NavDestination 的onInflate中 這里需要特別注意
dest.onInflate(mContext, attrs);
final int innerDepth = parser.getDepth() + 1;
int type;
int depth;
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& ((depth = parser.getDepth()) >= innerDepth
|| type != XmlPullParser.END_TAG)) {
if (type != XmlPullParser.START_TAG) {
continue;
}
if (depth > innerDepth) {
continue;
}
final String name = parser.getName();
if (TAG_ARGUMENT.equals(name)) {
inflateArgumentForDestination(res, dest, attrs, graphResId);
} else if (TAG_DEEP_LINK.equals(name)) {
inflateDeepLink(res, dest, attrs);
} else if (TAG_ACTION.equals(name)) {
inflateAction(res, dest, attrs, parser, graphResId);
} else if (TAG_INCLUDE.equals(name) && dest instanceof NavGraph) {
final TypedArray a = res.obtainAttributes(
attrs, androidx.navigation.R.styleable.NavInclude);
final int id = a.getResourceId(
androidx.navigation.R.styleable.NavInclude_graph, 0);
((NavGraph) dest).addDestination(inflate(id));
a.recycle();
} else if (dest instanceof NavGraph) {
((NavGraph) dest).addDestination(inflate(res, parser, attrs, graphResId));
}
}
return dest;
}
//NavGraph.java
@Override
public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs) {
super.onInflate(context, attrs);//這句話又會調用NavDestination 的onInflate方法
TypedArray a = context.getResources().obtainAttributes(attrs,
R.styleable.NavGraphNavigator);
//下面就更導航視圖文件的根節點解析到了第一個目的地
setStartDestination(
a.getResourceId(R.styleable.NavGraphNavigator_startDestination, 0));
mStartDestIdName = getDisplayName(context, mStartDestId);
a.recycle();
}
///NavDestination.java
public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs) {
final TypedArray a = context.getResources().obtainAttributes(attrs,
R.styleable.Navigator);
setId(a.getResourceId(R.styleable.Navigator_android_id, 0));
mIdName = getDisplayName(context, mId);
setLabel(a.getText(R.styleable.Navigator_android_label));
a.recycle();
}
@Override
public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs) {
super.onInflate(context, attrs);
TypedArray a = context.getResources().obtainAttributes(attrs,
R.styleable.NavGraphNavigator);
setStartDestination(
a.getResourceId(R.styleable.NavGraphNavigator_startDestination, 0));
mStartDestIdName = getDisplayName(context, mStartDestId);
a.recycle();
}
//NavGraph.java
2)、setGraph()
//NavController.java
public void setGraph(@NavigationRes int graphResId) {
setGraph(graphResId, null);
}
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>
@CallSuper
public void setGraph(@NavigationRes int graphResId, @Nullable Bundle startDestinationArgs) {
//下面的startDestinationArgs 是通過上一步傳遞進來的null
//這里進入inflate,參數就是導航文件
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;//此處將創建的NavDestination保存下來
onGraphCreated(startDestinationArgs);
}
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>
private void onGraphCreated(@Nullable Bundle startDestinationArgs) {
//此處省略了干擾代碼
if (mGraph != null && mBackStack.isEmpty()) {
boolean deepLinked = !mDeepLinkHandled && mActivity != null
&& handleDeepLink(mActivity.getIntent());
if (!deepLinked) {
// Navigate to the first destination in the graph
// if we haven't deep linked to a destination
//這里將第一個目的地作為第一個參數傳遞進去
navigate(mGraph, startDestinationArgs, null, null);
}
} else {
dispatchOnDestinationChanged();
}
}
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>
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);
// 此處使用Navigator 進行導航,而Navigator是抽象類,那么就進入他的實現類FragmentNavigator
NavDestination newDest = navigator.navigate(node, finalArgs,
navOptions, navigatorExtras);
//.....
}
3)、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;
}
//這里會根據目的地創建一個Fragment
final Fragment frag = instantiateFragment(mContext, mFragmentManager,
className, args);
frag.setArguments(args);
//下面就開始Fragment transcation了
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);
}
//這里使用創建好的目的地Fragment 來replace mContainerId,這個ID是系統內置的R.id.nav_host_fragment_container
ft.replace(mContainerId, frag);
ft.setPrimaryNavigationFragment(frag);
///............此處省略代碼
}
///>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
public Fragment instantiateFragment(@NonNull Context context,
@NonNull FragmentManager fragmentManager,
@NonNull String className, @SuppressWarnings("unused") @Nullable Bundle args) {
return fragmentManager.getFragmentFactory().instantiate(
context.getClassLoader(), className);
}
4)、創建目的地實例 FragmentFactory.java instantiate()
通過類加載器來加載第一個目的地的類型;
類構造器構造創建目的地實例。
//FragmentFactory.java
public Fragment instantiate(@NonNull ClassLoader classLoader, @NonNull String className) {
try {
//使用類加載器加載類
Class<? extends Fragment> cls = loadFragmentClass(classLoader, className);
//類構造器構造實例
return cls.getConstructor().newInstance();
} catch (java.lang.InstantiationException e) {
throw new Fragment.InstantiationException("Unable to instantiate fragment " + className
+ ": make sure class name exists, is public, and has an"
+ " empty constructor that is public", e);
} catch (IllegalAccessException e) {
throw new Fragment.InstantiationException("Unable to instantiate fragment " + className
+ ": make sure class name exists, is public, and has an"
+ " empty constructor that is public", e);
} catch (NoSuchMethodException e) {
throw new Fragment.InstantiationException("Unable to instantiate fragment " + className
+ ": could not find Fragment constructor", e);
} catch (InvocationTargetException e) {
throw new Fragment.InstantiationException("Unable to instantiate fragment " + className
+ ": calling Fragment constructor caused an exception", e);
}
}
public static Class<? extends Fragment> loadFragmentClass(@NonNull ClassLoader classLoader,
@NonNull String className) {
try {
Class<?> clazz = loadClass(classLoader, className);
return (Class<? extends Fragment>) clazz;
} catch (ClassNotFoundException e) {
throw new Fragment.InstantiationException("Unable to instantiate fragment " + className
+ ": make sure class name exists", e);
} catch (ClassCastException e) {
throw new Fragment.InstantiationException("Unable to instantiate fragment " + className
+ ": make sure class is a valid subclass of Fragment", e);
}
}
2、導航控制器導航目的地
2.1) 從調用開始
Navigation.findNavController(v).navigate(R.id.backtoSecond);
2.2)找到目的地ID
- 根據actionID找到導航視圖中對應的Action信息
- 從Action信息中得到新的目的地ID
- 查找目的地ID ,得到對應的NavDestation()
- 導航到新的目的地
//NavController.java
public void navigate(@IdRes int resId, @Nullable Bundle args, @Nullable NavOptions navOptions,
@Nullable Navigator.Extras navigatorExtras) {
NavDestination currentNode = mBackStack.isEmpty()
? mGraph
: mBackStack.getLast().getDestination();
if (currentNode == null) {
throw new IllegalStateException("no current navigation node");
}
@IdRes int destId = resId;
//根據傳入的ID找到當前Fragment節點中的action節點信息
final NavAction navAction = currentNode.getAction(resId);
Bundle combinedArgs = null;
if (navAction != null) {
if (navOptions == null) {
navOptions = navAction.getNavOptions();
}
//從action中獲取下一個目的地的id
destId = navAction.getDestinationId();
Bundle navActionArgs = navAction.getDefaultArguments();
if (navActionArgs != null) {
combinedArgs = new Bundle();
combinedArgs.putAll(navActionArgs);
}
}
if (args != null) {
if (combinedArgs == null) {
combinedArgs = new Bundle();
}
combinedArgs.putAll(args);
}
if (destId == 0 && navOptions != null && navOptions.getPopUpTo() != -1) {
popBackStack(navOptions.getPopUpTo(), navOptions.isPopUpToInclusive());
return;
}
if (destId == 0) {
throw new IllegalArgumentException("Destination id == 0 can only be used"
+ " in conjunction with a valid navOptions.popUpTo");
}
//查找下一目的地
NavDestination node = findDestination(destId);
if (node == null) {
final String dest = NavDestination.getDisplayName(mContext, destId);
if (navAction != null) {
throw new IllegalArgumentException("Navigation destination " + dest
+ " referenced from action "
+ NavDestination.getDisplayName(mContext, resId)
+ " cannot be found from the current destination " + currentNode);
} else {
throw new IllegalArgumentException("Navigation action/destination " + dest
+ " cannot be found from the current destination " + currentNode);
}
}
//導航到新的目的地
navigate(node, combinedArgs, navOptions, navigatorExtras);
}
///>>>>>>>>>>>>>>>>>>>>>>>
NavDestination findDestination(@IdRes int destinationId) {
if (mGraph == null) {
return null;
}
if (mGraph.getId() == destinationId) {
return mGraph;
}
NavDestination currentNode = mBackStack.isEmpty()
? mGraph
: mBackStack.getLast().getDestination();
NavGraph currentGraph = currentNode instanceof NavGraph
? (NavGraph) currentNode
: currentNode.getParent();
return currentGraph.findNode(destinationId);
}
////>>>>>>>>>>>>>>>>>>>
private void navigate(@NonNull NavDestination node, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
boolean popped = false;
boolean launchSingleTop = false;
if (navOptions != null) {
if (navOptions.getPopUpTo() != -1) {
popped = popBackStackInternal(navOptions.getPopUpTo(),
navOptions.isPopUpToInclusive());
}
}
Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator(
node.getNavigatorName());
Bundle finalArgs = node.addInDefaultArgs(args);
//這里進入FragmentNavigator的navigate()中,進行導航
NavDestination newDest = navigator.navigate(node, finalArgs,
navOptions, navigatorExtras);
//.........
}
2.3)類加載器加載新的目的地Fragment
這里和加載第一個目的地Fragment的方式一樣,通過類加載器加載目的地Fragment,然后通過替換顯示容器將目的地Fragment顯示出來。
總結
技術點:類加載、工廠模式、XML---PULL解析