App widget又叫应用程序小插件,是安卓1.5引入的新功能,把应用程序与android操作系统之间的集成提升到了一个新的高度。其历史比动态墙纸还要悠久,动态墙纸live wallpaer才不过是2.1引入的功能。但程序是否具有WIDGET功能,一是取绝于程序的需要,二是取绝于作者的意愿。虽然很多程序,没有WIDGET也可以运行的很好,而且即使你这样做了,也不一定会有多少客户愿意使用,这让WIDGET显得多少有些鸡肋。但学习WIDGET,并在合适的机会和合适的程序中展示他,无疑会给客户留下很深刻的印象。如果能让愿意客户把他天天放在桌面最显眼的位置,那就太酷太爽太兴奋太激动了,这在大拇指时代无疑是一张很好的名片加广告。
要创建应用程序小插件,首先就要了解AppWidgetProvider,我们的小插件类需要从AppWidgetProvider派生,并重载他的一些主要方法。这个类不是很复杂,从android文档中我们可以看到他的主要方法介绍:
// 编译自AppWidgetProvider.java (版本 1.5:49.0,超级位)
public class android.appwidget.AppWidgetProvider extendsandroid.content.BroadcastReceiver {
public AppWidgetProvider();
public voidonReceive(android.content.Context context, android.content.Intent intent);
public voidonUpdate(android.content.Context context, android.appwidget.AppWidgetManagerappWidgetManager, int[] appWidgetIds);
public voidonDeleted(android.content.Context context, int[] appWidgetIds);
public voidonEnabled(android.content.Context context);
public voidonDisabled(android.content.Context context);
}
这个类扩展了BroadcastReceiver 类,是为了方便类来处理App Widget广播。因为WIDGET不是运行在自己进程里,而是宿主进程,所以交互需要处理App Widget广播。AppWidgetProvider只接收和这个App Widget相关的事件广播,比如这个App Widget被更新,删除,启用,以及禁用。当这些广播事件发生时,AppWidgetProvider 将通过自己的方法来处理,这些方法包括:
onUpdate(Context, AppWidgetManager, int[])
这个方法调用来间隔性的更新App Widget,间隔时间用AppWidgetProviderInfo 里的updatePeriodMillis属性定义。这个方法也会在用户添加App Widget时被调用,因此它应该执行基础的设置,比如为视图定义事件处理器并启动一个临时的服务Service。
onDeleted(Context, int[])
当App Widget从宿主中删除时被调用。
onEnabled(Context)
当一个App Widget实例第一次创建时被调用。比如,如果用户添加两个你的App Widget实例,只在第一次被调用。如果你需要打开一个新的数据库或者执行其他对于所有的App Widget实例只需要发生一次的设置,那么这里是完成这个工作的好地方。
onDisabled(Context)
当你的App Widget的最后一个实例被从宿主中删除时被调用。你应该在onDisabled(Context)中做一些清理工作,比如关掉你的后台服务。
onReceive(Context, Intent)
这个接收到每个广播时都会被调用,而且在上面的回调函数之前。你通常不需要实现这个方法,因为缺省的AppWidgetProvider 实现过滤所有App Widget 广播并恰当的调用上述方法。但在Android 1.5中,据一些外国的大牛们认为,onDeleted()方法在该调用时有时会不被调用。为了规避这个问题,他们建议像Group post中描述的那样实现onReceive() 来接收这个onDeleted()回调。至于现在4.03还有没有这个问题,至少目前我在调试中是没有发现,但是出于安全考虑,你也许需要实现这个方法以避免出现一些用户抱怨投诉之类的麻烦。
随便创建一个工程。随便创建一个工程。基于AppWidgetProvider派生一个新类,名字自定,生成后代码如下:
package com.mpw; import android.appwidget.AppWidgetProvider; public class mypicwidprivider extends AppWidgetProvider { }这是一个空类,所有方法都调用其父类的实现。然后给我们的WIDGET创建一个布局文件,在RES文件夹里添加一个mypicwid.xml文件,放在layout文件夹下,我们的WIDGET只是显示一张图片,所以添加一个img view 控件,代码如下:
<?xml version="1.0" encoding="utf-8"?> <ImageView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/my_widget_img" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/mypic" />mypic你可以随便找一张或者网上下一张,名字改一下就行,这是我们的widget布局文件,显示一张图片。然后我们要把这个文件添加给我们的widget使用。并且我们要定义widget的参数设置,这时在res下新建一个XML文件夹,在文件夹中添加一个widget.xml的文件,其内容如下:
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" android:minWidth="72dp" android:minHeight="72dp" android:updatePeriodMillis="86400000" android:initialLayout="@layout/mypicwid" > </appwidget-provider>
最后要让系统了解我们的应用小插件,我们要在程序的androidmainifest.xml的文件里添加<receiver>标记,完整代码如下:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.mpw" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="15" /> <application android:icon="@drawable/ic_launcher" android:label="@string/app_name" > <receiver android:name=".mypicwidprivider"> <intent-filter> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> </intent-filter> <meta-data android:name="android.appwidget.provider" android:resource="@xml/widget" /> </receiver> <activity android:name=".MyPicWidActivity" android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
上面包含了我们创建的那个空类mypicwidprivider和widget声明文件xml/widget.xml,到这里,我们的一个小WIDGET就成功了,可以运行看效果。可以在添加小部件中添加,那么,WIDGET要更新怎么办?要提供多种风格给用户选择怎么办?这就需要继续深入开发了。
为widget添加配置窗口,这里简单的把我们创建工程时默认的Activity作为配置窗口调用.打开xml/widget.xml文件,添加如下语句:
android:configure="com.mpw.MypicwidActivity"
最终xml/widget.xml显示如下:
<?xml version="1.0" encoding="utf-8"?> <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" android:minWidth="72dp" android:minHeight="72dp" android:updatePeriodMillis="86400000" android:initialLayout="@layout/mypicwid" android:configure="com.mpw.MypicwidActivity"> </appwidget-provider>
Intent intent = getIntent();
Bundle extras = intent.getExtras();
if (extras != null) {
mAppWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID);
Intent resultValue = new Intent();
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
mAppWidgetId);
setResult(RESULT_OK, resultValue);
}
else
{
setResult(RESULT_CANCELED);
}
这里没有对WIDGET做特别处理,只是把选中的WIDGET传递给添加到desktop中,在以后的代码中可以完善这个配置,使用户能够通过一些设置来自定义WIDGET风格。但由于这关系到一些其他控件的知识,放在后面了解。这里上面的代码用到了Activity的参数传递,主要应用了intent方法和setResult方法。
上面的方法使得用户在使用 WIDGET 时可以先配置,再运行配置后的 WIDGET 。但如果我们在 WIDGET 运行期间,让用户可以随时点击 WIDGET 图标,打开 WIDGET 设置,修改 WIDGET 应该怎么做呢?很多 WIDGET 都是这样做的,其实也不难。为widget添加一个单击打的设置窗口需要使用 PendingIntent 类和 RemoteViews 类,当然这个窗口也不一定是设置窗口,也可以是另一个程序,或者是 WIDGET 控件功能的扩展。只要通过 RemoteViews.setOnClickPendingIntent(int viewId,PendingIntent pendingIntent)方法为WIDGET指定响应的行为。需要注意的是 PendingIntent 和 Intent 不是派生关系,虽然两者命名上看起来很像有种某种或近或远的亲戚关系,并且都可以用来打一个 activity ,但 Intent 直接打开, PendingIntent 不直接打开,可以理解为暂缓打开,通过RemoteViews的setOnClickPendingIntent方法,在用户点击WIDGET图标时打开设置窗口。Widget的显示使用 RemoteViews ,因为他不是在应用程序的进程中运行,而是运行在宿主进程中,修改他要使用 RemoteViews 。在 mypicwidprivider 类中添加一个自定义的方法,具体代码如下,我们只需要使用到PendingIntent.getActivity(Contextcontext, int requestCode, Intent intent, int flags)来获取一个PendingIntent实例;private static void updateWidgetView(Context context, int[] appWidgetIds) { Log.i("AAAAAAAAAAAAAAAAAA", "DDDDDDDDDDDDD"); // ComponentName simpleWidget = new ComponentName(context, // mypicwidprivider.class); AppWidgetManager appWidgetManager = AppWidgetManager .getInstance(context); int mAppWidgetId; final int N = appWidgetIds.length; for (int i = 0; i < N; i++) { mAppWidgetId = appWidgetIds[i]; RemoteViews remoteView = new RemoteViews(context.getPackageName(), R.layout.mypicwid); Intent launchAppIntent = new Intent(context, MypicwidActivity.class); launchAppIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId); PendingIntent launchAppPendingIntent = PendingIntent.getActivity( context,mAppWidgetId, launchAppIntent,PendingIntent.FLAG_UPDATE_CURRENT);remoteView.setOnClickPendingIntent(R.id.my_widget_img,launchAppPendingIntent);appWidgetManager.updateAppWidget(mAppWidgetId, remoteView);}}
上面的代码通过aunchAppIntent.putExtra把当前widget的ID传给了配置窗口,主要用于当前WIDGET具有多个实例,且每个实例各不相同的情况,如果你的widget只能运行一个实例或者多个实例在显示上功能上都没区分,可以不需要调用aunchAppIntent.putExtra方法,直接在appWidgetManager.updateAppWidget方法的第一个参数设置为数组appWidgetIds就行了。一定不能忽略appWidgetManager.updateAppWidget,他会把我们的setOnClickPendingIntent更新给当前按键,不更新,设置不生效。RemoteViews视图创建调用的是我们的在前一节说的widget的布局文件,而侦听的对象是我们布局文件里的img控件。重载mypicwidprivider方法onUpdate,把方法添加进入重载函数中,如下:
updateWidgetView(context, appWidgetIds);最终,mypicwidprivider类如下:
package com.mpw; import android.app.PendingIntent; import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProvider; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.util.Log; import android.widget.RemoteViews; public class mypicwidprivider extends AppWidgetProvider { @Override public void onDisabled(Context context) { Log.i("AAAAAAAAAAAAAAAAAA", "BBBBBBBBBBBBBBBBBBB"); super.onDisabled(context); } @Override public void onEnabled(Context context) { // update the remove view right away Log.i("AAAAAAAAAAAAAAAAAA", "CCCCCCCCCCCCCC"); super.onEnabled(context); } @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { // all instantiated widgets will read from the same data, so we don't // need to track by widget id // Just need to request the data be updated, the internal Service will // handle all the rest // NOTE: No matter what updatePeriodMillis is set, this will not come any // more frequently // than once every 30 minutes. Log.i("AAAAAAAAAAAAAAAAAA", "EEEEEEEEEEEEEEEEE"); updateWidgetView(context, appWidgetIds); } @Override public void onDeleted(android.content.Context context, int[] appWidgetIds) { super.onDeleted(context, appWidgetIds); } private static void updateWidgetView(Context context, int[] appWidgetIds) { Log.i("AAAAAAAAAAAAAAAAAA", "DDDDDDDDDDDDD"); // ComponentName simpleWidget = new ComponentName(context, // mypicwidprivider.class); AppWidgetManager appWidgetManager = AppWidgetManager .getInstance(context); int mAppWidgetId; final int N = appWidgetIds.length; for (int i = 0; i < N; i++) { mAppWidgetId = appWidgetIds[i]; RemoteViews remoteView = new RemoteViews(context.getPackageName(), R.layout.mypicwid); Intent launchAppIntent = new Intent(context, MypicwidActivity.class); launchAppIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId); PendingIntent launchAppPendingIntent = PendingIntent.getActivity( context,mAppWidgetId, launchAppIntent,PendingIntent.FLAG_UPDATE_CURRENT);remoteView.setOnClickPendingIntent(R.id.my_widget_img,launchAppPendingIntent); //remoteView.setImageViewResource( // R.id.my_widget_img, R.drawable.mypic1); appWidgetManager.updateAppWidget(mAppWidgetId, remoteView);}}}
重新运行代码,发现widget可以单击了。但由于我们的配置窗口到现在为止,并不能为用户提供有用的交互功能,要与用户交互,就要添加交互代码,我们这里设置一个简单的交互,在MypicwidActivity窗口中添加三个单选按钮,用来把WIDGET旋转90,180,270度和放缩。首先修改main.xml,在其中加入单选按钮,如下:
<RadioGroup android:id="@+id/RadioGroup01" android:layout_width="wrap_content" android:layout_height="wrap_content" > <RadioButton android:id="@+id/radioButton1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="90" /> <RadioButton android:id="@+id/radioButton2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="180" /> <RadioButton android:id="@+id/radioButton3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="270" /> </RadioGroup>完整代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/hello" />
<RadioGroup
android:id="@+id/RadioGroup01"
android:layout_width="wrap_content"
android:layout_height="wrap_content" >
<RadioButton
android:id="@+id/radioButton1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="90" />
<RadioButton
android:id="@+id/radioButton2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="180" />
<RadioButton
android:id="@+id/radioButton3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="270" />
</RadioGroup>
</LinearLayout>
然后修改代码,在MypicwidActivity中添加代码实现让用户配置生效,在MypicwidActivity类的onCreate方法函数添加代码:
//add setting final RadioGroup group = (RadioGroup)findViewById(R.id.RadioGroup01); //appWidgetManager.updateAppWidget(mAppWidgetId, views); //views.setImageViewResource( // R.id.my_widget_img, R.drawable.mypic1); //views.setImageViewBitmap(viewId, bitmap); group.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() { RemoteViews views = new RemoteViews(MypicwidActivity.this .getPackageName(), R.layout.mypicwid); AppWidgetManager appWidgetManager = AppWidgetManager .getInstance(MypicwidActivity.this); @Override public void onCheckedChanged(RadioGroup group, int checkedId) { // TODO Auto-generated method stub if (checkedId != -1) { RadioButton rb = (RadioButton)findViewById(checkedId); Bitmap rbm = null; if (rb != null) { rbm = BitmapFactory.decodeResource(getResources(), R.drawable.mypic); if (rb.getId() == R.id.radioButton1) { Matrix mat = new Matrix(); mat.preRotate(90); mat.preScale(2, 2); Bitmap rbm1 = Bitmap.createBitmap(rbm, 0,0,rbm.getWidth(), rbm.getHeight(), mat,false); views.setImageViewBitmap(R.id.my_widget_img, rbm1); } else if (rb.getId() == R.id.radioButton2) { Matrix mat = new Matrix(); mat.preRotate(180); mat.preScale(2, 2); Bitmap rbm1 = Bitmap.createBitmap(rbm, 0,0,rbm.getWidth(), rbm.getHeight(), mat,false); views.setImageViewBitmap(R.id.my_widget_img, rbm1); } else { Matrix mat = new Matrix(); mat.preRotate(270); mat.preScale(2, 2); Bitmap rbm1 = Bitmap.createBitmap(rbm, 0,0,rbm.getWidth(), rbm.getHeight(), mat,false); views.setImageViewBitmap(R.id.my_widget_img, rbm1); } Intent intent = new Intent(MypicwidActivity.this, MypicwidActivity.class); intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,mAppWidgetId); PendingIntent pendingIntent = PendingIntent.getActivity(MypicwidActivity.this, mAppWidgetId, intent, 0); views.setOnClickPendingIntent(R.id.my_widget_img, pendingIntent); appWidgetManager.updateAppWidget(mAppWidgetId, views); Intent resultValue = new Intent(); resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId); setResult(RESULT_OK, resultValue); finish(); } else { } } } });
SharedPreferences sp = MypicwidActivity.this.getSharedPreferences("mypicwid", Context.MODE_PRIVATE); Editor edit = sp.edit(); edit.putInt("rotion"+mAppWidgetId, 90); edit.commit();
int dv = 0; dv = sp.getInt("rotion"+mAppWidgetId, dv); Bitmap rbm = BitmapFactory.decodeResource(context.getResources(), R.drawable.mypic); Matrix mat = new Matrix(); mat.preRotate(dv); mat.preScale(2, 2); Bitmap rbm1 = Bitmap.createBitmap(rbm, 0,0,rbm.getWidth(), rbm.getHeight(), mat,false); remoteView.setImageViewBitmap(R.id.my_widget_img, rbm1); appWidgetManager.updateAppWidget(mAppWidgetId, remoteView);
最后有关widget的一个重要内容是更新,如果要动态更新WIDGET怎么办,可以使用服务。其实上面的代码只是随便写写,最好的方法是使用SharedPreferences 把配置保存,在mypicwidprivider创建一个服务监听onSharedPreferenceChanged修改,通过update刷新。
参考文档:http://blog.csdn.net/silenceburn/article/details/6093074
http://blog.csdn.net/iefreer/article/details/4626274