写在开始
本文从浅入深,介绍了navigate的使用和源码及使用封装,一文带你了解Google为什么设计这个组件
Navigation基本使用
fragment参数
- defaultNavHost:会和系统返回键相关联,会判断fragment返回栈里是否还有fragment来操作
- navGraph:页面路由结构
navigation参数
- startDestination :默认显示的fragment
页面参数标签
- argument 创建当前fragment携带的参数
- action 指定from和destination
- deeplink 三方应用或通过隐式方法拉起
deeplink原理
1、通过intent.getdata获取uri对象
2、通过uri调用matchDeepLink方法获取deepLinkMatch
3、通过deepLink对象matchingDeepLink.getDestination().buildDeepLinkIds()获取一个id数组
4、数组包含根节点到当前节点的一个id数组
5、遍历打开每一个节点
更多使用方法参照官方文档
NavController源码分析
NavHostFragment
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Context context = requireContext();
mNavController = new NavHostController(context);//创建了NavHostController来管理跳转
mNavController.setLifecycleOwner(this);
...
onCreateNavController(mNavController);//添加跳转器
}
onCreateNavController(mNavController);添加了Dialog的导航器和Fragment的导航器,代码如下
@CallSuper
protected void onCreateNavController(@NonNull NavController navController) {
navController.getNavigatorProvider().addNavigator(
new DialogFragmentNavigator(requireContext(), getChildFragmentManager()));
navController.getNavigatorProvider().addNavigator(createFragmentNavigator());
}
NavController类
public NavController(@NonNull Context context) {
...
mNavigatorProvider.addNavigator(new NavGraphNavigator(mNavigatorProvider));
mNavigatorProvider.addNavigator(new ActivityNavigator(mContext));
}
NavController的构造函数里也有两个导航器分别是Activity和NavGraph
private NavigatorProvider mNavigatorProvider = new NavigatorProvider();
mNavigatorProvider其实就是一个hashMap的封装
private static final HashMap<Class<?>, String> sAnnotationNames = new HashMap<>();
NavGraphNavigator:当页面节点信息解析完成后跳转到默认界面
Navigator:
public abstract class Navigator<D extends NavDestination>
需要传入一个NavDestination,这样一种Navigator只能创建一种页面节点跳转
名称 | 作用 |
---|---|
Name | 导航器名称,不能为空 |
Extras | 提供额外的行为,过场动画、过渡元素 |
navigate | 真正的跳转逻辑 |
popBackStack | 拦截系统返回键实现自己的返回栈 |
ActivityNavigator
@Navigator.Name("activity")
public class ActivityNavigator extends Navigator<ActivityNavigator.Destination>
name:activity
ActivityNavigator.Destination:用于获取目标页需要的信息,用来跳转的时候包装Intent
public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
if (destination.getIntent() == null) {//如果intent为null就不跳转
throw new IllegalStateException("Destination " + destination.getId()
+ " does not have an Intent set.");
}
Intent intent = new Intent(destination.getIntent());//构建intent
if (args != null) {
intent.putExtras(args);//添加参数
String dataPattern = destination.getDataPattern();
if (!TextUtils.isEmpty(dataPattern)) {
// Fill in the data pattern with the args to build a valid URI
StringBuffer data = new StringBuffer();
Pattern fillInPattern = Pattern.compile("\\{(.+?)\\}");
Matcher matcher = fillInPattern.matcher(dataPattern);
while (matcher.find()) {
String argName = matcher.group(1);
if (args.containsKey(argName)) {
matcher.appendReplacement(data, "");
//noinspection ConstantConditions
data.append(Uri.encode(args.get(argName).toString()));
} else {
throw new IllegalArgumentException("Could not find " + argName + " in "
+ args + " to fill data pattern " + dataPattern);
}
}
matcher.appendTail(data);
intent.setData(Uri.parse(data.toString()));
}
}
if (navigatorExtras instanceof Extras) {//额外信息
Extras extras = (Extras) navigatorExtras;
intent.addFlags(extras.getFlags());
}
if (!(mContext instanceof Activity)) {//如果当前页面不是activity需要在新activity栈打开
// If we're not launching from an Activity context we have to launch in a new task.
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
if (navOptions != null && navOptions.shouldLaunchSingleTop()) {
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
}
if (mHostActivity != null) {//***把当前节点id加进去,可以知道这个页面是谁打开的,多用于埋点等行为
final Intent hostIntent = mHostActivity.getIntent();
if (hostIntent != null) {
final int hostCurrentId = hostIntent.getIntExtra(EXTRA_NAV_CURRENT, 0);
if (hostCurrentId != 0) {
intent.putExtra(EXTRA_NAV_SOURCE, hostCurrentId);
}
}
}
final int destId = destination.getId();
intent.putExtra(EXTRA_NAV_CURRENT, destId);
if (navOptions != null) {
// For use in applyPopAnimationsToPendingTransition()
intent.putExtra(EXTRA_POP_ENTER_ANIM, navOptions.getPopEnterAnim());
intent.putExtra(EXTRA_POP_EXIT_ANIM, navOptions.getPopExitAnim());
}
//下面就是跳转
if (navigatorExtras instanceof Extras) {
Extras extras = (Extras) navigatorExtras;
ActivityOptionsCompat activityOptions = extras.getActivityOptions();
if (activityOptions != null) {
ActivityCompat.startActivity(mContext, intent, activityOptions.toBundle());
} else {
mContext.startActivity(intent);
}
} else {
mContext.startActivity(intent);
}
if (navOptions != null && mHostActivity != null) {
int enterAnim = navOptions.getEnterAnim();
int exitAnim = navOptions.getExitAnim();
if (enterAnim != -1 || exitAnim != -1) {
enterAnim = enterAnim != -1 ? enterAnim : 0;
exitAnim = exitAnim != -1 ? exitAnim : 0;
mHostActivity.overridePendingTransition(enterAnim, exitAnim);
}
}
// You can't pop the back stack from the caller of a new Activity,
// so we don't add this navigator to the controller's back stack
return null;
}
其实就是封装了Activity的跳转
DialogFragmentNavigator
@Navigator.Name("dialog")
public final class DialogFragmentNavigator extends Navigator<DialogFragmentNavigator.Destination>
name:dialog
DialogFragmentNavigator.Destination:只提供className
public NavDestination navigate(@NonNull final 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 = mFragmentManager.getFragmentFactory().instantiate(
mContext.getClassLoader(), className);//通过类名构造出Fragment
if (!DialogFragment.class.isAssignableFrom(frag.getClass())) {//判断当前fragment是否是DialogFragment的子类
throw new IllegalArgumentException("Dialog destination " + destination.getClassName()
+ " is not an instance of DialogFragment");
}
final DialogFragment dialogFragment = (DialogFragment) frag;
dialogFragment.setArguments(args);
dialogFragment.getLifecycle().addObserver(mObserver);
dialogFragment.show(mFragmentManager, DIALOG_TAG + mDialogCount++);
return destination;
}
封装了DialogFragment的跳转
FragmentNavigator
@Navigator.Name("fragment")
public class FragmentNavigator extends Navigator<FragmentNavigator.Destination>
name:fragment
FragmentNavigator.Destination:也只提供className
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);//创建fragment实例
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);
}
//直接调用replace并没有hide或者show,如果需要这里需要重写一个Navigator
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;
}
}
NavGraphNavigator
@Navigator.Name("navigation")
public class NavGraphNavigator extends Navigator<NavGraph>
name:navigation
NavGraph:这里传入的不是Destination而是NavGraph
public class NavGraph extends NavDestination
NavGraph是NavDestination的子类
final SparseArrayCompat<NavDestination> mNodes = new SparseArrayCompat<>();
mNodes对象存储着所有的节点信息
private int mStartDestId;
private String mStartDestIdName;
mStartDestId就是资源文件中的默认启动fragment
这个NavGraphNavigation就是在资源文件解析完成后创建的
NavController里有
public void setGraph(@NavigationRes int graphResId, @Nullable Bundle startDestinationArgs) {
setGraph(getNavInflater().inflate(graphResId), startDestinationArgs);
}
会把资源id传进来
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 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();
}
}
或者在资源文件里定义的属性
private NavDestination inflate(@NonNull Resources res, @NonNull XmlResourceParser parser,
@NonNull AttributeSet attrs, int graphResId)
throws XmlPullParserException, IOException {
Navigator<?> navigator = mNavigatorProvider.getNavigator(parser.getName());
final NavDestination dest = navigator.createDestination();
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));//递归调用添加所有的navigation
}
}
return dest;
}
inflat方法解析了argument,deepLink,Action和Include
public NavDestination navigate(@NonNull NavGraph destination, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Extras navigatorExtras) {
int startId = destination.getStartDestination();
if (startId == 0) {
throw new IllegalStateException("no start destination defined via"
+ " app:startDestination for "
+ destination.getDisplayName());
}
//获取默认的Destination
NavDestination startDestination = destination.findNode(startId, false);
if (startDestination == null) {
final String dest = destination.getStartDestDisplayName();
throw new IllegalArgumentException("navigation destination " + dest
+ " is not a direct child of this NavGraph");
}
//获取相应的Destination
Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator(
startDestination.getNavigatorName());
//跳转
return navigator.navigate(startDestination, startDestination.addInDefaultArgs(args),
navOptions, navigatorExtras);
}
Navigator的作用就是统一APP的跳转工作,所有的跳转方式都是由一种跳转方式来跳转
Navigation的动态使用
使用Kotlin DSL
可以在 Kotlin 代码中(而不是在 XML 资源内部)以声明方式构建图形。如果您希望为应用动态构建导航,该方法会非常有用。例如,您的应用可以从外部网络服务下载并缓存导航配置,然后使用该配置在 Activity 的 onCreate() 函数中动态构建导航图。
使用基于 XML 的导航图时,Android 构建流程会解析图形资源文件,并为图形中定义的每个 id 属性指定数字常量。代码中的这些常量可以通过生成的资源类 R.id 来获取。
例如,以下 XML 图代码段使用 id、home 声明了一个 Fragment 目的地:
<navigation ...>
<fragment android:id="@+id/home" ... />
...
</navigation>
使用 Kotlin DSL 以编程方式构建图形时,不会发生这种解析和生成常量的过程
必须为具有 id
值的每个目的地、操作和参数定义自己的常量。每个 ID 在配置更改中必须是唯一且一致的
object nav_graph {
// Counter for id's. First ID will be 1.
var id_counter = 1
val id = id_counter++
object dest {
val home = id_counter++
val plant_detail = id_counter++
}
object action {
val to_plant_detail = id_counter++
}
object args {
const val plant_id = "plantId"
}
}
class GardenActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_garden)
val navHostFragment = supportFragmentManager
.findFragmentById(R.id.nav_host) as NavHostFragment
navHostFragment.navController.apply {
graph = createGraph(nav_graph.id, nav_graph.dest.home) {
fragment<HomeViewPagerFragment>(nav_graph.dest.home) {
label = getString(R.string.home_title)
action(nav_graph.action.to_plant_detail) {
destinationId = nav_graph.dest.plant_detail
}
}
fragment<PlantDetailFragment>(nav_graph.dest.plant_detail) {
label = getString(R.string.plant_detail_title)
argument(nav_graph.args.plant_id) {
type = NavType.StringType
}
}
}
}
}
}
使用 fragment() DSL 构建器函数定义了两个 Fragment 目的地。该函数需要目的地的 ID。该函数还接受用于其他配置的可选 lambda(如目的地 label)以及用于操作、参数和深层链接的嵌入式构建器函数。
管理每个目的地界面的 Fragment 类将作为放在尖括号 (<>) 中的参数化类型传入。这与在使用 XML 定义的 Fragment 目的地上设置 android:name 属性具有相同的效果。
构建并设置图形后,即可使用 NavController.navigate()
从 home
导航到 plant_detail
,如以下示例所示:
private fun navigateToPlant(plantId: String) {
val args = bundleOf(nav_graph.args.plant_id to plantId)
findNavController().navigate(nav_graph.action.to_plant_detail, args)
}
fragment<FragmentDestination>(nav_graph.dest.fragment_dest_id) {
label = getString(R.string.fragment_title)
// arguments, actions, deepLinks...
}
activity(nav_graph.dest.activity_dest_id) {
label = getString(R.string.activity_title)
// arguments, actions, deepLinks...
activityClass = ActivityDestination::class
}
navigation(nav_graph.dest.nav_graph_dest, nav_graph.dest.start_dest) {
// label, arguments, actions, other destinations, deep links
}
// The NavigatorProvider is retrieved from the NavController
val customDestination = navigatorProvider[CustomNavigator::class].createDestination().apply {
id = nav_graph.dest.custom_dest_id
}
addDestination(customDestination)
还可以使用一元加号运算符 (+
) 将新构造的目的地直接添加到图形中
// The NavigatorProvider is retrieved from the NavController
+navigatorProvider[CustomNavigator::class].createDestination().apply {
id = nav_graph.dest.custom_dest_id
}
fragment示例
fragment<PlantDetailFragment>(nav_graph.dest.plant_detail) {
label = getString(R.string.plant_details_title)
deepLink("${baseUri}/{id}")
deepLink("${baseUri}/{id}?name={plant_name}")
argument(nav_graph.args.plant_id) {
type = NavType.IntType
}
argument(nav_graph.args.plant_name) {
type = NavType.StringType
nullable = true
}
action(nav_graph.action.to_plant_detail) {
destinationId = nav_graph.dest.plant_detail
navOptions {
anim {
enter = R.anim.nav_default_enter_anim
exit = R.anim.nav_default_exit_anim
popEnter = R.anim.nav_default_pop_enter_anim
popExit = R.anim.nav_default_pop_exit_anim
}
popUpTo(nav_graph.dest.start_dest) {
inclusive = true // default false
}
// if popping exclusively, you can specify popUpTo as
// a property. e.g. popUpTo = nav_graph.dest.start_dest
launchSingleTop = true // default false
}
}
}
使用注解编译时生成
(只提供思路和简单代码实现,以学习为主)
注解类
ActivityDestination
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS)
annotation class ActivityDestination(val pageUrl: String, val needLogin: Boolean = false, val asStart: Boolean = false)
FragmentDestination
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS)
annotation class FragmentDestination(val pageUrl: String, val needLogin: Boolean = false, val asStart: Boolean = false)
这两个注解类我们需要拿到页面的url,needLogin和asStart是否作为启动页面
编译时运行类
NavProcessor
@SupportedAnnotationTypes("com.babyname.libnavannotation.ActivityDestination", "com.babyname.libnavannotation.FragmentDestination")
@AutoService(Processor::class)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
class NavProcessor : AbstractProcessor() {
private val OUTPUT_FILE_NAME = "navigation.json"
lateinit var messager: Messager
lateinit var filer: Filer
override fun process(set: Set<TypeElement?>, roundEnvironment: RoundEnvironment): Boolean {
messager.printMessage(Diagnostic.Kind.NOTE,"开始生成json导航")
var fragmentDestination = roundEnvironment.getElementsAnnotatedWith(FragmentDestination::class.java)
var activityDestination = roundEnvironment.getElementsAnnotatedWith(ActivityDestination::class.java)
if (fragmentDestination.isNotEmpty() || activityDestination.isNotEmpty()) {
var destMap = HashMap<String, JSONObject>()
handleDestination(fragmentDestination, FragmentDestination::class.java, destMap)
handleDestination(activityDestination, ActivityDestination::class.java, destMap)
try {
val resourcePath = filer.createResource(StandardLocation.CLASS_OUTPUT, "", OUTPUT_FILE_NAME).toUri().path
val appPath = resourcePath.substring(0, resourcePath.indexOf("app") + 4)
val assetsPath = appPath + "src/main/assets/"
val file = File(assetsPath)
if (!file.exists()) {
file.mkdirs()
}
val outPutFile = File(file, OUTPUT_FILE_NAME)
if (outPutFile.exists()) {
outPutFile.delete()
}
outPutFile.createNewFile()
val content = JSON.toJSONString(destMap)
outPutFile.outputStream().writer(Charsets.UTF_8).buffered().use {
it.write(content)
}
} catch (e: Exception) {
}
}
return true
}
private fun handleDestination(destination: Set<Element>, javaClazz: Class<out Annotation>, destMap: HashMap<String, JSONObject>) {
destination.forEach { element ->
val typeElement = element as TypeElement
val clazzName = typeElement.qualifiedName.toString()
val id = abs(clazzName.hashCode())
var needLogin = false
var asStarter = false
var pageUrl = ""
var isFragment = false
typeElement.getAnnotation(javaClazz).let {
if (it is FragmentDestination) {
pageUrl = it.pageUrl
asStarter = it.asStart
needLogin = it.needLogin
isFragment = true
}
if (it is ActivityDestination) {
pageUrl = it.pageUrl
asStarter = it.asStart
needLogin = it.needLogin
}
if (destMap.containsKey(pageUrl)) {
messager.printMessage(Diagnostic.Kind.ERROR, "不允许有多个相同URL的页面")
} else {
val obj = JSONObject().apply {
put("id", id)
put("needLogin", needLogin)
put("asStarter", asStarter)
put("pageUrl", pageUrl)
put("clazzName", clazzName)
put("isFragment", isFragment)
}
destMap[pageUrl] = obj
}
}
}
}
@Synchronized
override fun init(processingEnvironment: ProcessingEnvironment) {
super.init(processingEnvironment)
messager = processingEnv.messager
filer = processingEnv.filer
}
}
这里我们主要就是重新process方法,根据注解的信息生成我们需要的navigation.json文件
这一步就是把需要写死的xml文件动态化
动态读取文件应用
AppConfig
object AppConfig {
@JvmStatic
private lateinit var sDestConfig: HashMap<String, Destination>
@JvmStatic
private lateinit var sBottomBar: BottomBar
val destConfig: HashMap<String, Destination>
get() {
if (!::sDestConfig.isInitialized) {
val content = parseFile("navigation.json")
sDestConfig = JSON.parseObject(content, object : TypeReference<HashMap<String, Destination>>() {})
}
return sDestConfig
}
fun parseFile(fileName: String): String {
val assets = AppGlobals.application.resources.assets
val fileBuilder = StringBuilder()
assets.open(fileName).reader().buffered().use {
var lines: String = ""
while (it.readLine().also { line -> if (line != null) lines = line } != null) {
fileBuilder.append(lines)
}
}
return fileBuilder.toString()
}
val bottomBar: BottomBar
get() {
if (!::sBottomBar.isInitialized) {
val content = parseFile("main_tabs_config.json")
sBottomBar = JSON.parseObject(content, BottomBar::class.java)
}
return sBottomBar
}
}
这里有两个方法一个是destConfig,一个是bottombar,我们先不关注bottomBar,这里其实就是读取destConfig用于动态生成NaviController的NaviGraph,下面是通过读取的destConfig的数据生成
object NavGraphBuilder {
fun build(ctrl: NavController, context: Context, fragmentManager: FragmentManager, containsId: Int) {
val provider = ctrl.navigatorProvider
//val fragmentNavigator = provider.getNavigator(FragmentNavigator::class.java)
val fragmentNavigator = FixFragmentNavigator(context, fragmentManager, containsId)
provider.addNavigator(fragmentNavigator)
val activityNavigator = provider.getNavigator(ActivityNavigator::class.java)
val naviGraph = NavGraph(NavGraphNavigator(provider))
val destConfig = AppConfig.destConfig
destConfig.values.forEach {
if (it.isIsFragment) {
fragmentNavigator.createDestination().run {
className = it.clazzName
id = it.id
addDeepLink(it.pageUrl)
naviGraph.addDestination(this)
}
} else {
activityNavigator.createDestination().run {
id = it.id
addDeepLink(it.pageUrl)
setComponentName(ComponentName(AppGlobals.application.packageName, it.clazzName))
naviGraph.addDestination(this)
}
}
if (it.isAsStarter) {
naviGraph.startDestination = it.id
}
}
ctrl.graph = naviGraph
}
}
**这里通过addDeepLink的方式将Fragment加入NaviGraph的destination,通过addDeepLink和setComponentName的方式将Acitivity加入NaviGraph的destination
FragmentNavigator的种种
要做什么?
找到解决Navigator的replace方案解决方法
基础知识
Fragment的加载方式有哪些?
① 静态加载
即静态的将Fragment添加在XML布局文件中,这种方式在平时开发中使用频率较低。
② 动态加载
即在代码中动态添加Fragment
步骤如下:
1、获取FragmentManager
2、通过FragmentManager获取FragmentTransaction
3、通过FragmentTransaction向指定布局区域添加或者替换Fragment
4、提交修改即可
Fragment的生命周期
注意:在Fragment和Activity一起启动时,启动前Activity的方法在Fragment方法之前执行。销毁时,Activity方法在Fragment方法之后执行,例如Activity onStart()方法执行完之后才会执行Fragment onStart()的方法,反之,销毁时,Fragment onPause()执行后才会执行Activity onPause()方法。
在onAttach()方法回调的时候,说明Fragment已经附着到了Activity上,在onAttach方法中获取它所在的Activity对象并且保存为全局属性,以便后面的方法中使用,切记不要使用getActivity
来获取它所在的activity对象,因为这个方法有的时候会返回为空,因为在某些情况下,Activity有可能被回收。
使用show() hizde()方法来切换多个Fragment页面的时候,Fragment的生命周期
当我们在某个Activity中add了多个Fragment实例,切换不同的Fragment页面的时候,是通过show()和hide()方法来实现的,那么Fragment的生命周期方法是不会走的,只会走onHiddenChanged(boolean hidden)方法,我们可以通过这个方法来监听Fragment页面的显示和隐藏。
但是当我们点击home键,把整体页面切到后台的时候,宿主Activity和它里面的各个Fragment的onPause(),onStop()方法都会执行。我们再点击应用图标把应用当前页面且回到前台的时候,宿主Activity和它里面的各个Fragment的onStart()、onResume()方法都会执行。当然Activity的onRestart()方法也会执行。
要规避Fragment执行逻辑,只需要用isVisible方法进行判断即可。
使用replace() 或者 add() remove() 方法来切换多个Fragment页面的时候,Fragment的生命周期
通过remove()和add()方法显示隐藏Fragment1,被add进来的Fragment的生命周期为
onAttach、onCreate、onCreateView、onActivityCreated、onStart、onResume
被remove掉的Fragment2的生命周期为
onPause、onStop、onDestroyView、onDestroy、onDetach
replace相当于add和remove两个方法一起作用的结果,所以结论和上面一样。这里需要注意的是,调用replace视图是会被销毁重建的,而且调用getActivity方法会返回null,使用Kotlin的时候需要获取Activity然后用?.let方法进行调用,就是出于这个原因,编译期给出的优化方案
当把隐藏(remove掉)的Fragment加入到回退栈中,对其生命周期有什么影响
被添加进来的Fragment2的生命周期和之前没有变化,但是被加入回退栈中的Fragment1的生命周期变为了
onPause、onStop、onDestroyView
只是把Fragment1的界面销毁了,Fragment1实例并没有销毁。
此时我们点击返回键
返回的Fragment1的生命周期为
onCreateView、onStart、onResume
Fragment2的生命周期为:
onPause、onStop、onDestroyView、onDestroy、onDetach
如果以ViewPager的形式添加多个Fragment,滑动切换Fragment,那么Fragment切换的时候,生命周期也不会执行,onHiddenChanged也不会执行,可以通过**setUserVisibleHint(boolean isVisibleToUser)**方法监听Fragment页面的显示与隐藏。
当把隐藏(hide掉)的Fragment加入到回退栈中,对其生命周期有什么影响
被添加进来的Fragment2的生命周期和之前没有变化,但是被加入回退栈中的Fragment1的生命周期变为了
空
Fragment1的界面没有任何变量,Fragment1实例也没有销毁。
此时我们点击返回键
返回的Fragment1的生命周期为
空
Fragment2的生命周期为:
onPause、onStop、onDestroyView、onDestroy、onDetach
不会调用onResume和Activity还是很不一样的,因为系统默认Fragment并没有消失,但是onHiddenChaged会调用
开始改造:
原FragmentNavigator
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));//注释1
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;
}
}
可以看到,原始的FragmentNavigator使用了replace方法替换了Fragment,我们要改造他让他用hide和show的方法来添加Fragment
@Navigator.Name("fixFragment")
class FixFragmentNavigator(private val mContext: Context, private val mFragmentManager: FragmentManager, private val mContainerId: Int) : FragmentNavigator(mContext, mFragmentManager, mContainerId) {
override fun navigate(destination: Destination, args: Bundle?,
navOptions: NavOptions?, navigatorExtras: Navigator.Extras?): NavDestination? {
if (mFragmentManager.isStateSaved) {
return null
}
var className = destination.className
if (className[0] == '.') {
className = mContext.packageName + className
}
//val frag = instantiateFragment(mContext, mFragmentManager,className, args)
//frag.arguments = args
var ft = mFragmentManager.beginTransaction()
var enterAnim = navOptions?.enterAnim ?: -1
var exitAnim = navOptions?.exitAnim ?: -1
var popEnterAnim = navOptions?.popEnterAnim ?: -1
var popExitAnim = navOptions?.popExitAnim ?: -1
//ft.replace(mContainerId, frag)
val fragmentNow = mFragmentManager.primaryNavigationFragment
val tag = destination.id.toString()
var frag: Fragment? = mFragmentManager.findFragmentByTag(tag)
if (fragmentNow != null && frag != null && frag == fragmentNow) {
return null
}
if (fragmentNow != null) {
ft.hide(fragmentNow)
}
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)
}
if (frag != null) {
ft.show(frag)
} else {
frag = instantiateFragment(mContext, mFragmentManager, className, args)
frag!!.arguments = args
ft.add(mContainerId, frag, tag)
}
ft.setPrimaryNavigationFragment(frag)
@IdRes val destId = destination.id
val mBackStack = FragmentNavigator::class.java.getDeclaredField("mBackStack")
.also { it.isAccessible = true }.get(this) as ArrayDeque<Int>
val initialNavigation = mBackStack.isEmpty()
// TODO Build first class singleTop behavior for fragments
val isSingleTopReplacement = (navOptions != null && !initialNavigation
&& navOptions.shouldLaunchSingleTop()
&& mBackStack.peekLast() == destId)
val isAdded = when {
initialNavigation -> {
true
}
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
// 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))
}
false
}
else -> {
true
}
}
if (navigatorExtras is Extras) {
for ((key, value) in navigatorExtras.sharedElements) {
ft.addSharedElement(key!!, value!!)
}
}
ft.setReorderingAllowed(true)
ft.commit()
// The commit succeeded, update our view of the world
return if (isAdded) {
mBackStack.add(destId)
destination
} else {
null
}
}
private fun generateBackStackName(backStackIndex: Int, destId: Int): String? {
return "$backStackIndex-$destId"
}
override fun createDestination(): Destination {
return FixDestination(this)
}
@NavDestination.ClassType(Fragment::class)
class FixDestination(fragmentNavigator: Navigator<out Destination?>) : Destination(fragmentNavigator) {
private var mClassName: String? = null
/**
* Construct a new fragment destination. This destination is not valid until you set the
* Fragment via [.setClassName].
*
* @param navigatorProvider The [NavController] which this destination
* will be associated with.
*/
constructor(navigatorProvider: NavigatorProvider) : this(navigatorProvider.getNavigator(FixFragmentNavigator::class.java)) {}
@CallSuper
override fun onInflate(context: Context, attrs: AttributeSet) {
super.onInflate(context, attrs)
val a = context.resources.obtainAttributes(attrs,
R.styleable.FragmentNavigator)
val className = a.getString(R.styleable.FragmentNavigator_android_name)
className?.let { setClassName(it) }
a.recycle()
}
override fun toString(): String {
val sb = StringBuilder()
sb.append(super.toString())
sb.append(" class=")
if (mClassName == null) {
sb.append("null")
} else {
sb.append(mClassName)
}
return sb.toString()
}
}
}
改造后的源码,主要思路就是用tag来判断是否是已经添加过的Fragment来hide和show,需要注意的是:
-
addToBackStack方法会pop出之前的Fragment,来节约内存。这里需要把注释1处的addToBackStack方法删除,避免缓存的Fragment被销毁
-
需要重写Destination为FixDestination,修改他的mNavigatorName为fixFragment,这里是根据@Navigator.Name(“fixFragment”)来拿到名字的,具体方法如下:
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; }
这里也刚好学一下注解的动态使用。主要思想就是拿到传进来类的注解,然后再拿出名字即可。
- 这了留一个问题,就是hide和show的改造只是适用于Navigator的Fragment,其他的继续使用原生的FragmentNavigator。
动态绑定NavController和navGraph
NAVGraphBuilder
object NavGraphBuilder {
fun build(ctrl: NavController, context: Context, fragmentManager: FragmentManager, containsId: Int) {
val provider = ctrl.navigatorProvider
val fragmentNavigator = FragmentNavigator(context, fragmentManager, containsId)
val fixFragmentNavigator = FixFragmentNavigator(context, fragmentManager, containsId)
provider.addNavigator(fixFragmentNavigator)
provider.addNavigator(fragmentNavigator)
val activityNavigator = provider.getNavigator(ActivityNavigator::class.java)
val navGraph = NavGraph(NavGraphNavigator(provider))
val destConfig = AppConfig.destConfig
destConfig.values.forEach {
if (it.isIsFragment) {
if (isBottomBar(it.pageUrl)) {
fixFragmentNavigator.createDestination(). run {
className = it.clazzName
id = it.id
addDeepLink(it.pageUrl)
navGraph.addDestination(this)
}
} else {
fragmentNavigator.createDestination().run {
className = it.clazzName
id = it.id
addDeepLink(it.pageUrl)
navGraph.addDestination(this)
}
}
} else {
activityNavigator.createDestination().run {
id = it.id
addDeepLink(it.pageUrl)
setComponentName(ComponentName(AppGlobals.application.packageName, it.clazzName))
navGraph.addDestination(this)
}
}
if (it.isAsStarter) {
navGraph.startDestination = it.id
}
}
ctrl.graph = navGraph
}
private fun isBottomBar(tag: String?): Boolean {
AppConfig.bottomBar.tabs.forEach {
if (it.isEnable && tag == it.pageUrl) {
return true
}
}
return false
}
}
思路:
- 创建FixFragmentNavigator和原生FragmentNavigator
- 获取原生的ActivityNavigator
- 为他们分别匹配解析出来的Destination,这里我根据底部bar的enable来判断哪些需要使用FixFragmentNavigator
- 绑定navGraph后返回即可
使用封装
BaseActivity
open abstract class BaseActivity : AppCompatActivity() {
val naviController by lazy { Navigation.findNavController(this, getNavCtrlResId()) }
abstract fun getNavCtrlResId():Int
}
BaseFragment
open abstract class BaseFragment : Fragment() {
val navController : NavController by lazy {
Navigation.findNavController(requireActivity(), getNavCtrlResId())
}
abstract fun getNavCtrlResId():Int
}
NavigationUtil
fun jump(navController: NavController, url: String, bundle: Bundle? = null) {
destConfig[url]?.id?.let { navController.navigate(it, bundle) }
}
调用
NavigationUtilKt.jump(getNavController(),"main/fragment/orderList",null);
NavigationUtilKt.jump(getNavController(), "main/activity/list", bundle);
BottomNavigationBar封装
BottomBarNavigation.json
{
"activeColor": "#333333",
"inActiveColor": "#666666",
"selectTab": 0,
"tabs": [
{
"size": 24,
"enable": true,
"index": 0,
"pageUrl": "main/tabs/qiming",
"title": "起名"
},
{
"size": 24,
"enable": true,
"index": 1,
"pageUrl": "main/tabs/jieming",
"title": "解名"
},
{
"size": 24,
"enable": true,
"index": 2,
"pageUrl": "main/tabs/vip",
"title": "会员"
},
{
"size": 24,
"enable": true,
"index": 3,
"pageUrl": "main/tabs/mine",
"title": "我的"
}
]
}
这个不用多说了,就是一个Navigationbar的一个描述性配置
解析AppConfig
object AppConfig {
@JvmStatic
private lateinit var sDestConfig: HashMap<String, Destination>
@JvmStatic
private lateinit var sBottomBar: BottomBar
val destConfig: HashMap<String, Destination>
get() {
if (!::sDestConfig.isInitialized) {
val content = parseFile("navigation.json")
sDestConfig = JSON.parseObject(content, object : TypeReference<HashMap<String, Destination>>() {})
}
return sDestConfig
}
fun parseFile(fileName: String): String {
val assets = AppGlobals.application.resources.assets
val fileBuilder = StringBuilder()
assets.open(fileName).reader().buffered().use {
var lines: String = ""
while (it.readLine().also { line -> if (line != null) lines = line } != null) {
fileBuilder.append(lines)
}
}
return fileBuilder.toString()
}
val bottomBar: BottomBar
get() {
if (!::sBottomBar.isInitialized) {
val content = parseFile("main_tabs_config.json")
sBottomBar = JSON.parseObject(content, BottomBar::class.java)
}
return sBottomBar
}
}
这里看bottomBar的逻辑就行了,其实就是解析了Json
BottomBar解析类
class BottomBar @SuppressLint("RestrictedApi") constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : BottomNavigationView(context!!, attrs, defStyleAttr) {
private val config: BottomBar = AppConfig.bottomBar
@JvmOverloads
constructor(context: Context?, attrs: AttributeSet? = null) : this(context, attrs, 0) {
}
companion object {
private val sIcons = intArrayOf(R.drawable.tab_menu_icon_qiming, R.drawable.tab_menu_icon_jieming, R.drawable.tab_menu_icon_vip, R.drawable.tab_menu_icon_mine)
fun getItemId(pageUrl: String): Int {
val destination: Destination = AppConfig.destConfig[pageUrl] ?: return -1
return destination.id
}
}
init {
val state = arrayOfNulls<IntArray>(2)
state[0] = intArrayOf(android.R.attr.state_selected)
state[1] = intArrayOf()
val colors = intArrayOf(Color.parseColor(config.activeColor), Color.parseColor(config.inActiveColor))
val stateList = ColorStateList(state, colors)
itemTextColor = stateList
itemIconTintList = null
//LABEL_VISIBILITY_LABELED:设置按钮的文本为一直显示模式
//LABEL_VISIBILITY_AUTO:当按钮个数小于三个时一直显示,或者当按钮个数大于3个且小于5个时,被选中的那个按钮文本才会显示
//LABEL_VISIBILITY_SELECTED:只有被选中的那个按钮的文本才会显示
//LABEL_VISIBILITY_UNLABELED:所有的按钮文本都不显示
labelVisibilityMode = LabelVisibilityMode.LABEL_VISIBILITY_SELECTED
val tabs: List<BottomBar.TabsBean> = config.tabs
for (tab in tabs) {
if (!tab.isEnable) {
continue
}
val itemId = getItemId(tab.pageUrl)
if (itemId < 0) {
continue
}
val menuItem: MenuItem = menu.add(0, itemId, tab.index, tab.title)
menuItem.setIcon(sIcons[tab.index])
}
//此处给按钮icon设置大小
var index = 0
for (tab in config.tabs) {
if (!tab.isEnable) {
continue
}
val itemId = getItemId(tab.pageUrl)
if (itemId < 0) {
continue
}
val iconSize = Util.dpToPx(context, tab.size.toFloat())
val itemView = (getChildAt(0) as BottomNavigationMenuView)
.getChildAt(index) as BottomNavigationItemView
itemView.setIconSize(iconSize)
if (TextUtils.isEmpty(tab.title)) {
val tintColor = if (TextUtils.isEmpty(tab.tintColor)) Color.parseColor("#ff678f") else Color.parseColor(tab.tintColor)
itemView.setIconTintList(ColorStateList.valueOf(tintColor))
//禁止掉点按时 上下浮动的效果
itemView.setShifting(false)
/**
* 如果想要禁止掉所有按钮的点击浮动效果。
* 那么还需要给选中和未选中的按钮配置一样大小的字号。
*
* 在MainActivity布局的AppBottomBar标签增加如下配置,
* @style/active,@style/inActive 在style.xml中
* app:itemTextAppearanceActive="@style/active"
* app:itemTextAppearanceInactive="@style/inActive"
*/
}
index++
}
//底部导航栏默认选中项
if (config.selectTab !== 0) {
val selectTab: BottomBar.TabsBean = config.tabs[config.selectTab]
if (selectTab.isEnable) {
val itemId = getItemId(selectTab.pageUrl)
//这里需要延迟一下 再定位到默认选中的tab
//因为 咱们需要等待内容区域,也就NavGraphBuilder解析数据并初始化完成,
//否则会出现 底部按钮切换过去了,但内容区域还没切换过去
post { selectedItemId = itemId }
}
}
}
}
Xml文件调用
<com.pjk.babyname.View.BottomBar
android:id="@+id/bottom_nav"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/color_FFFFFF"
app:itemBackground="@null"
app:itemTextColor="@drawable/navigate_color_selector"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"/>
问答
1. 为什么Fragment的getActivity在kotlin中不是非null
因为只有Fragment的onAttach调用之后,getActivity才不为null
2. 为什么官方的FragmentNavigator使用replace而不是hide/show
1. 为了节约内存空间,避免在多层嵌套的情况下内存占用爆炸的问题
2. 配合ViewModle和DataBinding等其他jetpack工具,视图是被缓存的,所以加载起来很快,故官方使用了replace,这样做对性能的影响没有想象中的那么大
3. 为什么Google要开发Navigation
通过Navigation,安卓统一了常见组件的跳转方式,为跳转协议等应用进行了铺垫
4. 可以对FragmentDialog进行改造吗
可以,针对类型进行区分,然后改造就行了