View - RemoteViews

設計Android的工程師起名字仍是挺規範的,並且一眼就知道是什麼意思。RemoteViews,顧名思義,遠程的View。Android爲了能讓進程A顯示進程B的View,設計了這麼一種View(其實不是真正的View)。其實咱們開發過程當中,發通知到狀態欄顯示也是利用了RemoteViews,咱們來了解一下RemoteViews吧。android

咱們先看看RemoteViews怎麼配合Notification使用:app

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佈局文件以下:ide

<?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 !!ui

 RemoteViews加載

咱們首先要知道狀態欄是SystemServer進程,而咱們定義的RemoteViews是在咱們App進程,狀態欄要加載並顯示咱們的RemoteViews,這確定是經過IPC,主要實現是Binder。this

RemoteViews會經過Binder傳遞給SystemServer進程,系統會根據RemoteViews的包名和佈局id等信息,獲取到應用的資源(佈局文件,圖標等),而後經過LayoutInflater加載RemoteViews中的佈局文件,最後在狀態欄和通知欄顯示出來。spa

RemoteViews更新

RemoteViews提供了不少個方法,更新RemoteViews的佈局文件:.net

// 部分方法
- 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

相關文章
相關標籤/搜索