Android-JetPack组件之Navigation导航器(支持动态配置)
- 没有效果图的示例简直就是扯淡
- Navigation是什么?
- Navigation最重要的三个关键部分组成是什么?
- Navigation的优势
- **如果你认真看了标题,应该就会发现题目为自定义Navigation。那么问题来了,为什么要自定义?自定义之后有什么好处?**
- 接下来我们直接看代码,从代码上理解原理。
- 有了配置文件之后,我们就需要将配置文件生成对应的model对象来使用
- 有了配置文件和实体类之后,我们就要解析对应的配置文件,生成可供我们使用的数据集
- 如此,可供我们使用的数据集已经准备就绪了,下来开始实现自定的bottomBar。
- 代码完成之后我们将其配置给我们的activity,xml如下:
- MainActivity.class如下
- 相信很多人看到之后都很好奇,为什么没有发现每一个item如何跳转到对应界面的逻辑,其实这里用的是注解来实现,老规矩,直接看代码:
- 这一步骤结束之后,我们还需要给每一个对应的界面添加注解,如下:
- 至此,所有的代码才算完整。
- 附上demo源码。
- 源码:[源码请点这里](https://download.csdn.net/download/qq_35840038/86404361)
- 如果下不了源码,可以加微信,手机号在下面。
- **如果有什么问题,欢迎大家指导。并相互联系,希望能够通过文章互相学习。**
没有效果图的示例简直就是扯淡
Navigation是什么?
Navigation 是一个框架,用于在 Android 应用中的“目标”之间导航,该框架提供一致的 API,无论目标是作为 Fragment、Activity 还是其他组件实现。
Navigation最重要的三个关键部分组成是什么?
- 导航图:在一个集中位置包含所有导航相关信息的 XML 资源。这包括应用内所有单个内容区域(称为目标)以及用户可以通过应用获取的可能路径。
- NavHost:显示导航图中目标的空白容器。导航组件包含一个默认 NavHost 实现 (NavHostFragment),可显示 Fragment 目标。
- NavController:在 NavHost 中管理应用导航的对象。当用户在整个应用中移动时,NavController 会安排 NavHost 中目标内容的交换。
注意:在应用中导航时,您告诉 NavController,您想沿导航图中的特定路径导航至特定目标,或直接导航至特定目标。NavController 便会在 NavHost 中显示相应目标。
Navigation的优势
- 默认情况下,正确处理往返操作。
- 为动画和转换提供标准化资源。
- 实现和处理深层链接。
- 包括导航界面模式(例如抽屉式导航栏和底部导航),用户只需完成极少的额外工作。
如果你认真看了标题,应该就会发现题目为自定义Navigation。那么问题来了,为什么要自定义?自定义之后有什么好处?
为什么要自定义Navigation?我们大家都知道部分app底部的导航栏都是直接在xml里面实现的,那么如果有一个需求是这样的:已经上架的包,需要将底部的第一个item和第二个item替换个位置,界面也要相对应切换,而且还不能重新发包,这个时候该怎么办呢?如果是在xm中直接配置的item,那么就必须要发包才能修改,否则的话就无法修改。这时候就可以使用自定义的Navigation来实现动态配置item,也是本篇文章的重点内容。
接下来我们直接看代码,从代码上理解原理。
{
"activeColor": "#ffbb00",
"inActiveColor": "#ffccdd",
"selectTab": 0,
"tabs": [
{
"size": 24,
"enable": true,
"index": 0,
"pageUrl": "main/tabs/home",
"title": "首页"
},
{
"size": 24,
"enable": true,
"index": 1,
"pageUrl": "main/tabs/type",
"title": "类型"
},
{
"size": 24,
"enable": true,
"index": 2,
"pageUrl": "main/tabs/publish",
"title": "发布"
},
{
"size": 24,
"enable": true,
"index": 3,
"pageUrl": "main/tabs/message",
"title": "消息"
},
{
"size": 24,
"enable": true,
"index": 4,
"pageUrl": "main/tabs/center",
"title": "我"
}
]
}
注:具体每个字段的含义在demo里面的同名类main_tabs_config.class里面
这个配置文件就是app首页底部的每一个item
{
"main/tabs/home": {
"isFragment": true,
"asStarter": true,
"needLogin": false,
"pageUrl": "main/tabs/home",
"className": "com.cc.navigatedemo.fragment.HomeFragment",
"id": 147242568
},
"main/tabs/type": {
"isFragment": true,
"asStarter": false,
"needLogin": false,
"pageUrl": "main/tabs/type",
"className": "com.cc.navigatedemo.fragment.TypeFragment",
"id": 946010952
},
"main/tabs/publish": {
"isFragment": true,
"asStarter": false,
"needLogin": true,
"pageUrl": "main/tabs/publish",
"className": "com.cc.navigatedemo.fragment.PublishFragment",
"id": 1509210903
},
"main/tabs/message": {
"isFragment": true,
"asStarter": false,
"needLogin": false,
"pageUrl": "main/tabs/message",
"className": "com.cc.navigatedemo.fragment.MessageFragment",
"id": 1729678456
},
"main/tabs/center": {
"isFragment": true,
"asStarter": false,
"needLogin": false,
"pageUrl": "main/tabs/center",
"className": "com.cc.navigatedemo.fragment.CenterFragment",
"id": 1407657752
}
}
注:具体每个字段的含义在demo里面的类DestinationModel.class里面
这个文件是每一个底部item对应的界面路由配置
上面这两个配置文件就是底部导航栏和对应界面的配置,可以从服务端返回。从而实现动态的修改和界面配置
有了配置文件之后,我们就需要将配置文件生成对应的model对象来使用
底部导航item的实体类对象
/**
* 底部导航model
*/
public class BottomBarModel {
/**
* activeColor : #333333
* inActiveColor : #666666
* tabs : [{"size":24,"enable":true,"index":0,"pageUrl":"main/tabs/home","title":"首页"},{"size":24,"enable":true,"index":1,"pageUrl":"main/tabs/sofa","title":"沙发"},{"size":40,"enable":true,"index":2,"tintColor":"#ff678f","pageUrl":"main/tabs/publish","title":""},{"size":24,"enable":true,"index":3,"pageUrl":"main/tabs/find","title":"发现"},{"size":24,"enable":true,"index":4,"pageUrl":"main/tabs/my","title":"我的"}]
*/
public String activeColor;
public String inActiveColor;
public List<Tab> tabs;
public int selectTab;//底部导航栏默认选中项
public static class Tab {
/**
* size : 24
* enable : true
* index : 0
* pageUrl : main/tabs/home
* title : 首页
* tintColor : #ff678f
*/
public int size;
public boolean enable;
public int index;
public String pageUrl;
public String title;
public String tintColor;
}
}
对应界面导航路由的实体类对象
/**
* 界面路由对象model
*/
public class DestinationModel {
//界面导航url
public String pageUrl;
//id
public int id;
//是否需要登录
public boolean needLogin;
//是否为启动界面
public boolean asStarter;
//是否为fragment(activity为false,fragment为true)
public boolean isFragment;
//具体类名
public String className;
}
有了配置文件和实体类之后,我们就要解析对应的配置文件,生成可供我们使用的数据集
生成数据集的类
/**
* 主要做解析xml数据用
*/
public class AppConfig {
//这两个都是会应用在首页的,不会被销毁的,所以用static来定义
private static HashMap<String, DestinationModel> sDestConfig;
private static BottomBarModel sBottomBar;
/**
* 获取界面信息
*
* @return
*/
public static HashMap<String, DestinationModel> getDestConfig() {
if (sDestConfig == null) {
String content = parseFile("destination.json");
sDestConfig = JSON.parseObject(content, new TypeReference<HashMap<String, DestinationModel>>() {
});
}
return sDestConfig;
}
/**
* 获取底部bottom对象数据
*
* @return
*/
public static BottomBarModel getBottomBarConfig() {
if (sBottomBar == null) {
String content = parseFile("main_tabs_config.json");
sBottomBar = JSON.parseObject(content, BottomBarModel.class);
}
return sBottomBar;
}
/**
* 解析本地xml
*
* @param fileName
* @return
*/
private static String parseFile(String fileName) {
AssetManager assets = AppGlobals.getApplication().getAssets();
InputStream is = null;
BufferedReader br = null;
StringBuilder builder = new StringBuilder();
try {
is = assets.open(fileName);
br = new BufferedReader(new InputStreamReader(is));
String line = null;
while ((line = br.readLine()) != null) {
builder.append(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (is != null) {
is.close();
}
if (br != null) {
br.close();
}
} catch (Exception e) {
}
}
return builder.toString();
}
}
注:这里引入了一个AppGlobals类,这个类其实是一个全局获取Application的工具类,如下:
/**
* 这种方式获取全局的Application 是一种拓展思路。
* 对于组件化项目,不可能把项目实际的Application下沉到Base,而且各个module也不需要知道Application真实名字
* 这种一次反射就能获取全局Application对象的方式相比于在Application#OnCreate保存一份的方式显示更加通用了
*/
public class AppGlobals {
//单例模式
private static Application sApplication;
//获取activity实例
public static Application getApplication() {
if (sApplication == null) {
try {
sApplication = (Application) Class.forName("android.app.ActivityThread")
.getMethod("currentApplication")
.invoke(null, (Object[]) null);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
return sApplication;
}
}
如此,可供我们使用的数据集已经准备就绪了,下来开始实现自定的bottomBar。
/**
* 自定义底部导航栏
*/
public class AppBottomBar extends BottomNavigationView {
//底部icon(也可随xml一起配置)
private static int[] sIcons = new int[]{
R.drawable.icon_tab_home,
R.drawable.icon_tab_sofa,
R.drawable.icon_tab_publish,
R.drawable.icon_tab_find,
R.drawable.icon_tab_mine
};
private BottomBarModel config;
public AppBottomBar(Context context) {
this(context, null);
}
public AppBottomBar(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
@SuppressLint("RestrictedApi")
public AppBottomBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
config = AppConfig.getBottomBarConfig();
int[][] state = new int[2][];
state[0] = new int[]{android.R.attr.state_selected};
state[1] = new int[]{};
int[] colors = new int[]{Color.parseColor(config.activeColor), Color.parseColor(config.inActiveColor)};
ColorStateList stateList = new ColorStateList(state, colors);
int[][] state2 = new int[2][];
state2[0] = new int[]{android.R.attr.state_selected};
state2[1] = new int[]{};
int[] colors2 = new int[]{Color.parseColor("#000000"), Color.parseColor("#e3e3e3")};
ColorStateList stateList2 = new ColorStateList(state2, colors2);
setItemTextColor(stateList);
setItemIconTintList(stateList2);
//LABEL_VISIBILITY_LABELED:设置按钮的文本为一直显示模式
//LABEL_VISIBILITY_AUTO:当按钮个数小于三个时一直显示,或者当按钮个数大于3个且小于5个时,被选中的那个按钮文本才会显示
//LABEL_VISIBILITY_SELECTED:只有被选中的那个按钮的文本才会显示
//LABEL_VISIBILITY_UNLABELED:所有的按钮文本都不显示
setLabelVisibilityMode(NavigationBarView.LABEL_VISIBILITY_LABELED);
List<BottomBarModel.Tab> tabs = config.tabs;
for (BottomBarModel.Tab tab : tabs) {
if (!tab.enable) {
continue;
}
int itemId = getItemId(tab.pageUrl);
if (itemId < 0) {
continue;
}
MenuItem menuItem = getMenu().add(0, itemId, tab.index, tab.title);
menuItem.setIcon(sIcons[tab.index]);
}
//此处给按钮icon设置大小
int index = 0;
for (BottomBarModel.Tab tab : config.tabs) {
if (!tab.enable) {
continue;
}
int itemId = getItemId(tab.pageUrl);
if (itemId < 0) {
continue;
}
int iconSize = dp2Px(tab.size);
BottomNavigationMenuView menuView = (BottomNavigationMenuView) getChildAt(0);
BottomNavigationItemView itemView = (BottomNavigationItemView) menuView.getChildAt(index);
itemView.setIconSize(iconSize);
if (TextUtils.isEmpty(tab.title)) {
int tintColor = TextUtils.isEmpty(tab.tintColor) ? Color.parseColor("#ff678f") : 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) {
BottomBarModel.Tab selectTab = config.tabs.get(config.selectTab);
if (selectTab.enable) {
int itemId = getItemId(selectTab.pageUrl);
//这里需要延迟一下 再定位到默认选中的tab
//因为 咱们需要等待内容区域,也就NavGraphBuilder解析数据并初始化完成,
//否则会出现 底部按钮切换过去了,但内容区域还没切换过去
post(() -> setSelectedItemId(itemId));
}
}
}
private int dp2Px(int dpValue) {
DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();
return (int) (metrics.density * dpValue + 0.5f);
}
private int getItemId(String pageUrl) {
DestinationModel destination = AppConfig.getDestConfig().get(pageUrl);
if (destination == null)
return -1;
return destination.id;
}
}
注:都有注释,相信大家不难理解的。
public class NavGraphBuilder {
public static void build(FragmentActivity activity, FragmentManager childFragmentManager, NavController controller, int containerId) {
NavigatorProvider provider = controller.getNavigatorProvider();
//NavGraphNavigator也是页面路由导航器的一种,只不过他比较特殊。
//它只为默认的展示页提供导航服务,但真正的跳转还是交给对应的navigator来完成的
NavGraph navGraph = new NavGraph(new NavGraphNavigator(provider));
FragmentNavigator fragmentNavigator = new FragmentNavigator(activity, childFragmentManager, containerId);
provider.addNavigator(fragmentNavigator);
ActivityNavigator activityNavigator = provider.getNavigator(ActivityNavigator.class);
HashMap<String, DestinationModel> destConfig = AppConfig.getDestConfig();
Iterator<DestinationModel> iterator = destConfig.values().iterator();
while (iterator.hasNext()) {
DestinationModel node = iterator.next();
if (node.isFragment) {
FragmentNavigator.Destination destination = fragmentNavigator.createDestination();
destination.setId(node.id);
destination.setClassName(node.className);
destination.addDeepLink(node.pageUrl);
navGraph.addDestination(destination);
} else {
ActivityNavigator.Destination destination = activityNavigator.createDestination();
destination.setId(node.id);
destination.setComponentName(new ComponentName(AppGlobals.getApplication().getPackageName(), node.className));
destination.addDeepLink(node.pageUrl);
navGraph.addDestination(destination);
}
//给APP页面导航结果图 设置一个默认的展示页的id
if (node.asStarter) {
navGraph.setStartDestination(node.id);
}
}
controller.setGraph(navGraph);
}
}
注:自此,整个应用层的代码就已经完成了。
代码完成之后我们将其配置给我们的activity,xml如下:
<?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"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="?attr/actionBarSize">
<com.cc.navigatedemo.widget.AppBottomBar
android:id="@+id/nav_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:background="?android:attr/windowBackground"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"/>
<fragment
android:id="@+id/nav_host_fragment_activity_main2"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
android:tag="nav_host_fragment_activity_main2"
app:layout_constraintBottom_toTopOf="@id/nav_view"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity.class如下
/**
* 主界面
*/
public class MainActivity2 extends AppCompatActivity implements BottomNavigationView.OnNavigationItemSelectedListener {
//路由导航器
private NavController navController;
//底部导航栏
private AppBottomBar navView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
//获取底部导航栏控件
navView = findViewById(R.id.nav_view);
//fragment
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment_activity_main2);
//绑定路由导航器
navController = NavHostFragment.findNavController(fragment);
NavGraphBuilder.build(this, fragment.getChildFragmentManager(), navController, fragment.getId());
//底部按钮选中回调
navView.setOnNavigationItemSelectedListener(this);
}
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
navController.navigate(item.getItemId());
return !TextUtils.isEmpty(item.getTitle());
}
@Override
public void onBackPressed() {
//当前正在显示的页面destinationId
int currentPageId = navController.getCurrentDestination().getId();
//APP页面路导航结构图 首页的destinationId
int homeDestId = navController.getGraph().getStartDestination();
//如果当前正在显示的页面不是首页,而我们点击了返回键,则拦截。
if (currentPageId != homeDestId) {
navView.setSelectedItemId(homeDestId);
return;
}
//否则 finish,此处不宜调用onBackPressed。因为navigation会操作回退栈,切换到之前显示的页面。
finish();
}
}
相信很多人看到之后都很好奇,为什么没有发现每一个item如何跳转到对应界面的逻辑,其实这里用的是注解来实现,老规矩,直接看代码:
/**
* APP页面导航信息收集注解处理器
* AutoService注解:就这么一标记,annotationProcessor project()应用一下,编译时就能自动执行该类了。
* SupportedSourceVersion注解:声明我们所支持的jdk版本
* SupportedAnnotationTypes:声明该注解处理器想要处理那些注解
*/
@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes({"com.cc.lib_destination.FragmentDestination", "com.cc.lib_destination.ActivityDestination"})
public class NavProcessor extends AbstractProcessor {
//日志打印工具
private Messager messager;
//文件处理工具
private Filer filer;
//文件名
private static final String OUTPUT_FILE_NAME = "destination.json";
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
//日志打印,在java环境下不能使用android.util.log.e()
messager = processingEnv.getMessager();
messager.printMessage(Diagnostic.Kind.NOTE, "ainit");
//文件处理工具
filer = processingEnv.getFiler();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
//通过处理器环境上下文roundEnv分别获取 项目中标记的FragmentDestination.class 和ActivityDestination.class注解。
//此目的就是为了收集项目中哪些类 被注解标记了
Set<? extends Element> fragmentElements = roundEnv.getElementsAnnotatedWith(FragmentDestination.class);
Set<? extends Element> activityElements = roundEnv.getElementsAnnotatedWith(ActivityDestination.class);
//判断fragmentElements或者activityElements不为空
if (!fragmentElements.isEmpty() || !activityElements.isEmpty()) {
HashMap<String, JSONObject> destMap = new HashMap<>();
//分别 处理FragmentDestination 和 ActivityDestination 注解类型
//并收集到destMap 这个map中。以此就能记录下所有的页面信息了
handleDestination(fragmentElements, FragmentDestination.class, destMap);
handleDestination(activityElements, ActivityDestination.class, destMap);
//app/src/main/assets
FileOutputStream fos = null;
OutputStreamWriter writer = null;
try {
//filer.createResource()意思是创建源文件
//我们可以指定为class文件输出的地方,
//StandardLocation.CLASS_OUTPUT:java文件生成class文件的位置,/app/build/intermediates/javac/debug/classes/目录下
//StandardLocation.SOURCE_OUTPUT:java文件的位置,一般在/ppjoke/app/build/generated/source/apt/目录下
//StandardLocation.CLASS_PATH 和 StandardLocation.SOURCE_PATH用的不多,指的了这个参数,就要指定生成文件的pkg包名了
FileObject resource = filer.createResource(StandardLocation.CLASS_OUTPUT, "", OUTPUT_FILE_NAME);
String resourcePath = resource.toUri().getPath();
messager.printMessage(Diagnostic.Kind.NOTE, "resourcePath:" + resourcePath);
//由于我们想要把json文件生成在app/src/main/assets/目录下,所以这里可以对字符串做一个截取,
//以此便能准确获取项目在每个电脑上的 /app/src/main/assets/的路径
String appPath = resourcePath.substring(0, resourcePath.indexOf("app") + 4);
messager.printMessage(Diagnostic.Kind.NOTE, "appPath:" + appPath);
String assetsPath = appPath + "src/main/assets/";
messager.printMessage(Diagnostic.Kind.NOTE, "assetsPath:" + assetsPath);
File file = new File(assetsPath);
if (!file.exists()) {
file.mkdirs();
}
//此处就是稳健的写入了
File outPutFile = new File(file, OUTPUT_FILE_NAME);
if (outPutFile.exists()) {
outPutFile.delete();
}
outPutFile.createNewFile();
//利用fastjson把收集到的所有的页面信息 转换成JSON格式的。并输出到文件中
String content = JSON.toJSONString(destMap);
messager.printMessage(Diagnostic.Kind.NOTE, "content:" + content);
fos = new FileOutputStream(outPutFile);
writer = new OutputStreamWriter(fos, "UTF-8");
writer.write(content);
writer.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (writer != null) {
try {
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
return true;
}
private void handleDestination(Set<? extends Element> elements, Class<? extends Annotation> annotationClaz, HashMap<String, JSONObject> destMap) {
for (Element element : elements) {
//TypeElement是Element的一种。
//如果我们的注解标记在了类名上。所以可以直接强转一下。使用它得到全类名
TypeElement typeElement = (TypeElement) element;
//全类名com.mooc.ppjoke.home
String clazName = typeElement.getQualifiedName().toString();
//页面的id.此处不能重复,使用页面的类名做hascode即可
int id = Math.abs(clazName.hashCode());
//页面的pageUrl相当于隐士跳转意图中的host://schem/path格式
String pageUrl = null;
//是否需要登录
boolean needLogin = false;
//是否作为首页的第一个展示的页面
boolean asStarter = false;
//标记该页面是fragment 还是activity类型的
boolean isFragment = false;
Annotation annotation = element.getAnnotation(annotationClaz);
if (annotation instanceof FragmentDestination) {
FragmentDestination dest = (FragmentDestination) annotation;
pageUrl = dest.pageUrl();
asStarter = dest.asStarter();
needLogin = dest.needLogin();
isFragment = true;
} else if (annotation instanceof ActivityDestination) {
ActivityDestination dest = (ActivityDestination) annotation;
pageUrl = dest.pageUrl();
asStarter = dest.asStarter();
needLogin = dest.needLogin();
isFragment = false;
}
if (destMap.containsKey(pageUrl)) {
messager.printMessage(Diagnostic.Kind.ERROR, "不同的页面不允许使用相同的pageUrl:" + clazName);
} else {
JSONObject object = new JSONObject();
object.put("id", id);
object.put("needLogin", needLogin);
object.put("asStarter", asStarter);
object.put("pageUrl", pageUrl);
object.put("className", clazName);
object.put("isFragment", isFragment);
destMap.put(pageUrl, object);
}
}
}
}
其实在我的代码里面有两个很重要的java库文件:
除了app还有两个lib开头的,这个是最重要、最重要、最重要、最重要、最重要、最重要的!!!
这一步骤结束之后,我们还需要给每一个对应的界面添加注解,如下:
至此,所有的代码才算完整。
附上demo源码。
源码:源码请点这里
如果下不了源码,可以加微信,手机号在下面。
Q:486789970(QQ现在很少用)
V:18588400509(如果着急,可以直接加微信)
email:mr.cai_cai@foxmail.com
如果有什么问题,欢迎大家指导。并相互联系,希望能够通过文章互相学习。
---财财亲笔