史上最方便的Android页面框架XPage使用指南

视频讲解

视频链接 : https://www.bilibili.com/video/BV1eD4y1R73U

简介

XPage是一个非常方便的fragment页面框架!天下武功,唯快不破,XPage最大的特点就是快,提高开发的效率!

XPage设计的初衷是希望能做一个通用的Activity作为壳,Fragment作为页面填充展示,并且能够像Activity那样自由的切换和数据交互。

特征

  • 支持assets下“corepage.json”静态配置Fragment页面信息。
  • 支持Application中动态配置Fragment页面信息。
  • 支持通过注解@Page的方式动态自动配置页面信息。
  • 支持自定义Fragment页面信息配置。
  • 支持4种默认Fragment页面切换动画。
  • 支持Fragment页面间参数传递。
  • 支持Fragment页面属性保存。
  • 支持Fragment页面的onKeyDown、onFragmentResult等生命周期
  • 支持Fragment和Fragment页面自由跳转以及数据交互。
  • 支持导航栏通过注解的方式自动添加及设置。
  • 支持进行内存泄露监测。
  • 支持自定义TitleBar全局主题属性。
  • 支持自定义Fragment页面容器。
  • 支持自定义Activity页面容器。
  • 支持Fragment之间、activity和fragment之间的数据交互。
  • 兼容kotlin和androidx。

设计原由

当初做Android开发时每当我写一个页面,都需要创建一个Activity,并且还需要在manifest中注册一堆Activity信息,这样既不方便,而且对资源的开销也比较大。因此当时我就设想能否创造出一个通用万能的Activity容器,可以全权负责Fragment的切换展示和数据交互,只需要一行代码即可完成所有的操作,还不需要自己手动去注册,可以一键生成。

设计思路

刚开始的时候真的很难,没有什么好的思路,最初只是简单封装了一个Activity,通过传入一些key值从而获取并加载对应的fragment,类似ARouter中Fragment发现那种。其实这样做并没有解决一个容器的问题,而且页面切换也不是很灵活,不够通用,使用起来也不是很方便。

突然有一天我发现Github上有个开源项目CorePage写得非常好,完美地解决了我对一个Activity容器的问题,于是我决定仔细研究其代码,并在其基础上设计出了XPage的最初版本。

就在XPage正式投入使用的过程中,我发现还是存在不少问题的:

  • 1.对外API不够灵活,使用起来不够方便;

  • 2.每个Fragment仍需要手动注册,很麻烦;

对于API不够灵活的问题,我在之后的版本中陆续通过构造者模式设计以及Android主题属性等手段解决了。

而对于手动注册的问题,我正是借鉴了ARouter的思路,通过Android APT技术,从而实现了Fragment信息的自动注册。

解决痛点

  • 只需要一个Activity容器就可以实现多个页面的交互。

  • Fragment自由切换和数据交互。

  • 无需在manifest中注册一堆Activity信息,通过@Page注解一键自动注册。


集成指南

添加Gradle依赖

1.在项目根目录的build.gradle的 repositories 添加jitpack仓库

allprojects {
     repositories {
        ...
        maven { url "https://jitpack.io" }
    }
}

2.在dependencies添加引用

以下是版本说明,选择一个即可。

  • androidx版本:3.0.0及以上
dependencies {
  ...
  // XPage
  implementation 'com.github.xuexiangjys.XPage:xpage-lib:3.0.0'
  annotationProcessor 'com.github.xuexiangjys.XPage:xpage-compiler:3.0.0'
  // ButterKnife的sdk
  implementation 'com.jakewharton:butterknife:10.1.0'
  annotationProcessor 'com.jakewharton:butterknife-compiler:10.1.0'
}
  • support版本:2.3.0及以下
dependencies {
  ...
  // XPage
  implementation 'com.github.xuexiangjys.XPage:xpage-lib:2.3.0'
  annotationProcessor 'com.github.xuexiangjys.XPage:xpage-compiler:2.3.0'
  // ButterKnife的sdk
  implementation 'com.jakewharton:butterknife:8.4.0'
  annotationProcessor 'com.jakewharton:butterknife-compiler:8.4.0'
}

【注意】如果你使用的是kotlin,请使用如下配置:

apply plugin: 'kotlin-kapt'

dependencies {
  ...
  //XPage
  implementation 'com.github.xuexiangjys.XPage:xpage-lib:3.0.0'
  kapt 'com.github.xuexiangjys.XPage:xpage-compiler:3.0.0'
  //ButterKnife的sdk
  implementation 'com.jakewharton:butterknife:10.1.0'
  kapt 'com.jakewharton:butterknife-compiler:10.1.0'
}

3.进行moduleName注册(非必要)

defaultConfig {
    ...

    javaCompileOptions {
        annotationProcessorOptions {
            arguments = [ moduleName : project.getName() ]
        }
    }
}

【注意】:如果不注册的话,默认ModuleName为app


页面注册

方法一:Application中动态注册【推荐】

1.自动进行页面注册【推荐】

使用apt编译时自动生成的页面注册配置类 "moduleName"+PageConfig 的getPages()进行注册。

PageConfig.getInstance()
        .setPageConfiguration(new PageConfiguration() { //页面注册
            @Override
            public List<PageInfo> registerPages(Context context) {
                //自动注册页面,是编译时自动生成的,build一下就出来了。如果你还没使用@Page的话,暂时是不会生成的。
                return AppPageConfig.getInstance().getPages(); //自动注册页面
            }
        })
        .debug("PageLog")       //开启调试
        .setContainActivityClazz(XPageActivity.class) //设置默认的容器Activity
        .enableWatcher(false)   //设置是否开启内存泄露监测
        .init(this);            //初始化页面配置

【注意】:如果你的项目中只是增加了依赖,还没有使用@Page注解XPageFragment页面的话,在编译时是不会自动生成注册页面的!!

2.手动动态进行页面注册

PageConfig.getInstance()
        .setPageConfiguration(new PageConfiguration() { //页面注册
            @Override
            public List<PageInfo> registerPages(Context context) {
                List<PageInfo> pageInfos = new ArrayList<>();
                addPageInfoAndSubPages(pageInfos, MainFragment.class);
                pageInfos.add(PageConfig.getPageInfo(DateReceiveFragment.class));
                return pageInfos;        //手动注册页面
            }
        })
        .debug("PageLog")       //开启调试
        .enableWatcher(false)   //设置是否开启内存泄露监测
        .init(this);            //初始化页面配置

方法二:assets中静态注册

在assets文件夹中新建“corepage.json“,然后进行如下配置:

[
  {
    "name": "测试页面1",
    "classPath": "com.xuexiang.xpagedemo.fragment.TestFragment1",
    "params": ""
  },
  {
    "name": "测试页面2",
    "classPath": "com.xuexiang.xpagedemo.fragment.TestFragment2",
    "params": {
      "key1":"这是参数1的值",
      "key2":"这是参数2的值"
    }
  },
]

混淆配置

# fastjson
-dontwarn com.alibaba.fastjson.**
-keep class com.alibaba.fastjson.** { *; }
-keepattributes Signature

# xpage
-keep class com.xuexiang.xpage.annotation.** { *; }

基础使用

页面跳转

使用XPage,Activity必须要继承XPageActivity,Fragment必须要继承XPageFragment,否则将无法调用页面跳转的openPage方法。

页面的简单打开和关闭

  • 使用openPage即可打开页面,入参可为跳转类的类名,也可以是类的标识(@Page标注的name字段)
// 使用类名打开
openPage(TestFragment.class);
// 使用标识打开
openPage("TestFragment");
  • 使用popToBack即可关闭页面。
// 关闭当前页,返回上一页
popToBack();
// 关闭当前页并跳转至某个页面
popToBack("popBackName", null);

页面打开等待结果返回

  • 1.使用openPageForResult即可,类似Activity里的startActivityForResult
openPageForResult(TestFragment.class, null, REQUEST_CODE);
  • 2.使用setFragmentResult来设置页面关闭的返回值,类似Activity里的setResult方法。
// 设置返回的数据,类似Activity里的setResult
Intent intent = new Intent();
intent.putExtra(KEY_BACK_DATA, "==【返回的数据】==");
setFragmentResult(500, intent);
// 返回操作
popToBack();
  • 3.重写Fragment的onFragmentResult方法来接收返回的数据,类似Activity里的onActivityResult方法。
@Override
public void onFragmentResult(int requestCode, int resultCode, Intent data) {
    super.onFragmentResult(requestCode, resultCode, data);
    if (data != null) {
        Bundle extras = data.getExtras();
        XToastUtils.toast("requestCode:" + requestCode + " resultCode:" + resultCode + " data:" + extras.getString(TestFragment.KEY_BACK_DATA));
    }
}

数据传递

  • 使用openPage打开页面时,可传入Bundle作为参数。
// 设置需要传递的参数
Bundle params = new Bundle();
params.putBoolean(DateReceiveFragment.KEY_IS_NEED_BACK, false);
int id = (int) (Math.random() * 100);
params.putString(DateReceiveFragment.KEY_EVENT_NAME, "事件" + id);
params.putString(DateReceiveFragment.KEY_EVENT_DATA, "事件" + id + "携带的数据");
// 把参数传入
openPage(DateReceiveFragment.class, params);
  • 数据接收

数据接收和普通Fragment接收数据一样,使用getArguments获取传入的数据。

Bundle arguments = getArguments();
String eventName = arguments.getString(DateReceiveFragment.KEY_EVENT_NAME);
String eventData = arguments.getString(DateReceiveFragment.KEY_EVENT_DATA);

页面转场动画

页面转场动画可以动态设置,也可以静态设置。

静态设置

在我们使用@Page进行页面注册的时候,我们可以静态设置转场动画、默认参数、拓展字段等。

Page注解的属性表:

属性名 类型 默认值 备注
name String 注解类的类名 页面的名称、唯一标识符
params String[] {""} 静态设置默认参数
anim CoreAnim CoreAnim.slide 页面转场动画
extra int -1 拓展字段

动态设置

使用openPage打开页面时,可传入CoreAnim枚举设置页面转场动画。

switch(position) {
    case 0:
        openPage(TestFragment.PAGE_NAME, null, CoreAnim.none);//没有动画
        break;
    case 1:
        openPage(TestFragment.PAGE_NAME, null, CoreAnim.present);//由下到上动画
        break;
    case 2:
        openPage(TestFragment.PAGE_NAME, null, CoreAnim.slide);//从左到右动画
        break;
    case 3:
        openPage(TestFragment.PAGE_NAME, null, CoreAnim.fade);//渐变
        break;
    case 4:
        openPage(TestFragment.PAGE_NAME, null, CoreAnim.zoom);//放大
        break;
    default:
        break;
}

设置自定义转场动画

//自定义动画
openPage(TestFragment.PAGE_NAME, null, new int[]{
        // OpenEnterAnimation, 页面打开进场动画
        R.anim.custom_open_enter,
        // OpenExitAnimation, 页面打开退场动画
        R.anim.custom_open_exit,

        // CloseEnterAnimation, 页面关闭进场动画
        R.anim.custom_close_enter,
        // CloseExitAnimation, 页面关闭退场动画
        R.anim.custom_close_exit
});

进阶使用

使用PageOption进行页面操作【推荐】

使用PageOption.to进行页面选项构建。

  • setAnim: 设置页面转场动画
  • setRequestCode: 设置页面打开的请求码,用于返回结果
  • setAddToBackStack: 设置是否加入堆栈
  • setNewActivity: 设置是否使用新的Activity打开
  • setContainActivityClazz:设置新打开Activity的容器
  • putBoolean、putString、putAll等:设置传递的参数
  • open:打开页面进行跳转
PageOption.to(TestFragment.class) //跳转的fragment
    .setAnim(CoreAnim.zoom) //页面转场动画
    .setRequestCode(100) //请求码,用于返回结果
    .setAddToBackStack(true) //是否加入堆栈
    .setNewActivity(true, ContainActivity.class) //是否使用新的Activity打开
    .putBoolean(DateReceiveFragment.KEY_IS_NEED_BACK, true) //传递的参数
    .open(this); //打开页面进行跳转

自定义TitleBar样式

可以设置XPageTitleBarStyle主题样式来自定义标题栏的默认样式。

<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
    <!-- Customize your theme here. -->
    <item name="colorPrimary">@color/xpage_default_actionbar_color</item>
    <item name="colorPrimaryDark">@color/xpage_default_actionbar_color</item>
    <item name="colorAccent">@color/xpage_default_actionbar_color</item>

    <!--标题栏的背景图片,优先使用背景图片,没有背景图片才使用背景颜色,可选-->
    <item name="xpage_actionbar_background">@null</item>
    <!--标题栏的背景颜色-->
    <item name="xpage_actionbar_color">@color/xpage_default_actionbar_color</item>
    <!--是否支持沉浸式标题栏, 默认false-->
    <item name="xpage_actionbar_immersive">false</item>
    <!--标题栏返回箭头, 默认R.drawable.xpage_ic_navigation_back_white-->
    <item name="xpage_actionbar_navigation_back">@drawable/xpage_ic_navigation_back_white</item>
    <!--标题栏的高度,默认52dp-->
    <item name="xpage_actionbar_height">60dp</item>
    <!--标题栏标题文字的大小,默认18sp-->
    <item name="xpage_actionbar_title_text_size">21sp</item>
    <!--标题栏副标题文字的大小,默认12sp-->
    <item name="xpage_actionbar_sub_text_size">14sp</item>
    <!--标题栏动作文字的大小,默认15sp-->
    <item name="xpage_actionbar_action_text_size">18sp</item>
    <!--标题栏动作图片的padding,默认5dp-->
    <item name="xpage_actionbar_action_padding">6dp</item>
    <!--标题栏两侧文字的padding,默认14dp-->
    <item name="xpage_actionbar_side_text_padding">16dp</item>
    
    <item name="XPageTitleBarStyle">@style/XPageTitleBar.Custom</item>
</style>

<style name="XPageTitleBar.Custom">
    <item name="tb_immersive">false</item>
    <item name="tb_centerGravity">center</item>
</style>

利用XPage来写程序的Tab主页

详细可参见BottomNavigationViewFragment

就像正常使用ViewPager加载Fragment那样。但是这里需要注意的两点是:

  • 由于使用ViewPager进行加载,而非XPage,因此Fragment的initTitleBar方法需要被覆盖。
@Override
protected TitleBar initTitleBar() {
    //不使用@Page标注的一定要注意覆盖这个方法
    return null;
}
  • 由于为了新开页面不影响Tab主页当前容器的状态,需要在打开新页面的使用设置使用新容器。
PageOption.to(TestFragment.class)
        //新建一个容器,以不影响当前容器
        .setNewActivity(true)
        .open(this);

复杂Activity界面容器的自定义

详细可参见ComplexActivity

1.自定义页面容器的布局,在布局中一定要包含idfragment_container

<FrameLayout
    android:id="@id/fragment_container"
    android:layout_width="match_parent"
    android:layout_height="400dp">
</FrameLayout>

2.在XPageActivity中设置页面容器的布局ID

@Override
protected int getLayoutId() {
    return R.layout.activity_complex;
}

3.使用changePage方法切换Fragment。

changePage(TestFragment.PAGE_NAME, null, CoreAnim.none);

【注意】在切换Fragment的时候,fragment并不会走onResume和onPause生命周期,建议使用onHiddenChanged代替。

4.使用getPage方法获取指定的Fragment,就可以获取该fragment页面中的数据。

TabAFragment tabAFragment = getPage(TabAFragment.class);
if (tabAFragment != null) {
    ToastUtils.toast(tabAFragment.getData());
} else {
    ToastUtils.toast("页面还未加载!");
}

常见问题

1.问:我使用的是自动注册,为什么我刚接入的时候,一直报错找不到AppPageConfig?

答:首先需要明确的是,AppPageConfig是需要编译之后才会出现的,如果你没有编译的话,是肯定没有的。如果你编译了还是找不到,你可以根据如下步骤依次进行排查:

  • 排查当前项目中是否有Fragment被@Page注解了,如果没有的话,即使编译了也是不会生成AppPageConfig文件的。
  • 排查是否进行了moduleName注册,因为自动生成的注册类是根据"moduleName"+PageConfig的规则进行自动生成的,如果没有配置moduleName的话,默认才是app,这样自动生成的注册类才是AppPageConfig。如果你配置了moduleName,而且模块的名称也不是app,那么自动生成的注册类肯定不是AppPageConfig
  • 查看编译时是否有其他报错,如果在编译的过程中就报错了,那么作为apt这种编译时自动生成的注册类也是无法生成的。
  • 如果以上都没能解决你的问题,那么考虑八成是你哪里集成出错了,所以需要你回头重新阅读集成指南,不能放过每一个细节。如果还是不行,考虑直接使用简化版的Android空壳模版工程 先熟悉一下集成和使用。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,924评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,781评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,813评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,264评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,273评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,383评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,800评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,482评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,673评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,497评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,545评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,240评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,802评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,866评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,101评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,673评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,245评论 2 341