Android开发五《理解RemoteViews》

2022-03-23  本文已影响0人  独自闯天涯的码农

RemoteViews表示的是一个View结构,它可以在其他进程中显示,它提供了一组基础的操作用于跨进程更新它的界面.


1.png

支持的布局:

一、RemoteViews的应用

使用场景:
1、通知栏:通过NotiicationManager的notify方法实现

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

 String id = "my_channel_01";
 CharSequence name = "channel";
 String description = "description";
 int importance = NotificationManager.IMPORTANCE_DEFAULT;
 NotificationChannel mChannel = new NotificationChannel(id, name, importance);

 mChannel.setDescription(description);
 mChannel.enableLights(true);
 mChannel.setLightColor(Color.RED);
 mChannel.enableVibration(true);
 mChannel.setVibrationPattern(new long[]{100, 200, 300, 400, 500, 400, 300, 200, 400});

 NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
 manager.createNotificationChannel(mChannel);


 RemoteViews remoteView = new RemoteViews(getPackageName(), R.layout.layout_notification);
 remoteView.setTextColor(R.id.re_text, Color.RED);
 remoteView.setTextViewText(R.id.re_text, "remote view demo");
 remoteView.setImageViewResource(R.id.re_image, R.drawable.btn_me_share);
 remoteView.setOnClickPendingIntent(R.id.notification, pendingIntent);

 Notification notification = new Notification.Builder(this, id)
 .setAutoCancel(false)
 .setContentTitle("title")
 .setContentText("describe")
 .setContentIntent(pendingIntent)
 .setSmallIcon(R.drawable.btn_me_share)
 .setOngoing(true)
 .setCustomContentView(remoteView)
 .setWhen(System.currentTimeMillis())
 .build();
 manager.notify(1, notification);

从上面代码发现,RemoteViews的方法使用起来很简单。利用构造函数new RemoteViews(packagename, layoutId) 来关联一个view的布局,并通过一些set 方法更新布局,最后利用notification.Builder().setCustomContentView(RemoteViews) 来设置通知栏的view

2、桌面小部件:通过AppWidgetProvider(本质是一个广播)来实现;
桌面小部件主要是利用RemoteViews和AppWidgetProvider结合使用,而AppWidgetProvider又是extends BroadcastReceiver, 所以再使用的时候,多了一些关于广播的知识。

public class NewAppWidget extends AppWidgetProvider {
  private static final String CLICK_ACTION = "com.taohuahua.action.click";

  static void updateAppWidget(final Context context, final AppWidgetManager appWidgetManager, final int appWidgetId) {
    final RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.layout_widget);

    Intent anIntent = new Intent();
    anIntent.setAction(CLICK_ACTION);
    PendingIntent anPendingIntent = PendingIntent.getBroadcast(context, 0, anIntent, 0);
    views.setOnClickPendingIntent(R.id.appwidget_text, anPendingIntent);
    appWidgetManager.updateAppWidget(appWidgetId, views);
  }

  @Override
 public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
    for (int appWidgetId : appWidgetIds) {
      updateAppWidget(context, appWidgetManager, appWidgetId);
    }
}

  @Override
  public void onEnabled(Context context) {
  // Enter relevant functionality for when the first widget is created
  }

  @Override
  public void onDisabled(Context context) {
  // Enter relevant functionality for when the last widget is disabled
  }

  @Override
  public void onReceive(Context context, Intent intent) {
    super.onReceive(context, intent);
    final RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.layout_widget);

    if (Objects.equals(intent.getAction(), CLICK_ACTION)) {
    Toast.makeText(context, "hello world", Toast.LENGTH_SHORT).show();

    //获得appwidget管理实例,用于管理appwidget以便进行更新操作
    AppWidgetManager manger = AppWidgetManager.getInstance(context);
    // 相当于获得所有本程序创建的appwidget
    ComponentName thisName = new ComponentName(context, NewAppWidget.class);
    //更新widget
    manger.updateAppWidget(thisName, views);
  }
}

从代码中可以看出,里面有几个重要的方法:

  1. onUpdate: 小部件被添加时或者每次更新时调用。更新时间由第二步配置中updatePeriodMills来决定,单位为毫秒。
  2. onReceive: 广播内置方法,用于分发接收到的事件。
  3. onEnable: 当该窗口小部件第一次添加时调用。
  4. onDelete:每删除一次调用一次。
  5. onDisabled:最后一个该桌面小部件被删除时调用。
    所以在onUpdate方法中利用RemoteViews来显示了新的布局,并利用pendingIntent来实现点击小部件控件跳转的方法。

二、RemoteViews的内部机制

RemoteView的作用是在其他进程中显示并更新View的界面,最常用的构造方法:

public RemoteViews(String packageName,int layoutId)

RemoteViews没有提供findViewById方法,必须通过RemoteViews所提供的一系列set方法来完成更新;这些set方法通过反射来完成的.


RemoteViews的部分set方法

通知栏以及小部件分别由NotificationManager和AppWidgetManager管理,而NotificationManager以及AppWidgetManager通过Binder分别和SystemServer进行中的NotificationManagerService以及AppWidgetService进行通信,因此,通知栏以及桌面小部件中的布局文件是在NotificationManagerService以及AppWidgetService中被加载的,而它们运行在系统的systemServer中,这就和我们的进行构成了跨进程通信的场景。
这里没有使用Binder进行进程通信,由于View的方法太多大量的IPC操作会影响效率,这里提供了Action的概念,Action代表一个View的操作,系统将Action操作封装到Action对象并将这些对象跨进程传输到远程进程中,接着直接Action对象中的Action操作。我们使用RemoteViews时,每调用一个set方法,就会添加一个Action对象,当我们通过NotificationManager和AppWidgetManager提交更新时,这些Action对象就会传输到远程进程中并依次执行。


内部机制

在RemoteViews的源码中,可以看到定义了一个Action对象的列表

 /**
 * An array of actions to perform on the view tree once it has been
 * inflated
 */
 private ArrayList<Action> mActions;</pre>

而Action的是对远程视图进行的操作的一个封装。因为我们无法通过RemoteViews的findViewById方法来操作视图,所以RemoteViews每次视图的操作都会创建一个action对象添加到列表中。

/**
 * Base class for all actions that can be performed on an
 * inflated view.
 *
 *  SUBCLASSES MUST BE IMMUTABLE SO CLONE WORKS!!!!!
 */
 private abstract static class Action implements Parcelable {
   public abstract void apply(View root, ViewGroup rootParent, OnClickHandler handler) throws ActionException;

   public static final int MERGE_REPLACE = 0;
   public static final int MERGE_APPEND = 1;
   public static final int MERGE_IGNORE = 2;

   public int describeContents() {
     return 0;
   }

   /**
   * Overridden by each class to report on it's own memory usage
   */
   public void updateMemoryUsageEstimate(MemoryUsageCounter counter) {
    // We currently only calculate Bitmap memory usage, so by default,
    // don't do anything here
   }

   public void setBitmapCache(BitmapCache bitmapCache) {
   // Do nothing
   }

   public int mergeBehavior() {
    return MERGE_REPLACE;
   }

   public abstract String getActionName();

   public String getUniqueKey() {
     return (getActionName() + viewId);
   }

   /**
     * This is called on the background thread. It should perform any non-ui computations
   * and return the final action which will run on the UI thread.
   * Override this if some of the tasks can be performed async.
   */
   public Action initActionAsync(ViewTree root, ViewGroup rootParent, OnClickHandler handler) {
     return this;
   }

   public boolean prefersAsyncApply() {
     return false;
   }

   /**
   * Overridden by subclasses which have (or inherit) an ApplicationInfo instance
   * as member variable
   */
   public boolean hasSameAppInfo(ApplicationInfo parentInfo) {
     return true;
   }
  
   int viewId;
 }

从源码中可以看出,action提供了一个抽象的方法

 public abstract void apply(View root, ViewGroup rootParent, OnClickHandler handler) throws ActionException;

在RemoteViews.java中发现了有很多Action的子类, 这里重点讲解一个类

 /**
 * Base class for the reflection actions.
 */
 private final class ReflectionAction extends Action {
 ...
 }

因为很多更新视图的方法最后都走到

addAction(new ReflectionAction(viewId, methodName, type, value));

可以发现,当RemoteViews通过set方法来更新一个视图时,并没有立即更新,而是添加到action列表中。这样可以大大提高跨进程通信的性能,至于什么时候更新,对于自定义通知栏,需要NotificationManager调用notify()之后;而对于桌面小部件,则需要AppWidgetManager调用updateAppWidget()之后。
最后进入ReflectionAction类中的apply方法看一下,发现内部就是利用反射机制设置相关视图的。

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

注意:当我们调用RemoteViews的set方法时,并不会立刻更新它们的界面,必须通过NotificationManager的notify方法以及AppWidgetManager的updateAppWidget方法才会更新他们的界面。

三、PendingIntent概述

peddingIntent表示一种处于pending状态的意图,pending状态表示一种待定、等待、即将发生。就是说接下来有一个Intent将要在某个待定的时刻发生。PendingIntent和Intent的区别在于,PendingIntent是在将来某个时刻发生,而Intent是立刻发生。PendingIntent典型的使用场景是给RemoteViews添加单机事件。由于RemoteViews运行在远程进程中,无法直接调用setOnClickListener方法来设置单击事件,就需要使用pendingIntent,PendingIntent通过send和cancel来发送和取消待定的Intent。

PendingIntent支持的三种待定意图:启动Activity、启动Service以及发送广播


PendingIntent的主要方法

这三个方法的参数意义是相同的,第一个和第三个参数比较好理解,主要是第二个和第四个;
第二个参数requestCode,requestCode表示PendingIntent方的请求码,多数情况设为0即可。requestCode会影响到flags的效果.
第四个参数flags有四种类型:

  1. FLAG_ONE_SHOT
    PendingIntent只被使用一次,然后被自动cancel,后续如果还有相同的PendIntent,那么它们的send方法调用失败。对通知栏消息来说,同类的通知只会使用一次,后续的通知单击后将无法打开。
  2. FLAG_NO_CREATE
    当前描述的PendIntent不会主动创建,如果当前PendIntent之前不存在,那么getActivity,getService,getBroadcast方法会直接返回null,即获取PendingIntent失败。
  3. FLAG_CANCEL_CURRENT
    当前描述的PendIntent如果已经存在,那么它们会被cancel,然后系统创建一个新的PendingIntent。对通知栏消息来说,那些被cancel的消息单击后将无法打开。
  4. FLAG_UPDATE_CURRENT
    当前描述的PendIntent如果已经存在,那么它们都会被更新。即它们的Intent中的Extras会被替换成最新的。

PendingIntent的匹配规则为:
如果两个PendingIntent它们内部的Intent相同并且 requestCode也相同则相同。
Intent的匹配规则为:
如果两个Intent的ComponentName和intent-filter都相同,则它们两个就是相同的。

通过通知栏消息理解这四个标记,分两种情况
manager.notify(1,notification)
1、通知ID是常量
多次调用notify只能弹出一个通知,后续的通知会把前面的通知完全替代掉;
2、通知ID是变量
多次调用notify会弹出多个通知;这也分两种情况

上一篇下一篇

猜你喜欢

热点阅读