简单理解RemoteView

来源:互联网 发布:变形金刚倾天柱 乐乎 编辑:程序博客网 时间:2024/06/08 14:57

一.介绍

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的两个参数分别为TextViewid和要设置的文本。


2.如果要给一个控件加单击事件,则要使用PendingIntent并通过setOnClickPendingIntent方法来实现,比如remoteViews.setonClickPendingIntent(R.id.open_activity2,openActivity2Pending-Intent)这句代码会给idopen_activityView加上单击事件。

关于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通过sendcancel方法来发送和取消特定的待定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.当我们通过NotificationManagerAppWidgerManager来提交更新时,这个Action对象就会传到远程进程中(SystemServer)并在远程进程中依次执行.RemoteVIewsapply方法内部则会去遍历所有的Action对象并调用他们的apply方法,具体的View更新操作是由Action对象的apply方法来完成的,remoteViewreApply则只会更新界面,apply会加载布局并更新界面

优点:不需要定义大量的Binder接口,其次通过在远程进程中批量执行RemoteViews的修改操作从而避免了大量的IPC操作这就提高了程序性能

 

 

 

 

 


关于单击事件,RemoteViews中只支持发起PendingIntent,不支持onClickListener那种模式。另外,我们需要注意setOnClickPendingIntent,setPendingIntentTemplatey以及setOnClickFillInIntent它们之间的区别和联系

 

 

 

 

AIDLRemoteVIew使用的考虑

现在有两个应用,

1一个应用需要能够更新另一个应用中的某个界面,这个时候我们当然可以选择AIDL去实现,但是如果对界面的更新比较频繁,这个时候就会有效率问题,同时AIDL接口就有可能会变得很复杂

2.这个时候如果采用RemoteView来实现就没有这个问题了,当然remoteView也会有点缺点,那就是他仅支持一些常见的View,对于自定VIew他是不支持的

 

 

3 1
原创粉丝点击