Android开发艺术探索读书笔记: 理解RemoteViews

RemoteViews用于跨进程更新View,常用使用场景有:通知栏和桌面widget。

RemoteViews的两个使用场景

通知栏的简单使用:

NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
PendingIntent intent = PendingIntent.getActivity(this, 0, new Intent(this, SecondActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);
RemoteViews remoteViews = new RemoteViews(this.getPackageName(),R.layout.notify_content);
Notification notification = builder.setTicker("Ticker")
        .setOngoing(true)
        .setTicker("Ticker",remoteViews)
        .setSmallIcon(R.drawable.ic)
        .setContentText("ContentText")
        .setWhen(System.currentTimeMillis())
        .setContentTitle("ContentTitle")
        .setContentIntent(intent)
        .build();
remoteViews.setTextViewText(R.id.textView,"IIIIIIIIIIIIIII");
notification.contentView = remoteViews;
notificationManager.notify(0,notification);

RemoteView没有像普通View那样有着findViewById方法,只提供了特定的方法来更新,例如 SetTextViewText(R.id.text,”Text”) 后面说明原因。

AppWigetProvider实质是一个广播,更新Wiget也用到了RemoteViews,使用步骤:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#09C"
    android:padding="@dimen/widget_margin">

    <TextView
        android:id="@+id/appwidget_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"
        android:layout_margin="8dp"
        android:background="#09C"
        android:contentDescription="@string/appwidget_text"
        android:text="@string/appwidget_text"
        android:textColor="#ffffff"
        android:textSize="24sp"
        android:textStyle="bold|italic" />

</RelativeLayout>
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:initialKeyguardLayout="@layout/my_app_widget"
    android:initialLayout="@layout/my_app_widget"
    android:minHeight="40dp"
    android:minWidth="40dp"
    android:previewImage="@drawable/example_appwidget_preview"
    android:resizeMode="horizontal|vertical"
    android:updatePeriodMillis="86400000"
    android:widgetCategory="home_screen" />
public class MyAppWidget extends AppWidgetProvider {

    private static final String TAG = "MyAppWidget";

    static int index = 0;

    void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {
        // Construct the RemoteViews object
        RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.my_app_widget);
        views.setTextViewText(R.id.appwidget_text, "Start");
        PendingIntent pendingIntent = PendingIntent.getBroadcast(context,0,new Intent("xml.onclick"),PendingIntent.FLAG_UPDATE_CURRENT);
        views.setOnClickPendingIntent(R.id.appwidget_text,pendingIntent);
        // Instruct the widget manager to update the widget
        appWidgetManager.updateAppWidget(appWidgetId, views);
    }
    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        //widget更新就会调用一次,包括添加以及自动
        Log.e(TAG,"onUpdate,appWidgetIds = " + Arrays.toString(appWidgetIds));
        for (int appWidgetId : appWidgetIds) {
            updateAppWidget(context, appWidgetManager, appWidgetId);
        }
    }

    @Override
    public void onEnabled(Context context) {
        //第一个widget开启就会被调用
        Log.e(TAG,"onEnabled");
    }

    @Override
    public void onDisabled(Context context) {
        //最后一个widget被删除就会被调用
        Log.e(TAG,"onDisabled");
    }

    @Override
    public void onRestored(Context context, int[] oldWidgetIds, int[] newWidgetIds) {
        Log.e(TAG,"onRestored,oldWidgetIds = " + Arrays.toString(oldWidgetIds) + ",newWidgetIds = " + Arrays.toString(newWidgetIds));
    }

    @Override
    public void onDeleted(Context context, int[] appWidgetIds) {
        //每删除一个widget就会调用一次
        Log.e(TAG,"onDeleted,appWidgetIds = " + Arrays.toString(appWidgetIds));
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        //必须调用父类的方法用于分发
        super.onReceive(context, intent);
        Log.e(TAG, "onReceive");
        if("xml.onclick".equals(intent.getAction())) {
            RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.my_app_widget);
            PendingIntent pendingIntent = PendingIntent.getBroadcast(context,0,new Intent("xml.onclick"),PendingIntent.FLAG_UPDATE_CURRENT);
            views.setOnClickPendingIntent(R.id.appwidget_text,pendingIntent);
            views.setTextViewText(R.id.appwidget_text, "Index = " + index++);
            AppWidgetManager.getInstance(context).updateAppWidget(new ComponentName(context,MyAppWidget.class),views);
        }
    }
}
 <receiver android:name="xml.MyAppWidget">
        <intent-filter>
            <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
            <action android:name="xml.onclick" />
        </intent-filter>

        <meta-data
            android:name="android.appwidget.provider"
            android:resource="@xml/my_app_widget_info" />
</receiver>

功能很简单,就是添加了widget之后,点击widget就会不断地更新wiget上面的文字,我们也可以通过在Activity或者Service里面发广播去更新widget。
AndroidStudio 可以直接新建AppWiget并给我们创建模版,上面代码就是 AndroidStudio 给生成后添加的一小段逻辑,如果要着手开发Wiget的话,建议详细阅读官方的教程

PendingIntent概述

PendingIntent可以理解为即将到来的Intent,不是即时发生的,就像上面代码里面给 RemoteViews 设置点击事件,点击之后就发送一个广播。通过 cancel 方法来取消PendingIntent。
PendingIntent支持三种意图: 启动Activity , 启动Service ,发送广播,对应的方法 getActivity,getService,getBroadcast,并且都有4个参数, (Context context,int requestCode,Intent intent,flags) 。

PendingIntent匹配规则为内部的Intent(只比较CompontName和intent-filter,不包括Extras)和requestCode相同即可。

flags的可选参数:

对于NotifycationManget#notify(int id, Notification notification), 如果第一个参数是一个常量,多次调用notify只能弹出一个通知,后续的通知就会被替换掉,如果不是常量,多次调用就会弹出多个通知。

RemoteViews的内部机制

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.

目前RemoteViews支持的类型:(不支持自定义View的他们的子类)

RemoteViews的典型方法:

setTextView(int viewId,Charsquence text); //设置TextView的文本
setInt(int viewId,String methodName,int value);//反射调用View对象参数为int类型的方法
setOnClickPendingIntent(int viewId,PendingIntent pendingIntent);//为View添加单击事件,事件类型只能为PendingIntent

我们先看一下updateWidget的调用过程:

MyAppWidget.java:

void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {
    RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.my_app_widget);
    views.setTextViewText(R.id.appwidget_text, "Start");
    appWidgetManager.updateAppWidget(appWidgetId, views);
}

RemoteViews.java:


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));
}

private void addAction(Action a) {
    //...
    if (mActions == null) {
        mActions = new ArrayList<Action>();
    }
    mActions.add(a);
    //...
}

AppWidgetManager.java:


private final IAppWidgetService mService;

public void updateAppWidget(int appWidgetId, RemoteViews views) {
    if (mService == null) {
        return;
    }
    updateAppWidget(new int[] { appWidgetId }, views);
}

public void updateAppWidget(int[] appWidgetIds, RemoteViews views) {
    if (mService == null) {
        return;
    }
    try {
        mService.updateAppWidgetIds(mPackageName, appWidgetIds, views);
    }
    catch (RemoteException e) {
        throw new RuntimeException("system server dead?", e);
    }
}

可以看得出来我们调用set方法给RemoteViews更新的时候,并没有真正的更新,而是把我们的操作添加到了一个 Action 的List里面,并且Action与RemoteViews都实现了Parcelable接口,就是用来进程间通信的。真正更新的的操作就是调用IAppWidgetService服务的操作,Notification也是利用这种类似的机制。

Notification和Widget是由NotificationManager和AppWidgetManager管理的,而NotificationManager和AppWidgetManager内部通过Binder机制(调用Service)和运行在SystemServer进程的NotifycationManagerService和AppWidgetService进行交互,完成我们的界面加载和更新。实质上我们定义的Notification和Widget在被SystemService加载和更新的。

这种批量更新的方式减轻了IPC的负担,因为我们很有可能一次更新View的很多内容。

上面只是介绍了客户端的基本调用行为,服务端则会调用apply或reapply来批量我们的操作并使用反射调用我们set的方法来更新我们的View,书中讲的不是很透彻,需要深入研究可以参考这几篇博客

apply会加载布局并更新界面,reapply则只会更新界面

RemoteViews的其他用途就是进程间更新View,例如Service和Activity不在同一个进程,有Service更新Activity的需求,就可以使用RemoteViews,感觉实际开发中不是很常用,具体参考作者的示例代码, 核心就是通过Intent的Extras存储我们的添加Action的RemoteViews,在接收方获取这个对象,调用apply或reapply就可以得到或者更新View。