一.问题
在android的学习中我们经常需要做做一些小demo。
(1)一个demo建立一个项目:
demo多了,项目就多了,会有各种不方便。
(2)于是,建立一个demo项目来,然后,第一个Activity呢,主界面是一个Activity.里面是各个具体的Activity的入口。
这个时候我的做法是。主界面的布局是一个LinearLayout然后里面多一个具体的Demo就多一个Button入口。
布局如下:
|
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
|
<
LinearLayout
xmlns:android
=
"http://schemas.android.com/apk/res/android"
xmlns:tools
=
"http://schemas.android.com/tools"
android:id
=
"@+id/root_view"
android:layout_width
=
"match_parent"
android:layout_height
=
"match_parent"
android:orientation
=
"vertical"
>
<
Button
android:id
=
"@+id/button1"
android:layout_width
=
"match_parent"
android:layout_height
=
"wrap_content"
android:text
=
"Demo1"
/>
<
Button
android:id
=
"@+id/button2"
android:layout_width
=
"match_parent"
android:layout_height
=
"wrap_content"
android:text
=
"Demo2"
/>
<
Button
android:id
=
"@+id/button3"
android:layout_width
=
"match_parent"
android:layout_height
=
"wrap_content"
android:text
=
"Demo3"
/>
</
LinearLayout
>
|
然后,在代码里为Button注册点击事件,处理事件,启动对应具体的Demo界面。
一般来说,这样的解决方案是够了。
但是有没有简洁,更具有扩展性的解决方案呢?
这个时候,我想到了Android的ApiDemos。我想看看Android自己是怎么做的。
于是我发现了一个一个更优雅更具有技术含量,更具有Android的技术特色的解决方案:
主要是这个Activity子类:
com.example.android.apis.ApiDemos它也是一个ListActivity子类,整个类文件100多行代码。比较优雅的实现了问题的主要功能。
这是我首先要告诉大家的是,当我们运行ApiDemos时,首次运行看到的界面(我称之为根目录)是ApiDemos这个Activity,
子目录的显示也是这个ApiDemos这个Activity.
提示:其实,如果你自己单步调试下,应该很快就可以了解了。就不用再往下看了。不是吗?
我们知道,根目录显示的入口如下:
清单一:
|
1
2
3
4
5
6
7
8
9
10
11
|
/-Accessibility
-Animation
-App
-Content
-Graphics
-Media
-NFC
-OS
-Preference
-Text
-Views
|
清单二:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
/-App
-ActionBar
-Activity
-Alarm
-Alert Dialogs
-Device Admin
-Fragment
-Launcher Shortcuts
-Loader
-Menu
-Notification
-Search
-Service
-Text-To-Speech
-Voice Recognition
|
在这里,我们就有几个疑问:
(0)ApiDemos是如何知道本应用中的所有Activity的呢?
(1)ApiDemos是如何知道,当前是应该显示根目录呢?还是子目录呢?
(2)ApiDemos是如何知道当前目录应该显示哪些入口条目呢?即ApiDemos是如何确定Activity的层级关系的呢?
(3)ApiDemos是如何让点击条目进去是显示子目录呢?还是显示具体的Demo的Activity呢?
我们所有的问题,都可以从代码中找出答案:
问题(0):
我们想是不是android平台提供了某种方法让我们可以直接查询出本应用的所有Activity.
好像没有但是android为我们了Intent这样一个东西。我们可以根据某一个Intent来查询所有符合些Intent条件的Activity.
我们可以从ApiDemos的Manifest可以看到,几乎所有的除ApiDemos这个Activity之外的Activity都有如下的一个Intent-filter:
清单三:
|
1
2
3
4
|
<intent-filter>
<action android:name=
"android.intent.action.MAIN"
/>
<category android:name=
"android.intent.category.SAMPLE_CODE"
/>
</intent-filter>
|
清单四:
|
1
2
|
Intent mainIntent =
new
Intent(Intent.ACTION_MAIN,
null
);
mainIntent.addCategory(Intent.CATEGORY_SAMPLE_CODE);
|
清单五:
|
1
2
|
PackageManager pm = getPackageManager();
List<ResolveInfo> list = pm.queryIntentActivities(mainIntent,
0
);
|
到此,问题(0)基本解决。关于PackageManger后面还会讲到。这里暂时不表。
接下来要做的就是为这些Activity组织显示层级入口了。
在所有的数据中查找某一个层级的目录入口。所以都可以算是查找工作了。
关于这个确定层级,我们可以想到的一种方案是以包名的层级来确定:
对于第一层子目录来说确实是可以这样的。但是对于这二层目录就不可以了。
就比如com.example.android.apis.app就没有子包了,但是还有不实际应用中还有几个层级的子目录呢。
所以,因为代码不是我们写所以这个层级确实方案不可行。但是如果是自己写代码倒还是可行的。
但是,android采用了一种类似的,但是我觉得更麻烦的方案,就是为每一个Activity设置一个label.
这样。在label中标明了此Activity的层级:
例如:HelloWorld这个Activity的在Manifest中声明如下:
清单六:
|
1
2
3
4
5
6
7
8
|
<
activity
android:name
=
".app.HelloWorld"
android:label
=
"@string/activity_hello_world"
>
<
intent-filter
>
<
action
android:name
=
"android.intent.action.MAIN"
/>
<
category
android:name
=
"android.intent.category.SAMPLE_CODE"
/>
</
intent-filter
>
</
activity
>
|
清单七:
|
1
2
3
|
<
string
name
=
"activity_hello_world"
>
App/Activity/<
b
>Hello <
i
>World</
i
></
b
>
</
string
>
|
事实也是如此。
于是当我们点击App时,我们可以向ApiDemos传递App这样一个标签前缀。只要其它的Activity的label的前缀也是
App/那么就是App下的子目录下的项了。
下面来看看具体的代码吧:
(a)组织根目录入口显示,(即,清单一 所显示的入口列表)
根目录,特殊一点,因为标签没有前缀。
先看onCreate()方法中的代码
清单八:
|
1
2
3
4
5
6
|
Intent intent = getIntent();
String path =intent.getStringExtra(
"com.example.android.apis.Path"
);
if
(path ==
null
) {
path =
""
;
}
|
然后尝试从intent读取指定标签前缀信息。
由于是第一个启动的Activity因此没有相信信息。path也就为null.
即标签前缀为空是根目录。
然后是根据这个前缀path来获得列表要显示的数据。
清单九:
|
1
2
3
4
5
6
7
8
|
setListAdapter(
new
SimpleAdapter(
this
,
getData(path),
android.R.layout.simple_list_item_1,
new
String[] {
"title"
},
new
int
[] { android.R.id.text1 }
)
);
|
getData(path)中获得整个应用demo Activity的代码在清单四,清单五中有说明:
然后是处理activity信息的标签信息:
清单十:
|
1
2
3
4
5
6
7
8
9
|
String[] prefixPath;
String prefixWithSlash = prefix;
if
(prefix.equals(
""
)) {
prefixPath =
null
;
}
else
{
prefixPath = prefix.split(
"/"
);
prefixWithSlash = prefix +
"/"
;
}
|
例如当prefix为"App/Activity"时,
那么prefixPath就等于:数组{"App","Activity"}
prefixWithSlash为"App/Activity/"
(提示:我想我应该提醒一下,slash就是斜杠的意思。
而prefixWithSlash就是因为如果prefix为App而以此作为前缀时,
可能会与前缀为AppThis,AppThat等等前缀弄混。虽然在ApiDemos目前没有这种情况。
所以加了一个slash.因为名称是不能有slash )
但是我们的组织根目录入口时,prefix为空字符串,所以prefixPath为null.
然后是一个for循环来查找所需要根目录入口:
首先是:
获得activity信息的标签信息,如果此activity没有声明标签信息则用activity名作为标签信息:
清单十一:
|
1
2
3
4
5
|
ResolveInfo info = list.get(i);
CharSequence labelSeq = info.loadLabel(pm);
String label = ( (labelSeq !=
null
) ?
labelSeq.toString()
: info.activityInfo.name);
|
对于HelloWorld这个Activity来说其activityInfo.name为“com.example.android.apis.app.HelloWorld”
即其类的全名。对于HelloWorld来说我们知道有标签的:
于是label为:“App/Activity/Hello World”,
得到label之后,接下来是一个if语句:
清单十二:
|
1
|
if
(prefixWithSlash.length() ==
0
|| label.startsWith(prefixWithSlash))
|
prefixWithSlash.length() == 0也就是说此时是查找根目录的入口。
label.startsWith(prefixWithSlash)也就是说,查找查看prefix为前缀的子Activity。
然后是找出为下一次传递给ApiDemos的标签前缀:
清单十三:
|
1
2
3
4
|
String[] labelPath = label.split(
"/"
);
String nextLabel = prefixPath ==
null
? labelPath[
0
]
: labelPath[prefixPath.length];
|
还是以HelloWorld这个Acitivity的标签信息为例:
label为"App/Activity/Hello World"
则labelPath为{"App","Activity","Hello World"}
显然对于根目录,prefixPath为空,所以取数组第一项作为一个子目录入口项:
于是,此入口项要传递给子目录的标签前缀
nextLabel为labelPath[0],
此时如下是查找App目录下的子目录,prefixPath为{"App"},数组长度为1.
则下一个标签则为“App",到App/Activity这一个层级的话,
nextLabel为"Activity"
传递过去的应该是:prefixPath+"/"+nextLabel.
下面会有。
清单十四:
|
1
|
if
((prefixPath !=
null
? prefixPath.length :
0
) == labelPath.length -
1
)
|
还是用于浏览子目录呢?
还是以HelloWorld为例,
label为"App/Activity/Hello World"
labelPath为{"App","Activity","Hello World"} ,数组长度为3
如果此时:prefixPath为{"App","Activity"} ,数组长度为2
2 == (3-1)于是此时入口应该是打开HelloWorld这个Activity
对于们根目录来说,prefixPath为null,
此时如果一个Activity的label为例如"JustADemo"
那么 0 = 1 - 1,
这也是一个应该直接打开的具体的Demo的Activity入口。
否则就是要打开一个子目录入口的目录项了:
将此项添加进列表中:
清单十六:
|
1
2
3
4
5
6
7
|
if
(entries.get(nextLabel) ==
null
) {
String nextPrefixLabel = prefix.equals(
""
) ? nextLabel : prefix +
"/"
+ nextLabel;
addItem(myData, nextLabel, browseIntent(nextPrefixLabel));
entries.put(nextLabel,
true
);
}
|
如"App"如果已经从之前的Activity的标签中找出App这个子目录入口了,就不需要添加了。
因为使用了entries这样一个Map<String,Boolean>这样一个数据结构来判断。
如果还没有得到子目录需要传递的下一个标签前缀。然后得到将下一个目录项添加进列表中。
我们来看看browseIntent()这个方法:
清单十七:
|
1
2
3
4
5
6
|
protected
Intent browseIntent(String path) {
Intent result =
new
Intent();
result.setClass(
this
, ApiDemos.
class
);
result.putExtra(
"com.example.android.apis.Path"
, path);
return
result;
}
|
这个方法构造出一个用于打开浏览子目录项的Intent.
显然可以看出,是用ApiDemos这个Activity来浏览的。
然后是:activityIntent()
清单十八:
|
1
2
3
4
5
|
protected
Intent activityIntent(String pkg, String componentName) {
Intent result =
new
Intent();
result.setClassName(pkg, componentName);
return
result;
}
|
清单十九:
|
1
2
3
4
5
6
|
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);
}
|
组织用于List显示的item.
清单二十:
|
1
2
3
4
5
6
7
8
|
@Override
@SuppressWarnings
(
"unchecked"
)
protected
void
onListItemClick(ListView l, View v,
int
position,
long
id) {
Map<String, Object> map = (Map<String, Object>)l.getItemAtPosition(position);
Intent intent = (Intent) map.get(
"intent"
);
startActivity(intent);
}
|
当用户点击某一个列表项时,启动对应Intent的Activity.
相信如果你看到这里,对于ApiDemos应该有相当的了解了吧。前面提供了问题都有答案了吧。
其实,如果你自己单步调试下,应该很快就可以了解了。不是吗?
如果你了解了,下面分享一个具体的应用场景:android指定分享到新浪微博
再回到我们之前提到的PackageManager。
一般的分享我们是这样做的:
|
1
2
3
4
5
6
7
8
9
10
11
12
|
private
void
share() {
Intent intent =
new
Intent(Intent.ACTION_SEND);
intent.setType(
"text/plain"
);
intent.putExtra(Intent.EXTRA_SUBJECT,
"分享我的文章"
);
intent.putExtra(
Intent.EXTRA_TEXT,
"我刚发表的文章,来看看吧,地址:http://aa.bb.cc/a.html"
);
intent.putExtra(Intent.EXTRA_TITLE,
"分享我的文章"
);
String title =
"分享我的文章给好友"
;
Intent chooser = Intent.createChooser(intent, title);
startActivity(chooser);
}
|
这段代码会弹出一个可以用于分享的选择器,然后选择某一项来分享。
但是我们需要直接分享到新浪微博呢?
首先,我们可以通在安装有新浪微博的设备上运行,
|
1
2
3
4
|
Intent intent =
new
Intent(Intent.ACTION_SEND);
intent.setType(
"text/plain"
);
PackageManager pm = getPackageManager();
List<ResolveInfo> list = pm.queryIntentActivities(intent,
0
);
|
还得到所以可以用于分享的组件,然后不管是通过调试或者打印得出来,某你所想要分享的组件的信息。
如新浪微博的的包名为"com.sina.weibo"
于是可以通过判断包名,来打开指定的分享intent,代码如下:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
PackageManager pm = getPackageManager();
List<ResolveInfo> matches = pm.queryIntentActivities(intent,
PackageManager.MATCH_DEFAULT_ONLY);
String packageName =
"com.sina.weibo"
;
ResolveInfo info =
null
;
for
(ResolveInfo each : matches) {
String pkgName = each.activityInfo.applicationInfo.packageName;
if
(packageName.equals(pkgName)) {
info = each;
break
;
}
}
if
(info ==
null
) {
ToastUtils.showShort(context,
"没有找到新浪微博"
);
return
;
}
else
{
intent.setClassName(packageName, info.activityInfo.name);
}
startActivity(intent);
|
好了,到这里整个文章结束了,欢迎指出存在的各种错误与不足。
本文深入探讨了Android API Demos的实现方式,展示了如何通过查询应用内所有Activity、确定层级关系及实现目录展示的优雅解决方案。通过分析API Demos的代码,揭示了其如何通过标签系统、Intent筛选机制等技术手段,实现目录结构清晰、易于导航的多级目录展示。此外,还介绍了如何将这一高效组织方式应用于实际场景,如指定分享到新浪微博的功能实现。
2万+

被折叠的 条评论
为什么被折叠?



