参照Google的文档,首先在建立一个类继承AppWidgetProvider
import android.appwidget.AppWidgetProvider; public class MyWidget extends AppWidgetProvider { }
<receiver android:name="com.alexchen.widget.MyWidget" > <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>android.appwidget.action.APPWIDGET_UPDATE 表明这个receiver能够接受一个APPWIDGET_UPDATE的广播,而且在这里,只能加入这一个action。
android.appwidget.provider 表明数据类型时widget提供者提供的数据,example_appwidget_info表明这个widget的参数配置文件名和位置
那么接下来就需要在res目录下建立一个xml文件夹,并且在其中建立一个example_appwidget_info.xml的配置文件,Google的文档中给出了示例有很多参数,实际上关键的参数只有下面的4个:
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" android:minWidth="94dp" android:minHeight="72dp" android:updatePeriodMillis="86400000" android:initialLayout="@layout/example_appwidget"> </appwidget-provider>
updatePeriodMillis代表数据更新的时间,这里86400000毫秒实际上是24小时,可能最开始看到这个参数会想我能否将其设的很小,每一秒刷新很多次?,实际上对于updatePeriodMillis这个参数而言,即算你设的再小也没用,Google设定widget控件这个参数控制的最短update时间为30分钟,就算将其设置在30分钟以内也会以30分钟的频率来更新数据。
initialLayout参数代表的是本widget空间的布局文件。
那么下一步就是定义出一个对应的布局文件。我们可以简单的在layout目录下建立一个布局文件example_appwidget.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="200dp" android:layout_height="80dp" android:background="@android:color/white" android:gravity="center_vertical" android:orientation="vertical" > <TextView android:id="@+id/tv_widget" android:layout_width="fill_parent" android:layout_height="wrap_content" android:gravity="center" android:text="widget控件测试" android:textColor="@android:color/black" android:textSize="15sp" /> <Button android:id="@+id/btn_clear" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:text="清理内存" android:textColor="#ff000000" /> </LinearLayout>
在Google的文档中有指出,并非所有的布局组件都可以在上面的这个布局中生效,有效的组件或布局为:
FrameLayout
LinearLayout
RelativeLayout
GridLayout
AnalogClock
Button
Chronometer
ImageButton
ImageView
ProgressBar
TextView
ViewFlipper
ListView
GridView
StackView
AdapterViewFlipper
大部分的Widget小控件都会需要在特定情况下更新上面显示的数据,那么这个是如何实现的呢,我们经过上面的代码不难发现实际上这个widget控件并没有一个Activity,所以说这个控件的显示实际上不是本应用来实现的,它实际上是桌面这个应用来显示的,所以我们也不可能直接去更新它上面的数据。
回过头去看看上面我们写的那个receiver,实际上没有实现任何方法。实际上AppWidgetProvider里面有几个比较重要的方法:onReceive、onUpdate、onDisabled、onEnabled
其中onReceive方法跟大多数广播接收者的onReceive方法一样,但是在这里,onReceive方法的调用并不是我们可以决定的,它依赖于显示该widget控件的Host组件,在这里也就是Android桌面应用,所以我们会发现在不同的手机上,将widget控件拖到桌面上显示的时候onReceive可能调用的次数和先后顺序可能完全不一样,这依赖于Host组件是如何实现的。
所以在这里onReceive方法对于我们刷新widget数据基本没有什么帮助。
而onUpdate方法则是由上面所说的updatePeriodMillis参数来控制的,经过上面的分析,我们都知道了,它的最小周期为30分钟。所以我们一般将这个参数设为0即可。那么在这个方法里,我们往往会在其中放置一些启动更新数据服务的功能,因为如果后台的更新数据的Service被意外停止了,那么每30分钟还会被重新启用,不至于一直启动不了了:
@Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { super.onUpdate(context, appWidgetManager, appWidgetIds); // System.out.println("onUpdate"); //每隔一段时间重新启动服务,防止服务中间被终止了之后没法重启 Intent intent = new Intent(context, UpdateWidgetService.class); context.startService(intent); }
我们知道,widget小控件是可以拖动多个到桌面上的,而onEnabled方法会在第一个widget控件拖到桌面上的时候调用一次,onDisabled会在最后一个widget控件从桌面被删除时调用一次,那么我们需要做的就是在onEnabled这个方法中启用一个刷新widget数据的服务,在onDisabled方法中使用stopService方法来停止这个服务。
@Override public void onDisabled(Context context) { super.onDisabled(context); System.out.println("onDisabled"); //停止数据刷新服务 Intent intent = new Intent(context, UpdateWidgetService.class); context.stopService(intent); } @Override public void onEnabled(Context context) { super.onEnabled(context); System.out.println("onEnabled"); //开启数据刷新服务 Intent intent = new Intent(context, UpdateWidgetService.class); context.startService(intent); }
那么下面的任务就只剩下UpdateWidgetService这个刷新数据的服务(Service)如何实现的问题了。
我们在这里的想法很简单,比如说每隔三秒钟来刷新一下widget中的数据。Android中定时执行任务的方法有很多,我们这里使用Timer和TimerTask来实现,之后我们需要关心的就是具体如何实现刷新widget中的数据,毕竟这些数据是在桌面应用中显示的。
并且我们需要用到一个API--AppWidgetManager,它有一个实例方法AppWidgetManager.updateAppWidget(ComponentName provider, RemoteViews views)来实现更新widget数据,我们都知道,如果需要调用另外应用的方法,需要使用远程调用的方法来实现,在这里起到在我们的应用和桌面应用之间的桥梁作用的就是这第二个参数:RemoteViews views,它会将我们设置的数据传送到桌面应用来刷新widget上的数据,我们需要经过下面几步:
1、定义一个RemoteViews的实例:
RemoteViews views = new RemoteViews(getPackageName(),R.layout.process_widget);
其中viewId是在我们前面定义的widget布局文件中的子组件的id,也就是我们要刷新内容的对象,这里就是R.id.tv_test,第二个参数是我们要更新的内容
3、定义好第一个参数ComponentName provider之后,就可以调用AppWidgetManager.updateAppWidget(ComponentName provider, RemoteViews views)来实现更新数据
if (timer == null && task == null) { //AppWidgetManager对象,用于更新widget的数据 awm = AppWidgetManager.getInstance(this); timer = new Timer(); task = new TimerTask() { @Override public void run() { ComponentName provider = new ComponentName(UpdateWidgetService.this, MyWidget.class); //远程view对象,用于在本应用和桌面应用中起传递数据的桥梁作用 RemoteViews views = new RemoteViews(getPackageName(),R.layout.example_appwidget); views.setTextViewText(R.id.tv_widget, "想刷新的数据的内容"); awm.updateAppWidget(provider, views); System.out.println("====刷新了widget===="); } //设置循环时间 timer.schedule(task, 0, 3000); }
要实现这个功能,我们需要再上面定时刷新数据服务中将定时刷新的内容改为当前内存所剩余的量,我们这里写一个工具类方法来实现返回内存剩余量;
另外我们还需要在widget控件的布局文件中添加一个button,并在更新widget数据的服务中,设置这个button的点击事件,但是这里也不像以前的点击事件,同样要应用到RemoteView对象,在这个点击事件中需要发送一个广播,Action为自定义的,我们这里设为:"com.alexchen.mobilesafeexercise.killall",之后,我们需要再写一个广播接收者,来接收这个广播,在onReceive方法中执行杀死后台进程的操作。这里也不能直接使用Intent,由于我们这个意图的Action不是由我们自己执行而是由其他应用程序(桌面应用)执行的,所以需要用到PendingIntent。
刷新widget数据的服务代码:
package com.alexchen.widget.service; import java.util.Timer; import java.util.TimerTask; import android.app.PendingIntent; import android.app.Service; import android.appwidget.AppWidgetManager; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.IBinder; import android.text.format.Formatter; import android.widget.RemoteViews; import com.alexchen.widget.MyWidget; import com.alexchen.widget.R; import com.alexchen.widget.utils.SystemInfoUtils; public class UpdateWidgetService extends Service { private Timer timer; private TimerTask task; private AppWidgetManager awm; @Override public IBinder onBind(Intent intent) { return null; } @Override public void onCreate() { super.onCreate(); startTimer(); } private void startTimer() { if (timer == null && task == null) { awm = AppWidgetManager.getInstance(this); timer = new Timer(); task = new TimerTask() { @Override public void run() { ComponentName provider = new ComponentName( UpdateWidgetService.this, MyWidget.class); RemoteViews views = new RemoteViews(getPackageName(), R.layout.example_appwidget); views.setTextViewText(R.id.tv_widget, "dd"); views.setTextViewText(R.id.tv_widget, "可用内存:"+ Formatter.formatFileSize(getApplicationContext(), SystemInfoUtils.getAvailableMem(getApplicationContext()))); // 自定义一个广播,杀死后台进程的事件 Intent intent = new Intent(); intent.setAction("com.alexchen.mobilesafeexercise.killall"); // 描述一个动作,这个动作是由另外一个应用程序执行的 PendingIntent pendingIntent = PendingIntent.getBroadcast( getApplicationContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); views.setOnClickPendingIntent(R.id.btn_clear, pendingIntent); awm.updateAppWidget(provider, views); System.out.println("====刷新了widget===="); } }; timer.schedule(task, 0, 3000); } } @Override public void onDestroy() { super.onDestroy(); stopTimer(); unregisterReceiver(offReceiver); unregisterReceiver(onReceiver); } private void stopTimer() { if (timer != null && task != null) { timer.cancel(); task.cancel(); task = null; timer = null; } } }
按键清理内存的广播接收者:
package com.alexchen.widget.receiver; import java.util.List; import android.app.ActivityManager; import android.app.ActivityManager.RunningAppProcessInfo; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; public class KillAllReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { System.out.println("自定义的广播消息接收到了...开始清理内存..."); ActivityManager am = (ActivityManager) context .getSystemService(Context.ACTIVITY_SERVICE); List<RunningAppProcessInfo> runningAppProcesses = am .getRunningAppProcesses(); for (RunningAppProcessInfo info : runningAppProcesses) { am.killBackgroundProcesses(info.processName); } } }
/** * 获取手机可用的剩余内存 * * @param context * 上下文 * @return */ public static long getAvailableMem(Context context) { ActivityManager am = (ActivityManager) context .getSystemService(Context.ACTIVITY_SERVICE); MemoryInfo outInfo = new MemoryInfo(); am.getMemoryInfo(outInfo); return outInfo.availMem; }