本篇文章主要介紹以下幾個知識點:
- RemoteViews 的應用
- RemoteViews 的內部機制
- RemoteViews 的意義
??RemoteViews 表示的是一種 View 的結構,它可以在其他的進程中顯示,其使用場景有兩種:通知欄和桌面小部件。
5.1 RemoteViews 的應用
5.1.1 RemoteViews 在通知欄上的應用
??通知欄除了默認效果還支持自定義布局,使用系統默認的樣式彈出一個通知如下:
/**
* 系統默認樣式
*/
private void defaultNotice() {
Intent intent = new Intent(this, NoticeActivity.class);
PendingIntent pi = PendingIntent.getActivity(this,0,intent,0);
// 1. 獲取 NotificationManager 實例來對通知進行管理
NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
// 2. 使用 Builder 構造器來創建 Notification 對象
Notification notification = new NotificationCompat.Builder(this)
.setContentTitle("This is content title")
.setContentText("This is content text")
.setWhen(System.currentTimeMillis())
.setSmallIcon(R.mipmap.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(getResources(),R.mipmap.ic_launcher))
.setContentIntent(pi) // 傳入pi
.build();
// 3. 顯示通知
manager.notify(1,notification);
}
??運行效果如下:
??為了滿足個性化需求,需要用到自定義通知,首先提供一個布局文件,然后通過 RemoteViews 加載這個布局文件即可,如下:
/**
* 自定義通知欄
*/
private void customizeNotice() {
Intent intent = new Intent(this, NoticeActivity.class);
PendingIntent pi = PendingIntent.getActivity(this,0,intent,0);
RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.chapter05_notice_item_layout);
remoteViews.setTextViewText(R.id.tv_title, "This is content title"); // 文字
remoteViews.setTextViewText(R.id.tv_content, "This is content text");// 文字
remoteViews.setImageViewResource(R.id.iv_notice, R.mipmap.ic_notice);// 圖片
remoteViews.setOnClickPendingIntent(R.id.ll_open_notice, pi); // 點擊
// 1. 獲取 NotificationManager 實例來對通知進行管理
NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
// 2. 使用 Builder 構造器來創建 Notification 對象
Notification notification = new NotificationCompat.Builder(this)
.setContent(remoteViews) // 傳入 remoteViews
.setSmallIcon(R.mipmap.ic_notice)
.setWhen(System.currentTimeMillis())
.build();
// 3. 顯示通知
manager.notify(2, notification);
}
??其中布局文件代碼如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/ll_open_notice"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<ImageView
android:id="@+id/iv_notice"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center_vertical"
android:orientation="vertical"
android:padding="5dp">
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/colorAccent" />
<TextView
android:id="@+id/tv_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/colorPrimary" />
</LinearLayout>
</LinearLayout>
??運行效果如下:
5.1.2 RemoteViews 在桌面小部件的應用
??AppWidgetProvider 是 Android 中提供的用于實現桌面小部件的類,其本質是一個廣播,即 BroadcastReceiver,其繼承關系如下:
??桌面小部件的開發步驟如下:
??1. 定義小部件的界面
??在 res/layout 下新建個 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:gravity="center_horizontal"
android:orientation="vertical">
<ImageView
android:id="@+id/iv_widget"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_header"/>
<TextView
android:id="@+id/tv_widget"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="5dp"
android:text="wonderful"/>
</LinearLayout>
??2. 定義小部件配置信息
??在 res/xml 下新建 widget_info.xml (名稱可自定義)如下:
<?xml version="1.0" encoding="utf-8"?>
<!-- initialLayout 小工具使用的初始化布局
minHeight、minWidth 小工具的最小尺寸
updatePeriodMillis 小工具的自動更新周期(毫秒) -->
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/widget"
android:minHeight="66dp"
android:minWidth="66dp"
android:updatePeriodMillis="86400000">
</appwidget-provider>
??3. 定義小部件的實現類
??這個類需繼承 AppWidgetProvider 如下:
/**
* Function:小部件的實現類
* Author:Wonderful on 2017/8/16 10:50
* Email:KXwonder@163.com
*/
public class MyWidgetProvider extends AppWidgetProvider{
public static final String CLICK_ACTION = "com.wonderful.androidartexplore.chapter05.action.CLICK";
public MyWidgetProvider() {
super();
}
@Override
public void onReceive(final Context context, final Intent intent) {
super.onReceive(context, intent);
// 判斷是自己的 action,做自己的事情,如小部件被單擊了要干什么,
// 這里是做一個動畫效果
if (intent.getAction().equals(CLICK_ACTION)) {
ToastUtils.show("clicked it");
new Thread(new Runnable() {
@Override
public void run() {
Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), R.mipmap.ic_header);
AppWidgetManager appWidgetManage = AppWidgetManager.getInstance(context);
for (int i = 0; i < 37; i++) {
float degree = (i * 10) % 360;
RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget);
remoteViews.setImageViewBitmap(R.id.iv_widget, rotateBitmap(bitmap, degree));
Intent intentClick = new Intent(CLICK_ACTION);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intentClick, 0);
remoteViews.setOnClickPendingIntent(R.id.iv_widget, pendingIntent);
appWidgetManage.updateAppWidget(new ComponentName(context, MyWidgetProvider.class), remoteViews);
SystemClock.sleep(30);
}
}
}).start();
}
}
/**
* 每次桌面小部件更新時都會調用一次該方法
*/
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
super.onUpdate(context, appWidgetManager, appWidgetIds);
for (int appWidgetId : appWidgetIds) {
RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget);
// 點擊桌面小部件發送廣播
Intent intentClick = new Intent(CLICK_ACTION);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intentClick, 0);
remoteViews.setOnClickPendingIntent(R.id.iv_widget, pendingIntent);
appWidgetManager.updateAppWidget(appWidgetId, remoteViews);
}
}
/**
* 動畫
*/
private Bitmap rotateBitmap(Bitmap bitmap, float degree) {
Matrix matrix = new Matrix();
matrix.reset();
matrix.setRotate(degree);
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
}
}
??上述代碼實現了一個簡單的桌面小部件,小部件上顯示一張圖片,點擊后旋轉一周。
??4. 在 AndroidManifest.xml 中聲明小部件
??桌面小部件本質是一個廣播組件,必須要注冊,如下:
<receiver android:name=".chapter05.MyWidgetProvider">
<intent-filter >
<!-- 用于識別小部件的點擊行為 -->
<action android:name="com.wonderful.androidartexplore.chapter05.action.CLICK"/>
<!-- 小部件的標識,必須存在 -->
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_info" />
</receiver>
??運行效果如下:
??上面描述了一個開發桌面小部件的完整過程,實際開發流程都是一樣的。 不管小部件的界面初始化還是界面的更新,在界面上的操作都是通過 RemoteViews。
??AppWidgetProvider 除了最常用的 onUpdate
方法,還有 onEnable
,onDisabled
,onDeleted
以及onReceiver
方法,這些方法會自動的被 onReceiver
在合適的時間調用,其含義如下:
onEnable 當該窗口小部件第一次添加到桌面的時候調用該方法,可添加多次但是只在第一次調用
onUpdate 小部件被添加或每次更新時都會調用一次該方法,更新機制由 updatePeriodMillis 來指定
onDeleted 每刪除一次小部件就調用一次
onDisabled 當最后一個該類型的小部件被刪除時調用
onReceiver 廣播內置的方法
5.1.3 PendingIntent 概述
??PendingIntent 表示一種處于待定、等待、即將發生的意思,它與 Intent 的區別在于,PendingIntent 是在將來某個不確定的時刻發生,Intent 是立刻發生。(給 RemoteViews 設置點擊事件必須使用 PendingIntent)
??PendingIntent 支持三種待定意圖:啟動Activity、啟動Service、發送廣播,具體如下:
??上圖中三個方法的參數都是一樣的,其中第二個參數 requstCode
表示 PendingIntent 發送方的請求碼,多數情況設為 0,它也會影響到參數 flags
的效果。
??PendingIntent 的匹配規則:若兩個 PendingIntent 內部的 Intent 相同并且 requstCode
也相同,那么這兩個 PendingIntent 就是相同的。
??Intent 的匹配規則:若兩個 Intent 的 ComponentName
和 intent-filter
都相同,那么這兩個 intent 就是相同的。(注:Extras 不參與匹配過程)
??下面介紹參數 flags
的含義:
FLAG_ONE_SHOT
當前 PendingIntent 只能被使用一次,然后被 cancel,若后續還有相同的 PendingIntent,則無效。通知欄,同類的通知只能使用一次,后續的無法打開。FLAG_NO_CREATE
當前 PendingIntent 不會主動去創建,若之前不存在,則獲取 PendingIntent 失敗(實際中無意義)。FLAG_CANCEL_CURRENT
當前 PendingIntent 若已存在,則會被 cancel,然后系統會創建一個新的 PendingIntent。通知欄,那些被 cancel 的消息將無法被打開。FLAG_UPDATE_CURRENT
當前 PendingIntent 若已存在,則會被更新,即它們的 intent 中的 Extras 會被替換成最新的。
??下面結合通知欄信息描述這四個標記位,如下代碼:
// 若 notify 的第一個參數 id 是常量,多次調用 notify 只彈出一個通知,后續的會把前面的替換掉
// 若每次 id 都不同,多次調用 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 的作用在其他進程中顯示并且更新 View 的界面,其最常用的構造方法:
// 兩個參數:第一個表示當前的包名,第二個是待加載的布局文件
public RemoteViews(String packageName, int layoutId) {
this(getApplicationInfo(packageName, UserHandle.myUserId()), layoutId);
}
??RemoteViews 支持的所有 View 類型如下:
??RemoteViews 中不能使用除了上述列表中以外的 View,也無法使用自定義 View。
??RemoteViews 沒有提供 findViewById
方法,無法直接訪問里面的 View 元素,必須通過它所提供的一系列 set 方法來完成。其部分 set 方法如下:
??關于 RemoteViews 的內部機制,有興趣的可以去看看書或源碼,這里提供一張圖,不多做介紹了:
5.3 RemoteViews 的意義
??下面打造一個模擬的通知欄效果并且實現跨進程的 UI 更新。
??有兩個 Activity 分別運行在兩個不同的進程,一個是A,一個是B,其中A扮演著通知欄的角色,而B則可以不停地發送通知欄消息。為了模擬通知欄的效果,修改A的 process 屬性使其運行在單獨的進程中,這樣A和B就構成了多進程通信的情形。在B中創建 Remoteviews 對象,然后通知A顯示這個 RemoteViews 對象。
??B每發送一次模擬通知,就會發送一個特定的廣播,然后A接收到廣播后就開始顯示B中定義的 RemoteViews 對象,此過程和系統的通知欄消息的顯示過程幾乎一致。
??首先看B的實現,B只要構造 RemoteViews 對象并將其傳輸給A即可,代碼如下:
/**
* Function:模擬通知效果 B activity
* Author:Wonderful on 2017/8/9 10:30
* Email:KXwonder@163.com
*/
public class B_Activity extends BaseActivity {
@Override
protected int initLayoutId() {
return R.layout.activity_chapter05_b;
}
@Override
protected void initView() {
}
@OnClick(R.id.btn_send)
public void onViewClicked() {
ToastUtils.show("發送廣播");
RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.chapter05_notice_item_layout);
remoteViews.setTextViewText(R.id.tv_title, "發送給 A 的通知");
remoteViews.setTextViewText(R.id.tv_content, "mag from process:" + Process.myPid());
//remoteViews.setImageViewResource(R.id.iv_notice, R.mipmap.ic_notice);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, A_Activity.class), PendingIntent.FLAG_UPDATE_CURRENT);
PendingIntent pendingIntent2 = PendingIntent.getActivity(this, 0, new Intent(this, NoticeActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);
remoteViews.setOnClickPendingIntent(R.id.iv_notice, pendingIntent);
remoteViews.setOnClickPendingIntent(R.id.ll_open_notice, pendingIntent2);
Intent intent = new Intent(Constants.REMOTE_ACTION);
// 將RemoteViews 對象通過Intent傳輸到A中
intent.putExtra(Constants.EXTRA_REMOTE_VIEWS, remoteViews);
sendBroadcast(intent);
}
}
??A中只需接收B中的廣播并顯示 RemoteViews 即可,代碼如下:
/**
* Function:模擬通知效果 A activity
* Author:Wonderful on 2017/8/9 10:30
* Email:KXwonder@163.com
*/
public class A_Activity extends BaseActivity {
@BindView(R.id.ll_remoteViews)
LinearLayout llRemoteViews;
@Override
protected int initLayoutId() {
return R.layout.activity_chapter05_a;
}
@Override
protected void initView() {
// 注冊廣播
IntentFilter intent = new IntentFilter(Constants.REMOTE_ACTION);
registerReceiver(mBroadcastReceiver, intent);
}
private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
LogUtils.e("A_activity", "接收廣播成功");
// 1. 當收到廣播后,從Intent中取出RemoteViews對象
RemoteViews remoteViews = intent.getParcelableExtra(Constants.EXTRA_REMOTE_VIEWS);
if (remoteViews != null) {
// 2. 通過apply方法加載布局并且執行更新操作,
View view = remoteViews.apply(context, llRemoteViews);
// 3. 將得到的 View 添加到A的布局中
llRemoteViews.addView(view);
}
}
};
@Override
protected void onDestroy() {
super.onDestroy();
// 解除廣播
unregisterReceiver(mBroadcastReceiver);
}
@OnClick(R.id.btn_to_b)
public void onViewClicked() {
// 跳轉到 B activity
IntentUtils.to(this, B_Activity.class);
}
}
??運行效果如下:
??現有兩應用,一個應用能更新另一個應用中的某個界面,這時可選擇以下兩種方式實現:
- AIDL 缺點:當對界面的更新頻繁時,會有效率問題,同時 AIDL 接口會變得復雜。
- RemoteViews 缺點:只支持一些常見的View,不支持自定義 View。
??面對這種問題,若界面中的 View 是一些簡單的且被 RemoteViews 支持的 View,可考慮采用 RemoteViews,否則就不適合用 RemoteViews。
??采用 RemoteViews 來實現兩應用間的界面更新,還有一個布局文件的加載問題。在上面的代碼中,直接通過 RemoteViews 的 apply
方法來加載并更新界面:
// 2. 通過apply方法加載布局并且執行更新操作,
View view = remoteViews.apply(context, llRemoteViews);
// 3. 將得到的 View 添加到A的布局中
llRemoteViews.addView(view);
??這種寫法在同一個應用的多進程情形下是適用的,但若A和B屬于不同應用,由于資源id不可能剛好一樣,B中的布局文件的資源id傳輸到A中后可能無效。
??面對這種情況,可通過資源名稱來加載布局文件。
??首先兩個應用要提前約定好 RemoteViews 中的布局的文件名稱,然后在A中根據名稱找到并加載,接著再調用 Remoteviews 的 reapply
方法即可將B中對View所做的一系列更新操作全作用到A中加載的View上。修改后的代碼如下:
int layoutId = getResources().getIdentifier("layout_simulated_notification","layout",getPackageName());
View view = getLayoutInflater().inflate(layoutId,llRemoteViews,false);
remoteViews.reapply(this,view);
llRemoteViews.addView(view);
??本篇文章就介紹到這。