RemoteViews的作用和原理

1. RemoteViews 是什么

查看RemoteViews的层级结构,RemoteViews没有继承View, 却实现了parcelable这个接口。

image

查看文档说明:

A class that describes a view hierarchy that can be displayed in another process. The hierarchy is inflated from a layout resource file, and this class provides some basic operations for modifying the content of the inflated hierarchy.

翻译出来: 这是一个可以跨进程显示view的类,显示的view是从布局文件inflate出来,且该类提供了一些基本的方法来修改这个view的内容。

那么提炼一下就是:

  • 跨进程使用

  • 显示view

  • 修改view的内容。

    接下来就以这三条线索来介绍RemoteViews的使用。

2. RemoteViews的使用

RemoteViews主要用在通知栏和桌面小部件上。

RemoteViews并不是一个view, 但可以表示一个layout的布局;又因为是继承parcelable,所以可以跨进程使用,但因为是跨进程,所以没办法像我们之前通过findviewById方法来访问布局里的每个view,所以RemoteViews提供了一些set方法来更新view 的显示;但RemoteViews可以支持的布局和控件是有限的。

支持的布局:

  • AdapterViewFlipper

  • FrameLayout

  • GridLayout

  • GridView

  • LinearLayout

  • ListView

  • RelativeLayout

  • StackView

  • ViewFlipper

支持的控件:

  • AnalogClock

  • Button

  • Chronometer

  • ImageButton

  • ImageView

  • ProgressBar

  • TextClock

  • TextView

并且官网上说:

Descendants of these classes are not supported.

即自定义的view就不支持了。

上面啰嗦了一堆,其实又可以提炼使用RemoteViews的关键字了

  • 展示一个layout显示的view

  • set方法更新内容

  • 支持有限的布局和控件

    接下来利用以上三点,来尝试使用RemoteViews

2.1 通知栏的使用

我们可以调用NotificationCompat.Builder.build()来创建一个通知,然后调用NotificationManager.notify()显示通知栏,但有时候我们需要自定义通知栏的UI,这时候就需要RemoteViews来帮忙了

第一步: 自定义个性化view

第二步:使用remoteviews

注意:手机8.0以上要使用notificationChannel 来实现通知栏。

 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.2 桌面小部件的使用

桌面小部件主要是利用RemoteViewsAppWidgetProvider结合使用,而AppWidgetProvider又是extends BroadcastReceiver, 所以再使用的时候,多了一些关于广播的知识。

同样,我们也是提炼出关键步骤。

第一步:自定义桌面小部件的布局

#layout_widget.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 android:background="#09C"
 android:padding="@dimen/widget_margin">
​
 <TextView
 android:id="@+id/appwidget_text"
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 android:layout_centerHorizontal="true"
 android:layout_centerVertical="true"
 android:layout_margin="8dp"
 android:background="#09C"
 android:contentDescription="@string/appwidget_text"
 android:text="@string/appwidget_text"
 android:textColor="#ffffff"
 android:textSize="24sp"
 android:clickable="true"
 android:textStyle="bold|italic"/>
​
</RelativeLayout>

第二步:配置

注意:该配置需要在目录res/xml内。

#app_widget_info.xml
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
 android:initialKeyguardLayout="@layout/layout_widget"
 android:initialLayout="@layout/layout_widget"
 android:minHeight="40dp"
 android:minWidth="40dp"
 android:previewImage="@drawable/example_appwidget_preview"
 android:resizeMode="horizontal|vertical"
 android:updatePeriodMillis="86400000"
 android:widgetCategory="home_screen">
</appwidget-provider>

第三步:使用RemoteViews

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

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

onUpdate: 小部件被添加时或者每次更新时调用。更新时间由第二步配置中updatePeriodMills来决定,单位为毫秒。

onReceive: 广播内置方法,用于分发接收到的事件。

onEnable: 当该窗口小部件第一次添加时调用。

onDelete:每删除一次调用一次。

onDisabled:最后一个该桌面小部件被删除时调用。

所以在onUpdate方法中利用RemoteViews来显示了新的布局,并利用pendingIntent来实现点击小部件控件跳转的方法。

最后一步:在AndroidManifest.xml中声明广播

<activity android:name=".MainActivity">
   <intent-filter>
     <action android:name="android.intent.action.MAIN" />​
     <category android:name="android.intent.category.LAUNCHER" />
   </intent-filter>
</activity>
​
 <receiver android:name=".NewAppWidget">
   <intent-filter>
     <action android:name="com.taohuahua.action.click" />
     <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
   </intent-filter>​
   <meta-data
     android:name="android.appwidget.provider"
     android:resource="@xml/app_widget_info" />
 </receiver>

这样,通过四步就可以完成一个桌面小部件了。

3 RemoteViews的原理

自定义通知栏和桌面小部件,是由NotificationManagerAppWidgetmanager管理,而NotificationManagerAppWidgetManager是通过Binder分别和SystemServer进程中的NotificationManagerServer以及AppWidgetService进行通信,他们是运行在系统进程中,即SystemServer进程, 而我们是要在自身的应用进程中来更新远程系统进程的UI。这样就构成来跨进程通信的场景。 最开始的一节我们知道RemoteViews 是实现了Parcelable接口的,这样就可以跨进程使用了。从构造方法开始,系统首先根据包名去得到该应用的资源,然后inflate出布局文件,在SystemServer进程中是一个普通的view,而在我们的进程看来这是一个RemoteViews,然后会通过一系列set方法来更新该RemoteViews

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

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

Action的是对远程视图进行的操作的一个封装。因为我们无法通过RemoteViewsfindViewById方法来操作视图,所以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);
 }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,902评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,037评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,978评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,867评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,763评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,104评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,565评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,236评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,379评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,313评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,363评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,034评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,637评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,719评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,952评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,371评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,948评论 2 341