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 控件,代码如下:
mypic你可以随便找一张或者网上下一张,名字改一下就行,这是我们的widget布局文件,显示一张图片。然后我们要把这个文件添加给我们的widget使用。并且我们要定义widget的参数设置,这时在res下新建一个XML文件夹,在文件夹中添加一个widget.xml的文件,其内容如下:
最后要让系统了解我们的应用小插件,我们要在程序的androidmainifest.xml的文件里添加
上面包含了我们创建的那个空类mypicwidprivider和widget声明文件xml/widget.xml,到这里,我们的一个小WIDGET就成功了,可以运行看效果。可以在添加小部件中添加,那么,WIDGET要更新怎么办?要提供多种风格给用户选择怎么办?这就需要继续深入开发了。
为widget添加配置窗口,这里简单的把我们创建工程时默认的Activity作为配置窗口调用.打开xml/widget.xml文件,添加如下语句:
android:configure="com.mpw.MypicwidActivity"
最终xml/widget.xml显示如下:
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,在其中加入单选按钮,如下:
完整代码如下:
android:layout_height="fill_parent"
android:orientation="vertical" >
android:layout_height="wrap_content"
android:text="@string/hello" />
android:layout_width="wrap_content"
android:layout_height="wrap_content" >
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="90" />
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="180" />
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="270" />
然后修改代码,在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