简单理解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的两个参数分别为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他是不支持的

 

 

以上是关于简单理解RemoteView的主要内容,如果未能解决你的问题,请参考以下文章

理解RemoteViews

无法在 RemoteView Android 上更改 ImageView 资源

将 URL 中的 ImageView 加载到主屏幕小部件的 RemoteView 中

如何在android通知中增加remoteview的高度

TextView不会滚动RemoteView

RemoteView 和 setGravity