第五章理解RemoteViews(Android开发艺术探索)

5.1、remoteViews的应用(主要用于通知栏和桌面小部件)

通知栏和桌面小部件不能直接去更新view,因为2者的页面都运行在其他进程中,确切来说是系统的SystemServer进程,为了跨进程更新页面,RemoteViews提供了一系列的set方法进行更新。

5.1.1、在通知栏的应用:

通知栏除了默认的效果还可以自定义布局,下面分别说明这2种情况:

默认的布局:

Notification notification = new Notification();
//设置图标
notification.icon = R.drawable.ic_launcher;
//设置内容
notification.tickerText = "hello world";
//要显示的时间,一般是当即显示,故填入系统当前时间。
notification.when = System.currentTimeMillis();
//// FLAG_AUTO_CANCEL表明当通知被用户点击时,通知将被自动清除。
notification.flags = Notification.FLAG_AUTO_CANCEL;

Intent intent = new Intent(this, DemoActivity_2.class);
////该语句的作用是定义了一个不是当即显示的activity,
// 只有当用户拉下notify显示列表,并且单击对应的项的时候,才会触发系统跳转到该activity.
PendingIntent pendingIntent = PendingIntent.getActivity(this,
        0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
//在此处设置在nority列表里的该norifycation得显示情况。
notification.setLatestEventInfo(this, "chapter_5", "this is notification.", pendingIntent);
NotificationManager manager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
// 通过通知管理器来发起通知。如果id不同,则每click,在statu那里增加一个提示
manager.notify(sId, notification);

自定义的布局(使用了remoteViews来加载):

Notification notification = new Notification();
notification.icon = R.drawable.ic_launcher;
notification.tickerText = "hello world";
notification.when = System.currentTimeMillis();
notification.flags = Notification.FLAG_AUTO_CANCEL;
Intent intent = new Intent(this, DemoActivity_1.class);
intent.putExtra("sid", "" + sId);
PendingIntent pendingIntent = PendingIntent.getActivity(this,
        0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
System.out.println(pendingIntent);
//包名和资源ID
RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.layout_notification);
remoteViews.setTextViewText(R.id.msg, "chapter_5: " + sId);
remoteViews.setImageViewResource(R.id.icon, R.drawable.icon1);
PendingIntent openActivity2PendingIntent = PendingIntent.getActivity(this,
        0, new Intent(this, DemoActivity_2.class), PendingIntent.FLAG_UPDATE_CURRENT);
remoteViews.setOnClickPendingIntent(R.id.open_activity2, openActivity2PendingIntent);
notification.contentView = remoteViews;
notification.contentIntent = pendingIntent;
NotificationManager manager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
manager.notify(sId, notification);

remoteViews使用起来很简单,2个参数即可创建,包名和资源id
更新remoteViews无法直接访问里面的view,必须通过remoteViews提供的方法进行更新

比如:

remoteViews.setTextViewText(R.id.msg, "chapter_5: " + sId);
remoteViews.setImageViewResource(R.id.icon, R.drawable.icon1);

5.1.2、在桌面小部件上面的应用:

Android系统为我们提供了一个实现桌面小部件的类:AppWidgetProvider ,它继承广播。

public class AppWidgetProvider extends BroadcastReceiver

1、自定义小部件界面:


<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/imageView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/icon1" />

LinearLayout>

2、定义小部件的配置信息,res/xml/myapp_widget.xml


<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:initialLayout="@layout/widget"//初始化布局
    android:minHeight="84dp"
    android:minWidth="84dp"
    android:updatePeriodMillis="86400000" //自动更新周期>

appwidget-provider>

3、定义小部件的实现类

package com.ryg.chapter_5;

import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.os.SystemClock;
import android.util.Log;
import android.widget.RemoteViews;
import android.widget.Toast;

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

    public MyAppWidgetProvider() {
        super();
    }

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

        // 这里判断是自己的action,做自己的事情,比如小工具被点击了要干啥,这里是做一个动画效果
        if (intent.getAction().equals(CLICK_ACTION)) {
            Toast.makeText(context, "clicked it", Toast.LENGTH_SHORT).show();

            new Thread(new Runnable() {
                @Override
                public void run() {
                    Bitmap srcbBitmap = BitmapFactory.decodeResource(
                            context.getResources(), R.drawable.icon1);
                    AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
                    for (int i = 0; i < 37; i++) {
                        float degree = (i * 10) % 360;
                        RemoteViews remoteViews = new RemoteViews(context
                                .getPackageName(), R.layout.widget);
                        remoteViews.setImageViewBitmap(R.id.imageView1,
                                rotateBitmap(context, srcbBitmap, degree));
                        Intent intentClick = new Intent();
                        intentClick.setAction(CLICK_ACTION);
                        PendingIntent pendingIntent = PendingIntent
                                .getBroadcast(context, 0, intentClick, 0);
                        remoteViews.setOnClickPendingIntent(R.id.imageView1, pendingIntent);
                        appWidgetManager.updateAppWidget(new ComponentName(
                                context, MyAppWidgetProvider.class),remoteViews);
                        SystemClock.sleep(30);
                    }

                }
            }).start();
        }
    }

    /**
     * 每次窗口小部件被点击更新都调用一次该方法
     */
    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager,
            int[] appWidgetIds) {
        super.onUpdate(context, appWidgetManager, appWidgetIds);
        Log.i(TAG, "onUpdate");

        final int counter = appWidgetIds.length;
        Log.i(TAG, "counter = " + counter);
        for (int i = 0; i < counter; i++) {
            int appWidgetId = appWidgetIds[i];
            onWidgetUpdate(context, appWidgetManager, appWidgetId);
        }

    }

    /**
     * 窗口小部件更新
     * 
     * @param context
     * @param appWidgeManger
     * @param appWidgetId
     */
    private void onWidgetUpdate(Context context,
            AppWidgetManager appWidgeManger, int appWidgetId) {

        Log.i(TAG, "appWidgetId = " + appWidgetId);
        RemoteViews remoteViews = new RemoteViews(context.getPackageName(),
                R.layout.widget);

        // "窗口小部件"点击事件发送的Intent广播
        Intent intentClick = new Intent();
        intentClick.setAction(CLICK_ACTION);
        PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0,
                intentClick, 0);
        remoteViews.setOnClickPendingIntent(R.id.imageView1, pendingIntent);
        appWidgeManger.updateAppWidget(appWidgetId, remoteViews);
    }

    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来完成。

4、在AndoridMenifest中申明小部件(跟注册广播差不多)

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

inten-filter中有2个action,第一个是小部件的点击行为,第二个是小部件的标识,必须存在

拓展AppWidgetProvider的方法:

● 1)onEnable:当窗口小部件第一次添加到桌面时会调用。
● 2)onUpdate:小部件被添加时或者每次小部件更新时会调用。
● 3)onDeleted:每删除一次桌面小部件jiuhuidiaoy。
● 4)onDisabled:当最后一个桌面小部件被删除时会调用。
● 5)onReceive:广播的内置方法。

*启动时AppWidgetProvider的执行流程:
*第一步:onReceive()
* 接到广播事件:android.appwidget.action.APPWIDGET_ENABLED
*第二步:onEnabled()
*第三步:onReceive()
* 接到广播事件:android.appwidget.action.APPWIDGET_UPDATE
*第四步:onUpdate()
*
*被删除时AppWidgetProvider
*第一步:onReceive()
* 接到广播事件:android.appwidget.action.APPWIDGET_DELETED
*第二步:onDelete();
*第三步:onReceive()
* 接到广播事件:android.appwidget.action.APPWIDGET_DISABLED
*第四步:onDisabled()

5.1.3、PendingIntent概述

pending : 等待,即将发生
pendingIntent表示将来某个不确定的时刻发生,Intent表示立刻发生
pendingIntent的典型事例是给remoteViews添加点击事件,因为remoteViews运行在远程进程中,所以无法像普通的View一样添加点击事件
pendingIntent支持3种待定意图:
启动activity,

public static PendingIntent getActivity(Context context, int requestCode,
        Intent intent, int flags) {
    return getActivity(context, requestCode, intent, flags, null);
}
启动service,
public static PendingIntent getService(Context context, int requestCode,
        Intent intent, int flags) {
发送广播,
public static PendingIntent getBroadcast(Context context, int requestCode,
        Intent intent, int flags) {
    return getBroadcastAsUser(context, requestCode, intent, flags,
            new UserHandle(UserHandle.myUserId()));
}

第一个参数和第三个参数好理解,我们来说说第二个参数requestCode和第四个参数flags
在了解这2个参数之前先要知道PendingIntent的匹配规则

PendingIntent的匹配规则是:intent和requestCode都相同,那么这2个pendingIntent就相同
Intent的匹配规则是:componentName和intent-filter相同

flags的类型:

//pendingIntent只能被使用一次,就会自动cancel
public static final int FLAG_ONE_SHOT = 1<<30;
//没有什么使用意义
public static final int FLAG_NO_CREATE = 1<<29;
//如果pendingIntent已经存在,那么它们都会被取消,然后创建一个新的pendingIntent
public static final int FLAG_CANCEL_CURRENT = 1<<28;
///如果pendingIntent已经存在,它们都会被更新,即Intent中的Extras会被替换成新的
public static final int FLAG_UPDATE_CURRENT = 1<<27;

下面结合通知栏消息再详细描述一下4个标记位:

manager.notify(id,notification)

第一种情况:如果id是常量,那么多次调用notify只能弹出一个通知,后面的通知会覆盖前面的
第二种情况:每次id都不一样,多次调用notify会弹出多个通知

5.2、RemoteViews的内部机制

remoteViews的作用是跨进程显示并更新UI,
remoteViews不支持自定义view,只支持下列view,下列view的子类也不支持
否则就会抛异常:android.view.inflateException
因为remoteViews是跨进程显示页面,所以无法findviewbyid获得view,但是它提供了一系列的set方法来控制view,这些方法都是通过反射来完成的

我们知道,
通知栏和桌面小程序分别由NotificationManager和AppWidgetManager管理;
而NotificationManager是通过Binder和NotificationManagerService通信,
AppWidgetManager是通过Binder和AppWidgetService通信 。
由此可见:
通知栏是在NotificationManagerService中被加载的
桌面小部件是在AppWidgetService中被加载的
而它们运行在系统的SystemServer中,那么就是跨进程通信的场景

第一步:RemoteViews会通过Binder传递到SystemServer进程(因为RemoteViews实现了Parcelable接口)
第二步:通过LayoutInflater加载RemoteViews中的layout布局(在SystemServer进程中返回的是一个普通的view)
第三步:调用RemoteViews的set方法进行UI的更新
第四步:通过NotificationManager和AppWidgetManager提交UI更新任务
这样,RemoteViews就在SystemServer进程中显示了(就是我们看到的通知栏消息和桌面小部件)

从理论上说:
系统完全可以通过Binder去支持所有的view和view的操作,但这样做代价太大了,因为view的方法太多了,另外就是大量的IPC操作会影响效率
Andorid系统怎么做的呢?

Android系统提供了一个Action,Action代表view的操作,action也实现了Parceable接口
Android系统在RemoteViews调用apply进行更新任务的提交时,其实是间接的调用了Action对象的apply方法进行提交的;然后在远程进程中批量执行RemoteViews的更新操作,避免了大量的IPC操作,提高了程序的性能,Android系统在这方面的确设计的很精妙。
(Action其实是利用了反射进行UI的更新)

RemoteViews在通知栏和桌面小部件的工作过程和上面描述的是一样的。
当我们调用set方法时它们并不会立即去更新UI,而是
通知栏必须要通过NotificationManager的notify方法更新页面
桌面小部件必须要通过AppWidgetManager的updateAppWidget方法更新页面
实际上在NotificationManager和AppWidgetManager的内部实现中,它们的确是通过RemoteViews的apply(加载并且更新页面)和reapply(只更新页面)来加载和更新页面的

//RemoteViews的内部类ReflectionAction
private final class ReflectionAction extends Action {
@Override
public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
    final View view = root.findViewById(viewId);
    if (view == null) return;

    Class param = getParameterType();
    if (param == null) {
        throw new ActionException("bad type: " + this.type);
    }

    try {
        getMethod(view, this.methodName, param).invoke(view, wrapArg(this.value));
    } catch (ActionException e) {
        throw e;
    } catch (Exception ex) {
        throw new ActionException(ex);
    }
}
}

看代码可以知道,ReflectionAction其实表示的是一个反射的动作
关于单击事件,RemoteViews只支持PendingIntent,另外注意,setOnclickPendingIntent给普通view设置点击事件,如果想给listview设置点击事件必须使用setPendingIntentTemplate和setOnclickFillInIntent

5.2、RemoteViews的意义

比如现在有2个应用,一个应用能够更新另一个应用的页面,这个时候我们当然可以选择AIDL实现,但是如果界面更新比较频繁,那么效率就会降低,AIDL接口也会变得复杂,这个时候如果采用RemoteViews就没有问题了,但是RemoteViews只支持一些简单的view,如果更新的界面比较简单可以采用RemoteViews。

//如果采用RemoteViews进行页面的更新,那么还有一个问题:布局文件加载的问题
View view = remoteViews.apply(this, mContent);
mContent.addView(view);

这种写法在同一个应用中的多进程是可以的,但是不同应用就访问不到了,
我们可以通过加载资源名称进行加载,两个应用约定好布局文件的名称。
可以通过如下代码实现:

int layoutId = getResources.getIdentifier("layoutId");
View view = getInflate.inflate(layoutId, mContent);
remoteViews.reapply(this, view);
mContent.addView(view);

你可能感兴趣的:(读书笔记)