您的位置:首页 > 移动开发 > Android开发

第五章理解RemoteViews(Android开发艺术探索)

2017-03-13 18:03 507 查看
5.1、remoteViews的应用(主要用于通知栏和桌面小部件)

通知栏和桌面小部件不能直接去更新view,因为2者的页面都运行在其他进程中,确切来说是系统的SystemServer进程,为了跨进程更新页面,RemoteViews提供了一系列的set方法进行更新。

5.1.1、在通知栏的应用:

通知栏除了默认的效果还可以自定义布局,下面分别说明这2种情况:

默认的布局:

Notification notification = new Notification();
//设置图标
notification.icon = R.drawable.ic_launcher;
//设置内容
notification.tickerText = "hello world";
//要显示的时间,一般是当即显示,故填入系统当前时间。
notification.when = System.currentTimeMillis();
//// FLAG_AUTO_CANCEL表明当通知被用户点击时,通知将被自动清除。
notification.flags = Notification.FLAG_AUTO_CANCEL;

Intent intent = new Intent(this, DemoActivity_2.class);
////该语句的作用是定义了一个不是当即显示的activity,
// 只有当用户拉下notify显示列表,并且单击对应的项的时候,才会触发系统跳转到该activity.
PendingIntent pendingIntent = PendingIntent.getActivity(this,
0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
//在此处设置在nority列表里的该norifycation得显示情况。
notification.setLatestEventInfo(this, "chapter_5", "this is notification.", pendingIntent);
NotificationManager manager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
// 通过通知管理器来发起通知。如果id不同,则每click,在statu那里增加一个提示
manager.notify(sId, notification);


自定义的布局(使用了remoteViews来加载):

Notification notification = new Notification();
notification.icon = R.drawable.ic_launcher;
notification.tickerText = "hello world";
notification.when = System.currentTimeMillis();
notification.flags = Notification.FLAG_AUTO_CANCEL;
Intent intent = new Intent(this, DemoActivity_1.class);
intent.putExtra("sid", "" + sId);
PendingIntent pendingIntent = PendingIntent.getActivity(this,
0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
System.out.println(pendingIntent);
//包名和资源ID
RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.layout_notification);
remoteViews.setTextViewText(R.id.msg, "chapter_5: " + sId);
remoteViews.setImageViewResource(R.id.icon, R.drawable.icon1);
PendingIntent openActivity2PendingIntent = PendingIntent.getActivity(this,
0, new Intent(this, DemoActivity_2.class), PendingIntent.FLAG_UPDATE_CURRENT);
remoteViews.setOnClickPendingIntent(R.id.open_activity2, openActivity2PendingIntent);
notification.contentView = remoteViews;
notification.contentIntent = pendingIntent;
NotificationManager manager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
manager.notify(sId, notification);


remoteViews使用起来很简单,2个参数即可创建,包名和资源id

更新remoteViews无法直接访问里面的view,必须通过remoteViews提供的方法进行更新

比如:

remoteViews.setTextViewText(R.id.msg, "chapter_5: " + sId);
remoteViews.setImageViewResource(R.id.icon, R.drawable.icon1);


5.1.2、在桌面小部件上面的应用:

Android系统为我们提供了一个实现桌面小部件的类:AppWidgetProvider ,它继承广播。

public class AppWidgetProvider extends BroadcastReceiver


1、自定义小部件界面:

<?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" >

<ImageView
android:id="@+id/imageView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/icon1" />

</LinearLayout>


2、定义小部件的配置信息,res/xml/myapp_widget.xml

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/widget"//初始化布局
android:minHeight="84dp"
android:minWidth="84dp"
android:updatePeriodMillis="86400000" //自动更新周期>

</appwidget-provider>


3、定义小部件的实现类

package com.ryg.chapter_5;

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;

public class MyAppWidgetProvider extends AppWidgetProvider {
public static final String TAG = "MyAppWidgetProvider";
public static final String CLICK_ACTION = "com.ryg.chapter_5.action.CLICK";

public MyAppWidgetProvider() {
super();
}

@Override
public void onReceive(final Context context, Intent intent) {
super.onReceive(context, intent);
Log.i(TAG, "onReceive : action = " + intent.getAction());

// 这里判断是自己的action,做自己的事情,比如小工具被点击了要干啥,这里是做一个动画效果
if (intent.getAction().equals(CLICK_ACTION)) {
Toast.makeText(context, "clicked it", Toast.LENGTH_SHORT).show();

new Thread(new Runnable() {
@Override
public void run() {
Bitmap srcbBitmap = BitmapFactory.decodeResource(
context.getResources(), R.drawable.icon1);
AppWidgetManager appWidgetManager = 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.imageView1,
rotateBitmap(context, srcbBitmap, degree));
Intent intentClick = new Intent();
intentClick.setAction(CLICK_ACTION);
PendingIntent pendingIntent = PendingIntent
.getBroadcast(context, 0, intentClick, 0);
remoteViews.setOnClickPendingIntent(R.id.imageView1, pendingIntent);
appWidgetManager.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");

final int counter = appWidgetIds.length;
Log.i(TAG, "counter = " + counter);
for (int i = 0; i < counter; i++) {
int appWidgetId = appWidgetIds[i];
onWidgetUpdate(context, appWidgetManager, appWidgetId);
}

}

/**
* 窗口小部件更新
*
* @param context
* @param appWidgeManger
* @param appWidgetId
*/
private void onWidgetUpdate(Context context,
AppWidgetManager appWidgeManger, int appWidgetId) {

Log.i(TAG, "appWidgetId = " + appWidgetId);
RemoteViews remoteViews = new RemoteViews(context.getPackageName(),
R.layout.widget);

// "窗口小部件"点击事件发送的Intent广播
Intent intentClick = new Intent();
intentClick.setAction(CLICK_ACTION);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0,
intentClick, 0);
remoteViews.setOnClickPendingIntent(R.id.imageView1, pendingIntent);
appWidgeManger.updateAppWidget(appWidgetId, remoteViews);
}

private Bitmap rotateBitmap(Context context, Bitmap srcbBitmap, float degree) {
Matrix matrix = new Matrix();
matrix.reset();
matrix.setRotate(degree);
Bitmap tmpBitmap = Bitmap.createBitmap(srcbBitmap, 0, 0,
srcbBitmap.getWidth(), srcbBitmap.getHeight(), matrix, true);
return tmpBitmap;
}
}


上面的代码实现了一个简单的桌面小部件,在小部件上面显示一张图片,点击图片后,图片旋转一周。

桌面小部件不管是初始化界面和后续的更新界面都必须使用remoteViews来完成。

4、在AndoridMenifest中申明小部件(跟注册广播差不多)

<receiver android:name=".MyAppWidgetProvider" >
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/appwidget_provider_info" >
</meta-data>

<intent-filter>
<action android:name="com.ryg.chapter_5.action.CLICK" />
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
</receiver>


inten-filter中有2个action,第一个是小部件的点击行为,第二个是小部件的标识,必须存在

拓展AppWidgetProvider的方法:

● 1)onEnable:当窗口小部件第一次添加到桌面时会调用。

● 2)onUpdate:小部件被添加时或者每次小部件更新时会调用。

● 3)onDeleted:每删除一次桌面小部件jiuhuidiaoy。

● 4)onDisabled:当最后一个桌面小部件被删除时会调用。

● 5)onReceive:广播的内置方法。

*启动时AppWidgetProvider的执行流程:

*第一步:onReceive()

* 接到广播事件:android.appwidget.action.APPWIDGET_ENABLED

*第二步:onEnabled()

*第三步:onReceive()

* 接到广播事件:android.appwidget.action.APPWIDGET_UPDATE

*第四步:onUpdate()

*

*被删除时AppWidgetProvider

*第一步:onReceive()

* 接到广播事件:android.appwidget.action.APPWIDGET_DELETED

*第二步:onDelete();

*第三步:onReceive()

* 接到广播事件:android.appwidget.action.APPWIDGET_DISABLED

*第四步:onDisabled()

5.1.3、PendingIntent概述

pending : 等待,即将发生

pendingIntent表示将来某个不确定的时刻发生,Intent表示立刻发生

pendingIntent的典型事例是给remoteViews添加点击事件,因为remoteViews运行在远程进程中,所以无法像普通的View一样添加点击事件

pendingIntent支持3种待定意图:

启动activity,

public static PendingIntent getActivity(Context context, int requestCode,
Intent intent, int flags) {
return getActivity(context, requestCode, intent, flags, null);
}
启动service,
public static PendingIntent getService(Context context, int requestCode,
Intent intent, int flags) {
发送广播,
public static PendingIntent getBroadcast(Context context, int requestCode,
Intent intent, int flags) {
return getBroadcastAsUser(context, requestCode, intent, flags,
new UserHandle(UserHandle.myUserId()));
}


第一个参数和第三个参数好理解,我们来说说第二个参数requestCode和第四个参数flags

在了解这2个参数之前先要知道PendingIntent的匹配规则

PendingIntent的匹配规则是:intent和requestCode都相同,那么这2个pendingIntent就相同
Intent的匹配规则是:componentName和intent-filter相同


flags的类型:

//pendingIntent只能被使用一次,就会自动cancel
public static final int FLAG_ONE_SHOT = 1<<30;
//没有什么使用意义
public static final int FLAG_NO_CREATE = 1<<29;
//如果pendingIntent已经存在,那么它们都会被取消,然后创建一个新的pendingIntent
public static final int FLAG_CANCEL_CURRENT = 1<<28;
///如果pendingIntent已经存在,它们都会被更新,即Intent中的Extras会被替换成新的
public static final int FLAG_UPDATE_CURRENT = 1<<27;


下面结合通知栏消息再详细描述一下4个标记位:

manager.notify(id,notification)


第一种情况:如果id是常量,那么多次调用notify只能弹出一个通知,后面的通知会覆盖前面的

第二种情况:每次id都不一样,多次调用notify会弹出多个通知

5.2、RemoteViews的内部机制

remoteViews的作用是跨进程显示并更新UI,

remoteViews不支持自定义view,只支持下列view,下列view的子类也不支持

否则就会抛异常:android.view.inflateException

因为remoteViews是跨进程显示页面,所以无法findviewbyid获得view,但是它提供了一系列的set方法来控制view,这些方法都是通过反射来完成的

我们知道,

通知栏和桌面小程序分别由NotificationManager和AppWidgetManager管理;

而NotificationManager是通过Binder和NotificationManagerService通信,

AppWidgetManager是通过Binder和AppWidgetService通信 。

由此可见:

通知栏是在NotificationManagerService中被加载的

桌面小部件是在AppWidgetService中被加载的

而它们运行在系统的SystemServer中,那么就是跨进程通信的场景

第一步:RemoteViews会通过Binder传递到SystemServer进程(因为RemoteViews实现了Parcelable接口)
第二步:通过LayoutInflater加载RemoteViews中的layout布局(在SystemServer进程中返回的是一个普通的view)
第三步:调用RemoteViews的set方法进行UI的更新
第四步:通过NotificationManager和AppWidgetManager提交UI更新任务
这样,RemoteViews就在SystemServer进程中显示了(就是我们看到的通知栏消息和桌面小部件)


从理论上说:

系统完全可以通过Binder去支持所有的view和view的操作,但这样做代价太大了,因为view的方法太多了,另外就是大量的IPC操作会影响效率

Andorid系统怎么做的呢?

Android系统提供了一个Action,Action代表view的操作,action也实现了Parceable接口
Android系统在RemoteViews调用apply进行更新任务的提交时,其实是间接的调用了Action对象的apply方法进行提交的;然后在远程进程中批量执行RemoteViews的更新操作,避免了大量的IPC操作,提高了程序的性能,Android系统在这方面的确设计的很精妙。
(Action其实是利用了反射进行UI的更新)


RemoteViews在通知栏和桌面小部件的工作过程和上面描述的是一样的。

当我们调用set方法时它们并不会立即去更新UI,而是

通知栏必须要通过NotificationManager的notify方法更新页面

桌面小部件必须要通过AppWidgetManager的updateAppWidget方法更新页面

实际上在NotificationManager和AppWidgetManager的内部实现中,它们的确是通过RemoteViews的apply(加载并且更新页面)和reapply(只更新页面)来加载和更新页面的

//RemoteViews的内部类ReflectionAction
private final class ReflectionAction extends Action {
@Override
public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
final View view = root.findViewById(viewId);
if (view == null) return;

Class<?> param = getParameterType();
if (param == null) {
throw new ActionException("bad type: " + this.type);
}

try {
getMethod(view, this.methodName, param).invoke(view, wrapArg(this.value));
} catch (ActionException e) {
throw e;
} catch (Exception ex) {
throw new ActionException(ex);
}
}
}


看代码可以知道,ReflectionAction其实表示的是一个反射的动作

关于单击事件,RemoteViews只支持PendingIntent,另外注意,setOnclickPendingIntent给普通view设置点击事件,如果想给listview设置点击事件必须使用setPendingIntentTemplate和setOnclickFillInIntent

5.2、RemoteViews的意义

比如现在有2个应用,一个应用能够更新另一个应用的页面,这个时候我们当然可以选择AIDL实现,但是如果界面更新比较频繁,那么效率就会降低,AIDL接口也会变得复杂,这个时候如果采用RemoteViews就没有问题了,但是RemoteViews只支持一些简单的view,如果更新的界面比较简单可以采用RemoteViews。

//如果采用RemoteViews进行页面的更新,那么还有一个问题:布局文件加载的问题
View view = remoteViews.apply(this, mContent);
mContent.addView(view);


这种写法在同一个应用中的多进程是可以的,但是不同应用就访问不到了,

我们可以通过加载资源名称进行加载,两个应用约定好布局文件的名称。

可以通过如下代码实现:

int layoutId = getResources.getIdentifier("layoutId");
View view = getInflate.inflate(layoutId, mContent);
remoteViews.reapply(this, view);
mContent.addView(view);
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: