转自我的新浪博文
一、概念
首先要区分widget和AppWidget这两个概念。
1、Widget
widget可以直译为小部件,它在Android中代表视图的概念,如TextView、Button、EditText等widget视图控件,及LinearLayout等视图布局。
2、AppWidget
AppWidget是放置在手机屏幕的桌面小组件应用,如时钟、日历、天气等组件,与一般应用程序有所不同。一般应用虽也可以以图标的形式(快捷方式)放在桌面,但必须点击运行和查看;而AppWidget一般不须点击即直观呈现其主要内容。当然,AppWidget也可以被设置为点击打开其它屏幕或应用等。
而且,AppWidget可以被定时更新,如日历每天更新,时钟每分钟更新等。当然,也可以在AppWidget的视图界面中加入类似刷新的小按钮,以进行实时更新,如天气预报。
一般在提到Widget部件或Widget程序时,指的是AppWidget;如果说到widget控件,则可能是指视图控件,如Button等。
3、操作
通过在桌面(HomeScreen)中长按,在弹出的对话框中选择AppWidget部件来进行创建;或者在应用程序列表的AppWidget程序列表中选择并长按来创建。同一个AppWidget部件可以在桌面同时创建多个。每新建一个,实际上是生成了一个新的AppWidget实例。
要删除桌面的Widget部件,只需长按并拖动到垃圾箱即可。
二、一个简单的AppWidget应用
1、简单AppWidget组成
一个简单的AppWidget应用只需包括以下部分:
AppWidgetProviderInfo对象:
这个对象为AppWidget提供元数据,包括布局、更新频率等信息,这个对象定义在xml文件中,不需要自己编写,由系统根据XML文件生成。
AppWidgetProvider类:
如图所示,AppWidgetProvider类,继承自BroadcastReceiver,可以接收并处理广播事件。这个类定义了AppWidget的基本生命周期函数:
onReceive(Context, Intent) 接收广播事件。
onUpdate(Context , AppWidgetManager, int[] appWidgetIds) 到达指定的更新时间或用户向桌面添加widget时调用;实际是接受并处理“android.appwidget.action.APPWIDGET_UPDATE”广播事件。appWidgetIds保存着已创建的各(桌面)AppWidget实例编号。在onUpdate方法中可以依次更新所有实例的界面内容。
onEnabled(Context) 当AppWidget实例第一次被创建时调用
onDeleted(Context, int[] appWidgetIds) 当一个AppWidget实例被删除时调用
onDisabled(Context) 当最后一个AppWidget实例被删除时调用
2、一个简单应用开发
该应用很简单,只是在桌面显示一行文字。
(1)应用的界面布局文件res/layout/appwidget_provider_layout.xml:
(2)应用的元数据定义文件res/xml/appwidget_provider.xml:
(3)Widget实例提供程序SimpleWidgetProvider.java文件:
package com.example.simpleappwidget;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.util.Log;
import android.widget.RemoteViews;
import com.example.simpleappwidget.R;
public class SimpleWidgetProvider extends AppWidgetProvider {
private String TAG = "widgetexample";
周期更新时调用
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds)
{
final int N = appWidgetIds.length;
Log.i(TAG,String.valueOf(N));
for (int i = 0; i < N; i++)
{
int appWidgetId = appWidgetIds[i];
String message = "目前有"+N+"个AppWidget实例";
构建RemoteViews对象来对桌面部件进行更新
RemoteViews views = new RemoteViews(context.getPackageName(),
R.layout.appwidget_provider_layout);
更新文本内容,指定布局的组件
views.setTextViewText(R.id.appwidget_text, message);
将RemoteViews的更新传入AppWidget进行更新
appWidgetManager.updateAppWidget(appWidgetId, views);
}
}
}
该应用比较简单,只是为了说明程序的结构。Provider类中仅提供了更新事件处理方法。运行界面如下图:
可以创建多个实例:
但并没有如估计的显示“目前有2个实例”。
关闭模拟器并重新启动打开,才显示有两个实例:
示例程序下载
问题解决:前面更新多个实例的问题,后面通过将以实例ID为参数逐一修改Widget组件实例的以下方法:
appWidgetManager.updateAppWidget(appWidgetId, views);
改为调用组件管理器的修改所有小组件实例的方法:
ComponentName myComponentName = new ComponentName(context, SimpleWidgetProvider.class);
appWidgetManager.updateAppWidget(myComponentName,views);
3、为AppWidget程序添加按钮事件处理
天气预报等桌面组件有类似功能,即点击一个小图标(按钮)刷新数据显示。这里只是简单模拟一下类似功能。
点击按钮刷新数据,可以有多种方式实现。如点击按钮打开一个Activity、点击发送广播消息、点击启动一个服务等。下面首先看一下点击打开Activity的关键代码实现:
(1)按钮事件处理可以在SimpleWidgetProvider类的onUpdate方法中实现:
......
for (int i = 0; i < N; i++)
{
int appWidgetId = appWidgetIds[i];
String message = "目前有"+N+"个AppWidget实例";
RemoteViews views = new RemoteViews(context.getPackageName(),
R.layout.appwidget_provider_layout);
views.setTextViewText(R.id.appwidget_text, message);
为按钮绑定点击事件处理器
Intent intent = new Intent(context, MyActivity.class);
intent.putExtra("appWidgetId", appWidgetId);
Log.i(TAG,"ID:"+(intent.getExtras()).getInt("appWidgetId"));
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent,
PendingIntent.FLAG_CANCEL_CURRENT);
views.setOnClickPendingIntent(R.id.mybutton, pendingIntent);
将RemoteViews的更新传入AppWidget进行更新
appWidgetManager.updateAppWidget(appWidgetId, views);
}
......
需要指出的是如图所示的PendingIntent的几个常量值(用于getActivity等方法的参数):
这里因为Intent带有数据,使用了PendingIntent.FLAG_CANCEL_CURRENT。
(2)MyActivity类的代码:
......
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_my);
Bundle bundle = getIntent().getExtras();
int appWidgetID = bundle.getInt("appWidgetId");
Log.i(TAG,"another ID:"+appWidgetID);
final Context context = this;
RemoteViews views = new RemoteViews(context.getPackageName(),
R.layout.appwidget_provider_layout);
更新文本内容,指定布局的组件
views.setTextViewText(R.id.appwidget_text, "点击按钮更新内容");
取得AppWidgetManager实例
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
appWidgetManager.updateAppWidget(appWidgetID, views);
this.finish();
}
......
示例程序源码下载
上述功能也可以通过广播消息进行处理。因为AppWidgetProvider本身就继承自BroadcastReceiver,所以可以在SimpleWidgetProvider类的onReceive方法中实现对自定义消息的处理。关键代码如下:
(1)SimpleWidgetProvider类代码:
......
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds)
{
final int N = appWidgetIds.length;
Log.i(TAG,String.valueOf(N));
for (int i = 0; i < N; i++)
{
int appWidgetId = appWidgetIds[i];
......
Intent intent = new Intent("update_appwidget_textview");
intent.putExtra("appWidgetId", appWidgetId);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent,
PendingIntent.FLAG_CANCEL_CURRENT);
点击按钮将触发广播,当前接收器将即时接收和处理广播消息
views.setOnClickPendingIntent(R.id.mybutton, pendingIntent);
appWidgetManager.updateAppWidget(appWidgetId, views);
}
}
......
@Override
public void onReceive(Context context, Intent intent)
{
String action = intent.getAction();
if(action.equals("update_appwidget_textview"))
{
Log.i(TAG,"update_appwidget_textview");
RemoteViews views = new RemoteViews(context.getPackageName(),
R.layout.appwidget_provider_layout);
views.setTextViewText(R.id.appwidget_text, "点击按钮更新内容");
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
int appWidgetId = (intent.getExtras()).getInt("appWidgetId");
appWidgetManager.updateAppWidget(appWidgetId, views);
}
else
super.onReceive(context, intent);
}
......
(2)AndroidManifest.xml文件:
(3)应用的界面布局文件res/layout/appwidget_provider_layout.xml:
(4)应用的元数据定义文件res/xml/appwidget_provider.xml:
示例程序代码下载
另外,从资料中还查到一种利用ComponentName类修改AppWidget实例的方法,只需对上面代码稍加改动:
onUpdate方法:
for (int i = 0; i < N; i++)
{
为了看到每次调用该方法时内容的变化
String message = System.currentTimeMillis()+"";
RemoteViews views = new RemoteViews(context.getPackageName(),
R.layout.appwidget_provider_layout);
views.setTextViewText(R.id.appwidget_text, message);
......
Intent intent = new Intent("update_appwidget_textview");
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent,
PendingIntent.FLAG_CANCEL_CURRENT);
views.setOnClickPendingIntent(R.id.mybutton, pendingIntent);
appWidgetManager.updateAppWidget(appWidgetId, views);
}
onReceive方法:
......
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
ComponentName componentName = new ComponentName(context, SimpleWidgetProvider.class);
appWidgetManager.updateAppWidget(componentName, views);
......
使用广播消息处理的方式当然也可以另外创建一个接收器,不再具体分析。示例程序下载
使用本地Service服务更新Widget实例的代码下载
4、一个较实用的例子
例子比较简单,只是在HomeScreen桌面实时显示时间。
(1)SimpleWidgetProvider类关键代码(onUpdate方法):
......
int appWidgetId = appWidgetIds[i];
Intent intent = new Intent("com.example.updatetime");
intent.putExtra("appWidgetId", appWidgetId);
context.startService(intent);
......
(2)ExampleService类代码:
......
static int appWidgetId;
static RemoteViews views;
static AppWidgetManager appWidgetManager;
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
views = new RemoteViews(this.getPackageName(),
R.layout.appwidget_provider_layout);
appWidgetManager = AppWidgetManager.getInstance(this);
appWidgetId = (intent.getExtras()).getInt("appWidgetId");
new TimeThread().start();
return super.onStartCommand(intent, flags, startId);
}
class TimeThread extends Thread
{
@Override
public void run ()
{
do{
try
{
Thread.sleep(1000);
views.setTextViewText(R.id.appwidget_text, DateFormat.format("hh:mm:ss",
System.currentTimeMillis()));
appWidgetManager.updateAppWidget(appWidgetId, views);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
} while(true);
}
}
......
桌面显示时钟的示例程序下载
该程序的更有效的代码
(5)时钟显示程序的另一种解决方法
主要变化是使用android.content.Intent.ACTION_TIME_TICK时钟服务,主要代码如下:
SimpleWidgetProvider类:
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds)
{
final int N = appWidgetIds.length;
Log.i(TAG,String.valueOf(N));
String message = N+"个Widget实例";
RemoteViews views = new RemoteViews(context.getPackageName(),
R.layout.appwidget_provider_layout);
views.setTextViewText(R.id.appwidget_text, message);
MyReceiver myReceiver = new MyReceiver();
IntentFilter myFilter = new IntentFilter();
myFilter.addAction(android.content.Intent.ACTION_TIME_TICK);
context.getApplicationContext().registerReceiver(myReceiver, myFilter);
ComponentName myComponentName = new ComponentName(context, SimpleWidgetProvider.class);
appWidgetManager.updateAppWidget(myComponentName,views);
}
public static void updateWidget(Context context,RemoteViews views)
{
ComponentName myComponentName = new ComponentName(context, SimpleWidgetProvider.class);
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
appWidgetManager.updateAppWidget(myComponentName,views);
}
MyReceiver类:
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if(action.equals("android.intent.action.TIME_TICK"))
{
RemoteViews views = new RemoteViews(context.getPackageName(),
R.layout.appwidget_provider_layout);
views.setTextViewText(R.id.appwidget_text, DateFormat.format("hh:mm",
System.currentTimeMillis()));
SimpleWidgetProvider.updateWidget(context, views);
}
}
示例程序源码下载
需要说明的是ACTION_TIME_TICK这个广播消息是系统时钟消息,该消息只能由系统以每分钟一次的形式发送。不能在自定义类中通过sendbroadcast方法发出,否则会抛出“Permission Denial: not allowed to send broadcast android.intent.action.TIME_TICK”异常。
而且,程序中不能通过在manifest.xml里注册的方式接收到这个广播,只能在代码里通过registerReceiver()方法注册。
SDK文档原文内容:Broadcast Action: The current time has changed. Sent every minute. You can not receive this through components declared in manifests, only by exlicitly registering for it withContext.registerReceiver().
通过测试,发现在配置文件中设置小组件更新周期不起作用。android:updatePeriodMillis="1000"设置一秒更新一次,完全没有反应。只在长按程序生成桌面组件时,重新启动模拟器后,才会调用onUpdate方法。
以上测试验证了其它资料中提到新版本的Android屏蔽了小组件更新周期设置的说法。如果需要修改组件界面,需要在程序中使用如updateAppWidget(componentName, views)语句主动更新。
参考文章:
App Widgets
Android―AppWidget技术路线
Appwidget深入 -- 按钮事件
Android之桌面组件App Widget案例
Android Service学习之本地服务
TextView显示系统时间
解析APP触发Widget实例
android.content.ReceiverCallNotAllowedException: 解决方法
android之IntentFilter的用法_Intent.ACTION_TIME_TICK在manifest.xml不起作用