分析代码,首先查看Androidmanifest.xml文件中的启动activity,如下所示:
<activity android:name="ApiDemos">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
通过名为"LAUNCHER"的category,可以知道Apidemo首先启动的activity是ApiDemos,我们就从ApiDemos.java开始分析。通过在模拟器中运行ApiDemos,可以看到ApiDemos包含了许多单独的实例,那么程序是如何组织起这些实例的呢?通过下面对具体代码的分析,我们可以发现实际上ApiDemos.java中提供了一个统一的管理界面,这个管理界面包含了一个树形结构的目录,这个树形结构的目录就是通过ListActivity实现的。
此处结合着网上一些人对ApiDemos.java文件的解析,为代码添加进去了自己的注释。如下所示:
package com.example.android.apis;
import android.app.ListActivity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Bundle;
import android.view.View;
import android.widget.ListView;
import android.widget.SimpleAdapter;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/*
* ApiDemos类通过解析AnadroidManifest.xml文件中注册的各个Activity的label信息,生成应用程序的各级显示目录。
* label信息约定为此Activity的目录信息,例如DialogActivity的label的信息为“@string/activity_dialog”,
* 即为“App/Activity/Dialog”。也就是想要运行DialogActivity,需要先进入第一级目录的App,然后第二级目录的
* Activity,最后点击第三级目录的Dialog,就会出现DialogActivity的运行效果。
*/
public class ApiDemos extends ListActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//获取启动此Activity的Intent
Intent intent = getIntent();
//获取到Intent中附带的path信息,在第一级目录中此path信息为空
String path = intent.getStringExtra("com.example.android.apis.Path");
if (path == null) {
path = "";
}
/*
* 此处并没有使用setContentView()函数来加载一个layout文件,而是使用setListAdapter(ListAdapter)
* 函数自动的加载一个ListView的显示。ListAdapter可以使用的子类包括:arrayAdapter, SimpleAdapter,
* CursorAdapter。此处新建了一个SimpleAdapter,其list数据源data通过getData()函数获得。
*/
setListAdapter(new SimpleAdapter(this, getData(path),
android.R.layout.simple_list_item_1, new String[] { "title" },
new int[] { android.R.id.text1 }));
/*
* 相当于“ListView lv = getListView(); lv.setTextFilterEnabled(true);”。
* 为ListView添加过滤功能,这一在某一级目录下根据用户输入的前几个字母,只显示过滤后相匹配的列表项。
*/
getListView().setTextFilterEnabled(true);
}
// 该方法用于获取当前所在的目录信息,并生成SimpleAdapter中list项。
protected List<Map<String, Object>> getData(String prefix) {
//新建一个名为myData的List,用于返回。
List<Map<String, Object>> myData = new ArrayList<Map<String, Object>>();
//新建一个action为MAIN的Intent
Intent mainIntent = new Intent(Intent.ACTION_MAIN, null);
//限定Intent的category为SAMPLE_CODE。这就限定了该Intent只能跳向符合这两项的Activity。
mainIntent.addCategory(Intent.CATEGORY_SAMPLE_CODE);
//建立一个包含全局包信息的PackageManager实例pm
PackageManager pm = getPackageManager();
//获取到所有可以由mainIntent跳转的Activity信息,返回值为List<ResolveInfo>
List<ResolveInfo> list = pm.queryIntentActivities(mainIntent, 0);
if (null == list){
return myData;
}
// 字符串数组prefixPath用于存放目录中去除“/”后的各个目录名字,
String[] prefixPath;
//字符串prefixWithSlash用于存放目录名
String prefixWithSlash = prefix;
if (prefix.equals("")) {
//只有在第一次进去的时候才会prefix才为空
prefixPath = null;
} else {
prefixPath = prefix.split("/");
prefixWithSlash = prefix + "/";
}
int len = list.size();
Map<String, Boolean> entries = new HashMap<String, Boolean>();
for (int i = 0; i < len; i++) {
ResolveInfo info = list.get(i);
/*
* 获取Activity的label信息,例如App/Activity/Hello World。
* 如果Activity没有label信息,返回info.activityInfo.name
*/
CharSequence labelSeq = info.loadLabel(pm);
//当labelSeq不为空的时候,将其转化为String类型赋值给label
String label = labelSeq != null
? labelSeq.toString()
: info.activityInfo.name;
//当前的prefixWithSlash长度是否为0,或者label是以prefixWithSlash开头的
if (prefixWithSlash.length() == 0 || label.startsWith(prefixWithSlash)) {
// 例如labelPath = [App, Activity, Hello World]
String[] labelPath = label.split("/");
/*
* 第一次运行,在第一级目录下,prefixPath为空,nextLabel=labelPath[0]为第一级目录下各个标签,
* 包括:App/Preference/Content/OS/Animation/Views/Graphics/Media/Telephony/Text/NFC
* nextLabel为当前可以看到的所有目录选项
*/
String nextLabel = prefixPath == null ? labelPath[0] : labelPath[prefixPath.length];
/*
* 此分支运行的条件是prefixPath为空,或者不为空的时候prefixPath.length=labelPath.length-1。
* 前者代表当前是第一级目录,后者代表当前所在的目录没有子目录,直接对应的是一个Activity。
*/
if ((prefixPath != null ? prefixPath.length : 0) == labelPath.length - 1) {
addItem(myData, nextLabel, activityIntent(
info.activityInfo.applicationInfo.packageName,
info.activityInfo.name));
} else {
/*
* 此分支运行的条件是entries列表中没有nextLabel,例如当前位于App目录,第一次nextLabel=Activity会运行到
* 此分支,下一次运行发生在App/Activity/*检查完,开始检查App/Fragment目录的时候。
*/
if (entries.get(nextLabel) == null) {
/*
* addItem将当前目录下可以看到的目录选项(nextLabel),和其对应的Intent对象添加到myData列表中
* 在第一级目录下,prefix为空,传给browseIntent的path为nextLabel,在第二级目录,比如说App目录下,
* nextLabel=Activity对应的path为App/Activity,此值即为进入到Activity目录后的prefix值
*/
addItem(myData, nextLabel, browseIntent(prefix.equals("") ? nextLabel : prefix + "/" + nextLabel));
entries.put(nextLabel, true);
}
}
}
}
// 排序
Collections.sort(myData, sDisplayNameComparator);
return myData;
}
private final static Comparator<Map<String, Object>> sDisplayNameComparator =
new Comparator<Map<String, Object>>() {
private final Collator collator = Collator.getInstance();
public int compare(Map<String, Object> map1, Map<String, Object> map2) {
return collator.compare(map1.get("title"), map2.get("title"));
}
};
/*
* 生成一个跳转到一个其他Activity的Intent,Activity的包名为info.activityInfo.name
*/
protected Intent activityIntent(String pkg, String componentName) {
Intent result = new Intent();
result.setClassName(pkg, componentName);
return result;
}
/*
* 生成一个跳转到自身的Intent,并更新path信息
*/
protected Intent browseIntent(String path) {
Intent result = new Intent();
//可以看到Intent设定的跳转类还是ApiDemos,跳转之后还是执行ApiDemos
result.setClass(this, ApiDemos.class);
//虽然还在ApiDemos执行,但是需要更新path信息
result.putExtra("com.example.android.apis.Path", path);
return result;
}
/*
* 将符合条件的选项加入List中
*/
protected void addItem(List<Map<String, Object>> data, String name, Intent intent) {
Map<String, Object> temp = new HashMap<String, Object>();
temp.put("title", name);
temp.put("intent", intent);
data.add(temp);
}
@Override
@SuppressWarnings("unchecked")
protected void onListItemClick(ListView l, View v, int position, long id) {
Map<String, Object> map = (Map<String, Object>)l.getItemAtPosition(position);
//addItem中添加的Intent标记为“intent”
Intent intent = (Intent) map.get("intent");
//跳转的intent标记的Activity,可能是ApiDemos自身或者是其他功能演示的Activity
startActivity(intent);
}
}
通过上面的分析可以看出来,ApiDemos中的Activity跳转有两个分支。如果当前的某一个子目录下还有子目录时,Activity跳转到自身;如果当前的某个子目录已经没有子目录,则跳转到用来演示某一功能的Activity。这也就是ApiDemos用来管理如此多Activity的机制。
ApiDemos中的目录信息可以分为两类:一类是label,包括label,labelPath, nextLabel;另外一类是prefix,包括prefix, prefixPath, prefixWithSlash。label是经过滤的用户数据包信息,通过getPackageManager()函数获得。prefix是上一次执行ApiDemos时存留的path信息,通过Intent传递过来。感觉每次执行ApiDemos都要运行一下getPackageManager()获取当前的数据包,有些浪费,能不能只在第一次启动应用程序的时候做一次label获取和过滤,以后直接读取就可以了。
1.第一级目录
for循环中,第一次运行,首先获取到label为App/Activity/HelloWorld。此时各个变量值为:
label=App/Activity/HelloWorld;
labelPath=[App,Activity, Hello World];
prefix=null;
prefixPath=null;
nextLabel=App;
这个时候由于entries为空,所以运行(entries.get(nextLabel)==null)分支,将nextLabel的值放入entries列表中。Entries列表中就存在了“<App,true>”这样的项目。分支结构中,还需要在map表myData中添加当前的nextLabel和其对应的Intent。browseIntent()函数用于获得此Intent,并将path信息添加到”com.example.android.apis.Path”
此后for循环开始穷举所有以App开头的目录,由于entries列表里面已经有了App,所以跳过了(entries.get(nextLabel)== null)分支。直到label检查到Preference/7.Fragment时,再次跳转到此分支。
2.第二级目录
进入到App目录,这个时候
label=App/Activity/HelloWorld;
=[App,Activity, Hello World];
prefix=App/;
prefixPath=App;
nextLabel=App;
参考:
http://blog.youkuaiyun.com/javatiger427/article/details/6571385