學(xué)習(xí)內(nèi)容:
- RemoteViews 在通知欄和桌面小部件上的應(yīng)用
- RemoteViews 的內(nèi)部機(jī)制
- RemoteViews 的意義
RemoteView 的應(yīng)用
實(shí)際開(kāi)發(fā)中,RemoteViews 主要用在通知欄和桌面小部件的開(kāi)發(fā)過(guò)程中。通知欄主要通過(guò) NotificationManager 的 notify 方法實(shí)現(xiàn)璃谨,桌面小部件則是通過(guò) AppWidgetProvider 來(lái)實(shí)現(xiàn),其本質(zhì)也是一個(gè)廣播。
通知欄和桌面小部件更新界面時(shí),RemoteView 無(wú)法像 View 一樣在 Activity 中直接更新猾编,因?yàn)榻缑孢\(yùn)行在系統(tǒng)的 SystemServer 進(jìn)程,需要跨進(jìn)程更新升敲。
下面簡(jiǎn)單介紹 RemoteView 的應(yīng)用
-
RemoteView 在通知欄上的應(yīng)用(主要為 自定義布局)
(適配 Android 8.0)
//創(chuàng)建NotificationManager實(shí)例 NotificationManager mManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); //創(chuàng)建NotificationChannel實(shí)例 //參數(shù)說(shuō)明: //id:NotificationChannel的唯一標(biāo)識(shí) //name:NotificationChannel的名稱(chēng)答倡,在Settings可看到 //importance:對(duì)channel設(shè)置重要性,更改見(jiàn)后續(xù)表格 NotificationChannel mChannel = new NotificationChannel("id","name",NotificationManager.IMPORTANCE_DEFAULT); mManager.createNotificationChannel(mChannel); //創(chuàng)建PendingIntent Intent intent = new Intent(this,SecondActivity.class); PendingIntent pendingIntent = PendingIntent.getActivity(this,0,intent,PendingIntent.FLAG_UPDATE_CURRENT); //創(chuàng)建RemoteView RemoteViews remoteViews = new RemoteViews(getPackageName(),R.layout.layout_notification); remoteViews.setTextViewText(R.id.msg,"xx"); remoteViews.setImageViewResource(R.id.icon,R.drawable.icon); remoteViews.setOnclidePendingIntent(R.id.clickable,pendingIntent); //創(chuàng)建builder,并設(shè)置一系列屬性 Notification.Builder builder = new Notification.Builder(this,"id"); builder.setSmallIcon(R.drawable.ic_launcher_background) .setContentTitle("title") .setContentText("text") //以上三個(gè)為必需的屬性 .setAutoCancel(true); //Android 7.0 之后需要通過(guò)Notification.Builder設(shè)置contentView builder.setCustomContentView(remoteViews). //創(chuàng)建通知 Notification notification = builder.build(); //推送通知 mManager.notify(1,notification);
? RemoteViews 和 View 不同驴党,每個(gè)方法中幾乎都要求傳入一個(gè) id 參數(shù)瘪撇,比如 setTextViewText(int viewId, CharSequence text),需要傳入TextView 的 id。
? 直觀原因 是因?yàn)?RemoteViews 沒(méi)有提供和 View 類(lèi)似的 findViewById 這個(gè)方法倔既,因此我們無(wú)法獲取到 RemoteView 中的子 View恕曲。(實(shí)際原因并非如此,后面詳細(xì)介紹)
-
RemoteViews 在桌面小部件上的應(yīng)用
利用 AppWidgetProvider渤涌,本質(zhì)是廣播佩谣。
-
定義小部件界面
在 res/layout/ 新建一個(gè) xml 文件,命名為 widget.xml实蓬,名稱(chēng)和內(nèi)容可自定義茸俭,視小部件具體需求而定。
<?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/imageView1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/icon" /> </LinearLayout>
-
定義小部件配置信息
在 res/xml/ 下新建 appwidget_provider_info.xml安皱,名稱(chēng)任意调鬓。
<?xml version="1.0" encoding="utf-8"?> <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" //使用的初始化布局 android:initialLayout="@layout/widget" //小工具的最小尺寸 android:minHeight="84dp" android:minWidth="84dp" //自動(dòng)更新周期,毫秒單位 android:updatePeriodMillis="864000"/>
-
定義小部件的實(shí)現(xiàn)類(lèi)
繼承 AppWidgetProvider酌伊,功能為簡(jiǎn)單的 點(diǎn)擊后隨機(jī)切換圖片袖迎。
public class MyAppWidgetProvider 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); } // 隨機(jī)更新圖片 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, MyAppWidgetProvider.class), remoteViews); } }
-
在 AndroidManifest.xml 中聲明小部件
原因:本質(zhì)是廣播組件,因此需要注冊(cè)
<receiver android:name=".MyAppWidgetProvider"> <meta-data android:name="android.appwidget.provider" android:resource="@xml/appwidget_provider_info"> </meta-data> <intent-filter> //識(shí)別小部件的單擊行為 <action android:name="com.whdalive.action.click" /> //作為小部件的標(biāo)識(shí)腺晾,必須存在 <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> </intent-filter> </receiver>
-
廣播分發(fā)
當(dāng)廣播到來(lái)之后,AppWidgetProvider 會(huì)自動(dòng)根據(jù)廣播的 Action 通過(guò) onReceive 來(lái)自動(dòng)分發(fā)廣播辜贵,相關(guān)方法如下
- onEnable: 當(dāng)該窗口小部件第一次添加到桌面時(shí)調(diào)用的方法悯蝉,可添加多次但只在第一次調(diào)用。
- onUpdate: 小部件被添加時(shí)或者每次小部件更新時(shí)都會(huì)調(diào)用一次該方法托慨,小部件的更新時(shí)機(jī)是有updatePeriodMillis來(lái)指定鼻由,每個(gè)周期小部件就會(huì)自動(dòng)更新一次。
- onDeleted: 每刪除一次桌面小部件就調(diào)用一次厚棵。
- onDisabled: 當(dāng)最后一個(gè)該類(lèi)型的小部件被刪除時(shí)調(diào)用該方法蕉世。
- onRestored:當(dāng)接收到 ACTION_APPWIDGET_RESTORED 廣播,從備份恢復(fù)小部件時(shí)調(diào)用
- onAppWidgetOptionsChanged:當(dāng)接收到 ACTION_APPWIDGET_OPTIONS_CHANGED 廣播婆硬,小部件的尺寸位置發(fā)生變化時(shí)調(diào)用狠轻。
- onReceive: 這是廣播的內(nèi)置方法,用于分發(fā)具體事件給其他方法彬犯。
-
-
PendingIntent 概述
-
基本介紹
- PendingIntent 表示一種處于 Pending(待定向楼、等待、即將發(fā)生)狀態(tài)的意圖谐区;
- 典型應(yīng)用場(chǎng)景是給 RemoteViews 添加點(diǎn)擊事件湖蜕,(RemoteViews 運(yùn)行在遠(yuǎn)程進(jìn)程)
- 通過(guò) send 和 cancel 方法來(lái)發(fā)送和取消特定的待定 Intent。
-
分類(lèi)
啟動(dòng) Activity ->
getActivity(Context context, int requestCode, Intent intent, int flags)
啟動(dòng) Service ->
getService(Context context, int requestCode, Intent intent, int flags)
發(fā)送廣播 ->
getBroadcast(Context context, int requestCode, Intent intent, int flags)
-
參數(shù)說(shuō)明:
1. requestCode 表示 PendingIntent 發(fā)送方的請(qǐng)求碼宋列,多數(shù)情況下設(shè)置 0 即可昭抒,另外 requestCode 會(huì)影響到 flags 的效果。
2. flags 參數(shù):
1. **FLAG_ONE_SHOP** 當(dāng)前的PendingIntent只能被使用一次,然后他就會(huì)自動(dòng)cancel灭返,如果后續(xù)還有相同的PendingIntent盗迟,那么它們的send方法就會(huì)調(diào)用失敗。
2. **FLAG_NO_CREATE** 當(dāng)前描述的PendingIntent不會(huì)主動(dòng)創(chuàng)建婆殿,如果當(dāng)前PendingIntent之前存在诈乒,那么getActivity、getService和getBroadcast方法會(huì)直接返回Null婆芦,即獲取PendingIntent失敗怕磨,無(wú)法單獨(dú)使用,平時(shí)很少用到消约。
3. **FLAG_CANCEL_CURRENT** 當(dāng)前描述的PendingIntent如果已經(jīng)存在肠鲫,那么它們都會(huì)被cancel,然后系統(tǒng)會(huì)創(chuàng)建一個(gè)新的PendingIntent或粮。對(duì)于通知欄消息來(lái)說(shuō)导饲,那些被cancel的消息單擊后無(wú)法打開(kāi)。
4. **FLAG_UPDATE_CURRENT** 當(dāng)前描述的PendingIntent如果已經(jīng)存在氯材,那么它們都會(huì)被更新渣锦,即它們的Intent中的Extras會(huì)被替換為最新的。
-
匹配規(guī)則
- 如果兩個(gè) PendingIntent 內(nèi)部的 Intent 相同且 requestCode 也相同氢哮,那么二者相同
- Intent 相同的匹配規(guī)則:Intent 的 ComponentName 和 intent-filter 都相同袋毙。Extras 不參與 Intent的匹配過(guò)程。
RemoteView 的內(nèi)部機(jī)制
-
構(gòu)造方法
public RemoteViews(String packageName, int layoutId)
參數(shù)說(shuō)明:
- packageName:當(dāng)前應(yīng)用的包名
- layoutId:待加載的布局文件
-
限制 -> 支持的 View 類(lèi)型有限
-
Layout :
FrameLayout
冗尤,LineanLayout
听盖,RelativeLayout
,GridLayout
-
View:
AnalogClock
裂七,Button
皆看,Chronometer
,ImageButton
背零,ImageView
腰吟,ProgressBar
,TextView
徙瓶,ViewFlipper
蝎困,ListView
,GridView
倍啥,StackView
禾乘,AdapterViewFlipper
,ViewStub
-
Layout :
-
特殊之處
- RemoteView 沒(méi)有提供 findViewById 方法虽缕,因此無(wú)法直接訪問(wèn)里面的 View 元素始藕,而必須通過(guò) RemoteViews 所提供的一些列 set 方法來(lái)完成,這時(shí)因?yàn)?RemoteView 在遠(yuǎn)程進(jìn)程中顯示
- 一系列 set 方法 是通過(guò)反射來(lái)完成的。
-
工作流程
前置:通知欄和桌面小部件分別由 NotificationManager 和 AppWidgetManager 管理伍派,而 NotificationManager 和 AppWidgetManager 通過(guò) Binder 分別和 SystemServer 進(jìn)程中的 NotificationManagerService(NMS) 以及 AppWidgetService(AWS) 進(jìn)行通信江耀。布局文件實(shí)際是在 NMS 和 AWS 中被加載的,而運(yùn)行在 SystemServer 中诉植,這就和我們的進(jìn)程構(gòu)成了 跨進(jìn)程通信 的場(chǎng)景祥国。
-
具體流程
首先 RemoteViews 通過(guò) Binder 傳遞到 System Server 進(jìn)程(RemoteViews 實(shí)現(xiàn)了 Parcelable 接口)。系統(tǒng)會(huì)根據(jù) RemoteViews 中的包名等信息去得到該應(yīng)用的資源晾腔。
然后通過(guò) LayoutInflater 去加載 RemoteViews 中的布局文件舌稀。(對(duì)于 SystemServer 進(jìn)程來(lái)講,加載的只是一個(gè)普通的 view灼擂,只不過(guò)對(duì)于我們的進(jìn)程來(lái)講是 遠(yuǎn)程的)
-
接著系統(tǒng)對(duì) View 執(zhí)行一系列界面更新任務(wù)壁查,這些任務(wù)通過(guò) set 方法來(lái)提交。這些更新不是立刻執(zhí)行剔应,而是在 RemoteViews 中記錄所有更新操作睡腿,等到 RemoteViews 被加載以后才能執(zhí)行。
到此時(shí)峻贮,RemoteViews 就可以在 SystemServer 進(jìn)程中顯示了席怪。
當(dāng)需要更新 RemoteViews 時(shí),調(diào)用一些列 set 方法并通過(guò) NotificationManager 和 AppWidgetManager 來(lái)提交更新任務(wù)纤控,具體操作也是在 SystemServer 進(jìn)程中完成挂捻。
-
進(jìn)一步說(shuō)明 -- 跨進(jìn)程
- 系統(tǒng)不直接通過(guò) Binder 支持所有的 View 和 View 操作,否則 View 的方法龐大嚼黔,同時(shí) IPC 操作會(huì)影響效率
- 系統(tǒng)提供了一個(gè) Action 概念, Action 實(shí)現(xiàn)了 Parcelable 接口惜辑,代表一個(gè) View 操作唬涧。
- 系統(tǒng)首先將 View 操作封裝到 Action 對(duì)象并將這些對(duì)象跨進(jìn)程傳輸?shù)竭h(yuǎn)程進(jìn)程,接著在遠(yuǎn)程進(jìn)程中執(zhí)行 Action 對(duì)象中的具體操作盛撑。遠(yuǎn)程進(jìn)程通過(guò) RemoteViews 的 apply 方法來(lái)進(jìn)行 View 的更新操作碎节,Remoteview 的 apply 方法內(nèi)部會(huì)遍歷所有的 Action 對(duì)象并調(diào)用它們的 apply 方法,進(jìn)而執(zhí)行具體的 View 的更新操作抵卫。
- 此方法避免了 定義大量的 Binder 接口狮荔,其次通過(guò)遠(yuǎn)程進(jìn)程中批量執(zhí)行修改擦歐總避免了大量 IPC 操作。
-
源碼說(shuō)明:
- 見(jiàn)原書(shū)吧介粘。殖氏。
-
補(bǔ)充說(shuō)明
- apply 和 reApply 的區(qū)別:前者會(huì)加載布局并更新界面,后者只會(huì)更新界面
- 關(guān)于點(diǎn)擊事件姻采。RemoteViews 中只支持發(fā)起 PendingIntent 不支持 onClickListener 那種模式雅采。另外, setOnClickPendingIntent 用于給普通 View 設(shè)置點(diǎn)擊事件,不能給集合(ListView / StackvView)中的 View 設(shè)置點(diǎn)擊事件婚瓜。如果要給 ListView / StackvView 中的 itemview 設(shè)置單擊事件宝鼓,必須將 setPendingIntentTemplate 和 setOnClickFillInIntent 組合使用才可以。
RemoteViews 的意義
-
從字面上就能猜到:RemoteViews 目的就是為了方便的更新遠(yuǎn)程 views 巴刻,即跨進(jìn)程更新 UI
當(dāng)一個(gè)應(yīng)用需要能夠更新另一個(gè)應(yīng)用中的某個(gè)界面愚铡,這時(shí)候如果通過(guò) AIDL實(shí)現(xiàn),那么可能會(huì)隨著界面更新操作的復(fù)雜導(dǎo)致效率變低胡陪。這種場(chǎng)景就很適合使用 RemoteViews沥寥。
RemoteViews 缺點(diǎn)在于 它只支持一些常見(jiàn)的 View,不支持自定義 View督弓。
-
布局文件的加載問(wèn)題
-
同一個(gè)應(yīng)用的多進(jìn)程情形
View view = remoteViews.apply(this,mRemoteViewsContent); mRemoteViewsContent.addView(view);
-
不同應(yīng)用時(shí)
主要是由于兩個(gè)應(yīng)用的資源 ID 不一定一致营曼,因此通過(guò)資源名稱(chēng)來(lái)加載布局文件
int layoutId = getResources().getIdentifier("layout_simulated_notification","layout",getPackageName()); view view = getLayoutInflater().inflate(layoutId,mRemoteViewsContent,flase); remoteViews.reapply(this,view); mRemoteViewsContent.addView(view);
-