View - RemoteViews
设计Android的工程师起名字还是挺规范的,而且一眼就知道是什么意思。RemoteViews,顾名思义,远程的View。Android为了能让进程A显示进程B的View,设计了这么一种View(其实不是真正的View)。其实我们开发过程中,发通知到状态栏显示也是利用了RemoteViews,我们来了解一下RemoteViews吧。
我们先看看RemoteViews怎么配合Notification使用:
import android.annotation.SuppressLint; import android.app.Activity; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.widget.RemoteViews; @SuppressLint("NewApi") public class MainActivity extends Activity { private RemoteViews contentView; private Notification notification; private NotificationManager notificationManager; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); sendNotification(); } private void sendNotification() { contentView = new RemoteViews(getPackageName(), R.layout.layout_remote); contentView.setTextViewText(R.id.remote_title, "Remote View Title"); contentView.setTextViewText(R.id.remote_content, "This Remote View Content ... \nThis Remote View Content ... \nThis Remote View Content ..."); Intent intent = new Intent(this, MainActivity.class); PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); // RemoteViews的事件只能是PendingIntent contentView.setOnClickPendingIntent(R.id.remote_content, pendingIntent); notification = new Notification.Builder(this) .setWhen(System.currentTimeMillis()) // 设置显示通知的时间 .setAutoCancel(true) // 设置是否可以手动取消 .setSmallIcon(R.mipmap.ic_launcher) // 设置在状态栏的小图标,如果没有设置,不显示通知 .setCustomBigContentView(contentView) // 设置自定义View,setCustomBigContentView可以显示remoteviews的完整高度,setCustomContentView只能显示系统通知栏高度。 .build(); notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); // 发通知 notificationManager.notify(1, notification); } }
其中R.layout.layout_remote布局文件如下:
<?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="wrap_content" android:padding="10dp" android:orientation="vertical" > <TextView android:id="@+id/remote_title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="18sp" android:textColor="@android:color/holo_blue_light" /> <TextView android:id="@+id/remote_content" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="3dp" android:textSize="16sp" android:textColor="@android:color/darker_gray" /> </LinearLayout>
效果如图所示:
因为我是调用setCustomBigContentView来加载RemoteViews的,所以RemoteViews可以显示完整,不受系统通知栏高度限制。
我们接下来解析状态栏是怎么加载我们定义的RemoteViews的,Let’s Go !!
RemoteViews加载
我们首先要知道状态栏是SystemServer进程,而我们定义的RemoteViews是在我们App进程,状态栏要加载并显示我们的RemoteViews,这肯定是通过IPC,主要实现是Binder。
RemoteViews会通过Binder传递给SystemServer进程,系统会根据RemoteViews的包名和布局id等信息,获取到应用的资源(布局文件,图标等),然后通过LayoutInflater加载RemoteViews中的布局文件,最后在状态栏和通知栏显示出来。
RemoteViews更新
RemoteViews提供了很多个方法,更新RemoteViews的布局文件:
// 部分方法 - setTextViewText(viewId, text) 设置文本 - setTextColor(viewId, color) 设置文本颜色 - setTextViewTextSize(viewId, units, size) 设置文本大小 - setImageViewBitmap(viewId, bitmap) 设置图片 - setImageViewResource(viewId, srcId) 根据图片资源设置图片 - setViewPadding(viewId, left, top, right, bottom) 设置Padding间距 - setOnClickPendingIntent(viewId, pendingIntent) 设置点击事件 - setInt(viewId, methodName, value) 反射调用参数为int的methodName方法 - setLong(viewId, methodName, value) 反射调用参数为long的methodName方法 ...
当调用以上方法来更新RemoteViews时,RemoteViews并 不会立刻更新,只是封装了一系列的Action,然后等待时机更新。
我们从源码分析,当我们调用setTextViewText来更新内容时:
private void updateNotification() { contentView.setTextViewText(R.id.remote_content, "This Remote View Update Content ... \nThis Remote View Update Content ... \nThis Remote View Update Content ..."); notificationManager.notify(1, notification); }
我们来看看setTextViewText源码:
// RemoteViews类 public void setTextViewText(int viewId, CharSequence text) { setCharSequence(viewId, "setText", text); }
调用了setCharSequence方法:
// RemoteViews类 public void setCharSequence(int viewId, String methodName, CharSequence value) { addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR_SEQUENCE, value)); }
addAction方法:
// RemoteViews类 private void addAction(Action a) { if (hasLandscapeAndPortraitLayouts()) { throw new RuntimeException("RemoteViews specifying separate landscape and portrait" + " layouts cannot be modified. Instead, fully configure the landscape and" + " portrait layouts individually before constructing the combined layout."); } if (mActions == null) { mActions = new ArrayList<Action>(); } mActions.add(a); a.updateMemoryUsageEstimate(mMemoryUsageCounter); }
可以看出,整个set过程,只是封装了一个Action并添加到mActions(一个List)中,所以这个过程并没有更新RemoteViews哦。我们看看ReflectionAction是什么:
// ReflectionAction类 private final class ReflectionAction extends Action { ... ReflectionAction(int viewId, String methodName, int type, Object value) { this.viewId = viewId; this.methodName = methodName; this.type = type; this.value = value; } ... }
就是储存了一些属性,主要是传递给SystemServer进程的一些更新RemoteViews布局信息。
当我们调用notificationManager.notify(1, notification)方法,RemoteViews布局才会开始更新。
我们来看看notify代码:
// NotificationManager类 public void notify(int id, Notification notification){ notify(null, id, notification); } public void notify(String tag, int id, Notification notification){ notifyAsUser(tag, id, notification, new UserHandle(UserHandle.myUserId())); }
最终调用了notifyAsUser方法:
// NotificationManager类 public void notifyAsUser(String tag, int id, Notification notification, UserHandle user){ ... INotificationManager service = getService(); ... final Notification copy = Builder.maybeCloneStrippedForDelivery(notification); try { service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id, copy, idOut, user.getIdentifier()); ... } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } }
service为INotificationManager的代理对象,调用了enqueueNotificationWithTag方法后,通过Binder,也就调用了NotificationManagerService(INotificationManager的存根对象,存在于SystemServer进程)的enqueueNotificationWithTag方法:
// NotificationManagerService类 public void enqueueNotificationWithTag(String pkg, String packageName, String tag, int id, Notification notification, int[] idOut, @UserIdInt userIdInt) { ... StatusBarNotification n = new StatusBarNotification(pkg, id, tag, r.uid, r.initialPid, notification); try { mStatusBar.updateNotification(r.statusBarKey, n) ... } ... }
调用了StatusBarNotification的updateNotification方法:
// StatusBarNotification类 public void updateNotification(IBinder key, StatusBarNotification notification) { ... final RemoteViews contentView = notification.notification.contentView; ... contentView.reapply(mContext, oldEntry.content); ... }
最终在SystemServer进程调用了RemoteViews的reapply方法:
// RemoteViews类 public void reapply(Context context, View v) { reapply(context, v, null); } public void reapply(Context context, View v, OnClickHandler handler) { RemoteViews rvToApply = getRemoteViewsToApply(context); if (hasLandscapeAndPortraitLayouts()) { if (v.getId() != rvToApply.getLayoutId()) { throw new RuntimeException("Attempting to re-apply RemoteViews to a view that" + " that does not share the same root layout id."); } } rvToApply.performApply(v, (ViewGroup) v.getParent(), handler); }
最终调用了RemoteViews的performApply方法:
// RemoteViews类 private void performApply(View v, ViewGroup parent, OnClickHandler handler) { if (mActions != null) { handler = handler == null ? DEFAULT_ON_CLICK_HANDLER : handler; final int count = mActions.size(); for (int i = 0; i < count; i++) { Action a = mActions.get(i); a.apply(v, parent, handler); } } }
我们之前setXXX方法时不是储存了Action吗,通过调用performApply方法,遍历所有Action,然后更新RemoteViews的布局文件。代码中,调用了Action的apply方法实现View的更新,Action是一个抽象类,apply方法由子类实现。我们看看ReflectionAction类的apply方法:
// ReflectionAction类 private final class ReflectionAction extends Action { ... @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) { throws new ActionException("bad type : " + this.type); } try { getMethod(view, this.methodName, param).invoke(view, wrapArg(this.value)); } catch (ActionException e) { throws e; } catch (Exception ex) { throws new ActionException(ex); } } ... }
主要是通过反射,调用View的方法,更新View。
注意
RemoteViews设置的布局文件并不支持所有的View,以下是RemoteViews所支持的View:
layout
FrameLayout,LinearLayout,RelativeLayout,GridLayout
view
Button,ImageView,ImageButton,TextView,ProgressBar,ListView,GridView,StackView,ViewStub,AdapterViewFlipper,ViewFlipper,AnalogClock,Chronometer
小结
通过对RemoteViews的了解,我们灵活的设计出多样式的RemoteViews,还可以在不用应用(A)显示自己应用(B)想要显示的View,这个有需要再探索。
转: https://blog.csdn.net/johanman/article/details/76019771