RemoteViews的应用

5.1 RemoteViews的应用

RemoteViews在实际开发中,主要用在通知栏和桌面小部件的开发过程中。通知栏每个人都不陌生,主要是通过NotificationManager的notify方法来实现的,它除了默认效果外,还有可以另外定制布局。桌面小部件则是通过AppWidgetProvider来实现的,AppWidgetProvider本质上是一个广播。通知栏和桌面小部件的开发过程中都会用到RemoteViews,它们在更新界面时无法像在Activity里面那样去直接更新View,这是因为二者的界面都运行在其他进程中,确切来说是系统的SystemServer进程。为了跨进程更新界面,RemoteViews提供了一系列set方法,并且这些方法只是View全部方法的子集,另外RemoteViews中所支持的View类型也是有限的,这一点会在5.2节中进行详细说明。下面简单介绍一下RemoteViews在通知栏和桌面小部件中的使用方法,至于它们更详细的使用方法请读者阅读相关资料即可,本章的重点是分析RemoteViews的内部机制。

5.1.1 RemoteViews在通知栏上的应用

首先我们看一下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);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
notification.setLatestEventInfo(this, "chapter_5", "this is notification.", pendingIntent);
NotificationManager manager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
manager.notify(1, notification);

上述代码会弹出一个系统默认样式的通知,单击通知后会打开DemoActivity_1同时会清除本身。为了满足个性化需求,我们还可能会用到自定义通知。自定义通知也很简单,首先我们要提供一个布局文件,然后通过RemoteViews来加载这个布局文件即可改变通知的样式,代码如下所示。

Notification notification = new Notification();
notification.icon = R.drawable.ic_launcher;
notification.tickerText = "hello world";
notification.when = System.currentTimeMillis();
notification.flag = Notification.FLAG_AUTO_CANCEL;
Intent intent = new Intent(this, DemoActivity_1.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.layout_notification);
remoteViews.setTextViewText(R.id.msg, "chapter_5");
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 = (NotifitionManager)getSystemService(Context.NOTIFICATION_SERVICE);
manager.notify(2, notification);

从上述内容来看,自定义通知的效果需要用到RemoteViews,自定义通知的效果如图5-1所示。

图5-1 自定义通知栏样式.jpg

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

5.1.2 RemoteViews在桌面小部件上的应用

AppWidgetProvider是Android中提供的用于实现桌面小部件的类,其本质是一个广播,即BroadcastReceiver,图5-2表示的是它的类继承关系。所以,在实际的使用中,把AppWidgetProvider当成一个BroadcastReceiver就可以了,这样许多功能就很好理解了。

[图片上传失败...(image-42303b-1585928039011)]

为了更好地展示RemoteViews在桌面小部件上的应用,我们先简单介绍桌面小部件的开发步骤,分为如下几步。

1. 定义小部件界面

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



    
    
    

2. 定义小部件配置信息

在res/xml/下新建appwidget_provider_info.xml,名称随意选择,添加如下内容:





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

3. 定义小部件的实现类

这个类需要继承AppWidgetProvider,代码如下:

public class MyAppWidgetProvider extends AppWidgetProvider {
    public static final String TAG = "MyAppWidgetProvider";
    public static final String CLICK_ACTION ="com.chenstyle.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);
                        SystemClick.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 appWidgetManager
     * @param appWidgetId
     */
    private void onWidgetUpdate(Context context, AppWidgetManager appWidgetManager, 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);
        appWidgetManager.updateAppWidget(appWidgetId, remoteViews);
    }
    
    priavte 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来加载布局文件,而当小部件被单击后的旋转效果则是通过不断的更新RemoteViews来实现的,由此可见,桌面小部件不管是初始化界面还是后续的更新界面都必须使用RemoteViews来完成。

4. 在AndroidManifest.xml中声明小部件

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


    
    
    
    
        
        
    

上面的代码中有两个Action,其中第一个Action用于识别小部件的单击行为,而第二个Action则作为小部件的标识而必须存在,这是系统的规范,如果不加,那么这个receiver就不是一个桌面小部件并且也无法出现在手机的小部件列表里。

AppWidgetProvider除了最常用的onUpdate方法,还有其他几个方法:onEnabled、onDisabled、onDeleted以及onReceive。这些方法会自动的被onReceive方法在合适的时间调用。确切来说,当广播到来以后,AppWidgetProvider会自动根据广播的Action通过onReceive方法来自动分发广播,也就是调用上述几个方法。这几个方法的调用时机如下所示。

  • onEnable:当该窗口小部件第一次添加到桌面时调用该方法,可添加多次但只在第一次调用。
  • onUpdate:小部件被添加时或者每次小部件更新时都会调用一次该方法,小部件的更新时机由updatePeriodMillis来指定,每个周期小部件都会自动更新一次。
  • onDeleted:没删除一次桌面小部件就调用一次。
  • onDisabled:当最后一个该类型的桌面小部件被删除时调用该方法,注意是最后一个。
  • onReceive:这是广播的内置方法,用于分发具体的事件给其他方法。

关于AppWidgetProvider的onReceive方法的具体分发过程,可以参看源码中的实现,如下所示。通过下面的代码可以看出,onReceive中会根据不同的Action来分别调用onEnable、onDisable和onUpdate等方法。

public void onReceive(Context context, Intent intent) {
    // Protect against rogue update broadcasts (not really a security issue,
    // just filter bad broadcasts 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_APPWINDGET_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_OPTIIONS);
            this.onAppWidgetOptionsChanged(context, AppWidgetManager.getInstance(context), appWidgetId, widgetExtras);
        } 
    } else if (AppWidgetManager.ACTION_APPWIDGET_ENABLED.equals(action)) {
        this.enabled(context);
    } else if (AppWidgetManager.ACTION_APPWIDGET_DISABLED.equals(action)) {
        this.disabled(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.gerIntArray(AppWidgetManager.EXTRA_APPWIDGET_IDS);
            if (oldIds != null && oldIds.length > 0) {
                this.onRestored(context, oldIds, newIds);
                this.onUpdate(context, AppWidgetManager.getInstance(context), newIds);
            }
        }
    }
}

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

5.1.3 PendingIntent概述

在5.2.1节中,我们多次提到PendingIntent,那么PendingIntent到底是什么东西呢?它和Intent的区别是什么呢?在本节中将介绍PendingIntent的使用方法。

顾名思义,PendingIntent表示一种处于pending状态的意图,而pending状态表示的是一种待定、等待、即将发生的意思,就是说接下来有一个Intent(即意图)将在某个待定的时刻发生。可以看出PendingIntent和Intent的区别在于,PendingIntent是在将来的某个不确定的时刻发生,而Intent是立刻发生,PendingIntent典型的使用场景是给RemoteViews添加单击事件,因为RemoteViews运行在远程进程中,因此RemoteViews不同于普通的View,所以无法直接像View那样通过setOnClickListener方法来设置单击事件。想要给RemoteViews设置单击事件,就必须使用PendingIntent,PendingIntent通过send和cancel方法来发送和取消特定的待定Intent。

PendingIntent支持三种待定意图:启动Activity、启动Service和发送广播,对应着它的三个接口方法,如表5-1所示。

表5-1 PendingIntent的主要方法

PendingIntent 方法
static PendingIntent getActivity(Context context, int requestCode, Intent intent, int flags)
获得一个PendingIntent,该待定意图发生时,效果相当于
Context.startActivity(Intent)
static PendingIntent getService(Context context, int requestCode, Intent intent, int flags)
获得一个PendingIntent,该待定意图发生时,效果相当于
Context.startService(Intent)
static PendingIntent getBroadcast(Context context, int requestCode, Intent intent, int flags)
获得一个PendingIntent,该待定意图发生时,效果相当于
Context.sendBroadcast(Intent)

如表5-1所示,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相同并且request也相同,那么这两个PendingIntent就是相同的。requestCode相同比较好理解,那么什么情况下Intent相同呢?Intent的匹配规则是:如果两个Intent的ComponentName和intent-filter都相同,那么这两个Intent就是相同的。需要注意的是Extra不参与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,并且这些通知都是可以打开的。

你可能感兴趣的:(RemoteViews的应用)