RemoteViews从字面上看是一种远程视图。RemoteViews具有View的结构,既然是远程View,那么它就可以在其他进程中显示。由于它可以跨进程显示,所以为了能够更新他的界面,RemoteViews提供一组基础的操作用于跨进程更新它的UI。
RemoteViews在Android日常开发中最常见的使用场景有两种:通知栏的通知和桌面小部件。通知栏通知大家应该都不陌生,因为这还经常用到的,notification主要是通过NotificationManager的notify方法来实现,它除了默认的效果外,开发人员还可以根据自己的需求自定义UI布局。桌面小部件是通过AppWidgetProvider来实现的,其实AppWidgetProvider是一个广播。通知栏通知和小部件的开发过程中经常会用到RemoteViews。它们在更新UI的时候无法像Activity和Fragment那样直接更新,前面讲过,因为它是跨进程的view,更确切一点来说的话,它是运行在SystemServer进程中。为了能够跨进程更新界面,RemoteViews提供了一些列可以跨进程更新UI的方法,内部有一些列的set方法,这些方法都是View的子集。
一、RemoteViews自定义Notification通知栏
1 public class MainActivity extends Activity implements View.OnClickListener { 2 3 private Button btn_one,btn_two,btn_three,btn_four; 4 private NotificationManager manager; 5 6 @Override 7 protected void onCreate(Bundle savedInstanceState) { 8 super.onCreate(savedInstanceState); 9 setContentView(R.layout.activity_main); 10 btn_one = (Button) findViewById(R.id.btn_one); 11 btn_one.setOnClickListener(this); 12 btn_two = (Button) findViewById(R.id.btn_two); 13 btn_two.setOnClickListener(this); 14 btn_three = (Button) findViewById(R.id.btn_three); 15 btn_three.setOnClickListener(this); 16 btn_four = (Button) findViewById(R.id.btn_four); 17 btn_four.setOnClickListener(this); 18 manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); 19 } 20 21 @Override 22 public void onClick(View view) { 23 switch (view.getId()) { 24 case R.id.btn_one://默认通知 25 PendingIntent pendingIntent1=PendingIntent.getActivity(this,0,new Intent(this,OtherActivity.class),PendingIntent.FLAG_UPDATE_CURRENT); 26 Notification notification1=new Notification(); 27 notification1.icon=R.drawable.head; 28 notification1.when=System.currentTimeMillis(); 29 notification1.tickerText="您有新短消息,请注意查收!"; 30 notification1.contentIntent=pendingIntent1; 31 manager.notify(1,notification1); 32 33 break; 34 case R.id.btn_two://API11之后使用 35 PendingIntent pendingIntent2=PendingIntent.getActivity(this,0,new Intent(this,OtherActivity.class),PendingIntent.FLAG_UPDATE_CURRENT); 36 Notification notification2=new Notification.Builder(this) 37 .setSmallIcon(R.drawable.head)//即便自定义了图标,但有时候还是显示系统默认的,应该与手机类型有关 38 .setContentTitle("好消息") 39 .setContentText("API11之后的通知") 40 .setTicker("hello world") 41 .setWhen(System.currentTimeMillis()) 42 .setContentIntent(pendingIntent2) 43 .setNumber(1) //在最右侧现实。这个number起到一个序列号的作用,如果多个触发多个通知(同一ID),可以指定显示哪一个。 44 .getNotification(); //build()是在API16及之后增加的,在API11中可以使用getNotificatin()来代替 45 manager.notify(1,notification2); 46 break; 47 case R.id.btn_three://API16之后使用 48 PendingIntent pendingIntent3=PendingIntent.getActivity(this,0,new Intent(this,OtherActivity.class),PendingIntent.FLAG_UPDATE_CURRENT); 49 Notification notification3=new Notification.Builder(this) 50 .setSmallIcon(R.drawable.head) 51 .setContentTitle("好消息") 52 .setContentText("API16之后的通知") 53 .setTicker("hello world") 54 .setWhen(System.currentTimeMillis()) 55 .setContentIntent(pendingIntent3) 56 .setNumber(1) 57 .build(); 58 manager.notify(1,notification3); 59 break; 60 case R.id.btn_four://自定义通知, 61 Notification notification = new Notification(); 62 notification.when = System.currentTimeMillis(); 63 notification.flags = Notification.FLAG_AUTO_CANCEL; 64 notification.tickerText = "hello world"; 65 notification.icon = R.drawable.head;//这是个坑,如果不设置icon,通知不显示 66 67 Intent intent = new Intent(MainActivity.this, OtherActivity.class); 68 PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); 69 RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.remote_layout); 70 remoteViews.setTextViewText(R.id.tv_title, "请假条"); 71 remoteViews.setTextViewText(R.id.tv_content, "世界这么大,我想去看看"); 72 remoteViews.setImageViewResource(R.id.iv_head, R.drawable.head); 73 74 notification.contentIntent = pendingIntent; 75 notification.contentView = remoteViews; 76 manager.notify(1, notification); 77 break; 78 } 79 } 80 }
通知栏的布局remote_layout
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:orientation="horizontal"> <ImageView android:id="@+id/iv_head" android:layout_width="40dp" android:layout_height="40dp" /> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingLeft="10dp" android:orientation="vertical"> <TextView android:id="@+id/tv_title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="标题" /> <TextView android:id="@+id/tv_content" android:layout_width="wrap_content" android:layout_height="wrap_content" android:paddingTop="3dp" android:text="内容。。。。" /> </LinearLayout> </LinearLayout>
二、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中动态地获取到当前的时间并更新到桌面小组件中,代码如下:
1 import android.app.Service; 2 import android.appwidget.AppWidgetManager; 3 import android.content.ComponentName; 4 import android.content.Intent; 5 import android.os.IBinder; 6 import android.support.annotation.Nullable; 7 import android.widget.RemoteViews; 8 9 import java.text.SimpleDateFormat; 10 import java.util.Date; 11 import java.util.Timer; 12 import java.util.TimerTask; 13 14 /** 15 * 定时器Service 16 */ 17 public class TimerService extends Service { 18 private Timer timer; 19 private SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); 20 21 @Override 22 public void onCreate() { 23 super.onCreate(); 24 timer = new Timer(); 25 timer.schedule(new TimerTask() { 26 @Override 27 public void run() { 28 updateViews(); 29 } 30 }, 0, 1000); 31 } 32 33 @Nullable 34 @Override 35 public IBinder onBind(Intent intent) { 36 return null; 37 } 38 39 private void updateViews() { 40 String time = formatter.format(new Date()); 41 RemoteViews remoteView = new RemoteViews(getPackageName(), R.layout.widget_main); 42 remoteView.setTextViewText(R.id.widget_main_tv_time, time); 43 AppWidgetManager manager = AppWidgetManager.getInstance(getApplicationContext()); 44 ComponentName componentName = new ComponentName(getApplicationContext(), WidgetProvider.class); 45 manager.updateAppWidget(componentName, remoteView); 46 } 47 48 @Override 49 public void onDestroy() { 50 super.onDestroy(); 51 timer = null; 52 } 53 }
然后,编写一个类继承自AppWidgetProvier,用来统一管理项目中的小组件,代码如下:
1 import android.appwidget.AppWidgetManager; 2 import android.appwidget.AppWidgetProvider; 3 import android.content.Context; 4 import android.content.Intent; 5 6 /** 7 * AppWidgetProvider的子类,相当于一个广播 8 */ 9 public class WidgetProvider extends AppWidgetProvider { 10 /** 11 * 当小组件被添加到屏幕上时回调 12 */ 13 @Override 14 public void onEnabled(Context context) { 15 super.onEnabled(context); 16 context.startService(new Intent(context, TimerService.class)); 17 } 18 19 /** 20 * 当小组件被刷新时回调 21 */ 22 @Override 23 public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { 24 super.onUpdate(context, appWidgetManager, appWidgetIds); 25 } 26 /** 27 * 当widget小组件从屏幕移除时回调 28 */ 29 @Override 30 public void onDeleted(Context context, int[] appWidgetIds) { 31 super.onDeleted(context, appWidgetIds); 32 } 33 34 /** 35 * 当最后一个小组件被从屏幕中移除时回调 36 */ 37 @Override 38 public void onDisabled(Context context) { 39 super.onDisabled(context); 40 context.stopService(new Intent(context, TimerService.class)); 41 } 42 }
最后,在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进程中进行具体的更新操作。