首先我们来回忆一下传统用Activity进行的页面切换,activity之间切换,首先需要新建intent对象,给该对象设置一些必须的参数,然后调用startActivity方法进行页面跳转。如果需要activity返回结果,则调用startActivityForResult方法,在onActivityResult方法中获得返回结果。此外,每一个要展示的activity需要在AndroidManifest.xml文件中注册。而且,如果在某些特定的情况下(比如65536方法数爆炸)要动态加载dex,还得手动管理activity的生命周期。那么,有没有这么一种方法进行页面切换时,无需在AndroidManifest.xml文件中声明这些信息,动态加载时又无需我们管理生命周期,等等优点呢。
我们来回忆一下,在android3.0之后,谷歌出了一个Fragment,这个东西依赖于activity,其生命周期由宿主activity进行管理,并且可以通过FragmentManager和FragmentTransaction等相关的类进行管理。那么我们能不能从Fragment入手,打造一个完全由Fragment组成的页面跳转框架呢。
使用Fragment其实很简单,首先开启一个事务,通过add,replace,remove等方法进行添加,替换,移除等操作,这一切的操作可能需要依赖一个容器,这个容器提供一个id,进行对应操作时将这个id作为参数传入。之后通过相应方法提交事务就可以了,就像这样子。
1
2
3
4
|
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.replace(R.id.fragment_container, fragment);
fragmentTransaction.commit();
|
然而我相信你一定有这样的经历,在使用Fragment进行页面切换时又得不断用代码控制其显示与隐藏的逻辑,那么有没有这样一种方法在程序中不断复用这段代码呢?
首先,我们希望Fragment能像Activity那样,进行正确的跳转。那么需要什么,答案是Fragment对象,我们肯定需要它的Class全类名,当然跳转的时候可能会带上一些参数,这个参数应该通过Bundle进行传递。而且,全类名可能太长,不便记忆,我们参考web的架构,应该还要取一个别名alias。就这样,一个Fragment页面的三个基本属性就被我们抽取出来了,组成了如下的实体类。在这个实体类中,页面传递的参数为json形式的String字符串对象,在需要使用的时候我们通过该json构造出bundle。页面名变量mName是整个程序唯一标示该页面的参数,其值唯一,但是其对应的class全类名可以不唯一,也就是说从name到class的映射可以一对多。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
public
class
CorePage
implements
Serializable {
private
static
final
long
serialVersionUID = 3736359137726536495L;
private
String mName;
//页面名
private
String mClazz;
//页面class
private
String mParams;
//传入参数,json object结构
public
CorePage(String name, String clazz, String params) {
mName = name;
mClazz = clazz;
mParams = params;
}
public
String getClazz() {
return
mClazz;
}
public
void
setClazz(String clazz) {
mClazz = clazz;
}
public
String getName() {
return
mName;
}
public
void
setName(String name) {
mName = name;
}
public
String getParams() {
return
mParams;
}
public
void
setParams(String params) {
mParams = params;
}
@Override
public
String toString() {
return
"Page{"
+
"mName='"
+ mName + '\
''
+
", mClazz='"
+ mClazz + '\
''
+
", mParams='"
+ mParams + '\
''
+
'}'
;
}
}
|
实体类编写好了,为了更方便的进行页面跳转,我们需要像Activity那样,有一个配置文件,里面存着Fragment名到其全类名的映射关系。那么这些数据存在哪呢。我们参考网络数据,一般从网络上获取的数据有两种格式,一种是json,一种是xml,json由于其优点,在网络传输中被大量使用,这里,我们优先使用json,选定了json之后,就要选定一个json解析的框架,我们不使用android系统自带的,我们使用阿里的fastjson,当然你也可以使用gson或者jackson。我们的Fragment有很多,所以这个Fragment的配置文件应该是一个json数组。就像这个样子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
[
{
"name"
:
"test1"
,
"class"
:
"cn.edu.zafu.corepage.sample.TestFragment1"
,
"params"
: {
"param1"
:
"value1"
,
"param2"
:
"value2"
}
},
{
"name"
:
"test2"
,
"class"
:
"cn.edu.zafu.corepage.sample.TestFragment2"
,
"params"
:
""
}
]
|
有了这个配置,我们就要在程序进入时读取这个配置。我们将这个配置放在assets目录下,当然你也可以放在其他目录下,只有你能读取到就行,甚至你可以放在压缩包里。然而,实际情况下,这个文件不应该暴露,因为一旦暴露就存在风险。因此,大家可以采用更安全的方式存这些数据,比如把数据压缩到压缩包中,增加一个密码,而读取文件的内容的代码我们移到native中取实现,毕竟java层太容易被反编译了,而c/c++层相对来说会毕竟有难度。
这里为了简单,我们暂时放在assets目录下,那么要从assets目录中读取这个文件内容就必须有这么一个读取该目录下文件的函数,该目录在android中也算是一个比较特殊的目录了,可以通过getAssets()函数,然后获得一个输入流,将文件内容读出,然后将对应的json解析出来就可以了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
/**
* 从assets目录下读取文件
*
* @param context 上下文
* @param fileName 文件名
* @return
*/
private
String readFileFromAssets(Context context, String fileName) {
String result =
""
;
try
{
InputStreamReader inputReader =
new
InputStreamReader(context.getResources().getAssets().open(fileName));
BufferedReader bufReader =
new
BufferedReader(inputReader);
String line =
""
;
while
((line = bufReader.readLine()) !=
null
)
result += line;
}
catch
(Exception e) {
e.printStackTrace();
}
return
result;
}
|
然后根据该文件内容读取json配置。读取出来后需要将这些数据保存下来。因此要有一个数据结构来保存这个对象,存完之后还要方便取出,存取的依据应该是Fragment的表示,即前面提到的name,因此Map这个数据结构是最适合不过了。
1
2
|
private
Map<String, CorePage> mPageMap =
new
HashMap<String, CorePage>();
//保存page的map
|
将配置读取出来存进该map,读取的时候判断name和class是否为空,为空则跳过。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
/**
* 从配置文件中读取page
*/
private
void
readConfig() {
Log.d(TAG,
"readConfig from json"
);
String content = readFileFromAssets(mContext,
"page.json"
);
JSONArray jsonArray = JSON.parseArray(content);
Iterator<Object> iterator = jsonArray.iterator();
JSONObject jsonPage =
null
;
String pageName =
null
;
String pageClazz =
null
;
String pageParams =
null
;
while
(iterator.hasNext()) {
jsonPage = (JSONObject) iterator.next();
pageName = jsonPage.getString(
"name"
);
pageClazz = jsonPage.getString(
"class"
);
pageParams = jsonPage.getString(
"params"
);
if
(TextUtils.isEmpty(pageName) || TextUtils.isEmpty(pageClazz)) {
Log.d(TAG,
"page Name is null or pageClass is null"
);
return
;
}
mPageMap.put(pageName,
new
CorePage(pageName, pageClazz, pageParams));
Log.d(TAG,
"put a page:"
+ pageName);
}
Log.d(TAG,
"finished read pages,page size:"
+ mPageMap.size());
}
|
此外,除了从配置文件中读取,我们应该可以动态添加,对外提供这个函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
/**
* 新增新页面
*
* @param name 页面名
* @param clazz 页面class
* @param params 页面参数
* @return 是否新增成功
*/
public
boolean
putPage(String name, Class<?
extends
BaseFragment> clazz, Map<String, String> params) {
if
(TextUtils.isEmpty(name) || clazz ==
null
) {
Log.d(TAG,
"page Name is null or pageClass is null"
);
return
false
;
}
if
(mPageMap.containsKey(name)) {
Log.d(TAG,
"page has already put!"
);
return
false
;
}
CorePage corePage =
new
CorePage(name, clazz.getName(), buildParams(params));
Log.d(TAG,
"put a page:"
+ name);
return
true
;
}
/**
* 从hashMap中得到参数的json格式
*
* @param params 页面map形式参数
* @return json格式参数
*/
private
String buildParams(Map<String, String> params) {
if
(params ==
null
) {
return
""
;
}
String result = JSON.toJSONString(params);
Log.d(TAG,
"params:"
+ result);
return
result;
}
|
文章开头已经说了,页面跳转的参数是json形式的字符串,我们还要这么一个函数,能够根据json字符串构造出一个bundle
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
/**
* 根据page,从pageParams中获得bundle
*
* @param corePage 页面
* @return 页面的参数
*/
private
Bundle buildBundle(CorePage corePage) {
Bundle bundle =
new
Bundle();
String key =
null
;
Object value =
null
;
if
(corePage !=
null
&& corePage.getParams() !=
null
) {
JSONObject j = JSON.parseObject(corePage.getParams());
if
(j !=
null
) {
Set<String> keySet = j.keySet();
if
(keySet !=
null
) {
Iterator<String> ite = keySet.iterator();
while
(ite.hasNext()) {
key = ite.next();
value = j.get(key);
bundle.putString(key, value.toString());
}
}
}
}
return
bundle;
}
|
以上配置读取的一系列函数,构成了页面管理类CorePageManager,我们对其应用单例模式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
/**
* 跳转页面管理
* User:lizhangqu([email protected])
* Date:2015-07-22
* Time: 09:34
*/
public
class
CorePageManager {
private
volatile
static
CorePageManager mInstance =
null
;
//单例
private
Context mContext;
//Context上下文
/**
* 构造函数私有化
*/
private
CorePageManager() {
}
/**
* 获得单例
*
* @return PageManager
*/
public
static
CorePageManager getInstance() {
if
(mInstance ==
null
) {
synchronized
(CorePageManager.
class
) {
if
(mInstance ==
null
) {
mInstance =
new
CorePageManager();
}
}
}
return
mInstance;
}
/**
* 初始化配置
*
* @param context 上下文
*/
public
void
init(Context context) {
try
{
mContext = context.getApplicationContext();
readConfig();
}
catch
(Exception e) {
e.printStackTrace();
}
}
}
|
其中init函数暴露给程序入口,进行配置文件的读取。一般放在Application的子类的onCreate方法中即可。
到这里为止,基本上我们一切已经就绪了。就差如何切换了。这里,在CorePageManager类中再提供两个核心函数,用于处理页面切换。
下面这个函数是页面切换的核心函数,首先根据参数从map中拿到对应的实体类,假设存在这个页面,通过class名用反射获得该Fragment对象,调用前面写好的创建Bundle的函数得到页面参数,并与当前函数中的入参bundle进行合并。将参数设置给fragment对象,开启一个fragment事务,查找id为fragment_container的fragment容器,如果该容器已经有fragment,则隐藏它,如果该函数传递了动画参数,则添加页面切换动画,然后将反射获得的fragment对象添加到该容器中,如果需要添加到返回栈,则调用addToBackStack,最后提交事务并返回该fragment对象。
整个函数很简单,就是我们平常在activity中写的切换fragment的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
/**
* 页面跳转核心函数之一
* 打开一个fragemnt
*
* @param fragmentManager FragmentManager管理类
* @param pageName 页面名
* @param bundle 参数
* @param animations 动画类型
* @param addToBackStack 是否添加到返回栈
* @return
*/
public
Fragment openPageWithNewFragmentManager(FragmentManager fragmentManager, String pageName, Bundle bundle,
int
[] animations,
boolean
addToBackStack) {
BaseFragment fragment =
null
;
try
{
CorePage corePage =
this
.mPageMap.get(pageName);
if
(corePage ==
null
) {
Log.d(TAG,
"Page:"
+ pageName +
" is null"
);
return
null
;
}
fragment = (BaseFragment) Class.forName(corePage.getClazz()).newInstance();
Bundle pageBundle = buildBundle(corePage);
if
(bundle !=
null
) {
pageBundle.putAll(bundle);
}
fragment.setArguments(pageBundle);
fragment.setPageName(pageName);
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
if
(animations !=
null
&& animations.length >=
4
) {
fragmentTransaction.setCustomAnimations(animations[
0
], animations[
1
], animations[
2
], animations[
3
]);
}
Fragment fragmentContainer = fragmentManager.findFragmentById(R.id.fragment_container);
if
(fragmentContainer !=
null
) {
fragmentTransaction.hide(fragmentContainer);
}
fragmentTransaction.add(R.id.fragment_container, fragment, pageName);
if
(addToBackStack) {
fragmentTransaction.addToBackStack(pageName);
}
fragmentTransaction.commitAllowingStateLoss();
//fragmentTransaction.commit();
}
catch
(Exception e) {
e.printStackTrace();
Log.d(TAG,
"Fragment.error:"
+ e.getMessage());
return
null
;
}
return
fragment;
}
|
而上面这个函数中的id值在一个基础的布局中,之后的Fragment都会添加到该布局中去。我们的基类Activity也将使用这个布局,这个后续编写BaseActivity的时候会提到
1
2
3
4
5
6
7
|
<?
xml
version
=
"1.0"
encoding
=
"utf-8"
?>
<
FrameLayout
xmlns:android
=
"http://schemas.android.com/apk/res/android"
android:id
=
"@+id/fragment_container"
android:layout_width
=
"fill_parent"
android:layout_height
=
"fill_parent"
>
</
FrameLayout
>
|
此外,我们再提供一个核心函数。就是如果返回栈中存在了目标fragment,则将其弹出,否则新建fragment打开。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
/**
* 页面跳转核心函数之一
* 打开一个Fragement,如果返回栈中有则出栈,否则新建
*
* @param fragmentManager FragmentManager管理类
* @param pageName 页面别名
* @param bundle 参数
* @param animations 动画
* @return 成功跳转到的fragment
*/
public
Fragment gotoPage(FragmentManager fragmentManager, String pageName, Bundle bundle,
int
[] animations) {
Log.d(TAG,
"gotoPage:"
+ pageName);
Fragment fragment =
null
;
if
(fragmentManager !=
null
) {
fragment = fragmentManager.findFragmentByTag(pageName);
}
if
(fragment !=
null
) {
fragmentManager.popBackStackImmediate(pageName,
0
);
}
else
{
fragment =
this
.openPageWithNewFragmentManager(fragmentManager, pageName, bundle, animations,
true
);
}
return
fragment;
}
|
细心的你可能已经注意到了页面跳转函数中用到了动画,其实这个动画是一个数组,为了方便使用,我们将其封装为枚举类,提供常见的几种动画形式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
package
cn.edu.zafu.corepage.core;
/**
* 页面切换动画类别
* User:lizhangqu([email protected])
* Date:2015-07-22
* Time: 09:42
*/
public
enum
CoreAnim {
none,
/* 没有动画 */
present,
/*由下到上动画 */
slide,
/* 从左到右动画 */
fade;
|