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

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的布局。
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    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了:
《Android开发艺术探索》第5章 理解RemoteViews_第1张图片

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类型有限。究竟选择什么方式,需要自行抉择。

你可能感兴趣的:(Android)