第5章 理解RemoteViews

作者: littlefogcat | 来源:发表于2019-04-03 22:01 被阅读0次

    RemoteViews提供了一种跨进程更新界面的方式,一般用于通知栏和AppWidget的开发中。

    5.1 RemoteViews的应用

    通知栏需要用到的NotificationManager和小部件所用的AppWidgetProvider,都是运行在系统的SystemServer进程之中。我们如果想要对其进行界面更新的话,就需要用到RemoteViews。

    要使用RemoteViews,需要以下几个步骤:

    1. 创建一个xml文件layout_notification.xml,作为RemoteViews的布局。我们在一个LinearLayout里面加入一个TextView和一个ImageView。
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
    
        <TextView
            android:id="@+id/text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
    
        <ImageView
            android:id="@+id/image"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
    </LinearLayout>
    
    1. 新建一个RemoteViews:
            RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.remote_view);
    
    1. RemoteViews提供了一系列的方法,使我们方便的设置其中控件的内容,例如:
            remoteViews.setTextViewText(R.id.text, "Sample Text"); // 设置TextView文字
            remoteViews.setImageViewResource(R.id.image, R.drawable.web_image); // 设置ImageView图片
    

    事实上,这两个方法追溯过去:

        public void setTextViewText(int viewId, CharSequence text) {
            setCharSequence(viewId, "setText", text);
        }
    
        public void setCharSequence(int viewId, String methodName, CharSequence value) {
            addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR_SEQUENCE, value));
        }
    
        public void setImageViewResource(int viewId, int srcId) {
            setInt(viewId, "setImageResource", srcId);
        }
    
        public void setInt(int viewId, String methodName, int value) {
            addAction(new ReflectionAction(viewId, methodName, ReflectionAction.INT, value));
        }
    

    可以看到,都是调用了RemoteViews.addAction(Action)方法,通过这个方法,把我们需要的操作保存在remoteViews的mAction变量中,然后在远端执行。而ReflectionAction看名字就知道,在远端找到控件之后,是通过反射的方式调用方法。

    5.1.1 通知栏

    1. 创建一个xml文件layout_notification.xml,作为RemoteViews的布局。
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="10dp">
    
        <ImageView
            android:id="@+id/image"
            android:layout_width="40dp"
            android:layout_height="40dp"
            android:src="@color/colorAccent" />
    
        <TextView
            android:id="@+id/text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="50dp"
            android:text="text" />
    </FrameLayout>
    
    1. 创建一个RemoteViews:
            RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.layout_notification);
            remoteViews.setTextViewText(R.id.text, "新设置的text");
            remoteViews.setImageViewResource(R.id.image, R.color.colorAccent);
            Intent intentClick = new Intent(this, DemoActivity_2.class);
            remoteViews.setOnClickPendingIntent(R.id.image, PendingIntent.getActivity(this, 0, intentClick, PendingIntent.FLAG_UPDATE_CURRENT));
    

    其中,remoteViews.setOnClickPendingIntent()方法设置了id为R.id.image的控件的点击事件。

    1. 发送通知:
            Intent intent = new Intent(this, DemoActivity_1.class);
            PendingIntent pi = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
            Notification notification = new Notification.Builder(this)
                    .setSmallIcon(R.drawable.ic_launcher_foreground)
                    .setContentText("Ticker Text")
                    .setWhen(System.currentTimeMillis())
                    .setAutoCancel(true)
                    .setContentIntent(pi)
                    .setContent(remoteViews)
                    .build();
            NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
            nm.notify(1, notification);
    

    可以看到,我们的通知布局已经换成我们自定义的layout_notification.xml了:


    通知

    5.1.2 桌面小部件

    桌面小部件是通过AppWidgetProvider来实现的,其实质是一个广播接收器BroadcastReceiver。

    开发桌面小部件的步骤:

    1. 新建布局文件widget.xml;

    2. 在res/xml/下新建文件appwidget_provider_info.xml;这个文件的作用是定义小部件的大小、刷新周期、布局等。

    3. 定义类MyAppWidgetProvider继承自AppWidgetProvider。定义方式类似于BroadcastReceiver,需要在AndroidManifest.xml中添加该类:

            <receiver android:name=".MyAppWidgetProvider">
                <meta-data
                    android:name="android.appwidget.provider"
                    android:resource="@xml/appwidget_provider_info" />
    
                <intent-filter>
                    <action android:name="top.littlefogcat.chapter05_action_CLICK" />
                    <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
                </intent-filter>
            </receiver>
    

    其中meta-data中定义了小部件的布局,intent-filter中第一项是我们自定义的点击action,第二项为系统规定,只有添加了才能被识别为小部件。

    系统在需要用到小部件的时候调用onUpdate()回调,所以我们需要在MyAppWidgetProvider中覆写onUpdate()方法,并调用AppWidgetManager.updateAppWidget(int, RemoteViews)来更新小部件的内容。

        public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
            super.onUpdate(context, appWidgetManager, appWidgetIds);
            Log.i(TAG, "onUpdate");
    
            int counter = appWidgetIds.length;
            Log.i(TAG, "onUpdate: counter = " + counter);
            for (int appWidgetId : appWidgetIds) {
                onWidgetUpdate(context, appWidgetManager, appWidgetId);
            }
        }
    
        private void onWidgetUpdate(Context context, AppWidgetManager manager, int appWidgetId) {
            Log.i(TAG, "onWidgetUpdate: appWidgetId = " + appWidgetId);
    
            RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget);
            Intent intentClick = new Intent();
            intentClick.setAction(CLICK_ACTION);
            PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intentClick, 0);
            remoteViews.setOnClickPendingIntent(R.id.image, pendingIntent);
            manager.updateAppWidget(appWidgetId, remoteViews);
        }
    

    其他的如onEnabled() onDisabled() onDeleted() 等回调,如果需要处理也可以进行覆写,当然这些action在onReceive()中都能收到,查看源码可以发现,sdk是在进行一系列处理之后分化出了这几个回调。

    5.1.3 PendingIntent概述

    在发送Notification的时候,我们用到了PendingIntent。(使用AlarmManager设置定时任务的时候也会用到)一个典型的例子:

            Intent intent = new Intent(this, DemoActivity_1.class);
            PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
            Notification notification = new Notification.Builder(this)
                    .setSmallIcon(R.drawable.web_image)
                    .setContentText("This is PendingIntent")
                    .setContentIntent(pendingIntent)
                    .build();
            NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
            manager.notify(1, notification);
    

    在这里,我们定义了一个PendingIntent,通过setContentIntent()方法设置给Notification,这样我们点击这个Notification,就会跳转到DemoActivity_1了。

    PendingIntent直译为“待定意图”,也就是说,它同Intent类似,是一种意图,但是PendingIntent是待定的,不是立即发生的。在这个意图生效之前,我们可以通过PendingIntent.cancel()方法来取消它。例如,我们把上面的代码做如下更改:

            Intent intent = new Intent(this, DemoActivity_1.class);
            PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
            Notification notification = new Notification.Builder(this)
                    .setSmallIcon(R.drawable.web_image)
                    .setContentText("This is PendingIntent")
                    .setContentIntent(pendingIntent)
                    .build();
            NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
            manager.notify(1, notification);
    
            // 加入以下代码
            Handler handler = new Handler();
            handler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    pendingIntent.cancel();
                }
            }, 10000);
    

    在10秒之后,调用pendingIntent.cancel(),这时候我们发现,再点击通知,就不会跳转到DemoActivity_1了。
    在获取PendingIntent的时候,如PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT),最后一个参数是flag,其取值和含义如下:

    • FLAG_ONE_SHOT
      这个PendingIntent只能发送一次,然后就会被cancel掉。之后再使用它,将会发送失败。如果这个PendingIntent已经存在并且还没有发送,那么返回它。

    • FLAG_NO_CREATE
      如果这个PendingIntent存在,那么就返回它,否则返回null。

    • FLAG_CANCEL_CURRENT
      如果这个PendingIntent已经存在,那么就取消掉之前的。返回一个新的PendingIntent。

    • FLAG_UPDATE_CURRENT
      如果这个PendingIntent已经存在,那么保持它,并且把Intent中的extras更换成最新的并返回。

    需要注意的是,如果两次PendingIntent.getActivity()传入的requestCode和intent(不包括Extras)一样的话,那么就被认为是相同的PendingIntent,即对于第二个PendingIntent来讲,它已经“存在”了。

    5.2 RemoteViews的内部机制

    RemoteViews不是支持所有的View。事实上它支持的View相当有限,包括:
    FrameLayout, LinearLayout, RelativeLayout, GridLayout, AnalogClock, Button, Chronometer, ImageButton, ImageView, ProgressBar, TextView, ViewFlipper, ListView, GridView, StackView, AdapterViewFlipper, ViewStub.
    其他类型的View传入RemoteViews会抛出异常。

    RemoteViews设置View内容是通过反射实现的。例如设置TextView显示文字的方法:

        public void setTextViewText(int viewId, CharSequence text) {
            setCharSequence(viewId, "setText", text);
        }
    
        public void setCharSequence(int viewId, String methodName, CharSequence value) {
            addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR_SEQUENCE, value));
        }
    

    同理假设要调用TextView.setHint(String),可以这么做:

        remoteViews.setCharSequence(R.id.text, "setHint", "This is Hint");
    

    对于通知和小部件来讲,布局都是在SystemServer进程中,分别通过NotificationManagerService和AppWidgetService加载的。
    RemoteViews.mActions记录了对View的具体操作。mActions是一个列表,包含若干个Action,每个Action对应了对View的操作。在远端进程加载布局文件之后,会遍历mAction,并且分别调用每个Action的apply()方法执行操作。

    5.3 RemoteViews的意义

    RemoteViews的主要意义在于跨进程的界面显示和更新。
    相比于AIDL,RemoteViews的方式高效且简洁,但是缺点是支持的View类型有限。究竟选择什么方式,需要自行抉择。

    相关文章

      网友评论

        本文标题:第5章 理解RemoteViews

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