Android开发艺术探索 第5章 理解RemoteViews

作者: HuDP | 来源:发表于2016-05-30 19:14 被阅读3165次

    RemoteViews是一种远程View,可以在其他进程中显示,为了能够更新它的界面,RemoteViews提供了一组基础操作用于跨进程更新它的界面。
    本章会介绍RemoteViews在通知栏和桌面小部件上的应用,分析RemoveViews的内部机制,最后分析RemoteViews的意义并给出一个采用RemoteViews来跨进程更新界面的示例。


    5.1 RemoteViews的应用

    RemoteViews主要用于通知栏和桌面小部件的开发。通知栏主要通过NotificationManager的notify方法来实现;桌面小部件则是通过AppWidgetProvider来实现的,AppWidgetProvider本质上是一个广播。因为RemoteViews运行在其他进程(SystemService进程),所以无法直接更新界面。

    5.1.1 RemoteViews在通知栏上的应用
        Notification notification = new Notification();
        notification.icon = R.mipmap.ic_launcher;
        notification.tickerText = "hello notification";
        notification.when = System.currentTimeMillis();
        notification.flags = Notification.FLAG_AUTO_CANCEL;
        Intent intent = new Intent(this, RemoteViewsActivity.class);
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
        RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.layout_notification);//RemoveViews所加载的布局文件
        remoteViews.setTextViewText(R.id.tv, "这是一个Test");//设置文本内容
        remoteViews.setTextColor(R.id.tv, Color.parseColor("#abcdef"));//设置文本颜色
        remoteViews.setImageViewResource(R.id.iv, R.mipmap.ic_launcher);//设置图片
        PendingIntent openActivity2Pending = PendingIntent.getActivity
                (this, 0, new Intent(this, MainActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);//设置RemoveViews点击后启动界面
        remoteViews.setOnClickPendingIntent(R.id.tv, openActivity2Pending);
        notification.contentView = remoteViews;
        notification.contentIntent = pendingIntent;
        NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        manager.notify(2, notification);
    
    5.1.2 RemoveViews在桌面小部件上的应用
    1. 定义好小部件界面
      在res/layout下新建一个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/iv"
            android:layout_width="360dp"
            android:layout_height="360dp"
            android:layout_gravity="center" />
    </LinearLayout>
    
    1. 定义小部件的配置信息
      在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="360dp"
          android:minWidth="360dp"
          android:updatePeriodMillis="864000"/>
    
    1. 定义小部件的实现类
      这个类需要继承AppWidgetProvider;我们这里实现一个简单的widget,点击它后,3张图片随机切换显示。
    public class ImgAppWidgetProvider extends AppWidgetProvider {
        public static final String TAG = "ImgAppWidgetProvider";
        public static final String CLICK_ACTION = "cn.hudp.androiddevartnote.action.click";
        private static int index;
    
        @Override
        public void onReceive(Context context, Intent intent) {
            super.onReceive(context, intent);
            if (intent.getAction().equals(CLICK_ACTION)) {
                RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget);
                AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
    
                updateView(context, remoteViews, appWidgetManager);
            }
        }
    
        @Override
        public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
            super.onUpdate(context, appWidgetManager, appWidgetIds);
            RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget);
    
            updateView(context, remoteViews, appWidgetManager);
        }
    
        // 由于onReceive 和 onUpdate中部分代码相同 则抽成一个公用方法
        public void updateView(Context context, RemoteViews remoteViews, AppWidgetManager appWidgetManager) {
            index = (int) (Math.random() * 3);
            if (index == 1) {
                remoteViews.setImageViewResource(R.id.iv, R.mipmap.haimei1);
            } else if (index == 2) {
                remoteViews.setImageViewResource(R.id.iv, R.mipmap.haimei2);
            } else {
                remoteViews.setImageViewResource(R.id.iv, R.mipmap.haimei3);
            }
            Intent clickIntent = new Intent();
            clickIntent.setAction(CLICK_ACTION);
            PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, clickIntent, 0);
            remoteViews.setOnClickPendingIntent(R.id.iv, pendingIntent);
            appWidgetManager.updateAppWidget(new ComponentName(context, ImgAppWidgetProvider.class), remoteViews);
        }
    }
    
    1. 在AndroidManifest.xml中声明小部件
      因为桌面小部件的本质是一个广播组件,因此必须要注册。
    <receiver android:name=".RemoveViews_5.ImgAppWidgetProvider">
        <meta-data
            android:name="android.appwidget.provider"
            android:resource="@xml/appwidget_provider_info">
            </meta-data>
        <intent-filter>
            <action android:name="cn.hudp.androiddevartnote.action.click" />
            <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
        </intent-filter>
    </receiver>
    

    上面代码中有两个action,第一个是用于识别小部件的单击行为,而第二个则是作为小部件的标识必须存在的;如果不加这个receiver就不是一个桌面小部件并且也无法显示在手机的小部件中。

    1. 广播到来的时候,AppWidgetProvider会自动根据广播的Action通过onReceive方法来分发广播,也就是调用
      onEnable: 当该窗口小部件第一次添加到桌面时调用的方法,可添加多次但只在第一次调用。
      onUpdate: 小部件被添加时或者每次小部件更新时都会调用一次该方法,小部件的更新时机是有updatePeriodMillis来指定,每个周期小部件就会自动更新一次。
      onDeleted: 每删除一次桌面小部件就调用一次。
      onDisabled: 当最后一个该类型的小部件被删除时调用该方法。
      onReceive: 这是广播的内置方法,用于分发具体事件给其他方法。
    5.1.3 PendingIntent概述

    PendingIntent表示一种处于pending(待定、等待、即将发生)状态的意图;PendingIntent通过send和cancel方法来发送和取消特定的待定Intent。
    PendingIntent支持三种待定意图:启动Activity、启动Service和发送广播。分别对应:

    getActivity / getService / getBroadcast
    参数相同,都为:(Context context, int requestCode, Intent intent, int flags) 
    

    其中第二个参数,requestCode表示PendingIntent发送方的请求码,多少情况下为0即可,requestCode会影响到flags的效果。
    PendingIntent的匹配规则是:如果两个PendingIntent他们内部的Intent相同并且requestCode也相同,那么这两个PendingIntent就是相同的。那么什么情况下Intent相同呢?Intent的匹配规则是,如果两个Intent的ComponentName和intent-filter都相同;那么这两个Intent也是相同的。

    flags参数的含义:
    FLAG_ONE_SHOP 当前的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会被替换为最新的。

    NotificationManager的notify方法分析

    manager.notify(1,notification);
    
    1. 如果notify方法的id是常量,那么不管PendingIntent是否匹配,后面的通知都会替换掉前面的通知。
    2. 如果notify的方法id每次都不一样,那么当PendingIntent不匹配的时候,不管在何种标记为下,这些通知都不会互相干扰。
    3. 如果PendingIntent处于匹配阶段,分情况:
    4. 采用FLAG_ONE_SHOT标记位,那么后续通知中的PendingIntent会和第一条通知保持一致,包括其中的Extras,单击任何一条通知后,其他通知均无法再打开;当所有通知被清除后。
    5. 采用FLAG_CANCEL_CURRENT标记位,只有最新的通知可以打开,之前弹出的所有通知均无法打开。
    6. 采用FLAG_UPDATE_CURRENT标记位,那么之前弹出的PendingIntent会被更新,最终它们和最新的一条保存完全一致,包括其中的Extras,并且这些通知都是可以打开的。

    5.2 RemoteViews的内部机制

    1. RemoteViews的构造方法:public RemoteViews(String packageName,int layoutId),第一个参数表示当前应用的包名,第二个参数表示待加载的布局文件。
    2. RemoveViews并不能支持所有View类型,支持以下:
      Layout:FrameLayout、LinearLayout、RelativeLayout、GridLayout。
      View:Button、ImageButton、ImageView、ProgressBar、TextView、ListView、GridView、ViewStub等(例如EditText是不允许在RemoveViews中使用的,使用会抛异常)。
    3. RemoteView没有findViewById方法,因此无法访问里面的View元素,而必须通过RemoteViews所提供的一系列set方法来完成,这是通过反射调用的。
    4. 通知栏和小组件分别由NotificationManager(NM)和AppWidgetManager(AWM)管理,而NM和AWM通过Binder分别和SystemService进程中的NotificationManagerService以及AppWidgetService中加载的,而它们运行在系统的SystemService中,这就和我们进程构成了跨进程通讯。
    5. 工作流程:首先RemoteViews会通过Binder传递到SystemService进程,因为RemoteViews实现了Parcelable接口,因此它可以跨进程传输,系统会根据RemoteViews的包名等信息拿到该应用的资源;然后通过LayoutInflater去加载RemoteViews中的布局文件。接着系统会对View进行一系列界面更新任务,这些任务就是之前我们通过set来提交的。set方法对View的更新并不会立即执行,会记录下来,等到RemoteViews被加载以后才会执行。
    6. 为了提高效率,系统没有直接通过Binder去支持所有的View和View操作。而是提供一个Action概念,Action同样实现Parcelable接口。系统首先将View操作封装到Action对象并将这些对象跨进程传输到SystemService进程,接着SystemService进程执行Action对象的具体操作。远程进程通过RemoteViews的apply方法来进行View的更新操作,RemoteViews的apply方法会去遍历所有的Action对象并调用他们的apply方法。这样避免了定义大量的Binder接口,也避免了大量IPC操作。
    7. apply和reApply的区别在于:apply会加载布局并更新界面,而reApply则只会更新界面。
    8. 关于单击事件,RemoteViews中只支持发起PendingIntent,不支持onClickListener那种模式。setOnClickPendingIntent用于给普通的View设置单击事件,不能给集合(ListView/StackView)中的View设置单击事件(开销大,系统禁止了这种方式)。如果要给ListView/StackView中的item设置单击事件,必须将setPendingIntentTemplate和setOnClickFillInIntent组合使用才可以。

    5.3 RemoteViews的意义

    RemoteViews最大的意义在于方便的跨进程更新UI。

    • 当一个应用需要更新另一个应用的某个界面,我们可以选择用AIDL来实现,但如果更新比较频繁,效率会有问题,同时AIDL接口就可能变得很复杂。如果采用RemoteViews就没有这个问题,但RemoteViews仅支持一些常用的View,如果界面的View都是RemoteViews所支持的,那么就可以考虑采用RemoteViews。
    • 利用RemoteViews加载其他App的布局文件与资源。
        final String pkg = "cn.hudp.remoteviews";//需要加载app的包名
        Resources resources = null;
        try {
            resources = getPackageManager().getResourcesForApplication(pkg);
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        if (resources != null) {
            int layoutId = resources.getIdentifier("activity_main", "layout", pkg); //获取对于布局文件的id
            RemoteViews remoteViews = new RemoteViews(pkg, layoutId);
            View view = remoteViews.apply(this, llRemoteViews);//llRemoteViews是View所在的父容器
            llRemoteViews.addView(view);
        }
    

    相关文章

      网友评论

      本文标题:Android开发艺术探索 第5章 理解RemoteViews

      本文链接:https://www.haomeiwen.com/subject/jsufdttx.html