简介
App Widgets 是一个可以嵌入到其他app(比如 HomeScreen)的能接收到周期性更新的一个微型的App 界面。这个view对用户来说相当于一个小插件或者小部件。如果我们把这个小部件放到桌面的话,可以方便用户不用打开app而能查看到重要的消息。
重要的类
AppWidgetProviderInfo
一个描述appWIdget
的类,此类中的信息表示我们的app widget layout
、最小高宽度或者更新频率等等信息。
AppWidgetProvider
这个类非常重要,我们在开发的时候也会继承它。这个类集成自broadcastreceiver
,而且我们可以跳进它源码中看到,在它的onReceive()
里面分发了很多事件,比如更新app widget
会分发到onUpdate()
,第一次添加app widget
会分发到onEnable()
,最后一个app widget
删除后,会调用onDisable()
等等。
RemoteViews
一个用于远程传输的view,其实它不是一个view,它会把我们的view和对该view的操作用action
封装起来,然后序列化传输到另外的进程,然后反序列化,然后再在另外的进程对view进行我们要进行的操作。
AppWidgetManager
连接app进程和我们widget所处的进程的一个桥梁。后面详细讲。
用法
我们知道,一个app widget
是放在homeScreen或者锁屏界面的,所以它实际上是运行在其他进程的,所以这里得用到进程间通信。这也是为什么开发一个app widget
很麻烦的原因。
AndroidManifest.xml
首先在我们的AndroidManifest.xml
中声明一个AppWidgetProvider
用于接收app widget的更新事件。AppWidgetProvider
实际上是一个广播,所以注册方式和广播类似。
<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>
上面代码中有一个meta-data
,后面的name必须是android.appwidget.provider
。因为系统正是根据这个来找到我们widget的配置文件的,下面resource就是它的配置文件,它对应着上面的AppWidgetProviderInfo
。我们在resource中配置的所有配置项都可以在这个类中找到。
xml/example_appwidget_info.xml
:
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="40dp"
android:minHeight="40dp"
android:updatePeriodMillis="86400000"
android:previewImage="@drawable/preview"
android:initialLayout="@layout/example_appwidget"
android:configure="com.example.android.ExampleAppWidgetConfigure"
android:resizeMode="horizontal|vertical"
android:widgetCategory="home_screen">
</appwidget-provider>
-
android:updatePeriodMillis
:更新时间(默认30min),每过一段时间,系统会给我们的AppWidgetProvider
发送一个update消息,我们可以在AppWidgetProvider
中的onUpdate()
中来处理更新widget界面。 -
android:previewImage
:预览图标,用户在桌面选择widget的时候可以看到这个图标。和ic_launcher
有点类似的感觉。 -
android:initialLayout
:layout,不用说都知道 -
android:configure
:配置。要是有需要的话,用户在添加widget的时候会打开这个Activity。来自定义配置相关的参数。 -
android:resizeMode
:widget可以被拉伸的方向。horizontal表示可以水平拉伸,vertical表示可以竖直拉伸 -
android:widgetCategory
: widget可以显示的地方,可以是桌面,也可以是锁屏。
在添加configure
的时候,我们也要在AndroidMainfest.xml中为要打开的Activity设置intent-filter:
<activity android:name=".ExampleAppWidgetConfigure">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
</intent-filter>
</activity>
layout
由于widget不是跑在我们自己的进程,所以对其界面有限制,因为系统会用到binder
通信,把我们的传到另外一个进程去。
widget现在支持的布局有:
FrameLayout
LinearLayout
RelativeLayout
GridLayout
支持的控件有:
AnalogClock
Button
Chronometer
ImageButton
ImageView
ProgressBar
TextView
ViewFlipper
ListView
GridView
StackView
AdapterViewFlipper
其实RemoteViews
也支持ViewStub
。
margin
在android4.0以前,我们得为widget的边缘添加margin,因为它不能触碰到屏幕边界。
在android 4.0以后,系统默认为我们添加了padding,所以我们在android4.0以后不用管。
AppWidgetProvider
我们的widget对应着AppWidgetProvider
,我们在AppWidgetProvider
中接收系统发给我们的更新、添加、删除widget消息。
onUpdate()
我们可以在这个函数里面做一些更新界面的事情。当updatePeriodMillis
所设置的时候到达后,就会运行这个函数。
onEnable()
用户添加第一个widget的时候会调用
onDisable()
和onEnable相反,删除最后一个widget的时候调用。
onAppWidgetOptionsChanged()
widget尺寸改变的时候调用(用户可以调整大小)。
onDeleted()
用户每次删除widget的时候调用
onReceive(Context, Intent)
上述函数都在这个函数里面分发。也就是说,实际上系统是给app发的一个个广播事件。在这个函数中根据不同的事件,调用上述不同的函数。
public class ExampleAppWidgetProvider extends AppWidgetProvider {
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
final int N = appWidgetIds.length;//该widget个数
for (int i=0; i<N; i++) {
int appWidgetId = appWidgetIds[i];
//pendingIntent是一种还没有发生的intent
//我们可以用pendingIntent来为view设置一个onClick后调用的Intent事件。
//这里是当用户点击了R.id.button按钮后,用intent打开ExampleActivity。
Intent intent = new Intent(context, ExampleActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.appwidget_provider_layout);
views.setOnClickPendingIntent(R.id.button, pendingIntent);
//用appwidgetManager来通知系统更新widget
appWidgetManager.updateAppWidget(appWidgetId, views);
}
}
}
上面代码只是一点击widget是某个button打开activity的例子。当然我们也可以在这里用PendingIntent来设置一个事件,让用户点击某个控件后可以向我们发送一个广播,然后在接收到广播后做我们想做的事情。
Intent intent = new Intent();
intent.addAction(xxx);
PendingIntent ppendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0);
当然,我们得在ExampleAppWidgetProvider
加入相应的action。
到这里,widget基本的操作已经完成。
Widget配合ListView等Collections使用
在我们日常开发中,ListView
等Collections的使用是必不可少的,这一节就讲一下关于如何在widget中使用collections,我这里用listview为代表,其他的都类似。
前面没有Collections
的时候,直接用RemoteViews
就可以直接填充视图,然后加上Listview后,你会发现,没法来填充ListView里面的Item。
这里就要介绍下在用Collections
的时候,其他几个重要的类了:
-
RemoteViewsService
:一个连接widget和app进程的service,我们可以继承这个类,来提供一个adapter给ListView/GridView
。 -
RemoteViewsService.RemoteViewsFactory
:一个类似BaseAdapter一样的东西,我们可以直接把它看成ListView的Adapter。
有了这两个类就好说了。首先RemoteViewsService
是一个Service,所以我们得在manifest.xml
里面注册,而且得申明绑定widget的权限。
<service android:name="MyWidgetService"
...
android:permission="android.permission.BIND_REMOTEVIEWS" />
申明后,我们的ListView得这么用(官方栗子):
public void onUpdate(Context context, AppWidgetManager appWidgetManager,
int[] appWidgetIds) {
//初始化listView并指定adapter
for (int i = 0; i < appWidgetIds.length; ++i) {
//先new一个intent来启动service
//这个service会提供给我们listview的item的界面。
Intent intent = new Intent(context, MyWidgetService.class);
//把app widget id 放到intent里面
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]);
intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
//widget_layout.xml里面包含一个listview
RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.widget_layout);
//为listview指定一个 remote adapter,这个adapter会用intent连接到一个特定的service(MyWidgetService)
//我们后面就可以在service里面来提供数据。
rv.setRemoteAdapter(appWidgetIds[i], R.id.list_view, intent);
//为我们的listview指定一个empty view,这个empty view得也在上面的widget_layout里面
rv.setEmptyView(R.id. , R.id.empty_view);
//这里为了让我们的item可以设置点击事件,我们得为listview设置一个pending intent template(一个pendingIntent模板)
//然后后面我们会在adapter里面为每个item设置一个fillInIntent
Intent toastIntent = new Intent(context, ExampleAppWidgetProvider.class);
//这里设置了一个action。
//ExampleAppWidgetProvider.TOAST_ACTION是我们自定义的一个action。这里得把这个action添加到广播接收者里面。以便我们能接收到按下item后的广播事件。
toastIntent.setAction(ExampleAppWidgetProvider.TOAST_ACTION);
toastIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]);
intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
PendingIntent toastPendingIntent = PendingIntent.getBroadcast(context, 0, toastIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
rv.setPendingIntentTemplate(R.id.list_view, toastPendingIntent);
appWidgetManager.updateAppWidget(appWidgetIds[i], rv);
}
super.onUpdate(context, appWidgetManager, appWidgetIds);
}
下面我们来看看MyWidgetService
:
public class MyWidgetServiceextends RemoteViewsService {
@Override
public RemoteViewsFactory onGetViewFactory(Intent intent) {
//为widget提供一个adapter
return new MyWidgetService(this.getApplicationContext(), intent);
}
}
class MyWidgetService implements RemoteViewsService.RemoteViewsFactory {
private static final int mCount = 10;
private List<WidgetItem> mWidgetItems = new ArrayList<WidgetItem>();
private Context mContext;
private int mAppWidgetId;
public MyWidgetService(Context context, Intent intent) {
mContext = context;
mAppWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID);
}
// 初始化data
public void onCreate() {
//这里不建议下载或者做一些很重的操作,因为这里停留20s,你就会收到一个ANR的奖励。
for (int i = 0; i < mCount; i++) {
mWidgetItems.add(new WidgetItem(i + "!"));
}
...
}
...
//相当于baseAdapter的getView()方法。
public RemoteViews getViewAt(int position) {
//这里用layout创建一个RemoteVies。
RemoteViews rv = new RemoteViews(mContext.getPackageName(), R.layout.widget_item);
rv.setTextViewText(R.id.widget_item, mWidgetItems.get(position).text);
//这里用fill-intent来填充前面设置的Intent模板
Bundle extras = new Bundle();
extras.putInt(ExampleAppWidgetProvider.EXTRA_ITEM, position);
Intent fillInIntent = new Intent();
fillInIntent.putExtras(extras);
//为item设置intent onClick。当点击后,会发出一个特定intent的广播。我们在ExampleAppWidgetProvider里面接收广播就可以做我们想做的事了。
//但是这里还是要记得在AndroidManifest.xml里面ExampleAppWidgetProvider申明接收这个action。
rv.setOnClickFillInIntent(R.id.widget_item, fillInIntent);
...
// 返回view
return rv;
}
...
}
但是如果我们要更新listview中的数据的话,下面官方文档里面给出一一幅图,很好地描述了工作流程,我们只要更新数据后,调用AppWidgetManager.notifyAppWidgetViewDataChanged()
就可以了。
大坑提醒:要是在开发中发现程序跑出来的效果不是我们所想象的, 最好把app下载了,然后重新添加桌面小插件可能就解决问题了,我在实际开发的时候,被这个问题坑了好几次==。
RemoteViews的工作原理
看了上面的开发流程,我们来大概分析一下它的工作原理。
RemoteViews
主要用于通知栏和桌面小部件,然而通知栏和桌面小部件又是分别由NotificationManager
和AppWidgetManager
管理的。而我们在开发的时候,经常还很把它们和NotificationManagerService
或者AppWidgetService
一起使用。而我们在其他Android开发的时候,经常会遇到各种Manager。还有很多的Service
,比如WindowManagerService
与WindowManager
。用多了我们就会发现,它们的用法有很多相似的地方。
要是稍微观察一下它们的工作机制的话,就会发现它们都是差不多的。
其实AppWidgetManager
是通过Binder
和SystemServer
进程中的AppWidgetService
通信的。
由此可见,桌面小插件实际上是在AppWidgetService
被加载的。而它们又是运行的SystemServer中,这就满足了跨进程通信的条件。
我们发现RemoteViews
实际上是实现了Parcelable
接口的,所以它也是便于在Binder中进行跨进程传输的。
还记得我们在new RemoteViews的时候,是传递了一个包名进去的么?
其实系统会根据RemoteViews
中指定的包名去得到相应的资源,然后用LayoutInflater来加载布局文件。这就在SystemServer中加载了一个View了。然后还记得我们对View的操作都是用set来完成的么?比如:
rv.setTextViewText(R.id.text_view,"hello");
然后来看看源码:
public void setTextViewText(int viewId, CharSequence text) {
setCharSequence(viewId, "setText", text);
}
public void setCharSequence(int viewId, String methodName, CharSequence value) {
addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR_SEQUENCE, value));
}
我们发现为viewsetText
的时候,本来平常view是通过view.setText(xxx)
来完成的,刚好这里把我们的id和方法setText还有text记录下来,然后new了一个action
.其实这里的ReflectionAction
就是继承Action
的。然后再addAction();
其实在RemoteViews里面有一个ArrayList<Action>
。addAction()会把我们的Action放到list
里面。当把这个RemoteViews
传输到SystemServer的时候 ,系统会调用RemoteViews
的apply()
方法:
public View apply(Context context, ViewGroup parent, OnClickHandler handler) {
RemoteViews rvToApply = getRemoteViewsToApply(context);
View result;
LayoutInflater inflater = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
// Clone inflater so we load resources from correct context and
// we don't add a filter to the static version returned by getSystemService.
inflater = inflater.cloneInContext(inflationContext);
inflater.setFilter(this);
result = inflater.inflate(rvToApply.getLayoutId(), parent, false);
rvToApply.performApply(result, parent, handler);
return result;
}
印证了刚刚说了用inflater加载布局,然后再调用perforApply()
方法:
private void performApply(View v, ViewGroup parent, OnClickHandler handler) {
if (mActions != null) {
handler = handler == null ? DEFAULT_ON_CLICK_HANDLER : handler;
final int count = mActions.size();
for (int i = 0; i < count; i++) {
Action a = mActions.get(i);
a.apply(v, parent, handler);
}
}
}
遍历Action ,然后调用其apply()
方法。我们来看看刚刚的ReflectionAction
的apply()
方法:
public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
final View view = root.findViewById(viewId);
if (view == null) return;
Class<?> param = getParameterType();
if (param == null) {
throw new ActionException("bad type: " + this.type);
}
try {
getMethod(view, this.methodName, param).invoke(view, wrapArg(this.value));
} catch (ActionException e) {
throw e;
} catch (Exception ex) {
throw new ActionException(ex);
}
}
豁然开朗,用反射调用我们实际操作的view.setText()
。
首先序列化,再通过Binder传递对象,然后再在SystemServer中来apply我们的操作。这就完成了我们想要的操作,方法非常巧妙。
总结
跨进程操作很巧妙,我们可以通过学习google的做法,在我们实际开发当中,也有不少的跨进程操作,我们也可以用这种方式来实现操作其他对象。特别是在开放SDK中,这种方式非常有帮助,异步调用要执行的代码。