工作原理

使用 AppWidgetManager 的 updateAppWidget 将 RemoteViews 通过 Binder 传到给桌面控件进程使用。桌面控件进程在初始化时会调用 RemoteViews 的 apply 方法。apply 方法会调用所有 Action 的 apply 方法,通过反射根据 viewId 调用其子 View 的 setXXX 方法设置属性。

为什么使用反射?由于 RemoteViews 在远程进程中显示,而布局资源则在本地进程中,所以 RemoteViews 没有 findViewById 的方法,不能获取其子 View 设置其属性,只有通过反射根据类名实现该功能。

RemoteViews

https://developer.android.com/reference/android/widget/RemoteViews

java.lang.Object
   ↳	android.widget.RemoteViews

一个类,描述可在另一个进程中显示的视图层次结构。使用布局资源文件 inflate 层次结构,并且此类提供了一些基本操作来修改层次结构里的内容。

常用方法

apply (Context context, ViewGroup parent):inflate 此 View 表示的视图层次结构并应用所有的 Action。parent - 生成的视图层次结构将附加到的父级。此方法生成正确的视图,不附加到层次结构,调用者应在适当的时候进行附加。
reapply (Context context, View v):将所有 Action 应用于提供的视图。v 是apply返回的结果。

apply和reapply的区别在于apply会加载布局并更新界面,而reapply只更新界面。

setTextViewText (int viewId, CharSequence text):等效于调用 TextView#setText(CharSequence)

AppWidgetProvider

https://developer.android.com/guide/topics/appwidgets#AppWidgetProvider

AppWidgetProvider 类扩展了 BroadcastReceiver 作为一个辅助类来处理应用微件广播。AppWidgetProvider 仅接收与应用微件有关的事件广播,例如当更新、删除、启用和停用应用微件时发出的广播。当发生这些广播事件时,AppWidgetProvider 会接收以下方法调用:

onUpdate():调用此方法可以按 AppWidgetProviderInfo 中的 updatePeriodMillis 属性定义的时间间隔来更新应用微件(见下文:控件配置信息)。当用户添加应用微件时也会调用此方法,所以它应执行基本设置,如定义视图的事件处理脚本以及根据需要启动临时的 Service。不过,如果您已声明了配置 Activity,则当用户添加应用微件时不会调用此方法,但会调用它来执行后续更新。由配置 Activity 负责在配置完成后执行首次更新。

onReceive(Context, Intent):针对每个广播调用此方法,并且是在上述各个回调方法之前调用。您通常不需要实现此方法,因为默认的 AppWidgetProvider 实现会过滤所有应用微件广播并视情况调用上述方法。

AppWidgetManager

https://developer.android.com/reference/android/appwidget/AppWidgetManager

java.lang.Object
   ↳	android.appwidget.AppWidgetManager

更新AppWidget状态;获取有关已安装的AppWidget providers 以及其他与AppWidget相关的状态的信息。

常用方法

updateAppWidget (ComponentName provider, RemoteViews views):设置 RemoteViews 为 provider 的所有 AppWidget 实例。可以在 ACTION_APPWIDGET_UPDATE 广播内调用,或者在 handler 线程外调用。仅 provider 的所有者(Context)调用此方法才有效。

updateAppWidget (int[] appWidgetIds, RemoteViews views)

updateAppWidget (int appWidgetId, RemoteViews views):告诉 AppWidgetManager 对当前应用程序小部件执行更新

控件样式

res/layout 中创建 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">

    <ImageView
        android:id="@+id/iv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/th"/>
</LinearLayout>

控件配置信息:创建 res/xml/appwidget_provider_info.xml 文件

<?xml version="1.0" encoding="utf-8" ?>
<appwidget-provider android:initialLayout="@layout/widget"
    android:minHeight="20dp"
    android:minWidth="20dp"
    android:updatePeriodMillis="86400000"
    xmlns:android="http://schemas.android.com/apk/res/android">

</appwidget-provider>

android:initialLayout:设置初始布局
android:minHeight & android:minWidth:定义控件的最小尺寸
android:updatePeriodMillis:更新周期,毫秒为单位,触发控件的更新方法

实现

控件实现类

继承 AppWidgetProvider(BroadcastReceiver 子类):

public class MyAppWidgetProvider extends AppWidgetProvider {
    public static final String TAG = "MyAppWidgetProvider";
    public static final String CLICK_ACTION = "com.example.RemoteView.action.CLICK";

    public MyAppWidgetProvider() {
    }

    @Override
    public void onReceive(final Context context, Intent intent) {
        super.onReceive(context, intent);
        Log.e(TAG, "onReceive : action = " + intent.getAction());

		// 这里判断自定义的 action,并实现 action。例如,控件被单击之后,执行一个动画:旋转图片一周,通过不断更新 RemoveViews 来实现。
        if (intent.getAction().equals(CLICK_ACTION)) {
            Toast.makeText(context, "ckicked it", Toast.LENGTH_SHORT).show();
			// 广播生命周期短,通过线程执行操作
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Bitmap srcbBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.th);
                    AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
                    for (int i = 1; i < 37; i++) {
                        float degree = (i * 10) % 360;	// 每次旋转 10 度,执行 36 次旋转一周回到原来状态
                        RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget);
                        remoteViews.setImageViewBitmap(R.id.iv, rotateBitmap(context, srcbBitmap, degree));
                        
                        interactToRemoteViews(context, remoteViews); 
                        appWidgetManager.updateAppWidget(new ComponentName(context, MyAppWidgetProvider.class), remoteViews);	// 将 remoteViews 设置到
                        SystemClock.sleep(1000);
                    }
                }
            }).start();
        }
    }

	// 使用旋转矩阵创建 Bitmap 返回
    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;
    }

	// RemoteViews 的交互事件:注册监听器,单击发送广播
	private void interactToRemoteViews (Context context, RemoteViews remoteViews) {
		Intent intentClick = new Intent();
		intentClick.setAction(CLICK_ACTION);
		PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intentClick, 0);
		remoteViews.setOnClickPendingIntent(R.id.iv, pendingIntent);
	}

	// 每次桌面控件更新时调用一次该方法
    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        super.onUpdate(context, appWidgetManager, appWidgetIds);
        Log.e(TAG, "onUpdate");

		// 更新逻辑的一般实现,对该 provider 的每个桌面控件执行一次更新操作
        final int counter = appWidgetIds.length;
        Log.e(TAG, "counter = " + counter);
        for (int i = 0; i < counter; i++) {
            int appWidgetId = appWidgetIds[i];
            onWidgetUpdate(context, appWidgetManager, appWidgetId);
        }
    }

	// 控件更新的实现
    private void onWidgetUpdate(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {
        Log.e(TAG, "appWidgetId = " + appWidgetId);
        RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget);

        interactToRemoteViews(context, remoteViews); 
        appWidgetManager.updateAppWidget(appWidgetId, remoteViews);
    }
}

需要在 AndroidManifest.xml 中静态注册以便随时接收广播:

<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.example.RemoteView.action.CLICK" />
        <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
    </intent-filter>
</receiver>

android:resource:指定配置文件

输出结果

/*
 * 方法调用:
 * MainActivity: onCreate
 * 添加 widget 时:
 * MyAppWidgetProvider:onReceive : action = android.appwidget.action.APPWIDGET_ENABLED
 * MyAppWidgetProvider:onUpdate
 * MyAppWidgetProvider: counter = 1
 * MyAppWidgetProvider: appWidgetId = 16
 * MyAppWidgetProvider: onReceive : action = android.appwidget.action.APPWIDGET_UPDATE
 * MyAppWidgetProvider: onReceive : action = android.appwidget.action.APPWIDGET_UPDATE_OPTIONS
 * 点击 widget 时:
 * MyAppWidgetProvider: onReceive : action = com.example.RemoteView.action.CLICK
 * 出错。。。
 * MainActivity: onDestroy
 * 删除 widget 时:
 * MyAppWidgetProvider: onReceive : action = android.appwidget.action.APPWIDGET_DELETED
 * MyAppWidgetProvider: onReceive : action = android.appwidget.action.APPWIDGET_DISABLED
 * */

bitmap 设置图片尺寸,避免 内存溢出 OutOfMemoryError的优化方法

跨进程传递视图

  1. 创建 RemoteViews 实例,对其初始化
RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.notification_contain);
// 设置 TextView 视图文本
remoteViews.setTextViewText(R.id.msg, "msg from process:" + Process.myPid());
// 设置 ImageView 视图资源
remoteViews.setImageViewResource(R.id.icon, R.drawable.th); // android.support.v7.widget.AppCompatImageView can't use method with RemoteViews: setImageResource(int)

// 设置监听器,单击时跳转到 Main2Activity
PendingIntent openActivity2PendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, Main2Activity.class), PendingIntent.FLAG_UPDATE_CURRENT);
remoteViews.setOnClickPendingIntent(R.id.open_activity2, openActivity2PendingIntent);
  1. 通过广播发出 RemoteViews
Intent intent = new Intent();
intent.putExtra("notification_view", remoteViews);
sendBroadcast(intent);
  1. 创建广播接收器接收广播,获取 RemoteViews,在 UI 中使用 RemoteViews
private BroadcastReceiver mRemoteViewsReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
    	if (intent.getAction().equals("com.example.RemoteView.action.REMOTE_VIEW")) {
	        RemoteViews remoteViews = intent.getParcelableExtra("notification_view");
	        if (remoteViews != null) {
	            updateUI(remoteViews);
	        }
	    }
    }
};
private void updateUI(RemoteViews remoteViews) {
	View view = remoteViews.apply(this, mRemoteViewsContent);
	mRemoteViewsContent.addView(view);
}

广播接收器需要注册、解注册:

// 动态注册
IntentFilter filter = new IntentFilter("com.example.RemoteView.action.REMOTE_VIEW");
registerReceiver(mRemoteViewsReceiver, filter);
// 解注册,不需要广播接收器时调用
unregisterReceiver(mRemoteViewsReceiver);