理解RemoteViews——RemoteViews的应用

本章所讲述的主题时RemoteViews,从明天可以看出,RemoteViews应该是一种远程View,那么什么是远程View呢?如果说远程服务可能比较好理解,但是远程View的确没有听说过,其实它和远程Service是一样的,RemoteViews表示的是一个View结构,它可以再其他进程中显示,由于它再其他进行中显示,为了能够更新它的界面,RemoteViews提供了一组基础的操作用于跨进程更新它的界面。这听起来有点神奇,竟然能跨进程更新界面!但是RemoteViews的确能够实现这个效果。 RemoteViews在Android中的使用场景右两种:通知栏和桌面小部件,为了更好地分析 RemoteViews的内部机制,本章先简单介绍 RemoteViews在通知栏和桌面小部件上的应用,接着分析 RemoteViews的内部机制,最后分析 RemoteViews的意义并给出一个采用 RemoteViews来跨进程更新界面的示例。

1.RemoteViews在通知栏上的应用

首先我们看一下 RemoteViews在通知栏上的应用,我们知道,通知栏除了默认的效果外还支持自定义布局,下面分别说明这两种情况。
使用系统默认样式演出一个通知是很简单的,代码如下:
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, "hello"); builder.setSmallIcon(R.mipmap.ic_launcher); builder.setWhen(System.currentTimeMillis()); builder.setTicker("hello world"); Intent intent = new Intent(this, DemoActivity_1.class); PendingIntent pendingIntent = PendingIntent.getActivity(this,0,intent,PendingIntent.FLAG_UPDATE_CURRENT); builder.setContentIntent(pendingIntent); builder.setContentTitle("chapter_5"); builder.setContentText("this is notification.");
        builder.setAutoCancel(true); NotificationManager manager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE); manager.notify(1,builder.build());

上述代码会弹出一个系统默认样式的通知,单击通知后会打开DemoActivity_1同时会清楚本身。为了满足个性化需求,我们还可能会用到自定义通知。自定义通知也很简单,首先我们要提供一个布局文件,然后通过RemoteViews来加载这个布局文件即可改变通知的样式,代码如下所示。
        RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.notification);
        remoteViews.setTextViewText(R.id.msg, "chapter_5");
        remoteViews.setImageViewResource(R.id.icon, R.drawable.icon);
        PendingIntent openActivity2PendingIntent = PendingIntent.getActivity(this,
                0, new Intent(this, DemoActivity_2.class), PendingIntent.FLAG_UPDATE_CURRENT);
        remoteViews.setOnClickPendingIntent(R.id.open_activity2, openActivity2PendingIntent);
        builder.setContent(remoteViews);

        Intent intent = new Intent(this, DemoActivity_1.class);
        PendingIntent pendingIntent = PendingIntent.getActivity(this,0,intent,PendingIntent.FLAG_UPDATE_CURRENT);
        builder.setContentIntent(pendingIntent);

        NotificationManager manager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
        manager.notify(1,builder.build());

从上述内容来看,自定义通知效果需要用到RemoteViews,自定义通知的效果如下图所示。
理解RemoteViews——RemoteViews的应用_第1张图片

RemoteViews的使用也很简单,只要提供当前应用的包名和布局文件的资源id即可创建一个 RemoteViews对象。如何更新 RemoteViews呢?这一点和更新View有很大的不同,更新RemoteViews时,无法直接访问里面的View,而必须通过RemoteViews所提供的一些列方法来更新View。比如设置TextView的文本,要采用如下方式:remoteViews.setTextViewText(R.id.msg, "chapter_5"),其中setTextView的两个参数分别为TextView的id和要设置的文本。而设置ImageView的图片也不能直接访问ImageView,必须通过如下方式:remoteViews.setImageViewResource(R.id.icon, R.drawable.icon),setImageViewResource的两个参数分别为ImageView的id和要设置的图片资源的id。如果要给一个空间加单击事件,则要使用PendingIntent并通过setOnClickPendingIntent方法来实现,比如remoteViews.setOnClickPendingIntent(R.id.open_activity2, openActivity2PendingIntent)这句代码会给id为open_activity2的View加上点击事件。关于PendingIntent,它表示的是一种待定的Intent,这个Intent中所包含的意图必须由用户来触发。为什么更新RemoteViews如此复杂呢?直观原因是因为RemoteViews并没有提供和View类似的findViewById这个方法,因此我们无法获取到RemoteViews中的子View,当然时机原因绝非如此,具体会在后面进行介绍。

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

AppWidgetProvider是Android中提供用于实现桌面小部件的类,其本质是一个广播即BroadcastReceiver。所以在实际的使用中,把 AppWidgetProvider当成一个 BroadcastReceiver就可以了,这样许多功能就很好理解了。
为了更好的展示RemoteViews在桌面小部件上的应用,我们先介绍桌面小部件的开发步骤,分为如下几步。

2.1 定义小部件界面
在res/layout下新建一个XML文件,命名为widget.xml,名称和内容可以自定义,看这个小部件要做成什么样子,内容如下所示。



    

2.2 定义小部件配置信息
在res/xml下新建appwidget_provider_info.xml,名称随意,添加如下内容。



上面几个参数的意义很明确,initialLayout就是指小工具所使用的初始化布局,minHeight和minWidth定义小工具的最小尺寸,updatePeriodMillis定义小工具的自动更新周期,毫秒为单位,每隔一个周期,肖工裤的自动更新就会触发。

2.3定义小部件的实现类
这个类需要继承AppWidgetProvider,代码如下所示。
public class MyAppWidgetProvider extends AppWidgetProvider {
    public static final String TAG = "MyAppWidgetProvider";
    public static final String CLICK_ACTION = "study.chenj.chapter5.CLICK";

    public MyAppWidgetProvider() {
        super();
    }

    @Override
    public void onReceive(final Context context, Intent intent) {
        super.onReceive(context, intent);
        Log.d(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 srcBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.icon);
                    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, srcBitmap, 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();
        }
    }

    /**
     * 每次桌面小部件更新时都调用一次该方法
     * @param context
     * @param appWidgetManager
     * @param appWidgetIds
     */
    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        super.onUpdate(context, appWidgetManager, appWidgetIds);
        Log.d(TAG, "onUpdate");
        final int counter = appWidgetIds.length;
        Log.d(TAG, "counter = " + counter);

        for (int i=0; iRemoteViews来实现的,由此可见,桌面小部件不管时初始化界面还是后续的更新界面都必须使用RemoteViews来完成。 
  

4.在AndroidManifest.xml声明小部件

这是最后一步,因为桌面小部件本质上是一个广播组件,因此必须要注册,如下所示。
        
            
            

            
                
                
            
        

上面代码中有两个Action,其中第一个Action用于识别小部件的单击行为,而第二个Action则作为小部件的标识而必须存在,这时系统的规范,如果不加,那么这个receiver就不是一个桌面小部件并且也无法出现在手机的小部件列表里。
AppWidgetProvider除了最常用的onUpdate方法,还有其他几个方法:onEnabled、onDisabled、onDelete以及onReceive。这些方法会自动地被onReceive方法在合适地时间调用。确切来说,当广播到来以后, AppWidgetProvider会自动更具广播地Action通过onReceive方法来自动分发广播,这就是调用上述几个方法。这几个方法地调用时机如下所示。
  • onEnable:当该窗口小部件第一次添加到桌面时调用该方法,可以添加多次但只在第一次调用。
  • onUpdate:小部件被添加时或者小部件更新时都会调用一次该方法,小部件的更新时机由updatePeriodMillis来指定,每个周期小部件都会自动更新一次。
  • onDelete:每次删除桌面小部件就调用一次。
  • onDisabled:当最后一个该类型的桌面小部件被删除时调用该方法,注意是最后一个。
  • onReceive:这是广播的内置方法,用于分发具体的事件给其他方法。
关于 AppWidgetProvider地onReceive方法的具体分发过程,可以参考源码中的实现,如下所示。通过下面的代码可以看出,onReceive中会更具不同的Action来分别调用 onEnabled、onDisabledonUpdate等方法。
    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概述

在2节中,我们多次提到 PendingIntent,那么PendingIntent到底是什么东西呢?它和intent的区别是什么呢?在本节中将介绍PendingIntent的使用方法。
顾名思义,PendingIntent表示一种处于pending状态的意图,而pending状态表示的是一种待定、等待、即将发生的意思,就是说接下来有一个Intent(即意图)将在某个待定的时刻发生。可以看出PendingIntent和Intent的区别在于,PendingIntent是将来的某个不确定的时刻发生,而Intent是立刻发生。PendingIntent典型的使用场景是给RemoteViews添加单击事件,因为RemoteViews运行在远程进程中,因此RemoteViews不同于普通的View,所以无法直接向View那样通过setOnClickListener方法来设置单击事件。想要给RemoteViews设置点击事件,就必须使用PendingIntentPendingIntent通过send和cancel方法来发送和取消特定的待定Intent。
PendingIntent支持三种待定意图:启动Activity、启动Service和发送广播,对应着它的三个接口方法,如下表所示。
理解RemoteViews——RemoteViews的应用_第2张图片

如上表所示,getActivity、getService和getBroadcast着三个方法的参数意义都是相同的,第一个和第三个参数比较好理解,这里主要说明下第二个参数requestCode和第四个参数flags,其中requestCode表示PendingIntent发送方的请求码,多数情况下设为0即可,另外requestCode会影响到flags的效果。flags常见的类型有:FLAG_ONE_SHOT、FLAG_NO_CREATE、FLAG_CANCEL_CURRENT和FLAG_UPDATE_CURRENT。在说明这四个标记位之前,必须要明白一个概念,那就是PendingIntent的匹配规则,即在什么情况下PendingIntent是相同的。
PendingIntent的匹配规则为:如果两个PendingIntent它们内部的Intent相同并且requestCode也相同,那么这两个PendingIntent就是相同的。requestCode相同比较好理解,那么什么情况下Intent相同呢?Intent的匹配规则是:如果两个Intent的ComponentName和intent-filter都相同,那么Intent就是相同的。需要注意的是Extras不参与Intent的匹配过程,只要Intent之间的ComponentName和intent-filter相同,即使它们的Extras不同,那么这两个Intent也是相同的。了解了PendingIntent的匹配规则后,就可以进一步理解flags参数的含义了,如下所示。

FLAG_ONE_SHOT

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

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

FLAG_CANCEL_CURRENT

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

FLAG_UPDATE_CURRENT

当前描述的PendingIntent如果已经存在,那么它们都会被更新,即它们的Intent中的Extras会被替换成最新的。

从上面的分析来看还是不太好理解这四个标记位,下面结合通知栏消息再描述一遍。这里分两种情况,如下代码中:manager.notify(1,notification),如果notify的第一个参数id是常量,那么多次调用notify只能弹出一个通知,后续的通知会把前面的通知完全替代掉,而如果每次id都不同,那么多次调用notify会弹出多个通知,下面一一说明。
如果 notify的第一个参数id是常量,那么不管PendingIntent是否匹配,后面的通知会直接替换前面的通知,这个很好理解。
如果notify的第一个参数id每次都不同,那么当PendingIntent不匹配时,这里的匹配是指PendingIntent中的Intent相同且requestCode相同,在这种情况下不管采用何种标记位,这些通知之间不会互相干扰。如果PendingIntent处于匹配状态是,这个时候要分情况讨论:如果采用了FLAG_ONE_SHOT标记位,那么后续通知通的中的PendingIntent会和第一条通知保持完全一致,包括其中的Extras,单击任何一条通知后,剩余的通知均无法再打开,当所有的通知被清楚后,会重复这个过程;如果采用FLAG_CANCEL_CURRENT标记位,那么只有最新的通知可以打开,之前弹出的所有通知均无法打开;如果采用FLAG_UPDATE_CURRENT,那么之前弹出的所有通知中的PendingIntent会被更新,最后它们和最新的一条通知保持完全一致,包括Extras,并且这些通知都是可以打开的。

你可能感兴趣的:(Android开发艺术探索笔记)