简单理解RemoteView
Posted dx我是大雄
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了简单理解RemoteView相关的知识,希望对你有一定的参考价值。
一.介绍
RemoteView表示的是一个View结构,他可以在其他进程中显示,由于它在其他进程中显示,为了能够更新他的界面,RemoteViews提供了一组基础的操作应用与跨进程更新它的界面。
二.应用场景
1.通知栏
2.桌面小部件
三.RemoteViews的应用
桌面小部件则是通过AppWidgetProvider来实现的,AppWidget本质是一个广播.
通知栏和桌面小部件的开发过程中都会用到RemoteView,它们在更新界面时无法像在Activity里面那样直接更新View,这是因为两者的界面都运行在其他线程中,确切的说是系统的SystemServer进程.为了跨进程更新界面,RemoteViews提供一系列set方法,并且这些方法只是View全部方法的子集,另外RemoteVIew支持的View类型也是有限的。
四.RemoteViews在通知栏和桌面小部件上的应用
(1)通知栏
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);
如何更新RemoteView呢?
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中所包含的意图必须由用户来触发。
(2)桌面小部件的(不用启动,只要在清单文件中配置好了,直接在桌面长按添加小部件即可)
<1>简单实现
1.在res/layout下新建一个XML文件,命名为Widget.xml名称和内容可以自定义。
//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>
2.在res/xml下新建
//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>
3.定义小部件的实现类
//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;
}
}
4.千万别忘记要在清单文件注册!!!
<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:这是广播的内置方法,用于分发具体的事件给其他方法
<2>简单实践(桌面有个控件实时展现占用内存,点击清理后台进程)
1.
自定义一个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);
}
}
2.
在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>
3.
@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>
4.
编写布局文件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();
}
}
5.PendingIntent概述
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他是不支持的
以上是关于简单理解RemoteView的主要内容,如果未能解决你的问题,请参考以下文章
无法在 RemoteView Android 上更改 ImageView 资源