在android平台中,显示在HOME界面的一些挂件,即桌面小部件,被称为AppWidget。在自己的程序中适当地加入AppWidget,不但使用户更方便,也能从一定程序上提高本程序的留存率。
下面通过我所写的一个课表应用来说明如何使用AppWidget。
我所写的AppWidget最终结果如下图:
1.首先在res/layout下编写AppWidget的布局文件。
我的代码如下:
appwidget_small.xml
<?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="60dp"
android:background="@drawable/appwidget_bg" >
<Button
android:id="@+id/widget_small_refresh"
android:layout_width="60dp"
android:layout_height="40dp"
android:layout_alignParentRight="true"
android:text="@string/widget_small_refrest"
android:textAppearance="@android:style/TextAppearance.Medium" />
<TextView
android:id="@+id/widget_small_day"
android:layout_width="60dp"
android:layout_height="match_parent"
android:layout_alignParentRight="true"
android:layout_below="@id/widget_small_refresh"
android:gravity="center"
android:textAppearance="@android:style/TextAppearance.Small" />
<include
android:id="@+id/widget_small_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="2dp"
android:layout_marginLeft="2dp"
android:layout_marginTop="2dp"
android:layout_toLeftOf="@id/widget_small_refresh"
layout="@layout/main_list_item"
android:background="@drawable/appwidget_list_bg" />
</RelativeLayout>
其中include的是课表信息部分的布局,它在我的MainActivity还用到,这里没有另外编写,直接使用include标签将它引用进来。
main_list_item.xml的代码如下:
<?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="wrap_content"
android:orientation="horizontal" >
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical" >
<TextView
android:id="@+id/list_item_class"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:singleLine="true"
android:textAppearance="@android:style/TextAppearance.Medium" />
<TextView
android:id="@+id/list_item_time"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:singleLine="true"
android:textAppearance="@android:style/TextAppearance.Small" />
</LinearLayout>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content" >
<LinearLayout
android:layout_width="120dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:orientation="vertical" >
<TextView
android:id="@+id/list_item_course"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:singleLine="true"
android:textAppearance="@android:style/TextAppearance.Medium" />
<TextView
android:id="@+id/list_item_teacher"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:singleLine="true"
android:textAppearance="@android:style/TextAppearance.Medium" />
</LinearLayout>
<LinearLayout
android:layout_width="120dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:orientation="vertical" >
<TextView
android:id="@+id/list_item_room"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:singleLine="true"
android:textAppearance="@android:style/TextAppearance.Medium" />
<TextView
android:id="@+id/list_item_week"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:singleLine="true"
android:textAppearance="@android:style/TextAppearance.Medium" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
在这里说明一下,对于appWidget的布局文件的根标签如果设置宽高为match_parent(fill_partent),则在HOME界面改变它的大小时它也会自动扩张,否则无论将它占的空间拉伸到多大,它都不会扩张。
2.在/res/xml下编写这个appwidget的信息文件。
widget_small_provider_info.xml代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/appwidget_small"
android:minHeight="60dp"
android:minWidth="240dp"
android:updatePeriodMillis="1800000" >
</appwidget-provider>
上面initialLayout即初始的布局,minHeight和minWidth即最小的宽高,updatePeriodMillis为更新周期。
需要注意的是关于这个更新周期,在我本机G14,android4.0.3上发现它并没有用,百度之后发现它存在着BUG,在有些系统有效,有些则没效。所以如果想在任何机型都能自动更新的话,还要自己写一个service去更新。这个可参考android SDK中的例子。
3.接下来需要编写一个类,继承自AppWidgetProvider。
代码如下:
/*
* @(#)TableWidgetProvider.java Project:UniversityTimetable
* Date:2013-2-11
*
* Copyright (c) 2013 CFuture09, Institute of Software,
* Guangdong Ocean University, Zhanjiang, GuangDong, China.
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.lurencun.cfuture09.universityTimetable.appwidget;
import java.util.Calendar;
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.database.Cursor;
import android.util.Log;
import android.widget.RemoteViews;
import com.lurencun.cfuture09.universityTimetable.R;
/**
* @Author Geek_Soledad ([email protected])
* @Function
*/
public class TableSmallWidgetProvider extends AppWidgetProvider {
private static final String TAG = "TableSmallWidgetProvider";
public static final String ACTION_UPDATE = "cfuture09.universityTimetable.action.TIMETABLE.APPWIDGET_SMALL_UPDATE";
@Override
public void onDeleted(Context context, int[] appWidgetIds) {
super.onDeleted(context, appWidgetIds);
Log.d(TAG, "onDeleted");
}
@Override
public void onDisabled(Context context) {
super.onDisabled(context);
Log.d(TAG, "onDisable");
}
@Override
public void onEnabled(Context context) {
super.onEnabled(context);
Log.d(TAG, "onEnabled");
}
@Override
public void onReceive(Context context, Intent intent) {
super.onReceive(context, intent);
Log.d(TAG, "onReceive");
}
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager,
int[] appWidgetIds) {
super.onUpdate(context, appWidgetManager, appWidgetIds);
Log.d(TAG, "update");
}
}
上面onEnabled在第一次创建AppWidget中执行。
在HOME界面中是可以多次插入同一个AppWidget的,每次插入一个AppWidget,onReceive和onUpdate都会被执行,这一次可以自己去做试验了解它的生命周期,在这里不赘述。
4.在Manifest中声明。
<receiver
android:name=".appwidget.TableSmallWidgetProvider"
android:label="@string/widget_small_4_1" >
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<intent-filter>
<action android:name="cfuture09.universityTimetable.action.TIMETABLE.APPWIDGET_SMALL_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_small_provider_info" />
</receiver>
其中label中引用的字符串即在插入Appwidget时出现的那个名字,如这里为"大学课程表(4*1)",如果不添加,默认为你的程序名,即在application标签中声明的label。然后加入的intent-filter,为其接收的广播,这里还定义了自己的一个action,它将在下面的例子中用到,因为我希望还能手动更新appwidget的数据。
在上面的例子中,一个简单的AppWidget就完成了。
但是我需要的还不够,我还希望这个AppWidget的控件内容是可以改变的,而不是写死在布局文件中的,这就需要用到RemoteViews了。
因为在android中,AppWidget与你的主程序是运行在不同的进程当中的,在这里需要用RemoteViews来进行它们之间的通信,而不是像在Activity中那样方便。
而对于RemoteViews在不同版本的API中,支持的控件(亦其提供的方法)也不同,越往后支持的越多,在2.2中,貌似还不支持AbsListView控件的更新,也只提供了简单的onclick事件绑定的方法。
首先创建一个RemoteViwews对象,RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.appwidget_small);
然后可以通过它的setTextViewText方法设置AppWidget中的TextView的内容,传入的参数为要设置的textview的id和内容。
如果想点击它而打开你的程序的activity,或者更新控件,则也如下面代码所示。
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager,
int[] appWidgetIds) {
super.onUpdate(context, appWidgetManager, appWidgetIds);
Log.d(TAG, "onUpdate");
for (int i = 0; i < appWidgetIds.length; i++) {
RemoteViews remoteViews = new RemoteViews(context.getPackageName(),
R.layout.appwidget_small);
// 设置星期几
remoteViews.setTextViewText(R.id.widget_small_day,
arrayWeeks[currentDay]);
// 打开程序
PendingIntent startIntent = PendingIntent.getActivity(context, 0,
new Intent(context, MainActivity.class), 0);
remoteViews.setOnClickPendingIntent(R.id.widget_small_content,
startIntent);
// 更新控件的事件绑定
Intent intent = new Intent();
intent.setAction(ACTION_UPDATE);
// 以发送广播消息的方式创建PendingIntent.
PendingIntent pending_intent = PendingIntent.getBroadcast(context,
0, intent, 0);
remoteViews.setOnClickPendingIntent(R.id.widget_small_refresh,
pending_intent);
appWidgetManager.updateAppWidget(appWidgetIds[i], remoteViews);
}
}
在更新控件中,由于它是发送了一个广播,onReceive将会被执行,但不会执行onUpdate方法,所以这里还需要再修改onReceive方法,否则无法达到更新的目的。
@Override
public void onReceive(Context context, Intent intent) {
Log.d(TAG, "onReceive");
if (ACTION_UPDATE.equals(intent.getAction())) {
RemoteViews remoteViews = new RemoteViews(context.getPackageName(),
R.layout.appwidget_small);
// 设置星期几
remoteViews.setTextViewText(R.id.widget_small_day,
arrayWeeks[currentDay]);
// 更新节课
updateWidgetViews(remoteViews, dto);
PendingIntent startIntent = PendingIntent.getActivity(context, 0,
new Intent(context, MainActivity.class), 0);
remoteViews.setOnClickPendingIntent(R.id.widget_small_content,
startIntent);
ComponentName componentName = new ComponentName(context,
TableSmallWidgetProvider.class);
AppWidgetManager.getInstance(context).updateAppWidget(
componentName, remoteViews);
} else {
super.onReceive(context, intent);
}
}