Android架构组件-Navigation的使用(二)
Android架构组件-Navigation的使用(一)
Android架构组件-Navigation的使用(二)
这篇主要介绍navigation和BottomNavigationView的使用,可以实现底部导航栏。
如果你的viewpager+tab是非滚动不左右滑动切换页面,可以更换了。
新建activity,选择Bottom Navigation Activity:
Bottom Navigation activity如果新建activity没有该选项的话,请升级Android studio
代码自动产生activity,xml,menu,drawable。创建4个fragment,此处代码忽略,然后创建navigation,如何创建参考使用一
nav_bottom_navagation.xml代码如下:
接着我们修改activity的XML,将自动产生的TextView替换为fragment
fragment的地方需要注意:
- 添加:android:name="androidx.navigation.fragment.NavHostFragment",如果自定义了NavHostFragment,就替换为自定义的路径+NavHostFragment
- 配置 app:defaultNavHost 和 app:navGraph属性:app:defaultNavHost="true"
app:navGraph="@navigation/nav_bottom_navagation"
需要注意:navigation里fragment的id需要和menu里item的id对应
下面就是修改activity:
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_bottom_navigation) var navHostFragment: NavHostFragment = supportFragmentManager.findFragmentById(R.id.frag_nav_bottom_navigation) as NavHostFragment var navController: NavController = navHostFragment.navController NavigationUI.setupWithNavController(navigation, navController)// bottom_navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener) }//需要重写onSupportNavigateUp方法override fun onSupportNavigateUp(): Boolean { return Navigation.findNavController(this, R.id.frag_nav_bottom_navigation).navigateUp() }
需要将该自动产生的代码里的listener去掉,因为setupWithNavController里set了,可以看下面的源码
public static void setupWithNavController( @NonNull final BottomNavigationView bottomNavigationView, @NonNull final NavController navController) { bottomNavigationView.setOnNavigationItemSelectedListener( new BottomNavigationView.OnNavigationItemSelectedListener() { @Override public boolean onNavigationItemSelected(@NonNull MenuItem item) { return onNavDestinationSelected(item, navController, true); } }); navController.addOnNavigatedListener(new NavController.OnNavigatedListener() { @Override public void onNavigated(@NonNull NavController controller, @NonNull NavDestination destination) { Menu menu = bottomNavigationView.getMenu(); for (int h = 0, size = menu.size(); h < size; h++) { MenuItem item = menu.getItem(h); if (matchDestination(destination, item.getItemId())) { item.setChecked(true); } } } }); }
运行试试看,一个简单的类似tabview+viewpager的效果。
点击BottomNavigationView的时候,发现当menu的数量>3的时候,出现了动画的效果,跟设计完全不符,查看源码会发现:
* support:design:27.1.1版本的:
BottomNavigationMenuView里有个Boolean变量mShiftingMode,他的值是根据mShiftingMode = mMenu.size() > 3;控制的,让人苦恼的他是private,且menu的布局在onMeasure时是根据他的值设置的,当他为false的时候,没有那个动画效果。那就想办法设置他为false吧
@SuppressLint("RestrictedApi")fun BottomNavigationView.disableShiftMode() { val menuView = this.getChildAt(0) as BottomNavigationMenuView try { val shiftingMode = menuView.javaClass.getDeclaredField("mShiftingMode") shiftingMode.isAccessible = true shiftingMode.setBoolean(menuView, false) shiftingMode.isAccessible = false for (i in 0 until menuView.childCount) { val item = menuView.getChildAt(i) as BottomNavigationItemView item.setShiftingMode(false) // set once again checked value, so view will be updated item.setChecked(item.itemData.isChecked) } } catch (e: NoSuchFieldException) { Timber.e("Unable to get shift mode field: $e") } catch (e: IllegalAccessException) { Timber.e("Unable to change value of shift mode: $e") }}
需要代码设置
bottomNavigationView.disableShiftMode()
运行看看,是不是4个menu均匀的分布了,这是27.1.1下的源码。
注意:27和28版本下navigation的源码是不一样的
如果你设置了4个menu均匀分布后,还会发现,他们带有选中变大的动画,这是因为,源码其实是2个view,他们有各自的大小值控制动画,附上源码:
public BottomNavigationItemView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); final Resources res = getResources(); int inactiveLabelSize = res.getDimensionPixelSize(R.dimen.design_bottom_navigation_text_size); int activeLabelSize = res.getDimensionPixelSize( R.dimen.design_bottom_navigation_active_text_size); mDefaultMargin = res.getDimensionPixelSize(R.dimen.design_bottom_navigation_margin); mShiftAmount = inactiveLabelSize - activeLabelSize; mScaleUpFactor = 1f * activeLabelSize / inactiveLabelSize; mScaleDownFactor = 1f * inactiveLabelSize / activeLabelSize; LayoutInflater.from(context).inflate(R.layout.design_bottom_navigation_item, this, true); setBackgroundResource(R.drawable.design_bottom_navigation_item_background); mIcon = findViewById(R.id.icon); mSmallLabel = findViewById(R.id.smallLabel); mLargeLabel = findViewById(R.id.largeLabel); }
inactiveLabelSize和activeLabelSize,
如果不想要这样的动画的话,我们只需要将这2个值设为一样就可以了。在自己项目里添加:
14dp 14dp
* support:design:28版本的:
可能是意识到BottomNavigationView的问题,他新添加了方法setLabelVisibilityMode,可以设置不同的LabelVisibilityMode,看源码有4种
@Retention(RetentionPolicy.SOURCE)public @interface LabelVisibilityMode { int LABEL_VISIBILITY_AUTO = -1; int LABEL_VISIBILITY_SELECTED = 0; int LABEL_VISIBILITY_LABELED = 1; int LABEL_VISIBILITY_UNLABELED = 2;}
可以在代码里设置:
navigation.labelVisibilityMode = LabelVisibilityMode.LABEL_VISIBILITY_SELECTED
也可以在xml里设置:
app:labelVisibilityMode="labeled"
每种效果不一样,具体的你可以每个运行看看。
有的同学的menu图片使用了PNG,也是可以的,但需要图片四周是透明的,不然会是整块都是颜色值。
如果不想使用他的特性,就想使用自己彩色的图片,可以代码设置:
bottomNavigationView.itemIconTintList = null
运行的过程中发现,每次点击切换menu的时候,fragment都会重新oncreate。
如何避免fragment被回收重新创建呢?
还有印象我前面提到的自定义NavHostFragment吗?
查看源码发现NavHostFragment在create的时候,会在NavigatorProvider里添加创建的FragmentNavigator,而在FragmentNavigator里的navigate方法里对fragment做了处理
@Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); final Context context = requireContext(); mNavController = new NavController(context); mNavController.getNavigatorProvider().addNavigator(createFragmentNavigator()); ... }
我们自定义个FragmentNavigator,重写NavHostFragment的createFragmentNavigator
@Navigator.Name("tab_fragment") // Use as custom tag at navigation.xmlclass TabNavigator ( private val context: Context, private val manager: FragmentManager, private val containerId: Int) : FragmentNavigator(context, manager, containerId) { override fun navigate(destination: Destination, args: Bundle?, navOptions: NavOptions?) { val tag = destination.id.toString() val transaction = manager.beginTransaction() val currentFragment = manager.primaryNavigationFragment if (currentFragment != null) { transaction.hide(currentFragment) } var fragment = manager.findFragmentByTag(tag) if (fragment == null) { fragment = destination.createFragment(args) transaction.add(containerId, fragment, tag) } else { transaction.show(fragment) } transaction.setPrimaryNavigationFragment(fragment) transaction.setReorderingAllowed(true) transaction.commit() dispatchOnNavigatorNavigated(destination.id, BACK_STACK_DESTINATION_ADDED) }}class TabNavHostFragment: NavHostFragment() { override fun createFragmentNavigator(): Navigator { return TabNavigator(requireContext(), childFragmentManager, id) }}
TabNavigator我们重命名了@Navigator.Name("tab_fragment"),在
navigation里我们需要更改fragment为tab_fragment
<?xml version="1.0" encoding="utf-8"?> ...
同时我们需要将xml里的fragment修改:
activity里在获取NavHostFragment也需要修改:
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_bottom_navigation) //使用自定义的TabNavHostFragment,优化fragment每次重新create问题 var navHostFragment: TabNavHostFragment = supportFragmentManager.findFragmentById(R.id.frag_nav_bottom_navigation) as TabNavHostFragment var navController: NavController = navHostFragment.navController NavigationUI.setupWithNavController(navigation, navController)
在每个fragment的oncreate方法添加log,运行查看是不是在点击menu的时候,不会每次都执行oncreate了,而且只有在点击menu的时候才会创建fragment,实现了类似懒加载的方式。
如果你想动态的手动添加menu和fragment,不妨往下继续看。
在xml里将fragment去除app:navGraph的配置,
BottomNavigationView去除app:menu的配置
代码手动的添加:
class BtNavTransactionActivity : AppCompatActivity() { var homeFragment: HomeFragment? = null var notificationsFragment: NotificationsFragment? = null var dashboardFragment: DashboardFragment? = null var testFragment: DashboardFragment? = null var currentFragment: Fragment? = null private val mOnNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item -> when (item.itemId) { 0 -> { if (homeFragment == null) { homeFragment = HomeFragment() } settingFragment(homeFragment!!) } 1 -> { if (notificationsFragment == null) { notificationsFragment = NotificationsFragment() } settingFragment(notificationsFragment!!) } 2 -> { if (dashboardFragment == null) { dashboardFragment = DashboardFragment() } settingFragment(dashboardFragment!!) } 3 -> { if (testFragment == null) { testFragment = DashboardFragment() } settingFragment(testFragment!!) } } true } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_bt_nav_transaction) bottomNavigationView.menu.add(0, 0, 0, resources.getString(R.string.title_home)).setIcon(R.drawable.ic_home_black_24dp) bottomNavigationView.menu.add(0, 1, 1, resources.getString(R.string.title_dashboard)).setIcon(R.drawable.ic_dashboard_black_24dp) bottomNavigationView.menu.add(0, 2, 2, resources.getString(R.string.title_notifications)).setIcon(R.drawable.ic_notifications_black_24dp) bottomNavigationView.menu.add(0, 3, 3, resources.getString(R.string.title_test)).setIcon(R.drawable.ic_notifications_black_24dp) if (homeFragment == null) { homeFragment = HomeFragment() } fragmentAdd(R.id.fragment, homeFragment!!) currentFragment = homeFragment// bottomNavigationView.itemIconTintList = null //如果使用的是自己的彩色png图片,需要设置 bottomNavigationView.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener) } /** *用FragmentTransaction控制fragment的show和hide *避免每次的oncreate **/ private fun settingFragment(fragment: Fragment) { if (currentFragment != fragment) { val transaction = supportFragmentManager.beginTransaction() if(!fragment.isAdded){ transaction.hide(currentFragment!!).add(R.id.fragment, fragment).commit() } else { transaction.hide(currentFragment!!).show(fragment).commit() } currentFragment = fragment } } fun fragmentAdd(viewId: Int, fragment: Fragment) { supportFragmentManager .beginTransaction() .add(viewId, fragment) .commitAllowingStateLoss() }}
OK,算是这几天navigation的使用总结吧。
补充注意:
按照上面步骤写代码的话,会出现fragment重叠的bug,可以重写onSaveInstanceState处理,这里就不介绍相关fragment重叠问题了。
更多相关文章
- 【Android】Android(安卓)Studio使用gradle导出jar包并混淆代码
- Android开发(十一)——ImageView的尺寸设置scaleType
- android源代码在线查看
- Android通知Notification的使用
- Android(安卓)自定义对话框去除白色边框代码
- Android(安卓)无法查看外部依赖jar的源码的问题
- Android里面编写退出主程序的提示代码
- Android学习之Pair类 (android.util.Pair )
- Android(安卓)与github的使用