Android-JetPack组件之自定义Navigation导航器

没有效果图的示例简直就是扯淡

在这里插入图片描述


Navigation是什么?

Navigation 是一个框架,用于在 Android 应用中的“目标”之间导航,该框架提供一致的 API,无论目标是作为 Fragment、Activity 还是其他组件实现。


Navigation最重要的三个关键部分组成是什么?

  1. 导航图:在一个集中位置包含所有导航相关信息的 XML 资源。这包括应用内所有单个内容区域(称为目标)以及用户可以通过应用获取的可能路径。
  2. NavHost:显示导航图中目标的空白容器。导航组件包含一个默认 NavHost 实现 (NavHostFragment),可显示 Fragment 目标。
  3. NavController:在 NavHost 中管理应用导航的对象。当用户在整个应用中移动时,NavController 会安排 NavHost 中目标内容的交换。

注意:在应用中导航时,您告诉 NavController,您想沿导航图中的特定路径导航至特定目标,或直接导航至特定目标。NavController 便会在 NavHost 中显示相应目标。


Navigation的优势

  1. 默认情况下,正确处理往返操作。
  2. 为动画和转换提供标准化资源。
  3. 实现和处理深层链接。
  4. 包括导航界面模式(例如抽屉式导航栏和底部导航),用户只需完成极少的额外工作。

如果你认真看了标题,应该就会发现题目为自定义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

如果有什么问题,欢迎大家指导。并相互联系,希望能够通过文章互相学习。

											                               	---财财亲笔
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

谁抢我的小口口

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值