之前的一篇文章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文件配置了,是不是簡單了靈活許多。
如何實現上面功能呢
- 原生的Navigation需要在res/navigation文件夾下創建xml文件來配置所有需要導航的頁面結點,這些結點上都有需要導航的頁面的全類名,程序運行的時候,會解析該xml文件,拿到全類名,組裝導航圖,然後交給然後交給ActivityNavigator、FragmentNavigator等類去實現跳轉。我們通過註解處理器在編譯的時候可以拿到所有自定義的註解標註過的類的全類名,然後保存到一個json文件中,放到asssets目錄下,程序運行的時候解析文件,自己組裝導航圖
- 原生的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);
}
}
}
}
- 註解處理器的目標是,掃描出所有帶FragmentDestination或者ActivityDestination的類,拿到註解中的參數和類的全類名,封裝成對象放到map中,使用fastjson將map生成json字符串,保存在src/main/assets目錄下面
- 在組件化開發的時候,每個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
}
構建導航圖有三個比較大的部分
- 將json文件解析爲map
- 遍歷map,判斷是fragment還是activity,根據不同 類型分別創建不同的Destination對象,並將這些Destination對象add到導航圖中,給導航圖設置起始頁面,最後把導航圖設置給主頁穿過來的NavController對象
- 上一篇文章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完成。