分类: LINUX
2009-06-16 17:11:48
前边有几篇针对Android 1.5 SDK “cupcake” 的讨论 (, )。 Android开发小组前些天发布了”early-look” android 1.5 SDK,其目的是让大家预先了解即将在新版本中出现的新功能。而且前边也提到过,在今后会陆续针对这个版本的新功能做一些介绍,帮助大家能更好的利用Android小组成员智慧的“结晶”。
这次将与大家一同了解 AppWidget framework,这个令人期待的新平台内嵌架构可以允许开发者编写”widgets”。用户可以将它直接拖放到主窗口作为互动的接口,或者可以通过主 窗口的Widget辅助某些APPs和后台服务程序的应用,例如:显示calendar中即将开始任务、查看后台正在播放的音乐信息等。
当有新的Widgets被拖放到主窗口的同时,它们也会被分配(或者预留)一个显示更多信息内容的区域,其信息可以是由它所服务的Apps提供,用 户就可以方便快捷的通过Widget对相应的app做出一些快速的操作或回应。可以有两种方式对当前Widget提供更新,其一是通过后台的服务,根据日 程设定来对某些内容作出更新,另外一种方法是直接应用AppWidget framework预置的自动更新框架来满足基本需要(Automatic update mechanism)。(注:这里所提到的更新,并不是指程序自身的更新,而是其所显示的内容或者某些应用级别的更新)。
从较高的实现层级来分析Widget的工作原理。可以发现它其实是BroadcastReceiver,并借助相应xml与其匹配来描述更多 widget自身细节信息,从而可以使AppWidget framework借助broadcast intents与某些Widget通讯。其通讯的信息内容,是借助于RemoteViews将Layout和内容捆绑为独立的对象,Widget作为一个载体将其所包含的内容显示在主窗口。
借用现有程序,可以非常容易为其添加Widget功能。这里提供了一个用于演示其所包含的所有内容(实例所实现的功能是显示维基词典的 “Word of the day”。接下来就一些比较重要和典型的代码做一些详尽的补充和解释。
首先需要创建一个XML Metadata文件作为描述Widget属性的载体,其内容包括这个Widget在主窗口需要被保留的区域、一个初始化的layout和定义更新频率的 属性。针对主窗口预留区域的尺寸,是基于标准尺寸而根据一定比例划分的,并不能随意定义尺寸的数值。下边这个是定义区域尺寸的计算公式(Width)。
Minimum size in dip = (Number of cells * 74dip) - 2dip
在这个例子中的尺寸是二倍的标准宽度(cell width)和一个单元高度(cell height),转化为实际的dip尺寸是:146 dip × 72 dip。同时定义Widget每天更新一次(由例子的需求而定),转化为milliseconds:86,400,000。下边是完成后的XML matedata:
|
下一步,将前面设置好的XML Matedata与相应的BroadcastReceiver通过manifest.xml相关联:
|
最后,实现上一操作中提到的BroadcastReceiver,由它来实际的负责有关AppWidget请求,帮助Widgets来管理不同的 Broadcast事件。这里用到一个比较关键的类 APPWidgetProvider(官方暂时还没有提供详尽的文档)。另外需要着重注意的一点,当通过后台的服务来负责Widget实际的更新需求时, 其所应用到的BroadcastReceivers从属于Application Not Responding (ANR) timer,在遇到某些延迟或者通讯响应过长时,ANR会促使系统为用户提供一个选择来强行关闭当前程序。针对基于网络应用情况,通常会遇到因为网络原因 而引起的延迟问题,通过创建相应的Service来避免前边所提到的情况(ANR Timeouts)。以下是实现这个例子中BroadcastReceiver的源代码:
01.
/**
02.
* Define a simple widget that shows the Wiktionary "Word of the day." To build
03.
* an update we spawn a background {@link Service} to perform the API queries.
04.
*/
05.
public
class
WordWidget
extends
AppWidgetProvider {
06.
@Override
07.
public
void
onUpdate(Context context, AppWidgetManager appWidgetManager,
08.
int
[] appWidgetIds) {
09.
// To prevent any ANR timeouts, we perform the update in a service
10.
context.startService(
new
Intent(context, UpdateService.
class
));
11.
}
12.
13.
public
static
class
UpdateService
extends
Service {
14.
@Override
15.
public
void
onStart(Intent intent,
int
startId) {
16.
// Build the widget update for today
17.
RemoteViews updateViews = buildUpdate(
this
);
18.
19.
// Push update for this widget to the home screen
20.
ComponentName thisWidget =
new
ComponentName(
this
, WordWidget.
class
);
21.
AppWidgetManager manager = AppWidgetManager.getInstance(
this
);
22.
manager.updateAppWidget(thisWidget, updateViews);
23.
}
24.
25.
/**
26.
* Build a widget update to show the current Wiktionary
27.
* "Word of the day." Will block until the online API returns.
28.
*/
29.
public
RemoteViews buildUpdate(Context context) {
30.
// Pick out month names from resources
31.
Resources res = context.getResources();
32.
String[] monthNames = res.getStringArray(R.array.month_names);
33.
34.
// Find current month and day
35.
Time today =
new
Time();
36.
today.setToNow();
37.
38.
// Build today's page title, like "Wiktionary:Word of the day/March 21"
39.
String pageName = res.getString(R.string.template_wotd_title,
40.
monthNames[today.month], today.monthDay);
41.
RemoteViews updateViews =
null
;
42.
String pageContent =
""
;
43.
44.
try
{
45.
// Try querying the Wiktionary API for today's word
46.
SimpleWikiHelper.prepareUserAgent(context);
47.
pageContent = SimpleWikiHelper.getPageContent(pageName,
false
);
48.
}
catch
(ApiException e) {
49.
Log.e(
"WordWidget"
,
"Couldn't contact API"
, e);
50.
}
catch
(ParseException e) {
51.
Log.e(
"WordWidget"
,
"Couldn't parse API response"
, e);
52.
}
53.
54.
// Use a regular expression to parse out the word and its definition
55.
Pattern pattern = Pattern.compile(SimpleWikiHelper.WORD_OF_DAY_REGEX);
56.
Matcher matcher = pattern.matcher(pageContent);
57.
if
(matcher.find()) {
58.
// Build an update that holds the updated widget contents
59.
updateViews =
new
RemoteViews(context.getPackageName(), R.layout.widget_word);
60.
61.
String wordTitle = matcher.group(
1
);
62.
updateViews.setTextViewText(R.id.word_title, wordTitle);
63.
updateViews.setTextViewText(R.id.word_type, matcher.group(
2
));
64.
updateViews.setTextViewText(R.id.definition, matcher.group(
3
).trim());
65.
66.
// When user clicks on widget, launch to Wiktionary definition page
67.
String definePage = res.getString(R.string.template_define_url,
68.
Uri.encode(wordTitle));
69.
Intent defineIntent =
new
Intent(Intent.ACTION_VIEW, Uri.parse(definePage));
70.
PendingIntent pendingIntent = PendingIntent.getActivity(context,
71.
0
/* no requestCode */
, defineIntent,
0
/* no flags */
);
72.
updateViews.setOnClickPendingIntent(R.id.widget, pendingIntent);
73.
74.
}
else
{
75.
// Didn't find word of day, so show error message
76.
updateViews =
new
RemoteViews(context.getPackageName(), R.layout.widget_message);
77.
CharSequence errorMessage = context.getText(R.string.widget_error);
78.
updateViews.setTextViewText(R.id.message, errorMessage);
79.
}
80.
return
updateViews;
81.
}
82.
83.
@Override
84.
public
IBinder onBind(Intent intent) {
85.
// We don't need to bind to this service
86.
return
null
;
87.
}
88.
}
89.
}
回顾文章所应用的例子,如果Widget在特定的条件下需要更新内容时,将通过一个在线的API接口取得最新的数据,然后通过APPWidget framework自动根据我们的需要来请求更新。正如将这个Widget拖放到Home Screen中,每天都会自动的加载最新的”word of the day”。
有关系统资源合理应用的提醒:如果Widgets被设计的过于频繁更新其内容,将会吃掉非常多的系统电量资源,所以为你的Widget更新提供一个 优化的更新周期是非常必要的。可以为用户提供一个可以自由设定更新周期的接口,这样就可以让用户根据当前设备状况来及时作出调整,另外可以让程序根据来自动调整更新周期。
我们期待着你所创造出方便用户使用的Widget,或者提供一些惊奇的想法和大家来共同实现。