关键词:Widget、AppWidgetManager、RemoteViews、Collection View Widget、RemoteViewsService、快速搜索框 QSB、LiveWallper

 

  • 创建主屏幕Widget
  • 创建基于集合的主屏幕Widget
  • 使用ContentProvider填充Widge
  • 在QuickSearchBox中显示搜索结果
  • 创建LiveWallpaper

 

主屏幕Widget

创建AppWidgets

AppWidgets作为BroadcastReceivers实现。它们使用RemoteViews来创建和更新寄存在另一个应用程序进程中的视图层次结构,大多数情况下该进程是主屏幕。

为了创建一个应用程序的Widget,我们需要建立以下三个组件:

  1. 一个定义了该WidgetUI的XML布局资源
  2. 一个描述了与该Widget相关联的元数据的XML文件
  3. 一个定义并控制该Widget的Intent接收器

App Widget Design Guidelines

 

Widget定义资源作为XML存储在项目的res/xml文件夹中。appwidget-provider标签使我们能够描述widget元数据。

<appwidget-provider
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:initialLayout="@layout/my_widget_layout"
  android:minWidth="110dp"
  android:minHeight="110dp"
  android:label="@string/widget_label"
  android:resizeMode="horizontal|vertical"
  android:previewImage="@drawable/widget_preview"
  android:updatePeriodMillis="3600000"
/>

initialLayout:创建WidgetUI时用到的布局资源

labe:在Widget:选取器中用户Widget所用到的标题

updatePeriodMillis:以毫秒为单位表示的Widget更新的最小周期。Android将会以这个速率唤醒设备以便更新用户Widget,因此应当将其指定为至少一个小时。AppWidgetManager最快不能以每30分钟一次的速率进行更新。理想情况下,用户的Widget每天使用该更新技术不应当超过一次或者两次。

configure:当将用户Widget添加到主屏幕时,可以有选择的指定启动一个完全限定的Activity。该Activity能够用于指定Widget设置和用户首选项。

icon:默认情况下,Android在Widget选取器中呈现widget时,会使用应用程序的图标。通过制定一个Drawable资源,可以使用一个不同的图标。

previewImage:Android3.0中引入了一个新的AppWidget选取器,用于显示Widget的预览,而不是显示其图标。这里指定的Drawable资源应该能够精确地表现出Widget在添加到主屏幕时的样子。

 

创建Widget Broadcast Receiver并将其添加到应用程序的manifest文件中

每一个Widget的Broadcast Receiver都指定Intent Filter,用于监听使用AppWidget不同动作请求更新的Broadcast Intent。

Receiving App Widget broadcast Intents

AppWidgetProvider is just a convenience class. If you would like to receive the App Widget broadcasts directly, you can implement your ownBroadcastReceiver or override the onReceive(Context, Intent) callback. The Intents you need to care about are as follows:

  • ACTION_APPWIDGET_UPDATE
  • ACTION_APPWIDGET_DELETED
  • ACTION_APPWIDGET_ENABLED
  • ACTION_APPWIDGET_DISABLED
  • ACTION_APPWIDGET_OPTIONS_CHANGED

以下示例显示了一个扩展AppWidgetProvider的Widget实现的基本代码。

public class SkeletonAppWidget extends AppWidgetProvider {
  @Override
  public void onUpdate(Context context,
                       AppWidgetManager appWidgetManager,
                       int[] appWidgetIds) {
    // 更新Widget UI
  }

  @Override
  public void onDeleted(Context context, int[] appWidgetIds) {
    // 处理删除Widget的操作
    super.onDeleted(context, appWidgetIds);
  }

  @Override
  public void onDisabled(Context context) {
    // Widget已被禁用
    super.onDisabled(context);
  }

  @Override
  public void onEnabled(Context context) {
    // Widget已被启用
    super.onEnabled(context);
  }
}

Widget必须被添加到应用程序manifest文件中,像其他Broadcast Receiver一样使用一个receiver标签。

为了将一个Broadcast Receiver指定为一个AppWidget,需要下面两个标签添加到它的manifest文件中。

  1. 一个用于android.appwidget.action.APPWIDGET_UPDATE动作的Intent.Filter。
  2. 一个对appwidget-provider元数据XML资源的引用,该XML资源描述了用户的Widget设置。
<receiver android:name=".MyAppWidget" android:label="@string/widget_label">
  <intent-filter>
    <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
  </intent-filter>
  <meta-data android:name="android.appwidget.provider" android:resource="@xml/widget_provider_info"/>
</receiver>

 

AppWidgetManager和RemoteViews简介

AppWidgetManager类用于更新AppWidget和提供AppWidget的相关信息。

RemoteViews类用作在另一个应用程序的进程中托管View的方法。例如,你的AppWidget使用的UI托管在它们自己的进程中(通常是主屏幕)中。要从运行在你的应用程序进程中的App Widget Provider修改这些View,需要使用RemoteViews。

 

创建和操作RemoteView

为创建一个新的RemoteView对象,需要将应用程序的包名和想要操纵的布局资源传入RemoteViews构造函数中。

RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.my_widget_layout);

RemoteViews

使用RemoteView向AppWidget中的View应用方法。

// 设置ImageView的image level
views.setInt(R.id.widget_image_view, "setImageLevel", 2);
// 显示TextView的光标
views.setBoolean(R.id.widget_text_view, "setCursorVisible", true);
// 将一个位图分配给一个ImageButton
views.setBitmap(R.id.widget_image_button, "setImageBitmap", myBitmap);

在一个AppWidget远程View中修改View属性

// 更新一个TextView
    views.setTextViewText(R.id.widget_text, "Updated Text");
    views.setTextColor(R.id.widget_text, Color.BLUE);
    // 更新一个ImageView
    views.setImageViewResource(R.id.widget_image, R.drawable.icon);
    // 更新一个ProgressBar
    views.setProgressBar(R.id.widget_progressbar, 100, 50, false);
    // 更新一个Chronometer
    views.setChronometer(R.id.widget_chronometer, SystemClock.elapsedRealtime(), null, true);

 

将RemoteViews应用到运行中的AppWidget

要将对RemoteViews所作的修改应用到处于活动状态的Widget,需要使用AppWidgetManager的updateAppWidget方法,并传入一个或多个要更新的Widget的标识符和要应用的RemoteViews作为其参数。

public class FullAppWidget extends AppWidgetProvider {
  @Override
  public void onUpdate(Context context,
                       AppWidgetManager appWidgetManager,
                       int[] appWidgetIds) {
 
    RemoteViews views = new RemoteViews(context.getPackageName(),
        R.layout.full_widget_layout);
    
    Bitmap myBitmap = null;
    views.setInt(R.id.widget_image_view, "setImageLevel", 2);
    views.setBoolean(R.id.widget_text_view, "setCursorVisible", true);
    views.setBitmap(R.id.widget_image_button, "setImageBitmap", myBitmap);

    views.setTextViewText(R.id.widget_text, "Updated Text");
    views.setTextColor(R.id.widget_text, Color.BLUE);
    views.setImageViewResource(R.id.widget_image, R.drawable.icon);
    views.setProgressBar(R.id.widget_progressbar, 100, 50, false);
    views.setChronometer(R.id.widget_chronometer, SystemClock.elapsedRealtime(), null, true);


    views.setViewVisibility(R.id.widget_text, View.VISIBLE);

    appWidgetManager.updateAppWidget(appWidgetIds, views);
  }
}

迭代每一个Widget

public class MyAppWidget extends AppWidgetProvider {
  
  @Override
  public void onUpdate(Context context,
                       AppWidgetManager appWidgetManager,
                       int[] appWidgetIds) {
    // 在迭代每个Widget的过程中,创建一个RemoteViews对象并将修改后的RemoteViews应用到每个Widget
    final int N = appWidgetIds.length;
    for (int i = 0; i < N; i++) {
      int appWidgetId = appWidgetIds[i];

      // 创建一个RemoteViews对象
      RemoteViews views = new RemoteViews(context.getPackageName(),
                                          R.layout.my_widget_layout);

      // 更新UI
      Intent intent = new Intent(context, MyActivity.class);
      PendingIntent pendingIntent = 
        PendingIntent.getActivity(context, 0, intent, 0);
      views.setOnClickPendingIntent(R.id.widget_text, pendingIntent);

      // 通知AppWidgetManager使用修改后的过程view更新widget
      appWidgetManager.updateAppWidget(appWidgetId, views);
    }
  }
}

也可以直接从一个Service、Activity或者Broadcast Receiver更新Widget。

为此,需要调用AppWidgetManager的getInstance静态方法并传入当前上下文,以获得AppWidgetManager的引用。

AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);

可以使用AppWidgetManager实例的getAppWidgetIds方法找出指定AppWidget的每个正在运行的实例的标识符。

ComponentName thisWidget = new ComponentName(context, MyAppWidget.class);
int[] appWidgetIds = appWidgetManager.getAppWidgetIds(thisWidget);

 

标准的WidgetUI更新模式

public class MyReceiver extends BroadcastReceiver {

  @Override
  public void onReceive(Context context, Intent intent) {
    // 获取AppWidgetManager实例
    AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);

    // 获得所选Widget的每个实例的标识符
    ComponentName thisWidget = new ComponentName(context, MyAppWidget.class);
    int[] appWidgetIds = appWidgetManager.getAppWidgetIds(thisWidget);

    final int N = appWidgetIds.length;
    // 遍历所有的Widget
    for (int i = 0; i < N; i++) {
      int appWidgetId = appWidgetIds[i];
      // 创建一个RemoteView对象
      RemoteViews views = new RemoteViews(context.getPackageName(),
                                          R.layout.my_widget_layout);

      // TODO 使用views对象更新Widget的UI。

      // 通知AppWidgetm使用修改后的远程View更新widget
      appWidgetManager.updateAppWidget(appWidgetId, views);
    }
  }

}

 

使用RemoteViews为Widget添加交互性

Widget的交互性一般被限制为以下几个方面:

  • 添加监听一个或多个View的ClickListener
  • 根据所选项变化改变UI
  • 在Collection View Widget中的View之间过渡

使用ClickListener

Intent intent = new Intent(context, MyActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
views.setOnClickPendingIntent(R.id.widget_text, pendingIntent);

根据选项焦点改变ImageView

 

刷新Widget

1.使用最小更新频率

<appwidget-provider
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:initialLayout="@layout/my_widget_layout"
  android:minWidth="110dp"
  android:minHeight="110dp"
  android:label="@string/widget_label"
  android:resizeMode="horizontal|vertical"
  android:previewImage="@drawable/widget_preview"
  android:updatePeriodMillis="3600000"
/>

一般来说,最小更新频率应当至少是一个小时,理想情况下每天不超过一次或者两次。

如果Widget需要更频繁地更新,可以考虑下面部分所描述的技术,使用一种使用了Alarm的更为有效地调度模型或是使用一种事件/Intent驱动的模型来进行更新。

2.使用Intent更新

因为Widget是作为Broadcast Receiver实现的,所以可以通过为它们注册监听附加的Broadcast Intent的Intent Filter,来触发更新以及UI刷新。

这是一种刷新Widget的动态方法,使用了一种更为有效地事件模型,而不是使用指定了一种短暂的具有最低刷新速率的非常消耗电池电量的方式。

以下代码将一个新的Intent Filter分配到了之前定义的Widget清单中。

<receiver android:name=".MyAppWidget" android:label="@string/widget_label">
  <intent-filter>
    <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
  </intent-filter>
  <intent-filter>
    <action android:name="com.paad.mywidget.FORCE_WIDGET_UPDATE" />
  </intent-filter>
  <meta-data android:name="android.appwidget.provider" android:resource="@xml/widget_provider_info"/>
</receiver>

通过更新Widget的onReceive方法处理程序,我们能够监听这个新的Broadcast Intent并使用它来更新Widget。

public static String FORCE_WIDGET_UPDATE = "com.paad.mywidget.FORCE_WIDGET_UPDATE";

@Override
public void onReceive(Context context, Intent intent) {
    super.onReceive(context, intent);

    if (FORCE_WIDGET_UPDATE.equals(intent.getAction())) {
        // TODO 更新Widget
    }
}

这种技术对于响应系统、用户或者应用程序事件来说尤其有用。

例如,一次数据刷新,或者一次用户操作,如单击Widget上的按钮。

我们也可以为系统事件广播进行注册,例如,网络连接性、电池电量或者屏幕亮度变化。

通过依赖于现有的事件来触发UI更新,可以将Widget更新的影响降到最低,同时使UI保持最新。

通过使用IntentFilter中指定的动作来广播Intent,也可以把这种技术及用于在任意时刻触发对Widget的更新,如下所示。

sendBroadcast(new Intent(MyAppWidget.FORCE_WIDGET_UPDATE));

3.使用Alarm

与最小刷新速率不同,Alarm只能被配置为在设备处于唤醒状态时触发,这为常规更新提供了一种更高效的方法。

使用Alarm刷新Widget类似于前面所描述的Intent驱动模型的使用。在自己的应用程序中,使用Alarm Manager创建一个Alarm,它会触发一个带有注册动作的Intent。

与Widget的刷新速率一样,Alarm也能够在触发时唤醒设备 - 所以尽量少使用它们以节省电池电量十分重要。

此外,当创建Alarm时,可以通过使用RTC或者ELAPSED_REALTIME模式来配置它,使它在指定时间或者指定间隔后出发,前提是设备已经被唤醒。

下例显示了如何调度一个重复Alarm,使其广播一个用来强制更新Widget的Intent。

AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
    
PendingIntent pi = PendingIntent.getBroadcast(context, 0, new Intent(MyAppWidget.FORCE_WIDGET_UPDATE), 0);

alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME, AlarmManager.INTERVAL_HOUR, AlarmManager.INTERVAL_HOUR, pi);

 

创建并使用Widget配置Activity

每当我们将Widget添加到主屏幕中时,一个AppWidget配置Activity就会立即启动。

它可以使用户应用程序中的任何Activity,我们假定它具有一个用于APPWIDGET_CONFIGURE动作的Intent Filter。

ACTION_APPWIDGET_CONFIGURE

<activity android:name=".MyWidgetConfigurationActivity">
    <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
    </intent-filter>
</activity>

为了将一个配置Activity分配到Widget中,我们必须使用configure标签将其添加到Widget的App Widget Provider Info中。该Activity必须由其完全限定的包名来指定。

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:initialLayout="@layout/full_widget_layout"
  android:minWidth="72dp"
  android:minHeight="146dp"
  android:label="@string/full_widget_label"
  android:updatePeriodMillis="3600000"
  android:resizeMode="horizontal|vertical"
  android:previewImage="@drawable/widget_preview"
  android:configure="com.paad.PA4AD_Ch14_MyWidget.MyWidgetConfigurationActivity"
/>

启动配置Activity的Intent将包含一个EXTRA_APPWIDGET_ID extra,该extra提供了它所配置的AppWidgetId。

在该Activity内,提供一个UI以便让用户完成配置并进行确认。在这个阶段,Activity的结果应该是RESULT_OK,并返回一个Intent。

返回的Intent必须包含一个extra,该extra使用EXTRA_APPWIDGET_ID常量描述了所配置的Widget的ID。

public class MyWidgetConfigurationActivity extends Activity {

  private int appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    Intent intent = getIntent();
    Bundle extras = intent.getExtras();
    if (extras != null) {
      appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
    }

    // 将结果设置为RESULT_CANCELED
    // 考虑到用户可能不接受配置更改/设置就退出Activity
    setResult(RESULT_CANCELED, null);

    // Configure the UI.
  }

  private void completedConfiguration() {
    // 保存于Widget ID对应的配置设置

    // 通知Widget Manager配置已经完成
    Intent result = new Intent();
    result.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
    setResult(RESULT_OK, result);
    finish();
  }
}

 

Collection View Widget

 

RemoteViewsService

 

使用快速搜索框显示应用程序搜索结果

Android1.6引入了通过通用的快速搜索框(QSB)为应用程序搜索结果服务的能力。

通过使用该机制显示应用程序中的搜索结果,为用户提供了一个通过实时搜索结果访问应用程序的附加访问点。

在快速搜索框中显示搜索结果

为了在快速搜索框中显示搜索结果,首先必须要在应用程序中实现搜索功能。

为了使搜索结果全局可用,需要修改描述了应用程序搜索元数据的searchable.xml文件,并添加两种新的属性。

searchSettingsDescription:用于在Settings菜单中描述搜索结果。当用户通过浏览在搜索中包含的应用程序结果时,看到的就是这些描述。

includeInGlobalSearch:将该值设置为true以便在QSB中显示这些结果。

Searchable Configuration

<searchable xmlns:android="http://schemas.android.com/apk/res/android"
        android:label="@string/search_label"
        android:searchSettingsDescription="@string/settings_description"
        android:includeInGlobalSearch="true"
        android:searchSuggestAuthority="dictionary"
        android:searchSuggestIntentAction="android.intent.action.VIEW">
</searchable>

为了避免误用的可能性,添加新的搜索提供器需要用户进行筛选,所以搜索结果将不会自动地直接在QSB中自动显示。

Creating a Search Interface

 

LiveWallper