转自我的新浪博文

一、概念

    首先要区分widget和AppWidget这两个概念。

1、Widget

    widget可以直译为小部件,它在Android中代表视图的概念,如TextView、Button、EditText等widget视图控件,及LinearLayout等视图布局。    

理解与应用Android桌面组件AppWidget_第1张图片

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类: 

理解与应用Android桌面组件AppWidget_第2张图片

    如图所示,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:

理解与应用Android桌面组件AppWidget_第3张图片

    (2)应用的元数据定义文件res/xml/appwidget_provider.xml:

理解与应用Android桌面组件AppWidget_第4张图片

    (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类中仅提供了更新事件处理方法。运行界面如下图:

理解与应用Android桌面组件AppWidget_第5张图片

    可以创建多个实例:

理解与应用Android桌面组件AppWidget_第6张图片

    但并没有如估计的显示“目前有2个实例”。

    关闭模拟器并重新启动打开,才显示有两个实例:

理解与应用Android桌面组件AppWidget_第7张图片


示例程序下载

 

问题解决:前面更新多个实例的问题,后面通过将以实例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等方法的参数):

理解与应用Android桌面组件AppWidget_第8张图片    这里因为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文件:     

理解与应用Android桌面组件AppWidget_第9张图片

    (3)应用的界面布局文件res/layout/appwidget_provider_layout.xml:

理解与应用Android桌面组件AppWidget_第10张图片

   (4)应用的元数据定义文件res/xml/appwidget_provider.xml:

理解与应用Android桌面组件AppWidget_第11张图片

示例程序代码下载

    另外,从资料中还查到一种利用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不起作用