一、RemoteViews的應用
RemoteViews在實際的開發中,主要用在通知欄和桌面小部件的開發過程中。通知欄每個人都不陌生,主要是通過NotificationManager的notify方法去實現的,它除了默認效果外,還可以另外自定義佈局。桌面小部件則是通過AppWidgetProvider來實現的,AppWidgetProvider本質上就是一個廣播。通知欄和桌面小部件的開發過程中都會用到RemoteViews,它們在更新界面時無法像在Activity裏面那樣直接更新View,這是因爲兩者的界面都運行在其它進程中,確切來說是系統的SystemService進程。爲了跨進程更新界面,RemoteViews提供了一系列的set方法,並且這些方法只是View全部方法的子集,另外RemoteViews中所支持的View類型也是有限的。
下面簡單介紹下RemoteViews在通知欄和桌面小部件中的使用方法。
1.RemoteViews在通知欄上的應用
首先來看下通知欄,我們先了解一下系統默認的樣式
String title = "通知標題";
String content = "通知內容";
String id = "channel_id_01";
String name="channel_id_01_name";
Context context = getApplication();
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
Notification notification = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {//解決android9.0上通知不顯示的問題。
NotificationChannel mChannel = new NotificationChannel(id, name, NotificationManager.IMPORTANCE_LOW);
notificationManager.createNotificationChannel(mChannel);
Intent intent = new Intent(this, testCustomViewActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
//新的sdk中找不到這個方法了。會報錯
//notification.setLatestEventInfo(this, "Test", "This is Notification", pendingIntent);
notification = new Notification.Builder(context)
.setChannelId(id)
.setContentTitle(title)
.setContentText(content)
.setContentIntent(pendingIntent)//設置跳轉到指定的activity
.setAutoCancel(true)//設置點擊跳轉後自動清除消息
.setSmallIcon(R.mipmap.ic_launcher).build();
} else {
NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(context)
.setContentTitle(title)
.setContentText(content)
.setSmallIcon(R.mipmap.ic_launcher)
.setOngoing(true);
notification = notificationBuilder.build();
}
notificationManager.notify(1008, notification);//注意第一個參數如果是一個常量,那麼每次通知都覆蓋。如果每次都不同,在通知欄就會出現多個消息
上面會彈出一個系統的默認的通知(兼容了android9.0)。點擊通知的時候會跳轉到指定的activity,並且清除本身。
效果如下:
爲了滿足個性化需求,我們還可能會用到自定義通知。自定義通知也很簡單,首先我們要提供一個佈局文件,然後通過RemoteViews來加載這個佈局文件改變通知的樣式,代碼如下所示:
String id = "channel_id_01";
String name="channel_id_01_name";
Context context = getApplication();
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
Notification notification = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {//解決android9.0上通知不顯示的問題。
NotificationChannel mChannel = new NotificationChannel(id, name, NotificationManager.IMPORTANCE_LOW);
notificationManager.createNotificationChannel(mChannel);
/** 生成跳轉的intent */
Intent intent = new Intent(this, testCustomViewActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
/** 構造RemoteView */
RemoteViews remoteViews = new RemoteViews(getPackageName(),R.layout.layout_notification);
remoteViews.setTextViewText(R.id.tv_title,"自定義標題_Hello");
remoteViews.setTextViewText(R.id.tv_content,"自定義內容_welcome to android world");
remoteViews.setImageViewResource(R.id.iv_img,R.mipmap.ic_launcher);
remoteViews.setOnClickPendingIntent(R.id.bt_confirm,pendingIntent);//給自定義view的按鈕設置一個跳轉監聽,如果不設置,點擊按鈕就跳轉不了
notification = new Notification.Builder(context)
.setChannelId(id)
.setCustomContentView(remoteViews)//設置自定義的佈局
.setContentIntent(pendingIntent)//設置跳轉到指定的activity。這個和上面那個按鈕的跳轉都是生效的。
.setAutoCancel(true)//設置點擊跳轉後自動清除消息
.setSmallIcon(R.mipmap.ic_launcher).build();
} else {
/** 生成跳轉的intent */
Intent intent = new Intent(this, testCustomViewActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
/** 構造RemoteView */
RemoteViews remoteViews = new RemoteViews(getPackageName(),R.layout.layout_notification);
remoteViews.setTextViewText(R.id.tv_title,"自定義標題_Hello");
remoteViews.setTextViewText(R.id.tv_content,"自定義內容_welcome to android world");
remoteViews.setImageViewResource(R.id.iv_img,R.mipmap.ic_launcher);
remoteViews.setOnClickPendingIntent(R.id.bt_confirm,pendingIntent);//給自定義view的按鈕設置一個跳轉監聽,如果不設置,點擊按鈕就跳轉不了
NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(context)
.setSmallIcon(R.mipmap.ic_launcher)
.setAutoCancel(true)//設置點擊跳轉後自動清除消息
.setCustomContentView(remoteViews)//設置自定義的佈局
.setOngoing(true);
notification = notificationBuilder.build();
}
/** 發送通知 */
notificationManager.notify(1008, notification);
佈局文件layout_notification.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/ll_nf"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:id="@+id/iv_img"
android:layout_width="20dp"
android:layout_height="20dp"
android:background="@mipmap/ic_launcher"
/>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center_vertical"
android:orientation="vertical"
android:paddingLeft="10dp">
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/black" />
<TextView
android:id="@+id/tv_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/black" />
</LinearLayout>
<Button
android:id="@+id/bt_confirm"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="OK"
/>
</LinearLayout>
以上代碼在Android9.0上測試通過。
注意:點擊按鈕跳轉的時候,通知並不會自動清除。
效果如下:
關於PendingIntent,它表示的是一種待定的Intent,這個Intent中所包含的意圖必須由用戶來觸發。爲什麼更新RemoteViews如此複雜呢?
直觀原因是因爲RemoteViews沒有提供和View類似的findViewById這個方法,因此我們無法獲取到RemoteViews中的子View,當然實際原因絕非如此,具體會在下面分析。
2.RemoteViews 在桌面小部件的應用
AppWidgetProvider是Android提供給的用於實現桌面小部件的類,其本質也就是一個廣播,即BroadcastReceived。所以實際使用中把他看成一個廣播即可,我們來看下怎麼去具體的實現一個小部件。
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="wrap_content"
android:orientation="horizontal"
android:background="#ffffff"
>
<ImageView
android:id="@+id/iv1"
android:layout_width="wrap_content"
android:layout_height="30dp"
android:src="@mipmap/ic_launcher"
android:layout_marginTop="4dp"
/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginLeft="8dp"
android:layout_marginTop="2dp"
android:layout_marginBottom="2dp"
>
<TextView
android:id="@+id/tv_singer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="鄧紫棋"
android:textSize="14dp" />
<TextView
android:id="@+id/tv_lyric"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="因爲成長,我們逼不得已要習慣,因爲成長。"
android:textSize="12dp"
/>
</LinearLayout>
</LinearLayout>
2.定義小部件配置信息
在res/xml/下中新建一個appwidget_provider_info.xml文件(名稱是隨意的,只要和後面的AndroidManifest中配置對應好就行)
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/widget"
android:minWidth="250dp"
android:minHeight="40dp"
android:updatePeriodMillis="86400000">
</appwidget-provider>
上面的幾個參數的含義很明確,android:initialLayout就是加載佈局,其它兩個就是最小的高寬,而updatePeriodMillis就是更新小組件的時間週期。
特別注意:這裏的寬高比就是在桌面添加的比例(例如:4 x 1)
3.定義小部件的實現類
這個類需要繼承AppWidgetProvider,代碼如下(已經在android9.0上測試通過):
package com.example.appWidgetProvider;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.os.SystemClock;
import android.util.Log;
import android.widget.RemoteViews;
import android.widget.Toast;
import com.example.test.R;
public class MyAppWidgetProvider extends AppWidgetProvider {
public static final String TAG = "MWP";
public static final String CLICK_ACTION = "com.example.test.action.CLICK";
private AppWidgetManager appWidgetManage;
private float degree;
private Bitmap bitmap;
public MyAppWidgetProvider() {
super();
}
@Override
public void onReceive(final Context context, Intent intent) {
super.onReceive(context, intent);
String action = intent.getAction();
Log.i(TAG, "onReceive , action:" + action);
if (CLICK_ACTION.equals(action)) {
Toast.makeText(context, "小部件接收到了自定義的點擊事件,onReceive調用", Toast.LENGTH_SHORT).show();
new Thread(new Runnable() {
@Override
public void run() {
bitmap = BitmapFactory.decodeResource(context.getResources(), R.mipmap.ic_launcher);
appWidgetManage = AppWidgetManager.getInstance(context);
for (int i = 0; i < 37; i++) {
degree = (i * 10) % 360;
RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget);
remoteViews.setImageViewBitmap(R.id.iv1, rotateBitmap(bitmap,degree));
Intent intentClick = new Intent();
intentClick.setClass(context,MyAppWidgetProvider.class);
intentClick.setAction(CLICK_ACTION);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intentClick, 0);
remoteViews.setOnClickPendingIntent(R.id.iv1, pendingIntent);
appWidgetManage.updateAppWidget(new ComponentName(context, MyAppWidgetProvider.class), remoteViews);
SystemClock.sleep(30);
}
}
}).start();
}
}
//每次更新都會調用
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
super.onUpdate(context, appWidgetManager, appWidgetIds);
Log.i(TAG, "onUpdate");
RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget);
Intent intentClick = new Intent();
intentClick.setClass(context,MyAppWidgetProvider.class);//必須要添加這個,否則點擊發送不了廣播
intentClick.setAction(CLICK_ACTION);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intentClick, 0);//發送廣播
// Intent intent = new Intent(context, testCustomViewActivity.class);//跳轉到指定的activity
// PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
Log.d(TAG,"pendingIntent = " + pendingIntent);
remoteViews.setOnClickPendingIntent(R.id.iv1, pendingIntent);
appWidgetManage = AppWidgetManager.getInstance(context);
appWidgetManage.updateAppWidget(new ComponentName(context, MyAppWidgetProvider.class), remoteViews);
}
//動畫
private Bitmap rotateBitmap(Bitmap srcBitmap ,float degree) {
Bitmap temBitmap = null;
try {
Matrix matrix = new Matrix();
matrix.reset();
matrix.setRotate(degree);
temBitmap = Bitmap.createBitmap(srcBitmap, 0, 0, srcBitmap.getWidth(), srcBitmap.getHeight(), matrix, true);
} catch (Exception e) {
Log.e(TAG,"error = " + e.getMessage());
}
return temBitmap;
}
}
上面的代碼實現一個類似歌詞的頁面,點擊圖標可以觸發對應的點擊事件。
4.在清單文件中聲明小部件
最後一步,因爲桌面小部件本質是一個廣播組件,因此必須要在AndroidManifest中註冊,如下:
<!--小部件 AppWidgetProvider-->
<receiver android:name="com.example.appWidgetProvider.MyAppWidgetProvider">
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/appwidget_provider_info" />
<intent-filter>
<action android:name="com.example.test.action.CLICK" />
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
</receiver>
上面的代碼有兩個Action,其中第一個是識別小部件的動作,第二個就是他的標識,必須存在,這是系統的規範
運行的效果如下:
我們還可以在activity中發送廣播來更新小部件的View,例如我們在主頁點擊下按鈕發送一個廣播:
Intent intent = new Intent();
intent.setClass(this, MyAppWidgetProvider.class);
intent.setAction("com.example.test.action.CLICK");
sendBroadcast(intent);
實際測試,小部件中的onReceive可以正常回調
在實現小部件的過程中遇到過幾個小地方需要注意下(在android9.0上,可能低版本不存在)
注意點:
1、在AndroidStudio中,默認創建的工程項目中有/res/mipmap-anydpi-v26這個文件夾 這個會導致 bitmap
= BitmapFactory.decodeResource(context.getResources(), R.mipmap.ic_launcher); 返回一個null,導致app掛掉。不能繼續點擊了。 解決方法:暫時把這個文件夾刪除就OK了。
2、點擊不能正常發送廣播(onReceived不能回調) Intent intentClick = new Intent();
intentClick.setClass(context,MyAppWidgetProvider.class);//需要設置這個,書中的寫法實際測試無效。
intentClick.setAction(CLICK_ACTION);
3、部件佔用的寬高設置 在appwidget_provider_info.xml的寬高比例,決定了小部件在桌面上的
android:minWidth=“250dp”
android:minHeight=“40dp” 例如設置成上面比例,就是4 x 1
AppWidgetProvider 除了最常用的onUpdate方法,還有其他幾個方法,onEnabled,onDisabled,onDeleted以及onReceive。這些方法都會被onReceive在適當的時候調用,所以含義如下:
- onEnabled:當該窗口小部件第一次添加到桌面的時候調用該方法,可添加多次但是隻有第一次調用。
- onUpdate:小部件被添加或者第一次更新的時候都會調用一次該方法,小部件的更新機制由
- updatePeriodMillis來指定,每個週期小部件都會自動更新一次。
- onDeleted:每刪除一次小部件就會調用一次。
- onDisabled:當最後一個該類型的桌面小部件被刪除時調用該方法,注意是最後一個。
- onReceiver:這是廣播的內置方法,用於分發具體的事件給其它方法。
關於AppWidgetProvider 的onReceiver方法的具體分發過程,可以參看源碼中的實現,如下所示:
public void onReceive(Context context, Intent intent) {
// Protect against rogue update broadcasts (not really a security issue,
// just filter bad broacasts out so subclasses are less likely to crash).
String action = intent.getAction();
if (AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(action)) {
Bundle extras = intent.getExtras();
if (extras != null) {
int[] appWidgetIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_IDS);
if (appWidgetIds != null && appWidgetIds.length > 0) {
this.onUpdate(context, AppWidgetManager.getInstance(context), appWidgetIds);
}
}
} else if (AppWidgetManager.ACTION_APPWIDGET_DELETED.equals(action)) {
Bundle extras = intent.getExtras();
if (extras != null && extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_ID)) {
final int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID);
this.onDeleted(context, new int[] { appWidgetId });
}
} else if (AppWidgetManager.ACTION_APPWIDGET_OPTIONS_CHANGED.equals(action)) {
Bundle extras = intent.getExtras();
if (extras != null && extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_ID)
&& extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS)) {
int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID);
Bundle widgetExtras = extras.getBundle(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS);
this.onAppWidgetOptionsChanged(context, AppWidgetManager.getInstance(context),
appWidgetId, widgetExtras);
}
} else if (AppWidgetManager.ACTION_APPWIDGET_ENABLED.equals(action)) {
this.onEnabled(context);
} else if (AppWidgetManager.ACTION_APPWIDGET_DISABLED.equals(action)) {
this.onDisabled(context);
} else if (AppWidgetManager.ACTION_APPWIDGET_RESTORED.equals(action)) {
Bundle extras = intent.getExtras();
if (extras != null) {
int[] oldIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_OLD_IDS);
int[] newIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_IDS);
if (oldIds != null && oldIds.length > 0) {
this.onRestored(context, oldIds, newIds);
this.onUpdate(context, AppWidgetManager.getInstance(context), newIds);
}
}
}
}
上面描述了開發一個桌面小部件的典型過程 ,例子比較簡單,實際開發過程中會稍微複雜一些,但是開發流程都是一樣的。
可以發現,桌面小部件在界面上的操作都要通過RemoteViews,不管是小部件的界面初始化還是界面更新都必須依賴它。
3.PendingIntent概述
如圖中所示,這三個方法的參數都是一樣的,主要理解的是第二個參數requstCode和第四個參數flags,code代表的是發送碼,多數情況下爲0,而且code會影響到flag,flag常見的有幾種我們下面會說,其實最主要是理解匹配規則,
PendingIntent的匹配規則爲:如果兩個PendingIntent他們內部的Intent相同並且requstCode也相同的話,那麼PendingIntent就是相同的,code比較好理解,那什麼情況下Intent相同呢,Intent的匹配規則是:如果兩個Intent的ComponentName的匹配過程,只要Intent之間的ComponentName和intent-filter相同,那麼這兩個intent就相同,需要注意的是Extras不參與匹配過程,只要intent之間的name和intent-filter相同就行,我們再來說下flags的參數含義
- FLAG_ONE_SHOT
當前描述的PendingIntent只能被使用一次,然後他就會被cancel,如果後續還有相同的PendingIntent,那麼他的send方法就會失敗,對於通知欄的消息來說,如果採用此標記位,那麼同類的通知只能使用一次,後續將無法打開
- FLAG_NO_CREATE
當前描述的PendingIntent不會主動去創建,如果當前PendingIntent之前不存在,那麼getActivity等方法都會直接返回null,即獲取PendingIntent失敗,這個標記位很少見,他無法單獨使用,因此在日常開發當中,並沒有太多的意義,這裏就不過多的介紹了
- FLAG_CANCEL_CURRENT
當前描述的PendingIntent如果已經存在,那麼就會被cancel,然後系統創建一個新的PendingIntent,對於通知欄來說,那些被cancel的消息將無法被打開
- FLAG_UPDATE_CURRENT
當前描述的PendingIntent如果已經存在的話,那麼他們就會被更新,他們的intent中的extras會被替換成新的