RemoteViews是一種遠程View,可以在其他進程中顯示,為了能夠更新它的界面,RemoteViews提供了一組基礎操作用于跨進程更新它的界面。
本章會介紹RemoteViews在通知欄和桌面小部件上的應用,分析RemoveViews的內部機制,最后分析RemoteViews的意義并給出一個采用RemoteViews來跨進程更新界面的示例。
5.1 RemoteViews的應用
RemoteViews主要用于通知欄和桌面小部件的開發。通知欄主要通過NotificationManager的notify方法來實現;桌面小部件則是通過AppWidgetProvider來實現的,AppWidgetProvider本質上是一個廣播。因為RemoteViews運行在其他進程(SystemService進程),所以無法直接更新界面。
5.1.1 RemoteViews在通知欄上的應用
Notification notification = new Notification();
notification.icon = R.mipmap.ic_launcher;
notification.tickerText = "hello notification";
notification.when = System.currentTimeMillis();
notification.flags = Notification.FLAG_AUTO_CANCEL;
Intent intent = new Intent(this, RemoteViewsActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.layout_notification);//RemoveViews所加載的布局文件
remoteViews.setTextViewText(R.id.tv, "這是一個Test");//設置文本內容
remoteViews.setTextColor(R.id.tv, Color.parseColor("#abcdef"));//設置文本顏色
remoteViews.setImageViewResource(R.id.iv, R.mipmap.ic_launcher);//設置圖片
PendingIntent openActivity2Pending = PendingIntent.getActivity
(this, 0, new Intent(this, MainActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);//設置RemoveViews點擊后啟動界面
remoteViews.setOnClickPendingIntent(R.id.tv, openActivity2Pending);
notification.contentView = remoteViews;
notification.contentIntent = pendingIntent;
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
manager.notify(2, notification);
5.1.2 RemoveViews在桌面小部件上的應用
- 定義好小部件界面
在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/iv"
android:layout_width="360dp"
android:layout_height="360dp"
android:layout_gravity="center" />
</LinearLayout>
- 定義小部件的配置信息
在res/xml/下新建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:minHeight="360dp"
android:minWidth="360dp"
android:updatePeriodMillis="864000"/>
- 定義小部件的實現類
這個類需要繼承AppWidgetProvider;我們這里實現一個簡單的widget,點擊它后,3張圖片隨機切換顯示。
public class ImgAppWidgetProvider extends AppWidgetProvider {
public static final String TAG = "ImgAppWidgetProvider";
public static final String CLICK_ACTION = "cn.hudp.androiddevartnote.action.click";
private static int index;
@Override
public void onReceive(Context context, Intent intent) {
super.onReceive(context, intent);
if (intent.getAction().equals(CLICK_ACTION)) {
RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget);
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
updateView(context, remoteViews, appWidgetManager);
}
}
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
super.onUpdate(context, appWidgetManager, appWidgetIds);
RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget);
updateView(context, remoteViews, appWidgetManager);
}
// 由于onReceive 和 onUpdate中部分代碼相同 則抽成一個公用方法
public void updateView(Context context, RemoteViews remoteViews, AppWidgetManager appWidgetManager) {
index = (int) (Math.random() * 3);
if (index == 1) {
remoteViews.setImageViewResource(R.id.iv, R.mipmap.haimei1);
} else if (index == 2) {
remoteViews.setImageViewResource(R.id.iv, R.mipmap.haimei2);
} else {
remoteViews.setImageViewResource(R.id.iv, R.mipmap.haimei3);
}
Intent clickIntent = new Intent();
clickIntent.setAction(CLICK_ACTION);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, clickIntent, 0);
remoteViews.setOnClickPendingIntent(R.id.iv, pendingIntent);
appWidgetManager.updateAppWidget(new ComponentName(context, ImgAppWidgetProvider.class), remoteViews);
}
}
- 在AndroidManifest.xml中聲明小部件
因為桌面小部件的本質是一個廣播組件,因此必須要注冊。
<receiver android:name=".RemoveViews_5.ImgAppWidgetProvider">
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/appwidget_provider_info">
</meta-data>
<intent-filter>
<action android:name="cn.hudp.androiddevartnote.action.click" />
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
</receiver>
上面代碼中有兩個action,第一個是用于識別小部件的單擊行為,而第二個則是作為小部件的標識必須存在的;如果不加這個receiver就不是一個桌面小部件并且也無法顯示在手機的小部件中。
- 廣播到來的時候,AppWidgetProvider會自動根據廣播的Action通過onReceive方法來分發廣播,也就是調用
onEnable: 當該窗口小部件第一次添加到桌面時調用的方法,可添加多次但只在第一次調用。
onUpdate: 小部件被添加時或者每次小部件更新時都會調用一次該方法,小部件的更新時機是有updatePeriodMillis來指定,每個周期小部件就會自動更新一次。
onDeleted: 每刪除一次桌面小部件就調用一次。
onDisabled: 當最后一個該類型的小部件被刪除時調用該方法。
onReceive: 這是廣播的內置方法,用于分發具體事件給其他方法。
5.1.3 PendingIntent概述
PendingIntent表示一種處于pending(待定、等待、即將發生)狀態的意圖;PendingIntent通過send和cancel方法來發送和取消特定的待定Intent。
PendingIntent支持三種待定意圖:啟動Activity、啟動Service和發送廣播。分別對應:
getActivity / getService / getBroadcast
參數相同,都為:(Context context, int requestCode, Intent intent, int flags)
其中第二個參數,requestCode表示PendingIntent發送方的請求碼,多少情況下為0即可,requestCode會影響到flags的效果。
PendingIntent的匹配規則是:如果兩個PendingIntent他們內部的Intent相同并且requestCode也相同,那么這兩個PendingIntent就是相同的。那么什么情況下Intent相同呢?Intent的匹配規則是,如果兩個Intent的ComponentName和intent-filter都相同;那么這兩個Intent也是相同的。
flags參數的含義:
FLAG_ONE_SHOP 當前的PendingIntent只能被使用一次,然后他就會自動cancel,如果后續還有相同的PendingIntent,那么它們的send方法就會調用失敗。
FLAG_NO_CREATE 當前描述的PendingIntent不會主動創建,如果當前PendingIntent之前存在,那么getActivity、getService和getBroadcast方法會直接返回Null,即獲取PendingIntent失敗,無法單獨使用,平時很少用到。
FLAG_CANCEL_CURRENT 當前描述的PendingIntent如果已經存在,那么它們都會被cancel,然后系統會創建一個新的PendingIntent。對于通知欄消息來說,那些被cancel的消息單擊后無法打開。
FLAG_UPDATE_CURRENT 當前描述的PendingIntent如果已經存在,那么它們都會被更新,即它們的Intent中的Extras會被替換為最新的。
NotificationManager的notify方法分析
manager.notify(1,notification);
- 如果notify方法的id是常量,那么不管PendingIntent是否匹配,后面的通知都會替換掉前面的通知。
- 如果notify的方法id每次都不一樣,那么當PendingIntent不匹配的時候,不管在何種標記為下,這些通知都不會互相干擾。
- 如果PendingIntent處于匹配階段,分情況:
- 采用FLAG_ONE_SHOT標記位,那么后續通知中的PendingIntent會和第一條通知保持一致,包括其中的Extras,單擊任何一條通知后,其他通知均無法再打開;當所有通知被清除后。
- 采用FLAG_CANCEL_CURRENT標記位,只有最新的通知可以打開,之前彈出的所有通知均無法打開。
- 采用FLAG_UPDATE_CURRENT標記位,那么之前彈出的PendingIntent會被更新,最終它們和最新的一條保存完全一致,包括其中的Extras,并且這些通知都是可以打開的。
5.2 RemoteViews的內部機制
- RemoteViews的構造方法:public RemoteViews(String packageName,int layoutId),第一個參數表示當前應用的包名,第二個參數表示待加載的布局文件。
- RemoveViews并不能支持所有View類型,支持以下:
Layout:FrameLayout、LinearLayout、RelativeLayout、GridLayout。
View:Button、ImageButton、ImageView、ProgressBar、TextView、ListView、GridView、ViewStub等(例如EditText是不允許在RemoveViews中使用的,使用會拋異常)。 - RemoteView沒有findViewById方法,因此無法訪問里面的View元素,而必須通過RemoteViews所提供的一系列set方法來完成,這是通過反射調用的。
- 通知欄和小組件分別由NotificationManager(NM)和AppWidgetManager(AWM)管理,而NM和AWM通過Binder分別和SystemService進程中的NotificationManagerService以及AppWidgetService中加載的,而它們運行在系統的SystemService中,這就和我們進程構成了跨進程通訊。
- 工作流程:首先RemoteViews會通過Binder傳遞到SystemService進程,因為RemoteViews實現了Parcelable接口,因此它可以跨進程傳輸,系統會根據RemoteViews的包名等信息拿到該應用的資源;然后通過LayoutInflater去加載RemoteViews中的布局文件。接著系統會對View進行一系列界面更新任務,這些任務就是之前我們通過set來提交的。set方法對View的更新并不會立即執行,會記錄下來,等到RemoteViews被加載以后才會執行。
- 為了提高效率,系統沒有直接通過Binder去支持所有的View和View操作。而是提供一個Action概念,Action同樣實現Parcelable接口。系統首先將View操作封裝到Action對象并將這些對象跨進程傳輸到SystemService進程,接著SystemService進程執行Action對象的具體操作。遠程進程通過RemoteViews的apply方法來進行View的更新操作,RemoteViews的apply方法會去遍歷所有的Action對象并調用他們的apply方法。這樣避免了定義大量的Binder接口,也避免了大量IPC操作。
- apply和reApply的區別在于:apply會加載布局并更新界面,而reApply則只會更新界面。
- 關于單擊事件,RemoteViews中只支持發起PendingIntent,不支持onClickListener那種模式。setOnClickPendingIntent用于給普通的View設置單擊事件,不能給集合(ListView/StackView)中的View設置單擊事件(開銷大,系統禁止了這種方式)。如果要給ListView/StackView中的item設置單擊事件,必須將setPendingIntentTemplate和setOnClickFillInIntent組合使用才可以。
5.3 RemoteViews的意義
RemoteViews最大的意義在于方便的跨進程更新UI。
- 當一個應用需要更新另一個應用的某個界面,我們可以選擇用AIDL來實現,但如果更新比較頻繁,效率會有問題,同時AIDL接口就可能變得很復雜。如果采用RemoteViews就沒有這個問題,但RemoteViews僅支持一些常用的View,如果界面的View都是RemoteViews所支持的,那么就可以考慮采用RemoteViews。
- 利用RemoteViews加載其他App的布局文件與資源。
final String pkg = "cn.hudp.remoteviews";//需要加載app的包名
Resources resources = null;
try {
resources = getPackageManager().getResourcesForApplication(pkg);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
if (resources != null) {
int layoutId = resources.getIdentifier("activity_main", "layout", pkg); //獲取對于布局文件的id
RemoteViews remoteViews = new RemoteViews(pkg, layoutId);
View view = remoteViews.apply(this, llRemoteViews);//llRemoteViews是View所在的父容器
llRemoteViews.addView(view);
}