RemoteViews,顧名思義,就是遠程的View,也就是可以運行在其他進程中的View。RemoteViews常用在通知和桌面小組件中。
一、RemoteViews應用到通知
首先來介紹一下系統自帶的通知(Notification)的使用。Notification的使用有兩種方法,分別是Notification直接創建的方式和使用Notification.Builder創建者模式創建的方式。
先來看一下使用Notification直接創建的方式的代碼:
Notification notification = new Notification(); notification.icon = R.mipmap.ic_launcher; // 小圖標 notification.largeIcon = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher_round); // 大圖標 notification.defaults = Notification.DEFAULT_ALL; // 設置默認的提示音、振動方式、燈光等 notification.category = "Category"; notification.when = System.currentTimeMillis(); // 設置通知發送的時間戳 notification.tickerText = "Ticker Text"; // 設置通知首次彈出時,狀態欄上顯示的文本 notification.flags = Notification.FLAG_AUTO_CANCEL; // 點擊通知后通知在通知欄上消失 notification.contentIntent = PendingIntent.getActivity(MainActivity.this, 0x001, new Intent(MainActivity.this, TargetActivity.class), PendingIntent.FLAG_UPDATE_CURRENT); // 設置通知的點擊事件 ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).notify(1, notification); // 發送系統通知
通過上面的代碼,就可以簡單地發送一條通知到通知欄中。使用這種方式不需要有API版本的限制,但可以進行的操作比較少。
下面來看一下使用Notification.Builder創建者模式創建通知的代碼:
PendingIntent pi = PendingIntent.getActivity(MainActivity.this, 0x001, new Intent(MainActivity.this, TargetActivity.class), PendingIntent.FLAG_UPDATE_CURRENT); Notification.Builder nb = new Notification.Builder(MainActivity.this) .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher_round)) // 大圖標 .setSmallIcon(R.mipmap.ic_launcher) // 小圖標 .setContentText("Content Text") // 內容 .setSubText("Sub Text") // 在通知中,APP名稱的副標題 .setContentTitle("Content Title") // 標題 .setTicker("Ticker") // 設置通知首次彈出時,狀態欄上顯示的文本 .setWhen(System.currentTimeMillis()) // 設置通知發送的時間戳 .setAutoCancel(true) // 點擊通知后通知在通知欄上消失 .setDefaults(Notification.DEFAULT_ALL) // 設置默認的提示音、振動方式、燈光等 .setContentIntent(pi); // 設置通知的點擊事件 ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).notify(1, nb.build()); // build()方法需要的最低API為16
使用這種方式,需要注意對項目的API版本進行一定的控制,上面這段代碼需要的API版本最低是16。
上面兩種方式都是發送系統自帶的通知。如果我們需要自定義通知的樣式,就需要使用到 RemoteViews 了。RemoteViews給我們提供了一種可以在其他進程中生成View並進行更新的機制,但是它可以控制和操作的View有一定的限制,具體如下:
Layout:
FrameLayout、LinearLayout、RelativeLayout、GridLayout
View:
AnalogClock、Button、Chronometer、ImageButton、ImageView、ProgressBar、TextView、ViewFlipper、ListView、GridView、StackView、AdapterViewFlipper、ViewStub
使用RemoteViews發送一個自定義系統通知的代碼如下:
PendingIntent pi = PendingIntent.getActivity(MainActivity.this, 0x001, new Intent(MainActivity.this, TargetActivity.class), PendingIntent.FLAG_UPDATE_CURRENT); RemoteViews remoteView = new RemoteViews(getPackageName(), R.layout.remoteview_main); remoteView.setTextViewText(R.id.remoteview_main_title, "Title"); remoteView.setTextViewText(R.id.remoteview_main_content, "ContentContentContent"); remoteView.setImageViewResource(R.id.remoteview_main_icon, R.mipmap.ic_launcher_round); remoteView.setOnClickPendingIntent(R.id.remoteview_main_view, pi); Notification.Builder nb = new Notification.Builder(MainActivity.this) .setSmallIcon(R.mipmap.ic_launcher) // 小圖標 .setCustomContentView(remoteView) // 設置自定義的RemoteView,需要API最低為24 .setWhen(System.currentTimeMillis()) // 設置通知發送的時間戳 .setAutoCancel(true) // 點擊通知后通知在通知欄上消失 .setDefaults(Notification.DEFAULT_ALL); // 設置默認的提示音、振動方式、燈光等 ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).notify(1, nb.build()); // build()方法需要的最低API為16
可以看到,我們在自定義的通知布局中設置了TextView和ImageView,並在代碼中動態地更新了其顯示的內容。
二、RemoteViews應用到桌面小組件
RemoteViews也可以應用到桌面小組件中。這里我們通過一個例子來了解RemoteViews應用到桌面小組件的步驟,它總共分為五步,分別是:設置桌面小組件的布局、編寫桌面小組件的配置文件、編寫桌面小組件更新的Service、編寫桌面小組件的控制類AppWidgetProvider、配置配置文件。
我們通過下面這個例子來介紹RemoteViews在桌面小組件中的應用。在這個例子中,我們向系統中添加一個小組件,在這個小組件中顯示當前的日期和時間。
首先,設置桌面小組件的布局,具體代碼如下:
<?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"> <TextView android:id="@+id/widget_main_tv_time" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textColor="@android:color/white" android:textSize="22.0sp" android:textStyle="bold" /> </LinearLayout>
然后,編寫桌面小組件的配置文件,具體步驟是:在項目res文件夾下新建一個xml文件夾,在xml文件夾中創建一個XML文件,具體代碼如下:
<?xml version="1.0" encoding="utf-8"?> <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" android:initialLayout="@layout/widget_main" android:minHeight="100.0dip" android:minWidth="150.0dip" android:updatePeriodMillis="8640000"> </appwidget-provider>
這個文件中的各個參數的解釋如下:
initialLayout:桌面小組件的布局XML文件
minHeight:桌面小組件的最小顯示高度
minWidth:桌面小組件的最小顯示寬度
updatePeriodMillis:桌面小組件的更新周期。這個周期最短是30分鍾
然后,編寫一個Service,在這個Service中動態地獲取到當前的時間並更新到桌面小組件中,代碼如下:
import android.app.Service; import android.appwidget.AppWidgetManager; import android.content.ComponentName; import android.content.Intent; import android.os.IBinder; import android.support.annotation.Nullable; import android.widget.RemoteViews; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Timer; import java.util.TimerTask; /** * 定時器Service */ public class TimerService extends Service { private Timer timer; private SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); @Override public void onCreate() { super.onCreate(); timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { updateViews(); } }, 0, 1000); } @Nullable @Override public IBinder onBind(Intent intent) { return null; } private void updateViews() { String time = formatter.format(new Date()); RemoteViews remoteView = new RemoteViews(getPackageName(), R.layout.widget_main); remoteView.setTextViewText(R.id.widget_main_tv_time, time); AppWidgetManager manager = AppWidgetManager.getInstance(getApplicationContext()); ComponentName componentName = new ComponentName(getApplicationContext(), WidgetProvider.class); manager.updateAppWidget(componentName, remoteView); } @Override public void onDestroy() { super.onDestroy(); timer = null; } }
然后,編寫一個類繼承自AppWidgetProvier,用來統一管理項目中的小組件,代碼如下:
import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProvider; import android.content.Context; import android.content.Intent; /** * AppWidgetProvider的子類,相當於一個廣播 */ public class WidgetProvider extends AppWidgetProvider { /** * 當小組件被添加到屏幕上時回調 */ @Override public void onEnabled(Context context) { super.onEnabled(context); context.startService(new Intent(context, TimerService.class)); } /** * 當小組件被刷新時回調 */ @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { super.onUpdate(context, appWidgetManager, appWidgetIds); } /** * 當widget小組件從屏幕移除時回調 */ @Override public void onDeleted(Context context, int[] appWidgetIds) { super.onDeleted(context, appWidgetIds); } /** * 當最后一個小組件被從屏幕中移除時回調 */ @Override public void onDisabled(Context context) { super.onDisabled(context); context.stopService(new Intent(context, TimerService.class)); } }
最后,在Manifest文件中配置剛剛編寫的Service和BroadcastReceiver(AppWidgetProvider相當於一個廣播),代碼如下:
<service android:name=".TimerService" /> <receiver android:name=".WidgetProvider"> <intent-filter> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> </intent-filter> <meta-data android:name="android.appwidget.provider" android:resource="@xml/widget" /> </receiver>
這里需要注意,<intent-filter>標簽中的action的name和<meta-data>標簽中的name的值是固定的,reousrce代表的是第二步中配置文件的位置。
編寫完上述代碼之后,運行結果如下圖所示:

三、RemoteViews原理
我們的通知和桌面小組件分別是由NotificationManager和AppWidgetManager管理的,而NotificationManager和AppWidgetManager又通過Binder分別和SystemServer進程中的NotificationManagerServer和AppWidgetService進行通信,這就構成了跨進程通信的場景。
RemoteViews實現了Parcelable接口,因此它可以在進程間進行傳輸。
首先,RemoteViews通過Binder傳遞到SystemServer進程中,系統會根據RemoteViews提供的包名等信息,去項目包中找到RemoteViews顯示的布局等資源,然后通過LayoutInflator去加載RemoteViews中的布局文件,接着,系統會對RemoteViews進行一系列的更新操作,這些操作都是通過RemoteViews對象的set方法進行的,這些更新操作並不是立即執行的,而是在RemoteViews被加載完成之后才執行的,具體流程是:我們調用了set方法后,通過NotificationManager和AppWidgetManager來提交更新任務,然后在SystemServer進程中進行具體的更新操作。
