listenzz / AndroidNavigation

A library managing navigation, nested Fragment, StatusBar, Toolbar for Android
MIT License
717 stars 95 forks source link
activity-fragment dialog fragment navigation statusbar swipeback toolbar

AndroidNavigation

A library managing nested Fragment, translucent StatusBar and Toolbar for Android.

You could use it as a single Activity Architecture Component.

This is also the subproject of hybrid-navigation.

Download demo apk

特性

6.0 screenshot:

android-navigation

android-navigation

Installation

implementation 'io.github.listenzz:AndroidNavigation:13.6.6'
implementation 'androidx.appcompat:appcompat:1.3.1'
allprojects {
    repositories {
        google()
        mavenCentral()
    }
}

Usage


构建 UI 层级

你的 Fragment 需要继承 AwesomeFragment。

你的 Activity 需要继承 AwesomeActivity,然后设置 rootFragment。

public class MainActivity extends AwesomeActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (savedInstanceState == null) {
            TestFragment testFragment = new TestFragment();
            setActivityRootFragment(testFragment);
        }
    }

}

你可以调用 setActivityRootFragment 多次,根据不同的 App 状态展示不同的根页面。比如一开始你只需要展示个登录页面,登陆成功后将根页面设置成主页面。

AwesomeFragment 同样部署了 setActivityRootFragment 接口,方便你随时随地切换 activity 的根。

你通常还需要另外一个 Activity 来做为闪屏页(Splash),这个页面则不必继承 AwesomeActivity。

为了处理常见的 Fragment 嵌套问题,提供了 StackFragmentTabBarFragmentDrawerFragment 三个容器类。它们可以作为 Activity 的 rootFragment 使用。这三个容器为 Fragment 嵌套提供了非常便利的操作。

StackFragment

StackFragment 以栈的形式管理它的子 Fragment,支持 push、pop 等操作,在初始化时,需要为它指定 rootFragment。

public class MainActivity extends AwesomeActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (savedInstanceState == null) {
            TestFragment testFragment = new TestFragment();
            StackFragment stackFragment = new StackFragment();
            // 把 TestFragment 设置为 StackFragment 的根
            stackFragment.setRootFragment(testFragment);
            // 把 StackFragment 设置为 Activity 的根
            setActivityRootFragment(stackFragment);
        }
    }
}

如果 TestFragment 的根布局是 LinearLayout 或 FrameLayout,会自动帮你创建 Toolbar,当由 A 页面跳转到 B 页面时,会为 B 页面的 Toolbar 添加返回按钮。更多关于 Toolbar 的配置,请参考 设置 Toolbar 一章。

在 TestFragment 中,我们可以通过 getStackFragment 来获取套在它外面的 StackFragment,然后通过 StackFragment 提供的 pushFragment 跳转到其它页面,或通过 popFragment 返回到前一个页面。关于导航的更多细节,请参考 导航 一章。

TabBarFragment

这也是一个比较常见的容器,一般 APP 主界面底下都会有几个 tab,点击不同的 tab 就切换到不同的界面。

public class MainActivity extends AwesomeActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (savedInstanceState == null) {

            // 首页
            HomeFragment homeFragment = new HomeFragment();
            homeFragment.setTabBarItem(new TabBarItem("首页", R.drawable.icon_home));

            // 通讯录
            ContactsFragment contactsFragment = new ContactsFragment();
            contactsFragment.setTabBarItem(new TabBarItem("通讯录", R.drawable.icon_contacts));

            // 添加 tab 到 TabBarFragment
            TabBarFragment tabBarFragment = new TabBarFragment();
            tabBarFragment.setFragments(homeFragment, contactsFragment);

            // 把 TabBarFragment 设置为 Activity 的根
            setActivityRootFragment(tabBarFragment);
        }
    }

}

在 HomeFragment 或 ContactsFragment 中,可以通过 getTabBarFragment 来获取它们所属的 TabBarFragment.

可以通过 TabBarFragment 的 setSelectedIndex 方法来动态切换 tab,通过 getTabBar 可以获取 TabBar, 然后可以调用 TabBar 提供的方法来设置红点,未读消息数等。

如果对提供的默认 TabBar 不满意,可以通过实现 TabBarProvider 来自定义 TabBar , 在设置 TabBarFragment 为其它容器的根前,调用 TabBarFragment#setTabBarProvider 来设置自定义的 TabBar, 参数可以为 null, 表示不需要 TabBar.

如果 HomeFragment 或 ContactsFragment 需要有导航的能力,可以先把它们嵌套到 StackFragment 中。

public class MainActivity extends AwesomeActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (savedInstanceState == null) {

            // 首页
            HomeFragment homeFragment = new HomeFragment();
            StackFragment homeNavigationFragment = new StackFragment();
            homeNavigationFragment.setRootFragment(homeFragment);
            homeNavigationFragment.setTabBarItem(new TabBarItem("首页", R.drawable.icon_home));

            // 通讯录
            ContactsFragment contactsFragment = new ContactsFragment();
            StackFragment contactsNavigationFragment = new StackFragment();
            contactsNavigationFragment.setRootFragment(contactsFragment);
            contactsNavigationFragment.setTabBarItem(new TabBarItem("通讯录", R.drawable.icon_contacts));

            // 添加 tab 到 TabBarFragment
            TabBarFragment tabBarFragment = new TabBarFragment();
            tabBarFragment.setFragments(homeNavigationFragment, contactsNavigationFragment);

            // 把 TabBarFragment 设置为 Activity 的根
            setActivityRootFragment(tabBarFragment);
        }
    }

}

DrawerFragment

这个容器内部封装了 DrawerLayout。使用时需要为它设置两个子 Fragment。

public class MainActivity extends AwesomeActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (savedInstanceState == null) {

            DrawerFragment drawerFragment = new DrawerFragment();
            drawerFragment.setContentFragment(new ContentFragment());
            drawerFragment.setMenuFragment(new MenuFragment());

            // 把 drawerFragment 设置为 Activity 的根
            setActivityRootFragment(drawerFragment);
        }
    }

}

在 ContentFragment 或 MenuFragment 中,我们可以通过 getDrawerFragment 来获取它们所属的 DrawerFragment。

DrawerFragment 提供了 toggleMenuopenMenucloseMenu 这几个方法来打开或关闭 Menu。

可以通过 getContentFragmentgetMenuFragment 来获取对应的 Fragment。

可以通过 setMinDrawerMarginsetMaxDrawerWidth 来设置 menu 的宽度

contentFragment 可以是一个像 TabBarFragment 这样的容器。可以参考 demo 中 MainActivity 中的设置。

自定义容器

如果以上容器都不能满足你的需求,你可以自定义容器。

容器在添加子 fragment 时一定要注意判断 savedInstanceState 是否为 null, 会不会在生命周期重启时,重复添加 fragment。

可以参考 demo 中 ViewPagerFragment 这个类,它就是个自定义容器。

自定义容器,继承 AwesomeFragment 并重写下面这个方法。

@Override
public boolean isLeafAwesomeFragment() {
    return false;
}

因为 AwesomeFragment 会为非容器类 Fragment 的 root view 添加背景。如果容器不表明它是容器,也会为容器添加背景,这样就会导致不必要的 overdraw。

可能需要有选择地重写以下方法

@Override
protected AwesomeFragment childFragmentForAppearance() {
    // 这个方法用来控制当前的 status bar 的样式是由哪个子 fragment 决定的
    // 如果不重写,则由容器自身决定
    // 可以参考 StackFragment、TabBarFragment
    // 是如何决定让哪个子 fragment 来决定 status bar 样式的
    return 一个恰当的子 fragment;
}

如何使不同 fragment 拥有不同的 status bar 样式,请参考 设置状态栏 一章

@Override
protected boolean onBackPressed() {
    // 这个方法用来控制当用户点击返回键时,到底要退出哪个子 fragment
    // 返回 true 表示当前容器消费了此事件,否则转发给上一层容器处理
    // 可以参考 DrawerFragment,StackFragment 是如何处理返回键的
    return super.onBackPressed();
}

非容器页面也可以重写 onBackPressed 来处理用户点击返回按钮事件。

所见即所得 Dialog

Fragment 可以作为 Dialog 显示,本库做了特殊处理,使得显示出来的 Dialog 布局和在 xml 预览中所见一模一样。实现细节请看这篇文章

导航

导航是指页面间的跳转和传值,实际上和容器如何管理它的子 Fragment 有很大关系。

present & dismiss

AwesomeActivity 和 AwesomeFragment 提供了两个基础的导航功能 present 和 dismiss

StackFragment

StackFragment 是个容器,以栈的方式管理子 fragment,支持 push、pop、popTo、popToRoot 操作,并额外支持 redirectTo 操作。

我们可以在它的子 Fragment 中(不必是直接子 fragment,可以是子 fragment 的子 fragment)通过 getStackFragment 来获取它的引用。

在初始化 StackFragment 时,你必须调用 setRootFragment 来指定它的根页面。

你可能已经猜到,pop 和 popToRoot 都是通过 popTo 来实现的。pop 的时候也可以通过 setResult 设置返回值,不过此时 requestCode 的值总是 0。

自定义导航

虽然 AwesomeFragment 和 StackFragment 提供的导航操作已经能满足大部分需求,但有时我们可能需要自定义导航操作,尤其是自定义容器的时候。

需要注意几个点

懒加载

本库 5.0.0 以上支持使用 onResumeonPause 实现懒加载

建议使用 ViewPager2 来代替 ViewPager

全局样式设置

可以通过重写 AwesomeActivity 如下方法来定制该 activity 下所有 fragment 的样式

@Override
protected void onCustomStyle(Style style) {

}

可配置项如下:

{
  screenBackgroundColor: int // 页面背景,默认是白色
  statusBarStyle: BarStyle // 状态栏和 toolbar 前景色,可选值有 DarkContent 和 LightContent
  statusBarColor: String // 状态栏背景色,仅对 4.4 以上版本生效, 默认值是 colorPrimaryDark
  navigationBarColor: Integer // 导航栏颜色,仅对 Android O 以上版本生效,建议保留默认设置
  toolbarBackgroundColor: int // toolbar 背景颜色,默认值是 colorPrimary
  elevation: int // toolbar 阴影高度, 仅对 5.0 以上版本生效,默认值为 4 dp
  shadow: Drawable // toolbar 阴影图片,仅对 4.4 以下版本生效
  backIcon: Drawable // 返回按钮图标,默认是个箭头
  toolbarTintColor: int // toolbar 按钮的颜色,默认根据 statusBarStyle 来推算
  titleTextColor: int // toolbar 标题颜色,默认根据 statusBarStyle 来推算
  titleTextSize: int // toolbar 标题字体大小,默认是 17 dp
  titleGravity: int // toolbar 标题的位置,默认是 Gravity.START
  toolbarButtonTextSize: int // toolbar 按钮字体大小,默认是 15 dp
  swipeBackEnabled: boolean // 是否支持手势返回,默认是 false
  badgeColor: String // Badge 背景颜色

  // BottomBar
  tabBarBackgroundColor: String // TabBar 背景,默认值是 #FFFFFF
  tabBarShadow: Drawable // TabBar 分割线
  tabBarItemColor: String // TabBarItem 颜色,当 tabBarSelectedItemColor 未设置时,该值为选中时的颜色,否则为未选中时的颜色
  tabBarSelectedItemColor: String // TabBarItem 选中时的颜色
}

所有的可配置项都是可选的。

如果某个 fragment 与众不同,可以为该 fragment 单独设置样式,只要重写该 fragment 的 onCustomStyle 方法,在其中设置那些不同的样式即可。

设置状态栏

设置方式非常简单,重写 AwesomeFragment 中的 onCustomStyle 方法即可。

@Override
protected void onCustomStyle(@NonNull Style style) {
    super.onCustomStyle(style);
    style.setToolbarTintColor(Color.WHITE);
    style.setToolbarBackgroundColor(Color.TRANSPARENT);
    style.setStatusBarStyle(BarStyle.LightContent);
    style.setStatusBarColor(Color.TRANSPARENT);
}

或者通过重写以下方法,返回期望值:

// AwesomeFragment.java
protected BarStyle preferredStatusBarStyle();
protected boolean preferredStatusBarHidden();
protected int preferredStatusBarColor();
protected boolean preferredStatusBarColorAnimated();

如果你当前页面的状态栏样式不是固定的,需要根据 App 的不同状态展示不同的样式,你可以在上面这些方法中返回一个变量,当这个变量的值发生变化时,你需要手动调用 setNeedsStatusBarAppearanceUpdate 来通知框架更新状态栏样式。可以参考 demo 中 CustomStatusBarFragment 这个类。

设置 Toolbar

当 fragment 的 parent fragment 是一个 StackFragment 时,会自动为该 fragment 创建 Toolbar。

当 Fragment 的根布局是 LinearLayout 时,Toolbar 作为 LinearLayout 的第一个子元素添加。当 Fragment 的根布局是 FrameLayout 时,Toolbar 作为 FrameLayout 的最后一个子元素添加,覆盖在其余子元素最上面。

你可以调用 AwesomeFragment 的以下方法来设置 Toolbar

请在 onViewCreated 中调用上面这些方法

Toolbar 的创建时机是在 Fragment onViewCreated 这个生命周期函数中,在此之前之前,调用 getAwesomeToolbar 得到的返回值为 null。

如果当前 fragment 不是 StackFragment 的 rootFragment,会自动在 Toolbar 上创建返回按钮。如果你不希望当前页面有返回按钮,可以重写以下方法。

protected boolean shouldHideBackButton() {
    return true;
}

如果你希望禁止用户通过返回键(物理的或虚拟的)或者手势退出当前页面,你可以重写以下方法,并返回 false。

protected boolean isBackInteractive() {
    return false;
}

如果只是希望禁止用户通过手势退出当前页面,重写以下方法,返回 false,此时用户仍然可以通过返回键退出当前页面。

protected boolean isSwipeBackEnabled() {
    return false;
}

如果你不希望自动为你创建 toolbar, 或者自动创建的 toolbar 所在 UI 层级不合适,你可以重写以下方法,返回 null 或者自定义的 toolbar。

protected AwesomeToolbar onCreateToolbar(View parent) {
    return null;
}

demo 中,NoToolbarFragment 返回 null, 表示不需要创建 toolbar。如果需要自定义 toolbar,请优先考虑基于 AwesomeToolbar 进行自定义,并在 onCreateAwesomeToolbar 返回自定义的 toolbar,就像 CoordinatorFragment 和 ViewPagerFragment 所做的那样。

如果在 toolbar 不透明的情况下,希望页面可以延伸到 toolbar 底部,那么重写以下方法,返回 true,参看 ToolbarColorTransitionFragment 这个例子

@Override
protected boolean extendedLayoutIncludesToolbar() {
    return true;
}

你还可以重写 onCustomStyle 这个方法,来修改 toolbar 的样式。

@Override
protected void onCustomStyle(@NonNull Style style) {
    super.onCustomStyle(style);
    style.setToolbarTintColor(Color.WHITE);
    style.setToolbarBackgroundColor(Color.TRANSPARENT);
    style.setStatusBarStyle(BarStyle.LightContent);
    style.setStatusBarColor(Color.TRANSPARENT);
}

设置导航栏(虚拟键)

仅对 Android 8 以上版本生效

使用 font icons

把你的 font icon 文件放到 assets/fonts 目录中,就像 demo 所做的那样。每个图标会有一个可读的 name, 以及一个 code point,我们通常通过 name 来查询 code point,当然也可以人肉查好后直接使用 code point,demo 中就是这样。

以下方法可以通过 code point 获取 glyph(字形)

public static String fromCharCode(int... codePoints) {
    return new String(codePoints, 0, codePoints.length);
}

获取 glyph 后构建如下格式的 uri

font://fontName/glyph/size/color

其中 fontName 就是你放在 assets/fonts 文件夹中的字体文件名,但不包括后缀。size 是字体大小,如 24,color 是字体颜色,可选,只支持 RRGGBB 格式。

可以参考 demo 中 MainActivity 中是怎样构建一个 fontUri 的。

代码规范