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

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


这门课的重心在于RemoteViews,RemoteViews可以理解为一种远程的View,其实他和远程的Service是一样的,RemoteViews表示的是一种View的结构,他可以在其他的进程中显示,最常用的就是通知栏和桌面小组件了,我们接下来就细细的品味一下吧!

一.RemoteViews的应用

RemoteViews在实际的开发中,就是通知栏和桌面小组件了,这个大家应该都不陌生,主要是通过NotificationManager的notify方法去实现通知栏,也有通过AppwidthProvider来实现桌面小部件的,其实小部件本质上就是一个广播,他们两个的更新都无法像Activity中直接更新View,这是因为两者都运行在其他大家进程中,确切来说是SystemService中,为了跨进程更新UI,RemoteViews提供了一系列的set方法,我们接下来就是来实际的演示了

1.RemoteViews在通知栏上的应用

首先来看下通知栏,我们先了解一下系统默认的样式

 //系统默认样式
 private void systemStyleNotirfication() {
        Notification notification = new Notification();
        notification.icon = R.mipmap.ic_launcher;
        notification.tickerText = "Hello LiuGuiLin";
        notification.when = System.currentTimeMillis();
        notification.flags = Notification.FLAG_AUTO_CANCEL;
        Intent intent = new Intent(this, TestActivity.class);
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
        notification.setLatestEventInfo(this, "LiuGuiLin", "This is Notification", pendingIntent);
        NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        notificationManager.notify(1, notification);
    }

上述的代码会弹出一个系统默认样式的通知栏,单击之后打开TestActivity,看下实际运行效果

Android开发艺术探索第五章——理解RemoteViews_第1张图片

虽说如此,但是往往有些需求是不满足的,所以需要自定义了,自定义其实也很简单,我们看下实际代码

    //自定义通知栏
    private void viewStyleNotirfication() {
        Notification notification = new Notification();
        notification.icon = R.mipmap.ic_launcher;
        notification.tickerText = "Hello LiuGuiLin";
        notification.when = System.currentTimeMillis();
        notification.flags = Notification.FLAG_AUTO_CANCEL;
        Intent intent = new Intent(this, TestActivity.class);
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
        RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.notification_item);
        remoteViews.setTextViewText(R.id.tv_title, "This is Title");
        remoteViews.setTextViewText(R.id.tv_content, "This is Notification Content");
        remoteViews.setImageViewResource(R.id.iv_img, R.mipmap.ic_launcher);
        remoteViews.setOnClickPendingIntent(R.id.ll_open, pendingIntent);
        notification.contentView = remoteViews;
        NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        notificationManager.notify(2, notification);
    }

我们实际看下运行的结果

Android开发艺术探索第五章——理解RemoteViews_第2张图片

可以看到,这是我自己写的布局notification_item


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/ll_open"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center_vertical"
    android:orientation="horizontal">

    <ImageView
        android:id="@+id/iv_img"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <LinearLayout
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:gravity="center_vertical"
        android:orientation="vertical"
        android:paddingLeft="10dp">

        <TextView
            android:id="@+id/tv_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="@android:color/black" />

        <TextView
            android:id="@+id/tv_content"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="@android:color/black" />

    LinearLayout>

LinearLayout>

这就是非常简单的通知栏了

RemoteViews的使用也是非常的简单,只要提供当前应用的包名和布局文件的资源ID就可以创建一个RemoteViews了,如何更新呢?这一点和View还是有很大的不同,RemoteViews更新的时候,无法直接访问里面的View,必须通过RemoteViews所提供的一系列方法来更新,比如设置TextView,那就需要

 remoteViews.setTextViewText(R.id.tv_title, "This is Title");

这里一共两个参数,一个是id,一个就是内容了,图片也是类似

remoteViews.setImageViewResource(R.id.iv_img, R.mipmap.ic_launcher);

如果需要点击事件的话,需要setOnClickPendingIntent来触发了,关于PendingIntent,他表示点击就是一种待定的意图,触发后才会操作,我们后面具体介绍

2.RemoteViews 在桌面小部件的应用

AppWidgetProvider是Android提供给我们的用于实现桌面小部件的类,其本质也就是一个广播,,所以实际使用中把他看成一个广播即可,我们来看下怎么去具体的实现一个小部件

1.定义小部件的界面

在res/layout下我们先写个widget.xml这里就是小部件的视图了,所以你尽量的随便写


<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/iv1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_launcher" />

    <ImageView
        android:id="@+id/iv2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_launcher" />

    <TextView
        android:id="@+id/tv_test"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Test"
        android:textSize="20dp" />

LinearLayout>

这里我写了两个图片和一个文本

2.定义小部件配置信息

在res/xml中定义一个appwidget_info.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>

上面的几个参数的含义很明确,android:initialLayout就是加载布局其他两个就是最小的高宽,而updatePeriodMillis就是更新小组件的时间周期

3.定义小组件的实现类

这个类需要继承AppWidgetProvider

package com.liuguilin.remotrview.provider;

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;

import com.liuguilin.remotrview.R;

/*
 *  项目名:  RemoteViewsSample 
 *  包名:    com.liuguilin.remotrview.provider
 *  文件名:   AppWidgetImpl
 *  创建者:   LiuGuiLin
 *  创建时间:  2017/1/14 0014 下午 4:35
 *  描述:    小组件
 */
public class AppWidgetImpl extends AppWidgetProvider {

    public static final String TAG = "AppWidgetImpl";
    public static final String CLICK_ACTION = "com.liuguilin.remotrview.provider.click_action";

    private AppWidgetManager appWidgetManage;
    private float degree;
    private Bitmap bitmap;

    public AppWidgetImpl() {
        super();
    }

    @Override
    public void onReceive(final Context context, final Intent intent) {
        super.onReceive(context, intent);
        String action = intent.getAction();
        Log.i(TAG, "action:" + action);
        if (CLICK_ACTION.equals(action)) {
            Toast.makeText(context, "click it", Toast.LENGTH_SHORT).show();

            new Thread(new Runnable() {
                @Override
                public void run() {
                    bitmap = BitmapFactory.decodeResource(context.getResources(), R.mipmap.ic_launcher);
                    appWidgetManage = AppWidgetManager.getInstance(context);
                    for (int i = 0; i < 37; i++) {
                        degree = (i * 10) % 360;
                        RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget);
                        remoteViews.setImageViewBitmap(R.id.iv1, rotateBitmap(bitmap));
                        remoteViews.setImageViewBitmap(R.id.iv2, rotateBitmap(bitmap));
                        Intent intentClick = new Intent(CLICK_ACTION);
                        PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intentClick, 0);
                        remoteViews.setOnClickPendingIntent(R.id.iv1, pendingIntent);
                        appWidgetManage.updateAppWidget(new ComponentName(context, AppWidgetImpl.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");
        RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget);
        Intent intentClick = new Intent(CLICK_ACTION);
        PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intentClick, 0);
        remoteViews.setOnClickPendingIntent(R.id.iv1, pendingIntent);
        appWidgetManage.updateAppWidget(new ComponentName(context, AppWidgetImpl.class), remoteViews);
    }

    //动画
    private Bitmap rotateBitmap(Bitmap bitmap) {
        Matrix matrix = new Matrix();
        matrix.reset();
        matrix.setRotate(degree);
        Bitmap temBitmap = Bitmap.createBitmap(this.bitmap, 0, 0, this.bitmap.getWidth(), this.bitmap.getHeight(), matrix, true);
        return temBitmap;
    }
}

上述的代码实现了一个简单的桌面小部件,这里加了个动画,其他到没什么。我们需要注册才能使用

4.在清单文件中声明小部件

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

            <intent-filter>
                <action android:name="com.liuguilin.remotrview.provider.click_action" />
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
            intent-filter>
 receiver>

上面的代码有两个Action,其中第一个是识别小部件的动作,第二个就是他的标识,必须存在,这是系统的规范

Android开发艺术探索第五章——理解RemoteViews_第3张图片

AppWidgetProvider 除了最常用的onUpdate方法,还有其他的方法,onEnable,onDisabled,onDeleted以及onReceiver,这些方法都会被onReceiver在适当的时候调用,所以含义如下

  • onEnable:当该窗口小部件第一次添加到桌面的时候调用该方法,可添加多次但是只有第一次调用
  • onUpdate:小部件被添加或者第一次更新的时候都会调用一次该方法,小部件的更新机制由updatePeriodMillis来指定
  • onDeleted:没删除一次小部件都会调用
  • onDisabled:当最后一个该类型的小部件会删除时调用
  • onReceiver:广播内置的方法

关于AppWidgetProvider 的onReceiver方法的具体分发过程,可以看源码中的实现,如下图,通过下面的代码可以看出,onReceiver会根据不同的Action来分别调用这几个方法

   public void onReceive(Context context, Intent intent) {
        // Protect against rogue update broadcasts (not really a security issue,
        // just filter bad broacasts out so subclasses are less likely to crash).
        String action = intent.getAction();
        if (AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(action)) {
            Bundle extras = intent.getExtras();
            if (extras != null) {
                int[] appWidgetIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_IDS);
                if (appWidgetIds != null && appWidgetIds.length > 0) {
                    this.onUpdate(context, AppWidgetManager.getInstance(context), appWidgetIds);
                }
            }
        } else if (AppWidgetManager.ACTION_APPWIDGET_DELETED.equals(action)) {
            Bundle extras = intent.getExtras();
            if (extras != null && extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_ID)) {
                final int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID);
                this.onDeleted(context, new int[] { appWidgetId });
            }
        } else if (AppWidgetManager.ACTION_APPWIDGET_OPTIONS_CHANGED.equals(action)) {
            Bundle extras = intent.getExtras();
            if (extras != null && extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_ID)
                    && extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS)) {
                int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID);
                Bundle widgetExtras = extras.getBundle(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS);
                this.onAppWidgetOptionsChanged(context, AppWidgetManager.getInstance(context),
                        appWidgetId, widgetExtras);
            }
        } else if (AppWidgetManager.ACTION_APPWIDGET_ENABLED.equals(action)) {
            this.onEnabled(context);
        } else if (AppWidgetManager.ACTION_APPWIDGET_DISABLED.equals(action)) {
            this.onDisabled(context);
        } else if (AppWidgetManager.ACTION_APPWIDGET_RESTORED.equals(action)) {
            Bundle extras = intent.getExtras();
            if (extras != null) {
                int[] oldIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_OLD_IDS);
                int[] newIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_IDS);
                if (oldIds != null && oldIds.length > 0) {
                    this.onRestored(context, oldIds, newIds);
                    this.onUpdate(context, AppWidgetManager.getInstance(context), newIds);
                }
            }
        }
    }

上面描述了一个开发桌面小部件的完整过程 ,例子比较简单,实际开发过程中会稍微的复杂一些,但是开发流程都是一样的, 可以发现,桌面小程序在界面上的操作都是通过RemoteViews,不管小部件的界面初始化还是界面的更新都需要依赖他

3.PendingIntent概述

我们在前面多次提到了PendingIntent,归根结底,他和Intent有什么区别呢我们就来说下

顾名思义,PendingIntent表示处于一种等待的意图,即特定,等待的意思,就是说你要在某种条件下才会触发,所以我们很容易的就联想到RemoteViews了

PendingIntent有三种待定的意图,就是Activity,Service,广播,可以看如下图

Android开发艺术探索第五章——理解RemoteViews_第4张图片

如图中所示,这三个方法的参数都是一样的,主要理解的是第二个参数requstCode和第四个参数flags,code代表的是发送码,多数情况下为0,而且code会影响到flag,flag常见的有几种我们下面会说,其实最主要是理解匹配规则,

PendingIntent的匹配规则为:如果两个PendingIntent他们内部的Intent相同并且requstCode也相同的话,那么PendingIntent就是相同的,code比较好理解,那什么情况下Intent相同呢,Intent的匹配规则是:如果两个Intent的ComponentName的匹配过程,只要Intent之间的ComponentName和intent-filter相同,那么这两个intent就相同,需要注意的是Extras不参与匹配过程,只要intent之间的name和intent-filter相同就行,我们再来说下flags的参数含义

  • FLAG_ONE_SHOT
    当前描述的PendingIntent只能被使用一次,然后他就会被cancel,如果后续还有相同的PendingIntent,那么他的send方法就会失败,对于通知栏的消息来说,如果采用此标记位,那么同类的通知只能使用一次,后续将无法打开

  • FLAG_NO_CREATE
    当前描述的PendingIntent不会主动去创建,如果当前PendingIntent之前不存在,那么getActivity等方法都会直接返回null,即获取PendingIntent失败,这个标记位很少见,他无法单独使用,因此在日常开发当中,并没有太多的意义,这里就不过多的介绍了

  • FLAG_CANCEL_CURRENT
    当前描述的PendingIntent如果已经存在,那么就会被cancel,然后系统创建一个新的PendingIntent,对于通知栏来说,那些被cancel的消息将无法被打开

  • FLAG_UPDATE_CURRENT
    当前描述的PendingIntent如果已经存在的话,那么他们就会被更新,他们的intent中的extras会被替换成新的

从上面的分析看还是不太好理解这四个标记位,下面结合实际的项目来,这里分析两种情况,如下代码,如果notify的第一个参数id是常量,那么多次通知就只能弹出一个通知,后续会把前面的替换掉,如果每次不一样,就会多弹出几个通知

如果notify方法的id是常量,那么不管PendingIntent是否匹配,后面的通知会替换前面的通知,这个很好理解

如果notify方法的id每次不同,那么当PendingIntent不匹配时,这里的匹配是指PendingIntent中Intent相同切requstCode相同,在这种情况下不管采用了何种标记位,这些通知之间不互相干扰。如果PendingIntent处于匹配状态,这个时候要分情况讨论,如果采用FLAG_ONE_SHOT标记位,那么后续通知中,PendingIntent会和第一条通知保持一致,包括其中的Extras,单击任何一条通知,剩余的都无法打开当所有的通知被清除后,会重复这个过程,如果采用FLAG_CANCEL_CURRENT标记位,那么只有最新的通知可以打开,之前弹出的所有通知均无法打开,如果采用FLAG_UPDATE_CURRENT标记位。那么之前弹出的通知PendingIntent会被更新,最终他们和最新的一条通知保持一致,包括其中的Extras,那么这些通知都可以被打开

二.RemoteViews内部机制

RemoteViews的作用在其他进程中显示并且更新View的界面,为了更好的理解他的内部机制,我们来看一下他的主要功能,首先我们看一下他的构造方法,这里介绍一个最常用的构造方法

    public RemoteViews(String packageName, int layoutId) {
        this(getApplicationInfo(packageName, UserHandle.myUserId()), layoutId);
    }

他接受两个参数,第一个表示当前的包名,第二个是加载的布局,这个很好理解,RemoteViews目前并不能支持所有的View类型,我们看下他支持哪些

  • Layout
    FrameLayout LinearLayout RelativeLayout GridLayout

  • View
    AnalogClock,Button,Chronometer,ImageButton,ImageView,ProgressBar,TextView,ViewFlipper ListView,GridView,stackView,AdapterViewFlipper,ViewStub

上面的描述是RemoteViews锁支持的View类,RemoteViews不支持他们的子类以及其他View的类型,也就是说RemoteViews中不能使用除了上述列表中以外的View,那么通知栏消息无法将弹出并且会抛出异常

RemoteViews也没有提供findViewById方法,因此无法直接访问里面的View元素,而必须通过RemoteViews所提供的一系列set方法来完成,当然这是因为RemoteViews在远程进程中显示,所以没办法直接findViewById,关于set方法,可以看下这表

Android开发艺术探索第五章——理解RemoteViews_第5张图片

从这张表可以看出,原本可以直接调用View的方法,现在都需要通过set来完成,而从这些方法的声明来看,很像是通过反射来完成的,事实上也是如此

下面我们继续来说下RemoteViews的内部机制,由于RemoteViews主要用于通知栏和通知栏和桌面小部件,这里说一下他的工作过程,我们知道小部件分别由NotificationManager和AppWidgetProvider管理,而NotificationManager和AppWidgetProvider通过Binder分别为SystemService进程中的NotificationManagerService和AppWidgetService,由此可见,通知栏和小部件实际上是这两个中加载出来的,这就是我们的进程构成了跨进程通信的原理

首先RemoteViews会通过Binder传递到SystemServer进程,这是因为RemoteViews实现了Parcelable接口,因此它可以跨进程传输,系统会根据RemoteViews中的包名等信息去得到该应用的资源。然后会通过Layoutinflater去加载RemoteViews中的布局文件,在SystemServer进程中加载后的布局文件是一个普通的View,只不过相对于我们的进程他是一个RemoteViews而已。接着系统会对View执行一系列界面更新任务,这些任务就是之前我们通过set方法来提交的。set方法对View所做的更新并不是立刻执行的,在RemoteViews内部会记录所有的更新操作,具体的执行时机要等到RemoteViews被加载以后才能执行,这样RemoteViews就可以在SystemServer进程中显示了,这就是我们所看到的通知栏消息或者桌面小部件。当需要更新RemoteViews时,我们需要调用一系列set法并通过NotificationManager和AppWidgetManager来提交更新任务,具体的更新操作也是在SystemServer进程中完成的。

从理论上来说,系统完全可以通过Binder去支持所有的View和View操作,但是这样做的话代价太大,因为View的方法太多了,另外就是大量的IPC操作会影响效率。为了解决这个问题,系统并没有通过Binder去直接支持View的跨进程访问,而是提供了一个Action,Action代表一个View操作,Action同样实现了Parcelable接口。系统首先将Vew操作封装装到Action对象并将这些对象跨进程传输到远程进程,接着在远程进程中执行Action对象中对象,当我们通过NotificationManager和AppWidgetManager来提交我们的更新时,这些Action对象就会传输到远程进程并在远程进程中依次执行,这个过程可以参看下面的图片,远程进程通过RemoteViews的apply方法来进行View的更新操作,RemoteViews的apply方法内部则回去遍历所有的Action对象并调用它们的apply方法,具体的View更新操作是由Acton对apply方法来完成的。上述做法的好处是显而易见的,首先不需要定义大量的Binder,其次通过在远程进程中批量执行RemoteViews的修改操作从而避免了大量的IPC视作这就提高了程序的性能,由此可见,Android系统在这方面的设计的确很精妙。

Android开发艺术探索第五章——理解RemoteViews_第6张图片

上面从理论上分析了RemoteViews的内部机制,接下来我们从源码的角度分析下Remoteviews的工作流程。它的构造方法就不用多说了,这里我们首先看他一系列的set方法,比如setTextViewText

    public void setTextViewText(int viewId, CharSequence text) {
        setCharSequence(viewId, "setText", text);
    }

上述ID中,viewId是被操作的View的id,setText是一个方法名,text是给TextView要设置的文本,可以联想一下,是不是清晰了很多,我们再来看下setCharSequence方法

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

从setCharSequence的实现来看,他的内部并没有对View进行直接的处理,然是添加了一个ReflectionAction独享,从名字来看,这应该是一个反射的动作,再看addAction的实现

    private void addAction(Action a) {
        if (hasLandscapeAndPortraitLayouts()) {
            throw new RuntimeException("RemoteViews specifying separate landscape and portrait" +
                    " layouts cannot be modified. Instead, fully configure the landscape and" +
                    " portrait layouts individually before constructing the combined layout.");
        }
        if (mActions == null) {
            mActions = new ArrayList();
        }
        mActions.add(a);

        // update the memory usage stats
        a.updateMemoryUsageEstimate(mMemoryUsageCounter);
    }

从上述代码可以得知RemoteViews内部有一个mActions 成员,他是一个ArrayList,外界每一次调用一个set方法,他都能保存下来并未对view进行实际的操作,这一点从上面的理论分析就可以知道,我们继续看,我们现在看一下apply的实现以及Action类的实现

    public View apply(Context context, ViewGroup parent, OnClickHandler handler) {
        RemoteViews rvToApply = getRemoteViewsToApply(context);

        View result;
        // RemoteViews may be built by an application installed in another
        // user. So build a context that loads resources from that user but
        // still returns the current users userId so settings like data / time formats
        // are loaded without requiring cross user persmissions.
        final Context contextForResources = getContextForResources(context);
        Context inflationContext = new ContextWrapper(context) {
            @Override
            public Resources getResources() {
                return contextForResources.getResources();
            }
            @Override
            public Resources.Theme getTheme() {
                return contextForResources.getTheme();
            }
        };

        LayoutInflater inflater = (LayoutInflater)
                context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

        // Clone inflater so we load resources from correct context and
        // we don't add a filter to the static version returned by getSystemService.
        inflater = inflater.cloneInContext(inflationContext);
        inflater.setFilter(this);
        result = inflater.inflate(rvToApply.getLayoutId(), parent, false);

        rvToApply.performApply(result, parent, handler);

        return result;
    }

从上面的代码可以看出,首先会通过LayoutInflater 去加载RemoteViews中的布局文件,RemoteViews中的布局文件可以通过getLayoutId这个方法获得,加载完布局之后会通过performApply去执行一些更新操作,代码如下

    private void performApply(View v, ViewGroup parent, OnClickHandler handler) {
        if (mActions != null) {
            handler = handler == null ? DEFAULT_ON_CLICK_HANDLER : handler;
            final int count = mActions.size();
            for (int i = 0; i < count; i++) {
                Action a = mActions.get(i);
                a.apply(v, parent, handler);
            }
        }
    }

performApply的实现是比较好理解的,他的作用就是遍历mActions 这个列表并执行每一个Action对象的apply方法,还记得mAction吗?每一次的set操作都会对应他里面的一个Action对象,因此也可以断定,Action对象的apply方法就是真正操作View的地方,实际上也是如此

RemoteViews在通知栏和桌面小部件中的工作过程和上面描述的过程是一致的,当我们调用RemoteViews的set方法时,并不会立刻更新它们的界面,而必须要通过NotificationManager的notify方法以及AppWidgetManager的updateAppWidget才能更新它们的界面。实际上在AppWidgetManager的 updateAppWidget的内部实现中,它们的确是通过RemoteViews的apply以及reapply方法来加载或者更新界面的,apply和reApply的区别在于:apply会加载布局并更新界面,而reApply则只会更新界面。通知栏和桌面小插件在初始化界面时会调用apply方法,而在后续的更新界面时则会调用reapply方法。这里先看一
下 BaseStatusBar的updateNotificationViews方法中,如下所示。

    private void updateNotificationViews(NotificationData.Entry entry,
            StatusBarNotification notification, boolean isHeadsUp) {
        final RemoteViews contentView = notification.getNotification().contentView;
        final RemoteViews bigContentView = isHeadsUp
                ? notification.getNotification().headsUpContentView
                : notification.getNotification().bigContentView;
        final Notification publicVersion = notification.getNotification().publicVersion;
        final RemoteViews publicContentView = publicVersion != null ? publicVersion.contentView
                : null;

        // Reapply the RemoteViews
        contentView.reapply(mContext, entry.expanded, mOnClickHandler);
        if (bigContentView != null && entry.getBigContentView() != null) {
            bigContentView.reapply(mContext, entry.getBigContentView(),
                    mOnClickHandler);
        }
        if (publicContentView != null && entry.getPublicContentView() != null) {
            publicContentView.reapply(mContext, entry.getPublicContentView(), mOnClickHandler);
        }
        // update the contentIntent
        final PendingIntent contentIntent = notification.getNotification().contentIntent;
        if (contentIntent != null) {
            final View.OnClickListener listener = makeClicker(contentIntent, notification.getKey(),
                    isHeadsUp);
            entry.row.setOnClickListener(listener);
        } else {
            entry.row.setOnClickListener(null);
        }
        entry.row.setStatusBarNotification(notification);
        entry.row.notifyContentUpdated();
        entry.row.resetHeight();
    }

很显然,上述代码表示当通知栏需要更新的时候,会通过RemoteViews的reapply方法来更新

接下来我们看一下AppWidgetHostView的updateAppWidget方法

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

从上面的代码,我们是通过IAppWidgetService的接口去实现的,实际上最红也是通过RemoteViews的reapply方法来更新

了解了apply和reapply的作用之后我们继续看Action的子类实现,首先看下ReflectionAction的具体实现,他的源码如下:

    private final class ReflectionAction extends Action {
        static final int TAG = 2;

        static final int BOOLEAN = 1;
        static final int BYTE = 2;
        static final int SHORT = 3;
        static final int INT = 4;
        static final int LONG = 5;
        static final int FLOAT = 6;
        static final int DOUBLE = 7;
        static final int CHAR = 8;
        static final int STRING = 9;
        static final int CHAR_SEQUENCE = 10;
        static final int URI = 11;
        // BITMAP actions are never stored in the list of actions. They are only used locally
        // to implement BitmapReflectionAction, which eliminates duplicates using BitmapCache.
        static final int BITMAP = 12;
        static final int BUNDLE = 13;
        static final int INTENT = 14;
        static final int COLOR_STATE_LIST = 15;

        String methodName;
        int type;
        Object value;

        ReflectionAction(int viewId, String methodName, int type, Object value) {
            this.viewId = viewId;
            this.methodName = methodName;
            this.type = type;
            this.value = value;
        }

        ReflectionAction(Parcel in) {
            this.viewId = in.readInt();
            this.methodName = in.readString();
            this.type = in.readInt();
            //noinspection ConstantIfStatement
            if (false) {
                Log.d(LOG_TAG, "read viewId=0x" + Integer.toHexString(this.viewId)
                        + " methodName=" + this.methodName + " type=" + this.type);
            }

            // For some values that may have been null, we first check a flag to see if they were
            // written to the parcel.
            switch (this.type) {
                case BOOLEAN:
                    this.value = in.readInt() != 0;
                    break;
                case BYTE:
                    this.value = in.readByte();
                    break;
                case SHORT:
                    this.value = (short)in.readInt();
                    break;
                case INT:
                    this.value = in.readInt();
                    break;
                case LONG:
                    this.value = in.readLong();
                    break;
                case FLOAT:
                    this.value = in.readFloat();
                    break;
                case DOUBLE:
                    this.value = in.readDouble();
                    break;
                case CHAR:
                    this.value = (char)in.readInt();
                    break;
                case STRING:
                    this.value = in.readString();
                    break;
                case CHAR_SEQUENCE:
                    this.value = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
                    break;
                case URI:
                    if (in.readInt() != 0) {
                        this.value = Uri.CREATOR.createFromParcel(in);
                    }
                    break;
                case BITMAP:
                    if (in.readInt() != 0) {
                        this.value = Bitmap.CREATOR.createFromParcel(in);
                    }
                    break;
                case BUNDLE:
                    this.value = in.readBundle();
                    break;
                case INTENT:
                    if (in.readInt() != 0) {
                        this.value = Intent.CREATOR.createFromParcel(in);
                    }
                    break;
                case COLOR_STATE_LIST:
                    if (in.readInt() != 0) {
                        this.value = ColorStateList.CREATOR.createFromParcel(in);
                    }
                    break;
                default:
                    break;
            }
        }

        public void writeToParcel(Parcel out, int flags) {
            out.writeInt(TAG);
            out.writeInt(this.viewId);
            out.writeString(this.methodName);
            out.writeInt(this.type);
            //noinspection ConstantIfStatement
            if (false) {
                Log.d(LOG_TAG, "write viewId=0x" + Integer.toHexString(this.viewId)
                        + " methodName=" + this.methodName + " type=" + this.type);
            }

            // For some values which are null, we record an integer flag to indicate whether
            // we have written a valid value to the parcel.
            switch (this.type) {
                case BOOLEAN:
                    out.writeInt((Boolean) this.value ? 1 : 0);
                    break;
                case BYTE:
                    out.writeByte((Byte) this.value);
                    break;
                case SHORT:
                    out.writeInt((Short) this.value);
                    break;
                case INT:
                    out.writeInt((Integer) this.value);
                    break;
                case LONG:
                    out.writeLong((Long) this.value);
                    break;
                case FLOAT:
                    out.writeFloat((Float) this.value);
                    break;
                case DOUBLE:
                    out.writeDouble((Double) this.value);
                    break;
                case CHAR:
                    out.writeInt((int)((Character)this.value).charValue());
                    break;
                case STRING:
                    out.writeString((String)this.value);
                    break;
                case CHAR_SEQUENCE:
                    TextUtils.writeToParcel((CharSequence)this.value, out, flags);
                    break;
                case URI:
                    out.writeInt(this.value != null ? 1 : 0);
                    if (this.value != null) {
                        ((Uri)this.value).writeToParcel(out, flags);
                    }
                    break;
                case BITMAP:
                    out.writeInt(this.value != null ? 1 : 0);
                    if (this.value != null) {
                        ((Bitmap)this.value).writeToParcel(out, flags);
                    }
                    break;
                case BUNDLE:
                    out.writeBundle((Bundle) this.value);
                    break;
                case INTENT:
                    out.writeInt(this.value != null ? 1 : 0);
                    if (this.value != null) {
                        ((Intent)this.value).writeToParcel(out, flags);
                    }
                    break;
                case COLOR_STATE_LIST:
                    out.writeInt(this.value != null ? 1 : 0);
                    if (this.value != null) {
                        ((ColorStateList)this.value).writeToParcel(out, flags);
                    }
                default:
                    break;
            }
        }

        private Class getParameterType() {
            switch (this.type) {
                case BOOLEAN:
                    return boolean.class;
                case BYTE:
                    return byte.class;
                case SHORT:
                    return short.class;
                case INT:
                    return int.class;
                case LONG:
                    return long.class;
                case FLOAT:
                    return float.class;
                case DOUBLE:
                    return double.class;
                case CHAR:
                    return char.class;
                case STRING:
                    return String.class;
                case CHAR_SEQUENCE:
                    return CharSequence.class;
                case URI:
                    return Uri.class;
                case BITMAP:
                    return Bitmap.class;
                case BUNDLE:
                    return Bundle.class;
                case INTENT:
                    return Intent.class;
                case COLOR_STATE_LIST:
                    return ColorStateList.class;
                default:
                    return null;
            }
        }

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

        public int mergeBehavior() {
            // smoothScrollBy is cumulative, everything else overwites.
            if (methodName.equals("smoothScrollBy")) {
                return MERGE_APPEND;
            } else {
                return MERGE_REPLACE;
            }
        }

        public String getActionName() {
            // Each type of reflection action corresponds to a setter, so each should be seen as
            // unique from the standpoint of merging.
            return "ReflectionAction" + this.methodName + this.type;
        }
    }

通过上述的代码可以看出,ReflectionAction 表示的是一个反射的动作,他通过对View的操作会反射的方式调用,其中getMethod就是根据方法名来反射所需要的方法,其中ReflectionAction 也有很多的set方法,除了ReflectionAction 还有Action,等,这里我们拿TextViewSizeAction来分析,具体实现如下

    private class TextViewSizeAction extends Action {
        public TextViewSizeAction(int viewId, int units, float size) {
            this.viewId = viewId;
            this.units = units;
            this.size = size;
        }

        public TextViewSizeAction(Parcel parcel) {
            viewId = parcel.readInt();
            units = parcel.readInt();
            size  = parcel.readFloat();
        }

        public void writeToParcel(Parcel dest, int flags) {
            dest.writeInt(TAG);
            dest.writeInt(viewId);
            dest.writeInt(units);
            dest.writeFloat(size);
        }

        @Override
        public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
            final TextView target = (TextView) root.findViewById(viewId);
            if (target == null) return;
            target.setTextSize(units, size);
        }

        public String getActionName() {
            return "TextViewSizeAction";
        }

        int units;
        float size;

        public final static int TAG = 13;
    }

他的实现是比较简单的,他之所以不用反射来实现,是因为setTextView这个方法有灵感参数,因此无法复用ReflectionAction,因为ReflectionAction 的反射调用只需要一个参数,这里就不一一分析了

关于单击事件,RemoteView中只支持PendingIntent,是不支持其他模式的,我们这里需要注意的就是setOnClickPendingIntent,setPendingIntentTemplate,以及setonClickFillinIntent的区别,首先setOnClickPendingIntent用于给普通view设置单击事件,但是不能给ListView之类的View设置,如果需要,就用后两者

三.RemoteViews的意义

这一章,我们来真正意义上的理解他的意义,通过打造一个模拟的通知栏效果并且实现跨进程的UI更新

首先我们两个Activity分别运行在了两个不同的进程,一个是A,一个是B,其中A扮演的是通知栏的角色,而B则可以不停地发送通知栏消息,当然这是模拟的消息。为了模拟通知栏的效果,我们修改A的process属性使其运行在单独的进程中,这样A和B就构成了多进程通信的情形。我们在B中创建Remoteviews对象,然后通知A显示这个RemoteViews对象。如何通知A显示B中的RemoteViews呢?我们可以像系统一样采用
Binder来实现,但是这里为了简单起见就采用了广播。B每发送一次模拟通知,就会发送一个特定的广播,然后A接收到广播后就开始显示B中定义的RemoteViews对象,这个过程和系统的通知栏消息的显示过程几乎一致,或者说这里就是复制了通知栏的显示过程而已。

首先看B的实现,B只要构造RemoteViews对象并将其传输给A即可,这一过程通知栏是采用Binder实现的,但是本例中采用广播来实现,RemoteViews对象通过Intent传输A中,代码如下所示。

public class BActivity extends Activity implements View.OnClickListener {

    private Button btn_send;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_b);
        initView();


    }

    private void initView() {
        btn_send = (Button) findViewById(R.id.btn_send);
        btn_send.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.btn_send:
                RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.layout_item);
                remoteViews.setTextViewText(R.id.textView1, "Hello");
                remoteViews.setImageViewResource(R.id.imageview1, R.mipmap.ic_launcher);
                PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, TestActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);
                remoteViews.setOnClickPendingIntent(R.id.imageview1, pendingIntent);
                Intent intent = new Intent("send_bro");
                sendBroadcast(intent);
                break;
        }
    }
}

A的代码比较简单只是接收一个广播就行

public class AActivity extends Activity {

    private LinearLayout mLinearLayout;

    private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            RemoteViews remoteViews = intent.getParcelableExtra("send_bro");
            if (remoteViews != null) {
                updateUI(remoteViews);
            }
        }
    };

    private void updateUI(RemoteViews remoteViews) {
        View view = remoteViews.apply(this, mLinearLayout);
        mLinearLayout.addView(view);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_a);

        initView();
    }

    private void initView() {
        mLinearLayout = (LinearLayout) findViewById(R.id.mLinearLayout);
        IntentFilter intent = new IntentFilter("send_bro");
        registerReceiver(mBroadcastReceiver, intent);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        unregisterReceiver(mBroadcastReceiver);
    }
}

上述代码很简单,除了注册和解除广播以外,最主要的逻辑其实就是updateUI,当A收到广播后,会从Intent中取出RemoteViews对象,然后通过apply方法加载布局并且执行更新操作,最后将得到的View添加到A的布局中即可。可以发现这个过程很简单,但是通知栏的底层是如何实现的呢?

木节这个例子是可以在实际中使用的,比如现在有两个应用,一个应用需要能够事更新另一个应用中的某个界面,这个时候我们当然可以选择AIDL去实现,但是如果对界面的更新比较频繁,这个时候就会有效率问题,同时AIDL接口就有可能会变得很复杂。这个时间如果采用RemoteViews来实现就没有这个问题了,当然RemoteViews也有缺点,那就是他只支持一些常见的View,对于自定义View它是不支持的。面对这种问题,到底是采用AIDL还是采用RemoteViews,这个要看具体情况,如果界面中的View都是一些简单的且被RemoteViews支持的View,那么可以考虑采用RemoteViews,否则就不适合用RemoteViews 了。

如果打算采用RemoteViews来实现两个应用之间的界面更新,那么这里还有一个问题,那就是布局文件的加载问题。在上面的代码中,我们直接通过RemoteViews的的apply方法来加载并更新界面,如下所示。’

 View view = remoteViews.apply(this, mLinearLayout);
 mLinearLayout.addView(view);

这种写法在同一个应用的多进程情形下是适用的,但是如果A和B属于不同应用,那么B中的布局文件的资源id传输到A中以后很有可能是无效的,因为A中的这个布局文件的资源id不可能刚好和B中的资源id一样,面对这种情况,我们就要适当的修改Remoteviews的显示过程的代码了。这里给出一种方法,既然资源不相同,那我们就通过资源名称来加载布局文件。首先两个应用要提前约定好RemoteViews中的布局的文件名称,比如“layout simulated notification”,然后在A中根据名称找到并加载,接着再调用Remoteviews 的的reapply方法即可将B中对View所做的一系列更新操作加载到View上了,关于applyHe reapply方法的差别在前面说了,这样历程就OK了

        int layoutId = getResources().getIdentifier("layout_simulated_notification","layout",getPackageName());
        View view = getLayoutInflater().inflate(layoutId,mLinearLayout,false);
        remoteViews.reapply(this,view);
        mLinearLayout.addView(view);

到这里,这章也就结束了,理解的很深收益很大,这本书真的不错哦

有兴趣就加群吧:555974449

PPT下载:点击下载

Sample下载:点击下载

你可能感兴趣的:(Android开发艺术探索第五章——理解RemoteViews)