0%

理解 RemoteViews(一):RemoteViews 的应用

1. RemoteViews 的概念

  • RemoteViews 表示的是一个 View 结构,它可以在其他进程中(系统的 SystemServer 进程)显示。由于它在其他进程中显示,为了能够更新它的界面,RemoteViews 提供了一组基础的操作用于跨进程更新它的界面
  • RemoteViews 在 Android 中的使用场景有两种:通知栏桌面小部件
    • 通知栏主要是通过 NotificationManager 的 notify 方法来实现的,除了默认效果,还可以另外定义布局
    • 桌面小部件是通过 AppWidgetProvider 来实现的,AppWidgetProvider 继承自 BroadcastReceiver,所以本质上是一个广播

2. RemoteViews 在通知栏上的应用

  • 使用系统默认的样式弹出一个通知:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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);
  • 为满足个性化需求,自定义通知,提供一个布局文件,通过 RemoteViews 加载布局文件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    Notification notification = new Notification();
    notification.icon = R.drawable.ic_launcher;
    notification.tickerText = "hello world";
    notification.when = System.currentTimeMillis();
    notification.flags = Notification.FLAG_AUTO_CANCLE;

    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 表示的是一种待定的 intent,这个 Intent 中所包含的意图必须由用户来触发
    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(2, notification);

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

  • 桌面小部件的开发步骤

    1. 定义小部件界面

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      // 在 res/layout/ 下新建一个 XML 文件,命名为 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"
      android:orientation="vertical" >

      <ImageView
      android:id="@+id/imageView1"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:src="@drawable/icon1" />
      </LinearLayout>
    2. 定义小部件配置信息

      1
      2
      3
      4
      5
      6
      7
      8
      9
      // 在 res/xm/ 下新建 appwidget_provider_info.xml,名称自定义
      <?xml version="1.0" encoding="utf-8"?>
      <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
      android:initialLayout="@layout/widget"
      android:minWidth="84dp"
      android:minHeight="84dp"
      android:updatePeriodMillis="86400000" >

      </appwidget-provider>
      • initialLayout: 小工具所使用的初始化布局
      • minWidth/minHeight: 定义小工具的最小尺寸
      • updatePeriodMillis: 定义小工具的自动更新周期,单位为毫秒,每隔一个周期,小工具的自动更新就会触发
    3. 定义小部件的实现类

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      // 实现类需要继承 AppWidgetProvider
      public class MyAppWidgetProvider extends AppWidgetProvider {
      public static final String TAG = "MyAppWidgetProvider";
      public static final String CLICK_ACTION = "com.szy.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 arcbBitmap = 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 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);
      }

      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 来加载布局文件,而当小部件被单击后的旋转效果则是通过不断地更新 RemoteViews 来实现
      • 桌面小部件不管是初始化界面还是后续的更新界面都必须使用 RemoteViews 来完成
    4. 在 AndroidManifest.xml 中声明小部件

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      <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.szy.chapter_5.action.CLICK" />
      <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
      </intent-filter>
      </receiver>
      • 因为桌面小部件本质上是一个广播,因此必须要注册
      • 第一个 action 用于识别小部件的单击行为
      • 第二个 action 作为小部件的标识必须存在,这是系统的规范。如果不加,那么这个 receiver 就不是一个桌面小部件并且也无法出现在手机的小部件列表里
  • AppWidgetProvider 除了最常用的 onUpdate() 方法,还有其他几个方法:onEnabled()onDisabled()onDeleted()onReceive()这些方法会自动地被 onReceive() 方法在合适的时间(根据不同的 Action)调用

  • 确切来说,当广播到来以后,AppWidgetProvider 会自动根据广播的 Action 通过 onReceive() 方法来自动分发广播,也就是调用上面几个方法。这几个方法的调用时机如下:

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

4. PendingIntent 概述

  • PendingIntent 的概念

    • PendingIntent 表示一种处于 pending 状态的意图,而 pending 状态表示的是一种待定、等待、即将发生的意思
    • PendingIntent 和 Intent 的区别:PendingIntent 是在将来的某个不确定的时刻发生,而 Intent 是立刻发生
    • PendingIntent 典型的使用场景是给 RemoteViews 添加单击事件,并通过 sendcancel 方法来发送和取消特定的 Intent
  • 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,该待定意图发生时,效果相当于 Contet.startService(Intent)
    static PendingIntent getBroadcast(Context context, int requestCode, Intent intent, int flags): 获得一个 PendingIntent,该待定意图发生时,效果相当于 Context.sendBroadcast(Intent)
  • 接口方法的参数含义

    • requestCode 表示 PendingIntent 发送方的请求码,多数情况下设为 0 即可,另外 requestCode 会影响到 flags 的效果
    • flags 常见的类型有:FLAG_ONE_SHOTFLAG_NO_CREATEFLAG_CANCEL_CURRENTFLAG_UPDATE_CURRENT
  • PendingIntent 的匹配规则

    • 如果两个 PendingIntent 它们内部的 Intent 相同并且 requestCode 也相同,那么这两个 PendingIntent 就是相同的
    • Intent 的匹配规则:如果两个 Intent 的 ComponentNameintent-filter 都相同,那么这两个 Intent 就是相同的(Extras 不参与 Intent 的匹配过程
  • flags 参数含义

    • FLAG_ONE_SHOT:

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

      • 当前描述的 PendingIntent 不会主动创建,如果当前 PendingIntent 之前不存在,那么 getActivity()getService()getBroadcast() 方法会直接返回 null,即获取 PendingIntent 失败
      • 这个标记位很少见,无法单独使用,日常开发中使用意义不大
    • FLAG_CANCEL_CURRENT:

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

      • 当前描述的 PendingIntent 如果已经存在,那么它们都会被更新,即它们的 Intent 中的 Extras 会被替换成最新的
-------------------- 本文结束感谢您的阅读 --------------------