通过widget定义,我们在widget列表中看到了我们的TestWidget,当我们拖拽widget到主页时,如果在appwidet-provider中定义了android:configure的java类,在widget实例创建后会马上唤起配置activity。这个activity主要完成两个任务:1、配置初始化数据;2、将配置数据适配到widget实例中。
widget数据可以保持在文件、Share preference,或者SQLite3中。widget作为小工具配置数据量小,通常可以方便地存贮在preference中。preference中数据存贮和读取使用BirthDayStoreData类来处理。我们在Pro Android学习笔记(六三):Preferences(7):代码控制首选项中的“利用preference保存状态”已经介绍过如何实现,在此,复习一下。
我们需要存贮的内容有widgetID,名字,生日,Preference是以键值对的方式保存,我们以name_widgetID作为key,生日作为value来进行信息存贮。
public class BirthDayStoreData {
private final static String BIRTHDAY_WIDGET_PROVIDER_NAME = "cn.wei.flowingflying.testwidget.provider";
//保存配置数据:创建widget实例,通过configure activity进行配置时,保存相关配置数据
public static void storeData(Context context,int widgetId, String name,String value){
String key = getKey(widgetId,name);
//第一个参数是preferences文件,如果不存在则创建之。具体为/data/data/cn.wei.flowingflying.testwidget/shared_prefs/cn.wei.flowingflying.testwidget.provider.xml,可以在DDMS中查看。
Editor editor = context.getSharedPreferences(BIRTHDAY_WIDGET_PROVIDER_NAME, Context.MODE_PRIVATE).edit();
editor.putString(key, value);
editor.commit();
}
//删除配置数据:删除widget实例的同时,需要删除该实例的相关数据
public static void removeData(Context context, int widgetId){
String key = getKeyById(context, widgetId);
if(key == null)
return;
Editor editor = context.getSharedPreferences(BIRTHDAY_WIDGET_PROVIDER_NAME, Context.MODE_PRIVATE).edit();
editor.remove(key);
editor.commit();
}
//清空全部的配置数据
public static void removeAllData(Context context){
Editor editor = context.getSharedPreferences(BIRTHDAY_WIDGET_PROVIDER_NAME, Context.MODE_PRIVATE).edit();
editor.clear();
editor.commit();
}
//显示配置数据:用于我们在LogCat中进行跟踪,在此,我们也演示了如何通过轮询方式,显示全部的数据,通过类似的方式,我们可以同widgetId查得对应的名字和生日,通过类似的方法,可根据widgetId查询key,名字,生日,相关代码从略。
public static void showData(Context context){
SharedPreferences prefs = context.getSharedPreferences(BIRTHDAY_WIDGET_PROVIDER_NAME, Context.MODE_PRIVATE);
Map<String,?> pairs = prefs.getAll();
Log.d("DATA","Total " + pairs.size() + " widgets:");
for(String key:pairs.keySet()){
String value = (String)pairs.get(key);
Log.d("DATA",key + " - " + value);
}
}
public static String getNameById(Context context, int widgetId){
… …
}
public static String getDateById(Context context ,int widgetId){
… …
}
private static String getKey(int widgetId, String name){
return name + "_" + widgetId;
}
private static String getKeyById(Context context,int widgetId){
… …
}
}
配置configure activity的代码如下:
public class ConfigBirthDayWidgetActivity extends Activity{
private static String tag = "ConfigActivity";
private int myWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;
@Override //配置activity的操作和普通activity的一样,但在被AppWidgetManage唤起时,intent是携带widgetId的信息,我们在onCreate()中获取Widget ID。
protected void onCreate(Bundle savedInstanceState) {
… …
Intent intent = getIntent();
Bundle b = intent.getExtras();
if(b != null){
myWidgetId = b.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID,AppWidgetManager.INVALID_APPWIDGET_ID);
}
if(myWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID){
Toast.makeText(this, "Widget Error : 无有效widget ID", Toast.LENGTH_LONG).show();
finish();
}
}
.. ….
//点击配置button后调用的方法
private void getAndStoreConfigInfo(){
… … String name为用户输入名字,String date为用户输入的有效日期
//【1】在preference中保持数据,并显示所有数据
BirthDayStoreData.storeData(this, myWidgetId, name, date);
BirthDayStoreData.showData(this);
//【2】将配置数据与具体的widget实例相关联,具体实现见后面
BirthDayStoreData.updateAppWidget(this, myWidgetId,name, date);
//【3】将结果返回给AppWidget Manager,以通知它Configurator已经完成。作用如同startActivityForResult()给出返回值,通知AppWidgetManager某个widgetId已经完成配置,可以在主页上显示创建的widget实例
Intent resultIntent = new Intent();
resultIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, myWidgetId);
setResult(RESULT_OK, resultIntent);
//【4】关闭activity
finish();
}
}
配置数据适配到widget实例中
Widget实例的view要通过RemoteViews进行控制,小例子采用静态方法的方式,代码片段如下:
public class BirthDayStoreData {
... ...
public static void updateAppWidget(Context context,int widgetId,String name, String date){
//【1】设置Remote view的信息
// 1.1)、获得remote view对象
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.birday_widget);
// 1.2)、对remote view进行setText()设置
views.setTextViewText(R.id.bd_name, widgetId + ":" + name);
views.setTextViewText(R.id.bd_date, date);
views.setTextViewText(R.id.bd_days, Long.toString(Utils.howFarInDays(Utils.getDate(date))));//Utils是处理日期的类
// 1.3)、通过PendingIntent设置某个view的点击处理,采用intent方式,可以打开activity,service,receiver等等。本小例子将打开某个网页
Intent intent = new Intent(Intent.ACTION_VIEW,Uri.parse("http://www.taobao.com"));
PendingIntent pi = PendingIntent.getActivity(context, 0, intent, 0);
views.setOnClickPendingIntent(R.id.bd_buy, pi);
//【2】通过AppWidgetManger,具体实施到widgetId实例上。
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
appWidgetManager.updateAppWidget(widgetId,views);
}
public static void updateAppWidget(Context context,int widgetId){
… …
}
}
根据widget定义,我们App Widget Provider的Java类为BirthDayWidgetProvider,这个类用于管理Widget的各个生命周期。
public class BirthDayWidgetProvider extends AppWidgetProvider{
private static String tag = "BirthDayWidgetProvider";
@Override /* 在3种情况下会调用OnUpdate()。onUpdate()是在main线程中进行,因此如果处理需要花费时间多于10秒,处理应在service中完成。
(1)在时间间隔到时调用,时间间隔在widget定义的android:updatePeriodMillis中设置;
(2)用户拖拽到主页,widget实例生成。无论有没有设置Configure activity,我们在Android4.4的测试中,当用户拖拽图片至主页时,widget实例生成,会触发onUpdate(),然后再显示activity(如果有)。这点和资料说的不一样,资料认为如果设置了Configure acitivity,就不会在一开始调用onUpdate(),而实验显示当实例生成(包括创建和重启时恢复),都会先调用onUpate()。在本例,由于此时在preference尚未有相关数据,创建实例时不能有效进行数据设置。
(3)机器重启,实例在主页上显示,会再次调用onUpdate()*/
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
Log.d(tag,"onUpdate() called. 有 " + appWidgetIds.length + "个widgets");
for(int i = 0 ; i< appWidgetIds.length; i ++){
Log.d(tag,"update widget ID " + appWidgetIds[i]);
BirthDayStoreData.updateAppWidget(context, appWidgetIds[i]);
}
}
@Override /* 某个/些widget从主页中删除,在此删除该widget的相关数据 */
public void onDeleted(Context context, int[] appWidgetIds) {
Log.d(tag,"onDeleted() called");
for(int i = 0 ; i < appWidgetIds.length; i ++){
Log.d(tag,"delete widget " + appWidgetIds[i] + " data");
BirthDayStoreData.removeData(context, appWidgetIds[i]);
}
BirthDayStoreData.showData(context);
}
@Override /* 一般无需重写此方法。App Widget provider本质是receiver,在此可以跟踪收到什么消息,这些消息包括AppWidgetManager.ACTION_APPWIDGET_DELETED/UPDATE/ENABLED/DISABLED,super.onReceiver()会根据消息类型触发不同的回调函数。如果采用AlarmManager或者自定义的广播,可以再次进行处理。 */
public void onReceive(Context context, Intent intent) {
Log.i(tag,"onReceive() : " + intent);
super.onReceive(context, intent);
}
@Override /* 表明至少有一个widget实例被拖到主页上,即当第一个widget出现时的回调函数。我们需要允许广播接收器接收消息,第一个widget出现了。我们可以在此注册其它感兴趣的自定义的广播*/
public void onEnabled(Context context) {
Log.d(tag,"onEnabled() called, context " + context.toString());
// setComponentEnabledSetting相当于在AndriodMenifest.xml文件中队组件设置android:enabled为true|false。此处是对receiver进行设置,如果true,则允许进行监听,包括开机重启。
PackageManager pm = context.getPackageManager();
/*使用new ComponentName("cn.wei.flowingflying.testwidget",".BirthDayWidgetProvider")出现不明原因错误,
* 可对类名采用完全名称,及new ComponentName("cn.wei.flowingflying.testwidget",
* "cn.wei.flowingflying.testwidget.BirthDayWidgetProvider"),
* 或通过系统获取组件名的方式new ComponentName(context, getClass())*/
pm.setComponentEnabledSetting(new ComponentName(context, getClass()),
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP);
}
@Override /*最后一个widget已从主页中删除,在此,确保删除所有配置数据,无需进行广播监听,色织enabled=false,如果有注册的自定义广播,在此unregister */
public void onDisabled(Context context) {
BirthDayStoreData.removeAllData(context);
PackageManager pm = context.getPackageManager();
pm.setComponentEnabledSetting(new ComponentName(context, getClass()),
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP);
}
}
如果主页没有实例,新实例的生成触发顺序为:onEnabled() –>onUpdate() –>Configure Activity,头两个顺序可能会出现变化。估计是AppWidgetManager的异步处理导致广播消息出现的先后顺序问题。如果已经有实例,新实例生成触发顺序为onUpdate() –> Configure activity。配置后,等待定义的时间间隔,进行定期触发onUpdate()。
机器重启 onEnabled() –>onUpdate() –> onUpdate(),同样头两个顺序可能会交换,此后,等待widget定义的时间间隔,进行定期触发onUpdate()。
如果我们更新或重装apk,实例并不会被删除,会触发onUpdate()。
补充:Widget图标
Widget在widget列表中显示的通常都是widget的外貌,Android模拟器有一个应用Widget Preview可以帮助我们获取widget的外观图标,如下:
通过adb pull将存贮在模拟器SD卡Download路径下的preview图片获取,作为列表显示图标。