登录

去注册

登录

注册

去登录

注册

方便管理Fragment、StatusBar 、Toolbar的库 AndroidNavigation

listenzz   2018-04-09   收藏

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 react-native-navigation-hybrid.

特性

  • 一行代码实现 Fragment 嵌套,一次性构建好嵌套层级
  • 一行代码实现 Fragment 跳转,不再需要写一大堆操作 fragment 的代码了,不用担心用错 FragmentManager 了
  • 一行代码开关沉浸式状态栏,兼容到 Android 4.4 并解决了相关 BUG
  • 自动为你创建 Toolbar,一行代码设置标题、按钮,支持关闭自动创建功能以实现定制
  • 一处设置全局样式,到处使用,并且支持不同页面个性化
  • 支持 font icons

下载 apk 体验

6.0 screenshot:

android-navigation

android-navigation

Installation

implementation 'me.listenzz:navigation:1.0.0'

Usage

构建UI层级

导航

懒加载

全局样式设置

设置状态栏

设置 Toolbar

使用 font icons

代码规范


构建UI层级

你的 Fragment 需要继承 AwesomeFragment。

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

public class MainActivity extends AwesomeActivity {

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

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

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

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

NavigationFragment

NavigationFragment 以栈的形式管理它的子 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();
            NavigationFragment navigationFragment = new NavigationFragment();
            // 把 TestFragment 设置为 NavigationFragment 的根
            navigationFragment.setRootFragment(testFragment);
            // 把 NavigationFragment 设置为 Activity 的根
            setRootFragment(navigationFragment);
        }
    }
}

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

在 TestFragment 中,我们可以通过 getNavigationFragment 来获取套在它外面的 NavigationFragment,然后通过 NavigationFragment 提供的 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 的根
            setRootFragment(tabBarFragment);
        }
    }
    
}

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

可以通过 TabBarFragment 的 setSelectedIndex 方法来动态切换 tab,通过 setBadge 来设置 badge,譬如未读消息数。

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

public class MainActivity extends AwesomeActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (savedInstanceState == null) {
            
            // 首页
            HomeFragment homeFragment = new HomeFragment();
            NavigationFragment homeNavigatoinFragment = new NavigationFragment();
            homeNavigationFraggment.setRootFragment(homeFragment);
            homeNavigatoinFragment.setTabBarItem(new TabBarItem(R.drawable.icon_home, "首页"));
            
            // 通讯录
            ContactsFragment contactsFragment = new ContactsFragment();
            NavigationFragment contactsNavigationFragment = new NavigationFragment();
            contactsNavigationFragment.setRootFragment(contactsFragment);
            contactsNavigationFragment.setTabBarItem(new TabBarItem(R.drawable.icon_contacts, "通讯录"));
            
            // 添加 tab 到 TabBarFragment 
            TabBarFragment tabBarFragment = new TabBarFragment();
            tabBarFragment.setFragments(homeNavigatoinFragment, contactsNavigationFragment);
            
            // 把 TabBarFragment 设置为 Activity 的根
            setRootFragment(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 的根
            setRootFragment(drawerFragment);
        }
    }
    
}

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

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

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

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

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

自定义容器

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

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

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

@Override
public boolean isParentFragment() {
    return true;
}

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

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

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

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

@Override
protected boolean onBackPressed() {
    // 这个方法用来控制当用户点击返回键时,到底要退出哪个子 fragment
    // 如果不重写,则退出容器本身
    // 可以参考 DrawerFragment 是如何处理返回键的
    return super.onBackPressed();
}

导航

导航是指页面间的跳转和传值。

present & dismiss

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

  • present

    present 是一种模态交互方式,只有关闭被 present 的页面,才可以回到上一个页面,通常要求 presented 的页面给 presenting 的页面返回结果,类似于 startActivityForResult

    比如 A 页面 present 出 B 页面

    // A.java
    presentFragment(testFragment, REQUEST_CODE);
    

    B 页面返回结果给 A 页面

    // B.java
    Bundle result = new Bundle();
    result.putString("text", resultEditText.getText().toString());
    setResult(Activity.RESULT_OK, result);
    dismissFragment();
    

    A 页面实现 onFragmentResult 来接收这个结果

    // A.java
    @Override
    public void onFragmentResult(int requestCode, int resultCode, Bundle data) {
        super.onFragmentResult(requestCode, resultCode, data);
        if (requestCode == REQUEST_CODE) {
            if (resultCode != 0) {
                String text = data.getString("text", "");
                resultText.setText("present result:" + text);
            } else {
                resultText.setText("ACTION CANCEL");
            }
        }
    }
    

    有些时候,比如选择一张照片,我们先要跳到相册列表页面,然后进入某个相册选择相片返回。这也是没有问题的。

    A 页面 present 出相册列表页面

    //AFragment.java
    NavigatoinFragment navigationFragment = new NavigationFragment();
    AlbumListFragment albumListFragment = new AlbumListFragment();
    navigationFragment.setRootFragment(albumListFragment);
    presentFragment(navigationFragment, 1)
    

    相册列表页面 push 到某个相册

    push 是 NavigationFragment 的能力,要使用这个功能,你的 fragment 外层必须有一个 NavigationFragment 做为容器。

    // AlbumListFragment.java
    AlbumFragment albumFragment = new AlbumFragment();
    getNavigationFragment.pushFragment(albumFragment);
    

    在相册页面选好相片后返回结果给 A 页面

    // AlbumFragment.java
    Bundle result = new Bundle();
    result.putString("uri", "file://...");
    setResult(Activity.RESULT_OK, result);
    dismissFragment();
    

    在 A 页面接收返回的结果(略)。

  • dismiss

    关闭 present 出来的 Fragment,可以在该 Fragment 的任意子 Fragment 中调用,请参看上面相册的例子。

present 所使用的 FragmentManager 是 Activity 的 getSupportFragmentManager,因此 present 出来的 fragment 是属于 Activity 的,它不属于任何 fragment 的子 fragment,这样就确保了 present 出来的 fragment 是模态的。

NavigationFragment

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

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

在初始化 NavigationFragment 时,你必须调用 setRootFragment 来指定它的根页面。请参考上面相册那个例子的做法。setRootFragment 只能调用一次,如果想更换根页面,可以使用 replaceToRootFragment 这个方法。

  • push

    入栈,由 A 页面跳转到 B 页面。

    // AFragment.java
    getNavigationFragment.pushFragment(bFragment);
    
  • pop

    出栈,返回到前一个页面。比如你由 A 页面 push 到 B 页面,现在想返回到 A 页面。

    // BFragment.java
    getNavigationFragment.popFragment();
    
  • popToRoot

    出栈,返回到当前导航栈根页面。比如 A 页面是根页面,你由 A 页面 push 到 B 页面,由 B 页面 push 到 C 页面,由 C 页面 push 到 D 页面,现在想返回到根部,也就是 A 页面。

    // DFragment.java
    getNavigationFragment.popToRootFragment();
    
  • popTo

    出栈,返回到之前的指定页面。比如你由 A 页面 push 到 B 页面,由 B 页面 push 到 C 页面,由 C 页面 push到 D 页面,现在想返回 B 页面。你可以把 B 页面的 sceneId 一直传递到 D 页面,然后调用 popToFragment("bSceneId") 返回到 B 页面。

    从 B 页面跳转到 C 页面时

    // BFragment.java
    CFragment cFragment = new CFragment();
    Bundle args = FragmentHelper.getArguments(cFragment);
    // 把 bSceneId 传递给 C 页面
    args.putString("bSceneId", getSceneId());
    getNavigationFragment().pushFragment(cFragment);
    

    从 C 页面跳到 D 页面时

    // CFragment.java
    DFragment dFragment = new DFragment();
    Bundle args = FragmentHelper.getArguments(dFragment);
    // 把 bSceneId 传递给 D 页面
    args.putString("bSceneId", getArguments().getString("bSceneId"));
    getNavigationFragment().pushFragment(dFragment);
    

    现在想从 D 页面 返回到 B 页面

    // DFragment.java
    String bSceneId = getArguments().getString("bSceneId");
    BFragment bFragment = (AwesomeFragment)getFragmentManager().findFragmentByTag(bSceneId);
    getNavigationFragment().popToFragment(bFragment);
    

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

  • replace

    出栈然后入栈,用指定页面取代当前页面,比如当前页面是 A,想要替换成 B

    // AFragment.java
    BFragment bFragment = new BFragment();
    getNavigationFragment().replaceFragment(bFragment);
    
  • replaceToRoot

    出栈然后入栈,把 NavigationFragment 的所有子 Fragment 替换成一个 Fragment。譬如 A 页面是根页面,然后 push 到 B、C、D 页面,此时 NavigationFragment 里有 A、B、C、D 四个页面。如果想要重置NavigationFragment ,把 E 页面设置成根页面。

    // DFragment.java
    EFragment eFragment = new EFragment();
    getNavigationFragment().replaceToRootFragment(eFragment);
    

    现在 NavigationFragment 里只有 EFragment 这么一个子 Fragment 了。

上面这些操作所使用的 FragmentManager,是 NavigationFragment 的 getChildFragmentManager,所有出栈或入栈的 fragment 都是 NavigationFragment 的子 fragment.

navigation-stack

如上图,A fragment 嵌套在 NavigationFragment 中,A1 fragment 嵌套在 A fragment 中,当我们从 A1 push B fragment 时,B fragment 会成为 NavigationFragment 的子 fragment,而不是 A 的子 fragment,它和 A 是兄弟,它是 A1 的叔叔。

自定义导航

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

需要注意几个点

  • 选择合适的 FragmentManager

    Activity#getSupportFragmentManager 会将 fragment 添加到 activity

    Fragment#getFragmentManager 拿到的是上一级的 fragmentManager, 通过它添加的 fragment 会成为当前 fragment 的兄弟。

    Fragment#getChildFragmentManager 会将 fragment 添加为当前 fragment 的子 fragment。

  • 设置正确的 tag

    总是使用有三个参数的 add、replace 等方法,最后一个 tag 传入目标 fragment 的 getSceneId 的值。

  • 正确使用 addToBackStack

    如果需要添加到返回栈,tag 参数不能为 null, 必须和传递给 add 或 replace 的 tag 一致,也就是目标 fragment 的 getSceneId 的值。

  • 如果不通过栈的形式来管理子 fragment 时,必须将当前子 fragment 设置为 primaryNavigationFragment

    参考 TabBarFragment 和 DrawerFragment,它们就不是用栈的形式管理子 fragment.

    getFragmentManager().setPrimaryNavigationFragment(fragment);
    

可以参考 demo 中 GridFragment 这个类,看如何实现自定义导航

懒加载

AwesomeFragment 提供了两个额外的生命周期回调

protected void onViewAppear();
protected void onViewDisappear();

可以通过它们实现懒加载

全局样式设置

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

@Override
protected void onCustomStyle(Style style) {
    
}

可配置项如下:

{
    screenBackgroundColor: int       // 页面背景,默认是白色
    statusBarStyle: BarStyle         // 状态栏和 toolbar 前景色,可选值有 DarkContent 和 LightContent
    statusBarColor: String           // 状态栏背景色,仅对 4.4 以上版本生效, 默认值是 colorPrimaryDark
    toolbarBackgroundColor: int      // toolbar 背景颜色,默认值是 colorPrimary
    elevation: int                   // toolbar 阴影高度, 仅对 5.0 以上版本生效,默认值为 4 dp
    shadow: Drawable                 // toolbar 阴影图片,仅对 4.4 以下版本生效 
    backIcon: Drawable               // 返回按钮图标,默认是个箭头
    toolbarTintColor: int            // toolbar 标题和按钮的颜色,默认根据 toolbarStyle 来推算
    titleTextColor: int              // toolbar 标题颜色,默认取 toolbarTintColor 的值
    titleTextSize: int               // toolbar 标题字体大小,默认是 17 dp
    titleGravity: int                // toolbar 标题的位置,默认是 Gravity.START
    toolbarButtonTintColor: int      // toolbar 按钮颜色,默认取 toolbarTintColor 的值
    toolbarButtonTextSize: int       // toolbar 按钮字体大小,默认是 15 dp
     
    // BottomBar
    bottomBarBackgroundColor: String // BottomNavigationBar 背景,默认值是 #FFFFFF
    bottomBarShadow: Drawable        // BottomNavigationBar 阴影图片,仅对4.4 以下版本生效 
    bottomBarActiveColor: String     // BottomNavigationTab 选中效果,默认取 colorAccent 的值
    bottomBarInactiveColor: String   // BottomNavigationTab 未选中效果,默认是灰色
}

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

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

设置状态栏

状态栏的设置支持 4.4 以上系统。

设置方式非常简单,只需要有选择地重写 AwesomeFragmet 中的方法即可。

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

    默认的返回值是全局样式的 style.getStatusBarStyle()

    BarStyle 是个枚举,有两个值。LightContent 表示状态栏文字是白色,如果你想把状态栏文字变成黑色,你需要使用 DarkContent

    仅对 6.0 以上版本以及小米、魅族生效

  • preferredStatusBarHidden

    状态栏是否隐藏,默认是不隐藏。如果你需要隐藏状态栏,重写这个方法,把返回值改为 true 即可。

  • preferredStatusBarColor

    状态栏的颜色,默认是全局样式 style.getStatusBarColor(),如果某个页面比较特殊,重写该方法,返回期待的颜色值即可。

  • preferredStatusBarColorAnimated

    当状态栏的颜色由其它颜色转变成当前页面所期待的颜色时,需不需要对颜色做过渡动画,默认是 true,使得过渡更自然。如果过渡到某个界面状态栏出现闪烁,你需要在目标页面关闭它。参考 demo 中 TopDialogFragment 这个类。

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

开启沉浸式

这里的沉浸式是指页面的内容延伸到 statusBar 底下

只需要调用 setStatusBarTranslucent(boolean translucent) 即可开关沉浸式,AwesomeActivity 和 AwesomeFragment 都有这个方法,这个方法会影响整个 Activity 中所有的 Fragment,请慎重使用。

AwesomeFragment 中有一个 onStatusBarTranslucentChanged(boolean translucent) 方法,你可以在这里处理开关沉浸式所要做的适配工作。

你也可以通过 isStatusBarTranslucent 来判断是否开启了沉浸式。

我们的 demo 在 MainActivity 中开启了沉浸式,你可以在 CustomStatusBarFragment 这个界面开关沉浸式

statusbar_custom

设置 Toolbar

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

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

  • setTitle

    设置 Toolbar 标题

  • setLeftBarButtonItem

    设置 Toolbar 左侧按钮

  • setLeftBarButtonItems

    为左侧设置多个按钮时,使用此方法

  • setRightBarButtonItem

    设置 Toolbar 右侧按钮,

  • setRightBarButtonItems

    为右侧设置多个按钮时,使用此方法

    当然,你也可以设置 Menu

    Menu menu = getToolbar().getMenu();
    MenuItem menuItem = menu.add(title);
    menuItem.setIcon(icon);
    menuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
    menuItem.setOnMenuItemClickListener();
    

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

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

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

protected boolean shouldHideBackButton() {
    return true;
}

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

protected boolean backInteractive() {
    return false;
}

如果你不希望自动为你创建 Toolbar, 你可以重写以下方法,并返回 null。

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

这样就不会为你创建 Toolbar 了,通过这种方式,你可以使用自定义的 Toolbar。

demo 中,CoordinatorFragment 和 ViewPagerFragment 就使用了自定义的 Toolbar。

如果开启了沉浸式,那么需要使用 appendStatusBarPadding 这个方法来给恰当的 view 添加 padding,请参考上面说到的那两个类。

使用 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 就是你放在 aseets/fonts 文件夹中的字体文件名,但不包括后缀。size 是字体大小,如 24,color 是字体颜色,可选,只支持 RRGGBB 格式。

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

代码规范

  • onActivityCreated 中配置和 Toolbar 相关的东西,比如设置标题、按钮。

  • 永远通过以下方式来获取 arguments, 否则后果很严重

    Bundle args = FragmentHelper.getArguments(fragment);
    

项目地址:https://github.com/listenzz/AndroidNavigation