Android添加桌面widget插件

##Widget的概述
可以添加桌面Widget是Android的一大特色,AppWidget是在HomeScreen上显示的小部件,提供直观的交互操作,有许多的表现形式,如视频、音乐、地图、新闻、游戏等等,它的根本思想来源于代码的复用。通过在HomeScreen长按,在弹出的对话框中选择Widget部件进行构建,长按部件并拖动到垃圾箱可以删除。

Android中的Widget主要设计以下几个类:

  • AppWidgerProvider
  • AppWidgetManager
  • RemoteViews

Widget的核心是AppWidgetProvider,属于广播接收者,是四大组件之一,在AppWidgetProvider这个类中,一共有五个方法,其中有一个onReceive(Context, Intent)方法,分别调用了其他四个空实现的方法onEnabled(Context context),onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds),onDeleted(Context context, int[] appWidgetIds),onDisabled(Context context),而被调用的这四个方法刚好就是一个Widget的生命周期。

RemoteView是用来描述一个跨进程显示的View,也就是说这个View是在另外一个进程中显示的,它可以获取layout的布局文件,并且提供了可以修改View内容的一些简答操作。AppWidgetProvider提供Widget的生命周期,真正显示界面的是AppWidgetHostView(这是在Launcher中实现的),这中间就是通过RemoteView来沟通。通过RemoteView来告诉Launcher你想要的AppWidget长什么样。

##如何创建一个Widget

###配置清单文件
要创建一个Widget,第一步就是要在清单文件中配置:

1
2
3
4
5
6
7
<receiver android:name="ExampleAppWidgetProvider" >
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data android:name="android.appwidget.provider"
android:resource="@xml/example_appwidget_info" />

</receiver>

###定义Widget的生命周期
然后要写一个类继承AppWidgetProvider,并重写AppWidgetProvider的代表生命周期的四个空方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package com.example.widget1;

import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.content.Intent;
import android.util.Log;

public class ExampleAppWidgetProvider extends AppWidgetProvider {

private static final String TAG = "ExampleAppWidgetProvider";

// 可用
@Override
public void onEnabled(Context context) {
// TODO Auto-generated method stub
super.onEnabled(context);
Log.i(TAG, "onEnabled");
// 启动服务
context.startService(new Intent(context, MyService.class));
}

// 更新
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager,
int[] appWidgetIds)
{

// TODO Auto-generated method stub
super.onUpdate(context, appWidgetManager, appWidgetIds);
Log.i(TAG, "onUpdate");
}

// 删除
@Override
public void onDeleted(Context context, int[] appWidgetIds) {
// TODO Auto-generated method stub
super.onDeleted(context, appWidgetIds);
Log.i(TAG, "onDeleted");
}

// 不可用
@Override
public void onDisabled(Context context) {
// TODO Auto-generated method stub
super.onDisabled(context);
Log.i(TAG, "onDisabled");
// 停止服务
context.stopService(new Intent(context, MyService.class));

}
}

###定义Widget的基本属性
然后呢,就需要提供一个XML文件来定义Widget的基本属性(位于res/xml/..)目录下,右键新建Android资源文件,选择类型AppWidgetProvider即可。

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="utf-8"?>
<!-- android:minWidth="294dp" 宽
android:minHeight="72dp"高
android:initialLayout="@layout/example_appwidget" widget的布局
-->

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="294dp"
android:minHeight="72dp"
android:initialLayout="@layout/example_appwidget"
>

</appwidget-provider>

###定义Widget的布局
接着需要提供一个Widget界面布局的XML文件(位于res/layout/…),需要注意的是该XML使用的组件必须是RemoteViews所支持的,目前原生API支持的组件如下:

  • FrameLayout
  • LinearLayout
  • RelativeLayout
  • AnalogClock
  • Button
  • Chronmeter
  • ImageButton
  • ImageView
  • ProgressBar
  • TextView

如果使用这些组件之外的组件,在Widget创建的时候,会出现一个android.view.InflateException的异常。

因此这也就导致一些功能或者样式无法实现,如很基本的List或文本编辑都是无法直接实现的,如果想自定义Widget中的View的话只能通过修改Framework来提供相应组件的支持。

代码示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >

<TextView
android:id="@+id/tv_time"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:text="这是widget桌面插件"
android:background="#f00"
android:textColor="@android:color/black"
android:gravity="right"/>

<Button
android:id="@+id/bt_open"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="复制号码到拨号盘"/>

</RelativeLayout>

###Widget动态实例

接下来,可以实例实现一个Widget显示系统时间和复制号码到拨号盘。

一般Widget所显示的内容都是需要动态变化的,因此需要开启服务在后台操作。AppWidgetManager就是负责更新Widget的内容,AppWidgetManager类中提供一个获取实例的方法,getInstance(Context context),AppWidgetManager也提供了三个更新Widget的方法,由于AppWidgetProvider是一个广播接收者,很显然,需要传递一个组件进去,所以在这里我需要选择updateAppWidget(ComponentName provider, RemoteViews views)这个方法来对AppWidget进行更新操作。在上面继承的AppWidgetProvider类,在Widget的生命周期方法中,已经做了启动和停止服务的操作,然后我只要写一个MyService类继承Service即可,当然,后台服务需要开启线程,要重写run()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
package com.example.widget1;

import java.text.SimpleDateFormat;
import java.util.Timer;
import java.util.TimerTask;

import android.app.PendingIntent;
import android.app.Service;
import android.appwidget.AppWidgetManager;
import android.content.ComponentName;
import android.content.Intent;
import android.net.Uri;
import android.os.IBinder;
import android.widget.RemoteViews;

public class MyService extends Service {

// 定时器
Timer timer = null;
TimerTask task = new TimerTask() {

@Override
public void run() {
// TODO Auto-generated method stub
// 更新app widget
// 使用AppWidgetManager 更新
// App Widget管理器
AppWidgetManager awm = AppWidgetManager
.getInstance(getApplicationContext());
// 组件
ComponentName provider = new ComponentName(getApplicationContext(),
ExampleAppWidgetProvider.class);
// 远端布局(不在activity上的布局)
RemoteViews views = new RemoteViews(getPackageName(),
R.layout.example_appwidget);
// 获取系统当前的时间
long millis = System.currentTimeMillis();
SimpleDateFormat format = new SimpleDateFormat(
"yyyy-MM-dd hh:mm:ss");
// 格式化系统时间
String text = format.format(millis);
// 设置显示内容
views.setTextViewText(R.id.tv_time, text);

// 复制号码到拨号盘
Intent intent = new Intent();
intent.setAction(Intent.ACTION_DIAL);
intent.setData(Uri.parse("tel:8888"));
PendingIntent pendingIntent = PendingIntent.getActivity(
getApplicationContext(), 100, intent, 0);
// 事件
views.setOnClickPendingIntent(R.id.bt_open, pendingIntent);

// 更新
awm.updateAppWidget(provider, views);
}
};

@Override
public void onCreate() {
// TODO Auto-generated method stub
super.onCreate();
timer = new Timer();
// task任务 delay延时执行 period 周期
timer.schedule(task, 0, 500);

}

@Override
public IBinder onBind(Intent intent) {
// TODO Auto-generated method stub
return null;
}

@Override
public void onDestroy() {
// TODO Auto-generated method stub
super.onDestroy();
// 取消任务
timer.cancel();
}

}

这样一个简单的桌面插件显示系统时间和复制号码到拨号盘的小程序就完成了。