Jetpack之自定义Navigation轻松实现路由导航

3,160 阅读14分钟

之前的一篇文章Android Jetpack之Navigation对Navigation的使用进行了练习,并且看了一下Navigation的源码。虽然Navigation的功能很强大,不过在xml中配置感觉还是不够灵活,随着项目的增大,页面多了之后xml会变的非常庞大不利于维护。而且使用Navigation做底部导航的时候,每次都会新建Fragment,这个也不是我们想要的,因此来改造一下Navigation

底部导航

通过上一篇中查看源代码我们知道,在xml中配置的navigation,最终会被解析成一个一个的Destination对象然后放到一个导航图NavGraph中。然后通过NavController交给ActivityNavigator、FragmentNavigator等去执行导航。

改造自后的Navigation,不用在xml中配置,只需在页面上添加相关的注解就可以了,然后通过注解拿到页面信息自己组建导航图,本部分思路来自慕课网短视频实战项目

先看一下改造之后的用法,4个Fragment和底部导航栏

@FragmentDestination(pageUrl = WanRouterKey.FRAGMENT_MAIN_TABLES_HOME, asStarter = true)
class HomeFragment : BaseFragment(){}

@FragmentDestination(pageUrl = WanRouterKey.FRAGMENT_MAIN_TABLES_APPLY)
class ApplyFragment : BaseFragment() {}

@FragmentDestination(pageUrl = WanRouterKey.FRAGMENT_MAIN_TABLES_FIND)
class FindFragment : BaseFragment() {}

@FragmentDestination(pageUrl = WanRouterKey.FRAGMENT_MAIN_TABLES_MINE)
class MineFragment : BaseFragment() {}

四个Fragment分别添加FragmentDestination注解,pageUrl是导航路径为常量。HomeFragment注解中的asStarter 参数代表是启动的第一个页面

MainActivity的xml中,继承系统BottomNavigationView自定义底部底部图标和文字,省去在res/menu文件夹下的xml配置

<androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <com.chs.lib_core.navigation.BottomBarView
            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"
            android:name="androidx.navigation.fragment.NavHostFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            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" />
</androidx.constraintlayout.widget.ConstraintLayout>

MainActivity的onCreate中

val navController = Navigation.findNavController(this, R.id.nav_host_fragment)
NavGraphBuilder.build(navController,this,R.id.nav_host_fragment)
nav_view.setNavController(navController)

到此一个底部导航就完成了,不用向原生Navigation一样在res/navitaion和res/menu文件夹中使用想xml文件配置了,是不是简单了灵活许多。

如何实现上面功能呢

  1. 原生的Navigation需要在res/navigation文件夹下创建xml文件来配置所有需要导航的页面结点,这些结点上都有需要导航的页面的全类名,程序运行的时候,会解析该xml文件,拿到全类名,组装导航图,然后交给然后交给ActivityNavigator、FragmentNavigator等类去实现跳转。我们通过注解处理器在编译的时候可以拿到所有自定义的注解标注过的类的全类名,然后保存到一个json文件中,放到asssets目录下,程序运行的时候解析文件,自己组装导航图
  2. 原生的BottomNavigationView也是一样,需要在res/menu文件夹下创建xml文件来配置底部按钮的文字和icon,我们也可以将底部按钮的信息保存到一个json文件中,然后自定义一个BottomBarView继承BottomNavigationView然后自己解析并组装底部栏

OK 思路有啦开始干,首先创建两个java Module:lib_annotation 和 lib_compiler 用来编写注解类和注解处理器

编写注解处理器

lib_annotation :中编写ActivityDestination和FragmentDestination分别用来标记activity和fragment

@Target(ElementType.TYPE)
public @interface ActivityDestination {

    /**
     * @return 页面路径
     */
    String pageUrl();

    /**
     *
     * @return 是否需要登录
     */
    boolean needLogin() default false;

    /**
     * @return 是否是启动页
     */
    boolean asStarter() default false;

    /**
     * @return 是否属于主页中的tab页面  首页tab有可能点击去一个新的activity
     */
    boolean isBelongTab() default false;
}
@Target(ElementType.TYPE)
public @interface FragmentDestination {

    /**
     * @return 页面路径
     */
    String pageUrl();

    /**
     *
     * @return 是否需要登录
     */
    boolean needLogin() default false;

    /**
     * @return 是否是启动页
     */
    boolean asStarter() default false;
}

lib_compiler 中编写注解处理器,来解析带有注解的类

首先在build.gradle中添加相关依赖

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation project(':lib_annotation')
    implementation 'com.alibaba:fastjson:1.2.59'
    compileOnly 'com.google.auto.service:auto-service:1.0-rc6'
    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
}

lib_annotation是前面定义的注解module,fastjson用来生成json对象,auto-service用来编译时自动执行注解处理器

注解处理器NavProcessor

@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes({"com.chs.lib_annotation.ActivityDestination","com.chs.lib_annotation.FragmentDestination"})
@SupportedOptions("moduleName")
public class NavProcessor extends AbstractProcessor {
    private Messager messager;
    private Filer filer;
    private String outFileName;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        //日志工具
        messager = processingEnv.getMessager();
        //文件处理工具
        filer = processingEnv.getFiler();
        //获取gradle中配置的内容作为生成文件的名字
        outFileName = processingEnv.getOptions().get("moduleName") + "_nav.json";
        messager.printMessage(Diagnostic.Kind.NOTE,"moduleName:"+outFileName);
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        //拿到带有这两个注解的类的集合
        Set<? extends Element> fragmentElement = roundEnv.getElementsAnnotatedWith(FragmentDestination.class);
        Set<? extends Element> activityElement = roundEnv.getElementsAnnotatedWith(ActivityDestination.class);

        if(!fragmentElement.isEmpty()||!activityElement.isEmpty()){
            Map<String, JSONObject> destMap = new HashMap<>();
            handleDestination(fragmentElement,FragmentDestination.class,destMap);
            handleDestination(activityElement,ActivityDestination.class,destMap);
            FileOutputStream fos = null;
            OutputStreamWriter writer = null;
            //将map转换为json文件,保存到app/src/asset中
            try {
                //filer.createResource方法用来生成源文件
                //StandardLocation.CLASS_OUTPUT java文件生成class文件的位置,/build/intermediates/javac/debug/classes/目录下
                FileObject resource = filer.createResource(StandardLocation.CLASS_OUTPUT, "", outFileName);
                String resourcePath = resource.toUri().getPath();
                messager.printMessage(Diagnostic.Kind.NOTE,"resourcePath:"+resourcePath);

                String appPath = resourcePath.substring(0,resourcePath.indexOf("build"));
                String assetPath = appPath + "src/main/assets";

                File assetDir = new File(assetPath);
                if(!assetDir.exists()){
                    assetDir.mkdir();
                }
                File assetFile = new File(assetDir,outFileName);
                if(assetFile.exists()){
                    assetFile.delete();
                }
                assetFile.createNewFile();
                String content = JSON.toJSONString(destMap);

                fos = new FileOutputStream(assetFile);
                writer = new OutputStreamWriter(fos, StandardCharsets.UTF_8);
                writer.write(content);
                writer.flush();
            } catch (IOException e) {
                e.printStackTrace();
            }finally {
                if(fos!=null){
                    try {
                        fos.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                if(writer!=null){
                    try {
                        writer.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        return true;
    }

    private void handleDestination(Set<? extends Element> elements, Class<? extends Annotation> desAnnotationClazz,
                                   Map<String, JSONObject> destMap) {

        for (Element element : elements) {
            //TypeElement代表类或者接口,因为定义的注解是写在类上面的,所以可以直接转换成TypeElement
            TypeElement typeElement = (TypeElement) element;
            //获取全类名
            String className = typeElement.getQualifiedName().toString();
            int id = Math.abs(className.hashCode());
            String pageUrl = null;
            boolean needLogin = false;
            boolean asStarter = false;
            boolean isFragment = true;
            boolean isBelongTab = false;
//            messager.printMessage(Diagnostic.Kind.NOTE,"className:"+className);
            Annotation annotation = element.getAnnotation(desAnnotationClazz);
            //根据不同的注解获取注解的参数
            if(annotation instanceof FragmentDestination){
                FragmentDestination destination =  (FragmentDestination) annotation;
                pageUrl = destination.pageUrl();
                needLogin = destination.needLogin();
                asStarter = destination.asStarter();
                isFragment = true;
            }else if(annotation instanceof ActivityDestination){
                ActivityDestination destination =  (ActivityDestination) annotation;
                pageUrl = destination.pageUrl();
                needLogin = destination.needLogin();
                asStarter = destination.asStarter();
                isFragment = false;
                isBelongTab = destination.isBelongTab();
            }
            //将参数封装成JsonObject后放到map中保存
            if(destMap.containsKey(pageUrl)){
               messager.printMessage(Diagnostic.Kind.ERROR,"不允许使用相同的pagUrl:"+className);
            }else {
                JSONObject jsonObject = new JSONObject();
                jsonObject.put("id",id);
                jsonObject.put("className",className);
                jsonObject.put("pageUrl",pageUrl);
                jsonObject.put("needLogin",needLogin);
                jsonObject.put("asStarter",asStarter);
                jsonObject.put("isFragment",isFragment);
                jsonObject.put("isBelongTab",isBelongTab);
                destMap.put(pageUrl,jsonObject);
            }
        }
    }
}
  1. 注解处理器的目标是,扫描出所有带FragmentDestination或者ActivityDestination的类,拿到注解中的参数和类的全类名,封装成对象放到map中,使用fastjson将map生成json字符串,保存在src/main/assets目录下面
  2. 在组件化开发的时候,每个module中都会生成一个src/main/assets目录和一个json文件,APP打包的时候,如果文件名字相同,只会使用app module下的json文件,子module的都会遗弃。所以需要在不同module下生成不同名字的json文件来保证所有添加自定义注解的类都能收集到。文件的名字使用的时候可以在使用该注解处理器的module的gradle中通过javaCompileOptions参数配置,配置完成就可以在注解处理器init方法中拿到该名字,然后给json文件命名

注解和注解处理器写完了,现在去项目中使用以下

首先在build.gradle中android闭包下面添加如下代码来配置生成json的名字前缀

 javaCompileOptions {
            annotationProcessorOptions {
                arguments = [moduleName: project.getName()]
            }
        }

然后引入注解和注解处理器,主项目是kotlin项目,所以使用kapt引入,java项目使用annotationProcessor引入

 implementation project(path: ':lib_annotation')
 kapt project(path: ':lib_compiler')

然后rebuild项目,可以看到app/src/main/assets目录下面生成了app_nav.json文件,打开文件可以看到

{
  "main/tabs/MineFragment": {
    "isFragment": true,
    "isBelongTab": false,
    "asStarter": false,
    "needLogin": false,
    "className": "com.chs.bigsea.ui.mine.MineFragment",
    "pageUrl": "main/tabs/MineFragment",
    "id": 1818876842
  },
  "main/tabs/FindFragment": {
    "isFragment": true,
    "isBelongTab": false,
    "asStarter": false,
    "needLogin": false,
    "className": "com.chs.bigsea.ui.find.FindFragment",
    "pageUrl": "main/tabs/FindFragment",
    "id": 1292676074
  },
  "main/tabs/ApplyFragment": {
    "isFragment": true,
    "isBelongTab": false,
    "asStarter": false,
    "needLogin": false,
    "className": "com.chs.bigsea.ui.apply.ApplyFragment",
    "pageUrl": "main/tabs/ApplyFragment",
    "id": 1185318390
  }
}

HomeFragment因为在另一module,所以生成的文件也在另一个module中了

构建导航图

json文件生成完成,下一步就是来构建导航图了,新建一个NavGraphBuilder类来构建

fun build(navController: NavController, activity: FragmentActivity, containerId: Int) {
            val navigatorProvider = navController.navigatorProvider
            val fragmentNavigator = CustomFragmentNavigator(
                activity, activity.supportFragmentManager,
                containerId
            )
            navigatorProvider.addNavigator(fragmentNavigator)
            val activityNavigator = navigatorProvider.getNavigator(ActivityNavigator::class.java)

            val destinationMap = NavConfig.getDestinationMap()
            val navGraph = NavGraph(NavGraphNavigator(navigatorProvider))

            for ((key, destination) in destinationMap) {
                if (destination.isFragment) {
                    val fragmentDestination = fragmentNavigator.createDestination()
                    fragmentDestination.className = destination.className!!
                    fragmentDestination.id = destination.id
                    fragmentDestination.addDeepLink(destination.pageUrl!!)
                    navGraph.addDestination(fragmentDestination)
                } else {
                    if(destination.isBelongTab){
                        val activityDestination = activityNavigator.createDestination()
                        activityDestination.id = destination.id
                        activityDestination.setComponentName(
                            ComponentName(
                                Utils.getApp().packageName,
                                destination.className!!
                            )
                        )
                        activityDestination.addDeepLink(destination.pageUrl!!)
                        navGraph.addDestination(activityDestination)
                    }
                }
                if (destination.asStarter) {
                    navGraph.startDestination = destination.id
                }
            }
            navController.graph = navGraph
        }

构建导航图有三个比较大的部分

  1. 将json文件解析为map
  2. 遍历map,判断是fragment还是activity,根据不同 类型分别创建不同的Destination对象,并将这些Destination对象add到导航图中,给导航图设置起始页面,最后把导航图设置给主页穿过来的NavController对象
  3. 上一篇文章Android Jetpack之Navigation中,我们知道,FragmentNavigator类在导航Fragment页面的时候,使用的是FragmentTransaction的replace方法,而replace方法每次都会重新创建Fragment对象,而对于首页导航,我们不希望每次都重建,重新走生命周期方法,所以这里需要自定义一个FragmentNavigator,将其内部的replace该给hide和show

解析json文件:

class NavConfig {

    companion object {
        private var sDestinationMap: HashMap<String, Destination> = HashMap()
        private var sBottomBar: BottomBar? = null

        fun getDestinationMap(): HashMap<String, Destination> {
            if (sDestinationMap.size == 0) {
                val jsons = parseNavFile()
                for (json in jsons){
                    val destination: HashMap<String, Destination> = GsonUtils.fromJson(json,
                        object : TypeToken<HashMap<String, Destination>>(){}.type)
                    sDestinationMap.putAll(destination)
                }
            }
            return sDestinationMap
        }

        fun getBottomBar(): BottomBar {
            if (sBottomBar == null) {
                val jsonContent = parseFile("main_tabs_config.json")
                sBottomBar = GsonUtils.fromJson(jsonContent, BottomBar::class.java)
            }
            return sBottomBar!!
        }

        /**
         * 解析assets中特定文件
         */
        private fun parseFile(s: String): String {
            val assets = Utils.getApp().resources.assets
            val open = assets.open(s)
            val stringBuilder = StringBuilder()
            val bufferedReader = BufferedReader(InputStreamReader(open))
            bufferedReader.use {
                var line: String?
                while (true) {
                    line = it.readLine() ?: break
                    stringBuilder.append(line)
                }
            }
            return stringBuilder.toString()
        }

        /**
         * 解析assets下的所有的导航相关的文件
         */
        private fun parseNavFile():List<String>{
            val jsons = mutableListOf<String>()
            val assets = Utils.getApp().resources.assets
            val list = assets.list("");
            if (list != null) {
                for (item in list){
                    if(item.contains("_nav")){
                        jsons.add(parseFile(item))
                    }
                }
            }
            return jsons
        }
    }
}

解析主要就是流的读取,遍历asssets目录下是文件,找到导航相关的json文件,解析成对象放到一个map中保存,供创建导航图的时候使用。

自定义FragmentNavigator:

@Navigator.Name("customfragment")
class CustomFragmentNavigator(context: Context, manager: FragmentManager, containerId: Int) :
    Navigator<FragmentNavigator.Destination>() {

    private val TAG = "CustomFragmentNavigator"
    private val KEY_BACK_STACK_IDS = "androidx-nav-fragment:navigator:backStackIds"

    private var mContext: Context = context
    private var mFragmentManager: FragmentManager = manager
    private var mContainerId = containerId
    private val mBackStack = ArrayDeque<Int>()

    override fun navigate(
        destination: FragmentNavigator.Destination,
        args: Bundle?,
        navOptions: NavOptions?,
        navigatorExtras: Extras?
    ): NavDestination? {
        if (mFragmentManager!!.isStateSaved) {
            Log.i(
                TAG,
                "Ignoring navigate() call: FragmentManager has already"
                        + " saved its state"
            )
            return null
        }
        var className = destination.className
        if (className[0] == '.') {
            className = mContext!!.packageName + className
        }
        val ft = mFragmentManager!!.beginTransaction()

        var enterAnim = navOptions?.enterAnim ?: -1
        var exitAnim = navOptions?.exitAnim ?: -1
        var popEnterAnim = navOptions?.popEnterAnim ?: -1
        var popExitAnim = navOptions?.popExitAnim ?: -1
        if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
            enterAnim = if (enterAnim != -1) enterAnim else 0
            exitAnim = if (exitAnim != -1) exitAnim else 0
            popEnterAnim = if (popEnterAnim != -1) popEnterAnim else 0
            popExitAnim = if (popExitAnim != -1) popExitAnim else 0
            ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim)
        }

        val frg = mFragmentManager!!.primaryNavigationFragment
        if (frg != null) {
            ft.hide(frg)
        }

        val tag = destination.id.toString()
        var fragment = mFragmentManager!!.findFragmentByTag(tag)
        if (fragment == null) {
            fragment = mFragmentManager.getFragmentFactory().instantiate(mContext.classLoader, className)
            fragment!!.arguments = args
            ft.add(mContainerId, fragment, tag)
        } else {
            ft.show(fragment)
        }
        ft.setPrimaryNavigationFragment(fragment)

        @IdRes val destId = destination.id
        val initialNavigation = mBackStack.isEmpty()

        var np = navOptions
        np = NavOptions.Builder().setLaunchSingleTop(true).build()
        val isSingleTopReplacement = (!initialNavigation
                && np.shouldLaunchSingleTop())

        val isAdded: Boolean
        isAdded = if (initialNavigation) {
            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))
            }
            false
        } else {
            ft.addToBackStack(generateBackStackName(mBackStack.size + 1, destId))
            true
        }
        if (navigatorExtras is FragmentNavigator.Extras) {
            for ((key, value) in navigatorExtras.sharedElements) {
                ft.addSharedElement(key!!, value!!)
            }
        }
        ft.setReorderingAllowed(true)
        ft.commit()
        (return if (isAdded) {
            mBackStack.add(destId)
            destination
        } else {
            null
        })
    }

    override fun createDestination(): FragmentNavigator.Destination {
        return FragmentNavigator.Destination(this)//To change body of created functions use File | Settings | File Templates.
    }

    override fun popBackStack(): Boolean {
        if (mBackStack.isEmpty()) {
            return false
        }
        if (mFragmentManager!!.isStateSaved) {
            Log.i(
                TAG,
                "Ignoring popBackStack() call: FragmentManager has already"
                        + " saved its state"
            )
            return false
        }
        mFragmentManager?.popBackStack(
            generateBackStackName(mBackStack.size, mBackStack.peekLast()),
            FragmentManager.POP_BACK_STACK_INCLUSIVE
        )
        mBackStack.removeLast()
        return true
    }

    override fun onSaveState(): Bundle? {
        val b = Bundle()
        val backStack = IntArray(mBackStack.size)
        var index = 0
        for (id in mBackStack) {
            backStack[index++] = id
        }
        b.putIntArray(
            KEY_BACK_STACK_IDS,
            backStack
        )
        return b
    }

    override fun onRestoreState(savedState: Bundle) {
        if (savedState != null) {
            val backStack =
                savedState.getIntArray(KEY_BACK_STACK_IDS)
            if (backStack != null) {
                mBackStack.clear()
                for (destId in backStack) {
                    mBackStack.add(destId)
                }
            }
        }
    }

    private fun generateBackStackName(backStackIndex: Int, destId: Int): String {
        return "$backStackIndex-$destId"
    }
}

自定义FragmentNavigator是为了把其内部的navigate方法中的replace改成hide和show,虽然可以继承FragmentNavigator重写navigate方法,但是该方法中用到的mBackStack变量是私有的,需要反射拿到,所以把FragmentNavigator中的相关代码复制一份到自定义的CustomFragmentNavigator中,这样不需要继承也不需要反射了。

自定义BottomNavigationView

首先定义一个json文件,里面保存底部导航栏的tab信息,放到app/src/assets 目录下面

{
  "activeColor": "#333333",
  "inActiveColor": "#666666",
  "selectTab": 0,
  "tabs": [
    {
      "size": 24,
      "enable": true,
      "index": 0,
      "pageUrl": "main/tabs/HomeFragment",
      "title": "首页"
    },
    {
      "size": 24,
      "enable": true,
      "index": 1,
      "pageUrl": "main/tabs/ApplyFragment",
      "title": "应用"
    },
    {
      "size": 24,
      "enable": true,
      "index": 2,
      "pageUrl": "main/tabs/FindFragment",
      "title": "发现"
    },
    {
      "size": 24,
      "enable": true,
      "index": 3,
      "pageUrl": "main/tabs/MineFragment",
      "title": "我的"
    }
  ]
}

然后继承BottomNavigationView,解析上面的json,创建出menu对象,添加到BottomNavigationView中

class BottomBarView : BottomNavigationView, BottomNavigationView.OnNavigationItemSelectedListener {

    private var navController: NavController? = null

    companion object {
        val sIcons = arrayOf(
            R.drawable.icon_tab_home, R.drawable.icon_tab_apply,
            R.drawable.icon_tab_find, R.drawable.icon_tab_mine
        )
    }

    constructor(context: Context) : this(context, null)
    constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)

    @SuppressLint("RestrictedApi")
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
        context,
        attrs,
        defStyleAttr
    ) {
        setOnNavigationItemSelectedListener(this)
        val bottomBar = NavConfig.getBottomBar()
        val states = arrayOfNulls<IntArray>(2)
        states[0] = intArrayOf(android.R.attr.state_selected)
        states[1] = intArrayOf()
        val colors = intArrayOf(
            Color.parseColor(bottomBar.activeColor),
            Color.parseColor(bottomBar.inActiveColor)
        )
        itemIconTintList = ColorStateList(states, colors)
        itemTextColor = ColorStateList(states, colors)
        //设置文本和字体一直都显示
        labelVisibilityMode = LabelVisibilityMode.LABEL_VISIBILITY_LABELED
        val tabs = bottomBar.tabs
        //添加menu
        for (tab in tabs) {
            if (!tab.enable) {
                continue
            }
            val itemId: Int = getItemId(tab.pageUrl)
            if (itemId < 0) {
                continue
            }
            val menu = menu
            val menuItem = menu.add(0, itemId, tab.index, tab.title)
            menuItem.setIcon(sIcons[tab.index])
        }
        //设置menu的大小 添加完所有的itemMenu之后才改变大小,因为每次添加都会先移除所有的item,排序之后再放入到容器中
        var index = 0
        for (tab in tabs) {
            if (!tab.enable) {
                continue
            }
            val itemId: Int = getItemId(tab.pageUrl)
            if (itemId < 0) {
                continue
            }
            val size = SizeUtils.dp2px(tab.size.toFloat())
            val menuView: BottomNavigationMenuView = getChildAt(0) as BottomNavigationMenuView
            val itemView: BottomNavigationItemView = menuView.getChildAt(index) as BottomNavigationItemView
            itemView.setIconSize(size)
            if (TextUtils.isEmpty(tab.title)) { //title为空的一般是中间的按钮 有那种中间变大的按钮
                val tintColor =
                    if (TextUtils.isEmpty(tab.tintColor)) Color.parseColor("#ff678f") else Color.parseColor(
                        tab.tintColor
                    )
                itemView.setIconTintList(ColorStateList.valueOf(tintColor))
                //禁止上下浮动的效果
                itemView.setShifting(false)
            }
            index++
        }
        //底部导航栏默认选中项
        if (0 != bottomBar.selectTab) {
            val selectTab = tabs[bottomBar.selectTab]
            if (selectTab.enable) {
                val itemId = getItemId(selectTab.pageUrl)
                //延迟一下在切换,等待NavGraphBuilder解析完成
                post { selectedItemId = itemId }
            }
        }
    }

    private fun getItemId(pageUrl: String): Int {
        val destination = NavConfig.getDestinationMap()[pageUrl]
        return destination?.id ?: -1
    }

    fun setNavController(navController: NavController) {
        this.navController = navController
    }

    override fun onNavigationItemSelected(item: MenuItem): Boolean {
        navController?.navigate(item.itemId)
        return true;
    }
}

原生的BottomNavigationView,会自动解析我们定义在res/menu中的xml文件,创建出MenuItem对象然后add到Menu对象中。

我们将底部信息配置在json文件中,自己解析,自己创建menu,这样就灵活多了,而且可以根据后台规则动态改变底部导航栏的数量。

到这里一个比较好用的底部导航+Fragment就完成了。

组件之间导航

前面的底部导航+Fragment的例子,所有页面都在首页,并且通过底部BottomNavigationView点击完成切换页面,那如果我们在另一个activity中,点击跳转到新的activity,或者进行组件之间跳转该怎么办呢。

我知道在组建完成一个导航图之后,会将这个导航图设置给NavController,NavController是最终用来控制导航的控制器。通过NavController中的navigate方法传入需要导航的页面在导航图中的id就可以实现跳转了。

然而NavController是从MainActivity中初始化的

val navController = Navigation.findNavController(this, R.id.nav_host_fragment)
NavGraphBuilder.build(navController,this,R.id.nav_host_fragment)
nav_view.setNavController(navController)

我们在别的类中不容易拿到NavController对象

  • 一种方式可以在初始换完成之后,将navController对象保存在一个单列类的静态变量中,这样全局就都能拿到该对象进行导航跳转了,但是静态变量在内存中并不安全,当内存不足的时候也是容易被回收的,回收之后也就无法完成导航功能了
  • 另一种方式可以通过反射,当navController对象为null的时候,反射MainActivity,然后拿到其内部NavHostFragment的实例,最终拿到navController对象。

好像都不够优雅,后来有看了遍文档,文中说Navigation其实主要是为了那种单activity多fragment的应用设计的。如果你是多Activity的应用,建议每个Activity对应一个导航图和一个NavHostFragment。

对哦,一个应用中不一定只有一个NavController,我们可以专门创建一个组件之间activity跳转的导航图和NavController,用来管理不同activity之间跳转,说干就干

class NavManager {

    companion object{
        private var sNavController:NavController? = null
        private val sMavManager = NavManager()
        fun get():NavManager{
            setNavController()
            return sMavManager
        }
        private fun setNavController() {
            if(sNavController == null){
                val navController = NavController(Utils.getApp())
                val navigatorProvider = navController.navigatorProvider
                val navGraph = NavGraph(NavGraphNavigator(navigatorProvider))
                val activityNavigator = navigatorProvider.getNavigator(ActivityNavigator::class.java)
                val destinationMap = NavConfig.getDestinationMap()
                val activityDestinationStart:ActivityNavigator.Destination = getStartDestination(activityNavigator)
                navGraph.addDestination(activityDestinationStart)
                for ((key, destination) in destinationMap) {
                    if (!destination.isFragment&&!destination.isBelongTab){
                        val activityDestination = activityNavigator.createDestination()
                        activityDestination.id = destination.id
                        activityDestination.setComponentName(
                            ComponentName(Utils.getApp().packageName, destination.className!!)
                        )
                        activityDestination.addDeepLink(destination.pageUrl!!)
                        navGraph.addDestination(activityDestination)
                    }
                }
                navGraph.startDestination = activityDestinationStart.id
                navController.graph = navGraph
                sNavController = navController
            }
        }

        private fun getStartDestination(activityNavigator:ActivityNavigator): ActivityNavigator.Destination {
            val activityDestination = activityNavigator.createDestination()
            activityDestination.id = R.id.bottom_start_activity
            activityDestination.setComponentName(
                ComponentName(Utils.getApp().packageName, "com.chs.lib_core.navigation.EmptyActivity")
            )
            return activityDestination
        }
    }

    fun build(toWhere: String) : Builder{
        val bundle = Bundle()
        return Builder(toWhere,bundle)
    }

    class Builder(private val toWhere: String,private val bundle: Bundle){

        fun withString(key:String,value:String):Builder{
            bundle.putString(key, value)
            return this
        }

        fun withInt(key:String,value:Int):Builder{
            bundle.putInt(key, value)
            return this
        }

        fun withLong(key:String,value:Long):Builder{
            bundle.putLong(key, value)
            return this
        }

        fun withDouble(key:String,value:Double):Builder{
            bundle.putDouble(key, value)
            return this
        }

        fun withBoolean(key:String,value:Boolean):Builder{
            bundle.putBoolean(key, value)
            return this
        }

        fun withByte(key:String,value:Byte):Builder{
            bundle.putByte(key, value)
            return this
        }

        fun withSerializable(key:String,value: Serializable):Builder{
            bundle.putSerializable(key, value)
            return this
        }

        fun withParcelable(key:String,value: Parcelable):Builder{
            bundle.putParcelable(key, value)
            return this
        }

        private fun getItemId(pageUrl: String): Int {
            val destination = NavConfig.getDestinationMap()[pageUrl]
            return destination?.id ?: -1
        }

        fun navigate(){
            sNavController?.navigate(getItemId(toWhere),bundle)
        }
    }
}

直接new一个NavController,和一个新的NavGraph,解析生成的json文件。在注解中添加一个新的属性isBelongTab,是不是主页tab中的页面,不是放到当前导航图中。

每一个导航图要求必须要有一个startDestination(起始页),给NavController设置导航图的时候,会默认显示出起始页面。我们当前的导航图不需要起始页面,可能会随机跳转页面,我们也不知道谁是起始页。所以用一个透明的,空的activity来当起始页,启动之后直接关闭

class EmptyActivity:AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        finish()
    }
}

创建出NavController对象就好说了,条用其navigate方法传入要跳转的页面的id就可以完成跳转了。navigate方法有很多重载的方法,我们还可以传入bundle参数,传入切换动画等等。

最终如果我们想要完成一个不同组件之间的activity跳转如下

需要跳转的目标页面添加注解

@ActivityDestination(pageUrl = WanRouterKey.ACTIVITY_MAIN_MINE_RANK)
class RankActivity : BaseActivity() {}

在点击事件中通过如下方式就可以愉快的进行跳转啦。

NavManager.get()
        .build(WanRouterKey.ACTIVITY_MAIN_MINE_RANK)
        .withString("stringparama","stringparama")
        .navigate()

OK,自定义Navigation完成。