5.1 RemoteViews的应用
RemoteViews在实际开发中,主要用在通知栏和桌面小部件的开发过程中。通知栏每个人都不陌生,主要是通过NotificationManager的notify方法来实现的,它除了默认效果外,还有可以另外定制布局。桌面小部件则是通过AppWidgetProvider来实现的,AppWidgetProvider本质上是一个广播。通知栏和桌面小部件的开发过程中都会用到RemoteViews,它们在更新界面时无法像在Activity里面那样去直接更新View,这是因为二者的界面都运行在其他进程中,确切来说是系统的SystemServer进程。为了跨进程更新界面,RemoteViews提供了一系列set方法,并且这些方法只是View全部方法的子集,另外RemoteViews中所支持的View类型也是有限的,这一点会在5.2节中进行详细说明。下面简单介绍一下RemoteViews在通知栏和桌面小部件中的使用方法,至于它们更详细的使用方法请读者阅读相关资料即可,本章的重点是分析RemoteViews的内部机制。
5.1.1 RemoteViews在通知栏上的应用
首先我们看一下RemoteViews在通知栏上的应用,我们知道,通知栏除了默认的效果外还支持自定义布局,下面分别说明这两种情况。
使用系统默认的样式弹出一个通知是很简单的,代码如下:
Notification notification = new Notification();
notification.icon = R.drawable.ic_launcher;
notification.tickerText = "hello world";
notification.when = System.currentTimeMillis();
notification.flags = Notification.FLAG_AUTO_CANCEL;
Intent intent = new Intent(this, DemoActivity_1.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
notification.setLatestEventInfo(this, "chapter_5", "this is notification.", pendingIntent);
NotificationManager manager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
manager.notify(1, notification);
上述代码会弹出一个系统默认样式的通知,单击通知后会打开DemoActivity_1同时会清除本身。为了满足个性化需求,我们还可能会用到自定义通知。自定义通知也很简单,首先我们要提供一个布局文件,然后通过RemoteViews来加载这个布局文件即可改变通知的样式,代码如下所示。
Notification notification = new Notification();
notification.icon = R.drawable.ic_launcher;
notification.tickerText = "hello world";
notification.when = System.currentTimeMillis();
notification.flag = Notification.FLAG_AUTO_CANCEL;
Intent intent = new Intent(this, DemoActivity_1.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.layout_notification);
remoteViews.setTextViewText(R.id.msg, "chapter_5");
remoteViews.setImageViewResource(R.id.icon, R.drawable.icon1);
PendingIntent openActivity2PendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, DemoActivity_2.class), PendingIntent.FLAG_UPDATE_CURRENT);
remoteViews.setOnClickPendingIntent(R.id.open_activity2, openActivity2PendingIntent);
notification.contentView = remoteViews;
notification.contentIntent = pendingIntent;
NotificationManager manager = (NotifitionManager)getSystemService(Context.NOTIFICATION_SERVICE);
manager.notify(2, notification);
从上述内容来看,自定义通知的效果需要用到RemoteViews,自定义通知的效果如图5-1所示。
RemoteViews的使用也很简单,只要提供当前应用的包名和布局文件的资源id即可创建一个RemoteViews对象。如何更新RemoteViews呢?这一点和更新View有很大的不同,更新RemoteViews时,无法直接访问里面的View,而必须通过RemoteViews所提供的一系列方法来更新View。比如设置TextView的文本,要采用如下方式:remoteViews.setTextViewText(R.id.msg, "chapter_5"),其中setTextViewText的两个参数分别为TextView的id和要设置的文本。而设置ImageView的图片也不能直接访问ImageView,必须通过如下方式:remoteViews.setImageViewResource(R.id.icon, R.drawable.icon1),setImageViewResource的两个参数分别为ImageView的id和要设置的图片资源的id。如果要给一个控件加单击事件,则要使用PendingIntent并通过setOnClickIntent方法来实现,比如remoteViews.setOnClickPendingIntent(R.id.open_activity_2, openActivity2PendingIntent)这句代码会给id为open_activity2的View加上单击事件。关于PendingIntent,它表示的是一种待定的Intent,这个Intent中所包含的意图必须由用户来触发。为什么更新RemoteViews如此复杂呢?直观原因是因为RemoteViews并没有提供和View类似的findViewById这个方法,因此我们无法获取到RemoteViews中的子View,当然实际原因绝非如此,具体会在5.2节中进行详细介绍。
5.1.2 RemoteViews在桌面小部件上的应用
AppWidgetProvider是Android中提供的用于实现桌面小部件的类,其本质是一个广播,即BroadcastReceiver,图5-2表示的是它的类继承关系。所以,在实际的使用中,把AppWidgetProvider当成一个BroadcastReceiver就可以了,这样许多功能就很好理解了。
[图片上传失败...(image-42303b-1585928039011)]
为了更好地展示RemoteViews在桌面小部件上的应用,我们先简单介绍桌面小部件的开发步骤,分为如下几步。
1. 定义小部件界面
在res/layout/下新建一个XML文件,命名为widget.xml,名称和内容可以自定义,看这个小部件要做成什么样子,内容如下所示。
2. 定义小部件配置信息
在res/xml/下新建appwidget_provider_info.xml,名称随意选择,添加如下内容:
上面几个参数的意义很明确,initialLayout就是指工具所使用的初始化布局,minHeight和minWidth定义小工具的最小西村,updatePeriodMillis定义小工具的自动更新周期,毫秒为单位,每隔一个周期,小工具的自动更新就会触发。
3. 定义小部件的实现类
这个类需要继承AppWidgetProvider,代码如下:
public class MyAppWidgetProvider extends AppWidgetProvider {
public static final String TAG = "MyAppWidgetProvider";
public static final String CLICK_ACTION ="com.chenstyle.chapter_5.action.CLICK";
public MyAppWidgetProvider() {
super();
}
@Override
public void onReceive(final Context context, Intent intent) {
super.onReceive(context, intent);
Log.i(TAG, "onReceive : action = " + intent.getAction());
// 这里判断是自己的action,做自己的事情,比如小部件被单击了要干什么,这里是做一个动画效果
if (intent.getAction().equals(CLICK_ACTION)) {
Toast.makeText(context, "clicked it", Toast.LENGTH_SHORT).show();
new Thread(new Runnable(){
@Override
public void run() {
Bitmap srcbBitmap = BitmapFactory.decodeResource(context.getResources(). R.drawable.icon1);
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
for (int i = 0; i < 37; i++) {
float degree = (i * 10) % 360;
RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget);
remoteViews.setImageViewBitmap(R.id.imageView1, rotateBitmap(context, srcbBitmap, degree));
Intent intentClick = new Intent();
intentClick.setAction(CLICK_ACTION);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intentClick, 0);
remoteViews.setOnClickPendingIntent(R.id.imageView1, pendingIntent);
appWidgetManager.updateAppWidget(new ComponentName(context, MyAppWidgetProvider.class), remoteViews);
SystemClick.sleep(30);
}
}
}).start();
}
}
/**
* 每次桌面小部件更新时都调用一次该方法
*/
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
super.onUpdate(context, appWidgetManager, appWidgetIds);
Log.i(TAG, "onUpdate");
final int counter = appWidgetIds.length;
Log.i(TAG, "counter = " + counter);
for (int i = 0; i < counter; i++) {
int appWidgetId = appWidgetIds[i];
onWidgetUpdate(context, appWidgetManager, appWidgetId);
}
}
/**
* 桌面小部件更新
*
* @param context
* @param appWidgetManager
* @param appWidgetId
*/
private void onWidgetUpdate(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {
Log.i(TAG, "appWidgetId = " + appWidgetId);
RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget);
// “桌面小部件”单击事件发送的Intent广播
Intent intentClick = new Intent();
intentClick.setAction(CLICK_ACTION);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intentClick, 0);
remoteViews.setOnClickPendingIntent(R.id.imageView1, pendingIntent);
appWidgetManager.updateAppWidget(appWidgetId, remoteViews);
}
priavte Bitmap rotateBitmap(Context context, Bitmap srcbBitmap, float degree) {
Matrix matrix = new Matrix();
matrix.reset();
matrix.setRotate(degree);
Bitmap tmpBitmap = Bitmap.createBitmap(srcbBitmap, 0, 0, srcbBitmap.getWidth(), srcbBitmap.getHeight(), matrix, true);
return tmpBitmap;
}
}
上面的代码实现了一个简单的桌面小部件,在小部件上面显示一张图片,单击它后,这个图片就会旋转一周。当小部件被添加到桌面后,会通过RemoteViews来加载布局文件,而当小部件被单击后的旋转效果则是通过不断的更新RemoteViews来实现的,由此可见,桌面小部件不管是初始化界面还是后续的更新界面都必须使用RemoteViews来完成。
4. 在AndroidManifest.xml中声明小部件
这是最后一步,因为桌面小部件本质上是一个广播组件,因此必须要注册,如下所示。
上面的代码中有两个Action,其中第一个Action用于识别小部件的单击行为,而第二个Action则作为小部件的标识而必须存在,这是系统的规范,如果不加,那么这个receiver就不是一个桌面小部件并且也无法出现在手机的小部件列表里。
AppWidgetProvider除了最常用的onUpdate方法,还有其他几个方法:onEnabled、onDisabled、onDeleted以及onReceive。这些方法会自动的被onReceive方法在合适的时间调用。确切来说,当广播到来以后,AppWidgetProvider会自动根据广播的Action通过onReceive方法来自动分发广播,也就是调用上述几个方法。这几个方法的调用时机如下所示。
- onEnable:当该窗口小部件第一次添加到桌面时调用该方法,可添加多次但只在第一次调用。
- onUpdate:小部件被添加时或者每次小部件更新时都会调用一次该方法,小部件的更新时机由updatePeriodMillis来指定,每个周期小部件都会自动更新一次。
- onDeleted:没删除一次桌面小部件就调用一次。
- onDisabled:当最后一个该类型的桌面小部件被删除时调用该方法,注意是最后一个。
- onReceive:这是广播的内置方法,用于分发具体的事件给其他方法。
关于AppWidgetProvider的onReceive方法的具体分发过程,可以参看源码中的实现,如下所示。通过下面的代码可以看出,onReceive中会根据不同的Action来分别调用onEnable、onDisable和onUpdate等方法。
public void onReceive(Context context, Intent intent) {
// Protect against rogue update broadcasts (not really a security issue,
// just filter bad broadcasts out so subclasses are less likely to crash).
String action = intent.getAction();
if (AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(action)) {
Bundle extras = intent.getExtras();
if (extras != null) {
int[] appWidgetIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWINDGET_IDS);
if (appWidgetIds != null && appWidgetIds.length > 0) {
this.onUpdate(context, AppWidgetManager.getInstance(context), appWidgetIds);
}
}
} else if (AppWidgetManager.ACTION_APPWIDGET_DELETED.equals(action)) {
Bundle extras = intent.getExtras();
if (extras != null && extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_ID) {
final int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID);
this.onDeleted(context, new int[] {appWidgetId});
}
} else if (AppWidgetManager.ACTION_APPWIDGET_OPTIONS_CHANGED.equals(action)) {
Bundle extras = intent.getExtras();
if (extras != null && extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_ID) && extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS)) {
int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID);
Bundle widgetExtras = extras.getBundle(AppWidgetManager.EXTRA_APPWIDGET_OPTIIONS);
this.onAppWidgetOptionsChanged(context, AppWidgetManager.getInstance(context), appWidgetId, widgetExtras);
}
} else if (AppWidgetManager.ACTION_APPWIDGET_ENABLED.equals(action)) {
this.enabled(context);
} else if (AppWidgetManager.ACTION_APPWIDGET_DISABLED.equals(action)) {
this.disabled(context);
} else if (AppWidgetManager.ACTION_APPWIDGET_RESTORED.equals(action)) {
Bundle extras = intent.getExtras();
if (extras != null) {
int[] oldIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_OLD_IDS);
int[] newIds = extras.gerIntArray(AppWidgetManager.EXTRA_APPWIDGET_IDS);
if (oldIds != null && oldIds.length > 0) {
this.onRestored(context, oldIds, newIds);
this.onUpdate(context, AppWidgetManager.getInstance(context), newIds);
}
}
}
}
上面描述了开发一个桌面小部件的典型过程,例子比较简单,实际开发中会稍微复杂一些,但是开发流程是一样的。可以发现,桌面小部件在界面上的操作都要通过RemoteViews,不管是小部件的界面初始化还是界面更新都必须依赖它。
5.1.3 PendingIntent概述
在5.2.1节中,我们多次提到PendingIntent,那么PendingIntent到底是什么东西呢?它和Intent的区别是什么呢?在本节中将介绍PendingIntent的使用方法。
顾名思义,PendingIntent表示一种处于pending状态的意图,而pending状态表示的是一种待定、等待、即将发生的意思,就是说接下来有一个Intent(即意图)将在某个待定的时刻发生。可以看出PendingIntent和Intent的区别在于,PendingIntent是在将来的某个不确定的时刻发生,而Intent是立刻发生,PendingIntent典型的使用场景是给RemoteViews添加单击事件,因为RemoteViews运行在远程进程中,因此RemoteViews不同于普通的View,所以无法直接像View那样通过setOnClickListener方法来设置单击事件。想要给RemoteViews设置单击事件,就必须使用PendingIntent,PendingIntent通过send和cancel方法来发送和取消特定的待定Intent。
PendingIntent支持三种待定意图:启动Activity、启动Service和发送广播,对应着它的三个接口方法,如表5-1所示。
表5-1 PendingIntent的主要方法
PendingIntent | 方法 |
---|---|
static PendingIntent | getActivity(Context context, int requestCode, Intent intent, int flags) 获得一个PendingIntent,该待定意图发生时,效果相当于 Context.startActivity(Intent) |
static PendingIntent | getService(Context context, int requestCode, Intent intent, int flags) 获得一个PendingIntent,该待定意图发生时,效果相当于 Context.startService(Intent) |
static PendingIntent | getBroadcast(Context context, int requestCode, Intent intent, int flags) 获得一个PendingIntent,该待定意图发生时,效果相当于 Context.sendBroadcast(Intent) |
如表5-1所示,getActivity、getService和getBroadcast这三个方法的参数和意义都是相同的,第一个和第三个比较好理解,这里主要说下第二个参数requestCode和第四个参数flags,其中requestCode表示PendingIntent发送方的请求码,多数情况下设为0即可,另外requestCode会影响到flags的效果。flags常见的类型有:FLAG_ONE_SHOT、FLAG_NO_CREATE、FLAG_CANCEL_CURRENT和FLAG_UPDATE_CURRENT,在说明这四个标记位之前,必须要明白一个概念,那就是PendingIntent的匹配规则,即在什么情况下两个PendingIntent是相同的。
PendingIntent的匹配规则为:如果两个PendingIntent它们内部的Intent相同并且request也相同,那么这两个PendingIntent就是相同的。requestCode相同比较好理解,那么什么情况下Intent相同呢?Intent的匹配规则是:如果两个Intent的ComponentName和intent-filter都相同,那么这两个Intent就是相同的。需要注意的是Extra不参与Intent的匹配过程,只要Intent之间的ComponentName和intent-filter相同,即使它们的Extras不同,那么这两个intent也是相同的。了解了PendingIntent的匹配规则后,就可以进一步理解flags参数的含义了,如下所示。
FLAG_ONE_SHOT
当前描述的PendingIntent只能被使用一次,然后它就会被自动cancel,如果后续还有相同的PendingIntent,那么它们的send方法就会调用失败。对于通知栏消息来说,如果采用此标记位,那么同类的通知只能使用一次,后续的通知单击后将无法打开。
FLAG_NO_CREATE
当前描述的PendingIntent不会主动创建,如果当前PendingIntent之前不存在,那么getActivity、getService和getBroadcast方法会直接返回null,即获取PendingIntent失败。这个标记位很少见,它无法单独使用,因此在日常开发中它并没有太多的使用意义,这里就不再过多介绍了。
FLAG_CANCEL_CURRENT
当前描述的PendingIntent如果已经存在,那么它们都会被cancel,然后系统会创建一个新的PendingIntent。对于通知栏消息来说,那些被cancel的消息单击后将无法打开。
FLAG_UPDATE_CURRENT
当前描述的PendingIntent如果已经存在,那么它们都会被更新,即它们的Intent中的Extras会被替换成最新的。
从上面的分析来看还是不太好理解这四个标记位,下面结合通知栏消息再描述一遍。这里分两种情况,如下代码中:manager.notify(1, notification),如果notify的第一个参数id是常量,那么多次调用notify只能弹出一个通知,后续的通知会把前面的通知完全替代掉,而如果每次id都不同,那么多次调用notify会弹出多个通知,下面一一说明。
如果notify方法的id是常量,那么不管PendingIntent是否匹配,后面的通知会直接替换前面的通知,这个很好理解。
如果notify方法的id每次都不同,那么当PendingIntent不匹配时,这里的匹配是指PendingIntent中的Intent相同并且requestCode相同,在这种情况下不管采用何种标记位,这些通知之间不会相互干扰。如果PendingIntent处于匹配状态时,这个时候要分情况讨论:如果采用了FLAG_ONE_SHOT标记位,那么后续通知中的PendingIntent会和第一条通知保持完全一致,包括其中的Extras,单击任何一条通知后,剩下的通知均无法再打开,当所有的通知都被清楚后,会再次重复这个过程;如果采用FLAG_CANCEL_CURRENT标记位,那么只有最新的通知可以打开,之前弹出的所有通知均无法打开;如果采用FLAG_UPDATE_CURRENT标记位,那么之前弹出的通知中的PendingIntent会被更新,最终它们和最新的一条通知保持完全一致,包括其中的Extras,并且这些通知都是可以打开的。