项目地址
这一篇我们将使用Navigation
搭建我们App
的基础架构,我们先看下效果
Navigation
介绍官网地址
快速入门
导航组件由以下三个关键部分组成:
Navigation
使用这里我们直接使用AS自带的模板创建一个Activity
创建完毕之后,我们看下都导入了哪些依赖
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'com.google.android.material:material:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.vectordrawable:vectordrawable:1.1.0'
//这俩是最主要的
implementation 'androidx.navigation:navigation-fragment:2.2.2'
implementation 'androidx.navigation:navigation-ui:2.2.2'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
接着我们看下整体项目目录
一个Activity
和三个Fragment
,接着我们从Activity
看起
<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.google.android.material.bottomnavigation.BottomNavigationView
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"
app:menu="@menu/bottom_nav_menu" />
<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"
app:navGraph="@navigation/mobile_navigation" />
androidx.constraintlayout.widget.ConstraintLayout>
看下它的布局,很简单就是两部分,底部的BottomNavigationView
和上面的fragment
,我们先看下上面的这个fragment
,可以看见它是一个叫NavHostFragment
的Fragment
,我们在上面介绍里面说过,这个是一个容器,我们简单看下它的源码
public class NavHostFragment extends Fragment implements NavHost {
......
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Context context = getContext();
//实例化NavController,它才是真正处理导航的,并且把FragmentNavigator实例化添加到SimpleNavigatorProvider
mNavController = new NavController(context);
mNavController.getNavigatorProvider().addNavigator(createFragmentNavigator());
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
FrameLayout frameLayout = new FrameLayout(inflater.getContext());
frameLayout.setId(getId());
return frameLayout;
}
//onViewCreated传入当前的根布局以及NavController实例,并且给根布局设置一个tag,所以在每次findNavController的时候都会通过这个tag取唯一的实例,如果取不到会循环从父布局在去找。
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (!(view instanceof ViewGroup)) {
throw new IllegalStateException("created host view " + view + " is not a ViewGroup");
}
View rootView = view.getParent() != null ? (View) view.getParent() : view;
Navigation.setViewNavController(rootView, mNavController);
}
public static void setViewNavController(@NonNull View view,
@Nullable NavController controller) {
view.setTag(R.id.nav_controller_view_tag, controller);
}
......
}
通过上面源码我们知道NavHostFragment
里面实例化一个NavController
,他是处理导航的,主要就是通过navigate
方法实现跳转的
/**
* @param resId {@link NavDestination#getAction(int) action}的ActionId或者destinationId
* @param args arguments参数
* @param navOptions 导航操作的选项
*/
public void navigate(int resId, Bundle args, NavOptions navOptions) {
//回退栈为null返回NavGraph
//不为null返回回退栈中的最后一项
NavDestination currentNode = mBackStack.isEmpty() ? mGraph : mBackStack.peekLast();
if (currentNode == null) {
throw new IllegalStateException("no current navigation node");
}
int destId = resId;
//获取resId对应的NavAction
final NavAction navAction = currentNode.getAction(resId);
if (navAction != null) {
if (navOptions == null) {
navOptions = navAction.getNavOptions();
}
//通过NavAction获取目的地id
destId = navAction.getDestinationId();
}
//若destId为0而navOptions又不为null则弹出到该navOptions的指定的页面
if (destId == 0 && navOptions != null && navOptions.getPopUpTo() != 0) {
popBackStack(navOptions.getPopUpTo(), navOptions.isPopUpToInclusive());
return;
}
//为0报错
if (destId == 0) {
throw new IllegalArgumentException("Destination id == 0 can only be used" + " in conjunction with navOptions.popUpTo != 0");
}
//找到准备前往的目的地
NavDestination node = findDestination(destId);
if (node == null) {
final String dest = NavDestination.getDisplayName(mContext, destId);
throw new IllegalArgumentException("navigation destination " + dest
+ (navAction != null
? " referenced from action " + NavDestination.getDisplayName(mContext, resId)
: "")
+ " is unknown to this NavController");
}
if (navOptions != null) {
//是否清除回退栈
if (navOptions.shouldClearTask()) {
popBackStack(0, true);
mBackStack.clear();
} else if (navOptions.getPopUpTo() != 0) {
//导航之前弹出栈到指定栈
// 是否将该页面也弹出
popBackStack(navOptions.getPopUpTo(), navOptions.isPopUpToInclusive());
}
}
//进行导航
node.navigate(args, navOptions);
}
这里我们还需要介绍两个类NavDestination
和NavGraph
,NavGraph
是NavDestination
的集合,NavDestination
就相当于是节点,它的源码有一个这个方法
这个就是通过解析xml实例化自己的,那么这个xml是哪来的呢?其实就是我们activity_main
中fragment
节点里面有个navGraph
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/mobile_navigation"
app:startDestination="@+id/navigation_home">
<fragment
android:id="@+id/navigation_home"
android:name="com.hfs.navgationdemo.ui.home.HomeFragment"
android:label="@string/title_home"
tools:layout="@layout/fragment_home" />
<fragment
android:id="@+id/navigation_dashboard"
android:name="com.hfs.navgationdemo.ui.dashboard.DashboardFragment"
android:label="@string/title_dashboard"
tools:layout="@layout/fragment_dashboard" />
<fragment
android:id="@+id/navigation_notifications"
android:name="com.hfs.navgationdemo.ui.notifications.NotificationsFragment"
android:label="@string/title_notifications"
tools:layout="@layout/fragment_notifications" />
navigation>
接下来说说我们activity_main
中的另一个布局BottomNavigationView
<com.google.android.material.bottomnavigation.BottomNavigationView
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"
app:menu="@menu/bottom_nav_menu" />
用它的话你需要添加依赖
implementation 'com.google.android.material:material:1.1.0'
我们需要给他配置一个menu
,告诉它我们需要几个tab,比如我们这个例子中配置了3个
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/navigation_home"
android:icon="@drawable/ic_home_black_24dp"
android:title="@string/title_home" />
<item
android:id="@+id/navigation_dashboard"
android:icon="@drawable/ic_dashboard_black_24dp"
android:title="@string/title_dashboard" />
<item
android:id="@+id/navigation_notifications"
android:icon="@drawable/ic_notifications_black_24dp"
android:title="@string/title_notifications" />
menu>
我们回到MainActivity
中看看它们是怎么使用的
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
BottomNavigationView navView = findViewById(R.id.nav_view);
// Passing each menu ID as a set of Ids because each
// menu should be considered as top level destinations.
AppBarConfiguration appBarConfiguration = new AppBarConfiguration.Builder(
R.id.navigation_home, R.id.navigation_dashboard, R.id.navigation_notifications)
.build();
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);
NavigationUI.setupWithNavController(navView, navController);
}
}
主要是在NavigationUI.setupWithNavController(navView, navController);
上,我们点进去看下
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);
}
});
final WeakReference<BottomNavigationView> weakReference =
new WeakReference<>(bottomNavigationView);
navController.addOnDestinationChangedListener(
new NavController.OnDestinationChangedListener() {
@Override
public void onDestinationChanged(@NonNull NavController controller,
@NonNull NavDestination destination, @Nullable Bundle arguments) {
BottomNavigationView view = weakReference.get();
if (view == null) {
navController.removeOnDestinationChangedListener(this);
return;
}
Menu menu = view.getMenu();
for (int h = 0, size = menu.size(); h < size; h++) {
MenuItem item = menu.getItem(h);
if (matchDestination(destination, item.getItemId())) {
item.setChecked(true);
}
}
}
});
}
这里分别监听了BottomNavigationView
和NavController
,NavController
我们在上面说了下,这里我们简单看下BottomNavigationView
,
public static boolean onNavDestinationSelected(@NonNull MenuItem item,
@NonNull NavController navController) {
NavOptions.Builder builder = new NavOptions.Builder()
.setLaunchSingleTop(true)
.setEnterAnim(R.anim.nav_default_enter_anim)
.setExitAnim(R.anim.nav_default_exit_anim)
.setPopEnterAnim(R.anim.nav_default_pop_enter_anim)
.setPopExitAnim(R.anim.nav_default_pop_exit_anim);
if ((item.getOrder() & Menu.CATEGORY_SECONDARY) == 0) {
builder.setPopUpTo(findStartDestination(navController.getGraph()).getId(), false);
}
NavOptions options = builder.build();
try {
//TODO provide proper API instead of using Exceptions as Control-Flow.
navController.navigate(item.getItemId(), null, options);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
最终还是会调用navController.navigate(item.getItemId(), null, options);
OK,我们运行下看看效果
这里跟我们要实现的效果很像,但是还有些问题,一个是我们想动态添加底部,不在xml写死;我们在点击底部item的时候不要这个放大缩小动画;切换的时候Fragment
不反复重建,源码使用的是replace
方式,我们需要改进;这些问题我们将在下一篇解决