remoteViews简介 二、RemoteViews应用到桌面小组件 三、RemoteViews原理

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应用到桌面小组件的步骤,它总共分为五步,分别是:设置桌面小组件的布局、编写桌面小组件的配置文件、编写桌面小组件更新的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简介
二、RemoteViews应用到桌面小组件
三、RemoteViews原理

三、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进程中进行具体的更新操作。