RemoteView表示的是一个View结构,他可以在其他进程中显示,由于它在其他进程中显示,为了能够更新他的界面,RemoteViews提供了一组基础的操作应用与跨进程更新它的界面。
1.通知栏
2.桌面小部件
桌面小部件则是通过AppWidgetProvider来实现的,AppWidget本质是一个广播.
通知栏和桌面小部件的开发过程中都会用到RemoteView,它们在更新界面时无法像在Activity里面那样直接更新View,这是因为两者的界面都运行在其他线程中,确切的说是系统的SystemServer进程.为了跨进程更新界面,RemoteViews提供一系列set方法,并且这些方法只是View全部方法的子集,另外RemoteVIew支持的View类型也是有限的。
String apkName = fileName.substring(fileName.lastIndexOf("/") + 1); String time = new SimpleDateFormat("hh:MM:ss").format(new Date()); Notification notification = new Notification( R.drawable.stat_sysl_complete, "下载失败", System.currentTimeMillis()); notification.flags = Notification.FLAG_AUTO_CANCEL; notification.contentView = new RemoteViews(mContext.getPackageName(), R.layout.notifyed); notification.contentView.setImageViewResource(R.id.notifyLog, icon); notification.contentView.setTextViewText(R.id.notifyMessage, "下载失败,点击重新下载"); notification.contentView.setTextViewText(R.id.notifyTitle, apkName); notification.contentView.setTextViewText(R.id.notifyTime, time); Intent intent = new Intent(SisterReceiver.ACTION_DOWNLOAD_FAILED); intent.putExtra("url", url); intent.putExtra("notifyId", notifyId); NotificationManager notifyM = (NotificationManager) context .getSystemService(Context.NOTIFICATION_SERVICE); notification.contentIntent = PendingIntent.getBroadcast(mContext, notifyId, intent, 0); notifyM.notify(notifyId, notification);
1.无法直接访问里面的View,而必须通过RemoteView所提供的一系列方法来更新View,比如设置TextView的文本,要采用如下方式:RemoteView.setTextVIewText(R.id.msg,”Chapter_5”),其中setTextViewText的两个参数分别为TextView的id和要设置的文本。
2.如果要给一个控件加单击事件,则要使用PendingIntent并通过setOnClickPendingIntent方法来实现,比如remoteViews.setonClickPendingIntent(R.id.open_activity2,openActivity2Pending-Intent)这句代码会给id为open_activity的View加上单击事件。
关于PendingIntent,它表示的是一种待定的Intent,这个Intent中所包含的意图必须由用户来触发。
//Widget.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <ImageView android:id="@+id/imageView1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/icon1" /> </LinearLayout>
//appwidget_provider_info.xml
<?xml version="1.0" encoding="utf-8"?> <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" android:initialLayout="@layout/widget" android:minHeight="84dp" android:minWidth="84dp" android:updatePeriodMillis="86400000" > </appwidget-provider>
//MyAppWidgetProvide.java
public class MyAppWidgetProvider extends AppWidgetProvider { public static final String TAG = "MyAppWidgetProvider"; public static final String CLICK_ACTION = "com.ryg.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); SystemClock.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 appWidgeManger * @param appWidgetId */ private void onWidgetUpdate(Context context, AppWidgetManager appWidgeManger, 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); appWidgeManger.updateAppWidget(appWidgetId, remoteViews); } private 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; } }
<receiver android:name=".MyAppWidgetProvider" > <meta-data android:name="android.appwidget.provider" android:resource="@xml/appwidget_provider_info" > </meta-data> <intent-filter> <action android:name="com.ryg.chapter_5.action.CLICK" /> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> </intent-filter> </receiver>
android.appwidget.action.APPWIDGET_UPDATE作为小部件的标识必须存在,不加就不会出现在小部件列表里
onEnable :当窗口小部件第一次添加到桌面时调用该方法,可添加多次,但只在第一次调用.
onUpdate:小部件被添加时或者每次小部件更新时都会调用一次该方法,小部件的更新时机由updatePeriodMIllis来指定,每个周期小部件都会自动更新一次。
onDelete:每删除一次桌面小部件就调用一次
onDisable:当最后一个该类型的桌面小部件被删除时调用该方法,注意是最后一个
onReceiver:这是广播的内置方法,用于分发具体的事件给其他方法
自定义一个MyAppWidget(类名自定义)类继承AppWidgetProvider
功能:
1.在onupdate中开启服务
2.在ondiable中关闭服务
public class MyAppWidget extends AppWidgetProvider { @Override public void onReceive(Context context, Intent intent) { System.out.println("onreceiver"); super.onReceive(context, intent); } @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { //只要有新的widget创建就会调用onupdate,所以就在这里开启服务 System.out.println("onupdate"); Intent intent = new Intent(context, UpdateWidgetService.class); context.startService(intent); super.onUpdate(context, appWidgetManager, appWidgetIds); } @Override public void onDeleted(Context context, int[] appWidgetIds) { System.out.println("onDeleted"); super.onDeleted(context, appWidgetIds); } @Override public void onEnabled(Context context) { System.out.println("onEnabled"); super.onEnabled(context); } @Override public void onDisabled(Context context) { System.out.println("onDisabled"); Intent intent = new Intent(context, UpdateWidgetService.class); context.stopService(intent); super.onDisabled(context); } }
在AndroidManifest.xml 中注册MyAppWidget,因为AppWidgetProvider 继承了BroadcastReceiver
<receiver android:name="com.daxiong.appwidget.MyAppWidget" > <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>
@xml/example_appwidget_info这个文件要重写
该文件在res->xml 目录中。
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" android:minWidth="294dp" //最小长度 android:minHeight="72dp"//最小高度 android:updatePeriodMillis="1800000" //更新时间,这里单位是毫秒半小时 android:initialLayout="@layout/process_widget" //需要重写 > </appwidget-provider>
编写布局文件process_widget.xml(文件名自定义),该布局文件用于widget 显示界面
1.widget的系统框架允许的最短更新时间是0.5hour,设置小于这个值也没有用。因为考虑到过于频繁更新比较耗电,但是有些需求就需要实时更新,比如桌面的时钟,或者天气之类的。
2.widget是显示在另外一个应用程序里(比如桌面原生的桌面),后期别人修改的桌面会导致widget的声明周期不一样,只要记住
不要记生命周期调用的先后顺序.
onenable 方法什么时候调用(第一次创建的时候)
ondisabled 方法什么时候调用(桌面所有该app的widget都被删除了才会调用)
onupdate方法 在每次创建新的widget的时候都会调用 , 并且当时间片(也就是最少半小时)到的时候也会调用
3.所以只能在widget中onupdate中开启一个服务来更新widget,这样避免了即使服务异常终止,导致更新异常,onupdate只要本app有新的widget创建就会开启服务,而且每过半小时,onupdate就会执行一次
如果只有上面的步骤那么我们的widget 就可以运行了,但是widget 一般都是需要动态更新的,比如我们的widget 是需要动态显示当前系统的内存信息的,因此我们还需要在我们的广播中开启一个service,在service 中对
widget 进行动态更新。
下面将把上面步骤的详细过程和代码清单展示出来。
5.用Service 动态更新widget
思路:
1.获取系统桌面更新widget的服务
awm = AppWidgetManager.getInstance(this)
2.获取准备更新的组件名称
provider = new ComponentName(this,MyAppWidget.class);
3.告诉桌面布局文件去哪里找(这里获取的views不是真正的view,而是一个描述(包括包名和布局文件),view对象市创建在桌面空间里,桌面应用来修改里面的内容),然后桌面获得这个描述然后通过他的布局文件,然后转成view对象,然后再找里面的个控件,所以我们只要告诉他找哪个控件就行
views = new RemoteViews(getPackageName(),R.layout.process_widget);
4.告诉桌面修改哪个控件就行
views.setTextViewText(R.id.process_count,"正在运行的软件个数"+SystemInfoUtils.getCountRunningProcess(getApplicationContext()));
views.setTextViewText(R.id.process_memory,"剩余内存"+Formatter.formatFileSize(getApplicationContext(),SystemInfoUtils.getAvailRam(getApplicationContext())));
5.最后不要忘了,显示出来
awm.updateAppWidget(provider, views);
代码:
//UpdateWidgetService.Java
public class UpdateWidgetService extends Service { private Timer timer; private TimerTask task; private PendingIntent pendingIntent; @Override public IBinder onBind(Intent intent) { // TODO Auto-generated method stub return null; } @Override public void onCreate() { //1.获取系统桌面更新的widget的服务 final AppWidgetManager awm = AppWidgetManager.getInstance(this); //2.获取组件的名称 final ComponentName provider = new ComponentName(this, MyAppWidget.class); //3.告诉桌面布局文件去哪里找 //getApplicationContext() //A class that describes a view hierarchy that can be displayed in another process. //The hierarchy is inflated from a layout resource file, //and this class provides some basic operations for modifying the content of the inflated hierarchy. //一个描述一个view层级关系的view,可以在其他进程中显示 //这个关系从是从一个xml布局文件中填充出来的 //同时这个类提供一写基本的操作来修改这个布局文件的内容 final RemoteViews views = new RemoteViews(getPackageName(), R.layout.process_widget); //4. //一创建服务就开始计时每五秒更新一次 if (timer == null && task == null) { timer = new Timer(); task = new TimerTask() { @Override public void run() { System.out.println("更新了"); views.setTextViewText(R.id.process_count, "正在运行的软件个数" + SystemInfoUtils.getCountRunningProcess(getApplicationContext())); views.setTextViewText(R.id.process_memory, "剩余内存" + Formatter.formatFileSize(getApplicationContext(), SystemInfoUtils.getAvailRam(getApplicationContext()))); //重要:pengdingintent延期的意图,因为这个意图不是由当前应用程序执行的,而是传递给别的线程,由别的线程执行。pengdingintent可以开启activity,broadcast,service,这里需要点一次就清除一次,所以就传递广播 //如果被点击了,自动清除所有线程//出发广播接收者 Intent intent = new Intent(); intent.setAction("com.daxiong.killallprocess"); pendingIntent = PendingIntent.getBroadcast(getApplicationContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); //告诉桌面哪个地方设置点击事件 views.setOnClickPendingIntent(R.id.btn_clear, pendingIntent); //组件名-更新 谁,远程的View对象!!!!!!!!!!!!!!!! awm.updateAppWidget(provider, views); } }; } timer.schedule(task, 0, 5000); super.onCreate(); } @Override public void onDestroy() { if (timer != null && task != null) { timer.cancel(); task.cancel(); timer = null; task = null; } super.onDestroy(); } }
//KillAllProcessReceiver.java
public class KillAllProcessReceiver extends BroadcastReceiver { @Override public void onReceive(Context context,Intent intent) { ActivityManager am = (ActivityManager) context.getSystemService(context.ACTIVITY_SERVICE); List<RunningAppProcessInfo> list = am.getRunningAppProcesses(); for (RunningAppProcessInfo runningAppProcessInfo : list) { am.killBackgroundProcesses(runningAppProcessInfo.processName); } Toast.makeText(context, "清除完毕了", 0).show(); } }
PendingIntent表示一种处于pending状态的意图,而pending状态表示的是一种待定,等待,即将发生的意思,就是说接下来有一个Intent(即意图)将在某个待定的时刻发生,而Intent是立即发生。
PendingIntent典型的使用场景是给RemoteView添加单击事件,因为RemoteViews运行在远程进程中,要想给RemoteViews设置单击事件,就必须使用PendingIntent,PendingIntent通过send和cancel方法来发送和取消特定的待定Intent.
PendingIntent支持三种待定意图:启动Activity,启动Service和发送广播,对应着它的三个接口方法
Static PengdingIntent |
getActivity(Context context,Int requestCode , Intent intent,int flags) 获得一个PendingIntent,该待定意图发生时,效果相当于Context.startActivity(Intent) |
Static PengdingIntent |
getService(Context context,int requestCode ,Intent intent,inte flags) 获得一个PendingIntent,该待定意图发生时,效果相当于Context.startService(intent) |
Static PengdingIntent |
getBroadcast(Contex context,int requestCode,Intent intent ,int flags) 获得一个PendingIntent,该待定意图发生时,效果相当于 Context.sendBroadCast |
requestCode表示PendingIntent发送方的请求码,多数情况下设为0即可,PS:requestCode会影响到flags
常见Flag
FLAG_ONE_SHOT
当面描述的PendingIntent只能使用一次,然后他就会被自动cancel,如果后续还有相同的PendingIntent,那么它们的send方法就会调用失败.对于通知栏消息来说,如果采用此标记位,那么同类的通知只能使用一次,后续通知单击后将无法打开。
FLAG_NO_CREATE
当前描述的PendingIntent不会主动创建,如果当前PendingIntent之前不存在,那么getActivity,getService,getBroadcast方法会直接返回null,即获取PendingIntent失败,这个基本不用,不用看
FLAG_CANCEL_CURRENT
如果已经存在,那么它们都会被cancel,然后创建一个新的,对于通知栏消息来说,那些被cancel的消息单击将无法打开。
FLAG_UPDATE_CURRENT
如果已经存在,那么它们会被更新,即他们的Intent中的Extras会被替换成最新的.
Manager.notify(id,notification)
其实上面的各种flag的运用就分为下面两种情况
1.id是常量,那么不管PendingIntent是否匹配,后面的通知会直接替换前面的通知
2.Id每次都不同
<1>pendingIntent匹配
(1)FLAG_ONE_SHOT 产生新的通知并与第一条通知保持一致,点击任意一条,其他的都无法点击
(2)FLAG_CANCEL_CURRENT 产生新的通知,只有最新的才能打开,之前的都打不开
(3)FLAG_UPDATA_CURRENT 产生新的通知,之前的通知也会被更新,最后跟新产生的通知保持一致
<2>pendingIntent不匹配 这时候不管采用任何FlAG,这些通知都不会互相干扰
PendingIntent的匹配规则
(1)Intent
<1>ComponentName(就是new Intent(ComponentName),就是(MainActivity.this,MyActivity.class))
<2>intent-filter
只要上面的两者相同就行,即使他们的Extra不同,那么这两个Intent也是相同的
(2)requestCode
这两者相同那么就是同一个PendingIntent
<3>RemoteVIews的内部机制
Public RemoteViews(String packageName ,int layoutId),它接受两个参数,第一个表示当前应用的包名,第二个表示待加载的布局文件,这个很好理解
他所支持的所有类型如下:
Layout
FrameLayout,LInearLayout,RelativeLayout,GridLayout
View
AnalogClock,Button,Chornometer,ImageButton,ImageView,ProgressBar,TextView,ViewFlipper,ListView,GridView,StackView,AdapterViewFlipper,ViewStub
上面所描述的是RemoteViews所支持的所有View类型,RemoteViews不支持他们的子类以及其他View类型,也就是说RemoteViews中不能使用除了上述列表中以外的View,也无法使用自定义的view.
RemoteViews的部分set方法
事实上大部分set方法是通过反射来完成的
1.RemoteViews会通过Binder传递到SystemServer进程(因为RemoteViews实现了Parcelable接口,因此它可以跨进程传输)
2.然后会通过LayoutInflater去加载RemoteViews中的布局文件,然后在SystemServer中加载的是一个普通的View,只不过相对于我们的进程他是一个RemoteView而已
3.接着系统会对View执行一系列界面更新任务,这些任务就是之前我们通过set方法提交的,set方法对View所做的更新并不是立刻执行的(具体的执行时机要等到RemoteViews被加载以后才能执行,这样RemoteViews就可以在SystemServer进程中显示了),在RemoteViews内部会记录所有的更新操作!!!
1.我们的应用中没调用一次Set方法,RemoteViews中就会添加一个对应的Action对象
2.当我们通过NotificationManager和AppWidgerManager来提交更新时,这个Action对象就会传到远程进程中(SystemServer)并在远程进程中依次执行.RemoteVIews的apply方法内部则会去遍历所有的Action对象并调用他们的apply方法,具体的View更新操作是由Action对象的apply方法来完成的,remoteView的reApply则只会更新界面,apply会加载布局并更新界面
优点:不需要定义大量的Binder接口,其次通过在远程进程中批量执行RemoteViews的修改操作从而避免了大量的IPC操作这就提高了程序性能
关于单击事件,RemoteViews中只支持发起PendingIntent,不支持onClickListener那种模式。另外,我们需要注意setOnClickPendingIntent,setPendingIntentTemplatey以及setOnClickFillInIntent它们之间的区别和联系
AIDL和RemoteVIew使用的考虑
现在有两个应用,
1一个应用需要能够更新另一个应用中的某个界面,这个时候我们当然可以选择AIDL去实现,但是如果对界面的更新比较频繁,这个时候就会有效率问题,同时AIDL接口就有可能会变得很复杂
2.这个时候如果采用RemoteView来实现就没有这个问题了,当然remoteView也会有点缺点,那就是他仅支持一些常见的View,对于自定VIew他是不支持的