第五章 理解RemoteViews
RemoteViews可以理解为一种远程的View,其实他和远程的Service是一样的。一个View结构,可以在其他进程中显示,可以提供一组基础的操作用于跨进程更新它的界面。应用场景是通知栏和桌面小部件,本章内容包括:RemoteViews在通知栏和桌面小部件的应用、RemoteViews的内部机制、分析RemoteViews的意义并给出一个采用RemoteViews跨进程更新界面的示例。越来越觉得存在着理解能力不足的问题,这本书太偏理论了。
(一)RemoteViews的应用
主要是是通知栏和桌面小部件,前者通过NotificationManager的notify方法去实现通知栏,可定义布局。桌面小部件由AppwidgetProvider(本质广播)来实现,但缺陷是:他们的更新都无法像Activity中直接更新View,这是因为两者都运行在其他进程SystemService中,为了跨进程更新UI,RemoteViews提供了一系列的set方法,我们接下来就是来实际的演示了。
NotificationManager的代码:
//复习通知。主要是通过NotificationManager 和pendingIntent来实现的。
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private Button btn_send_notice;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btn_send_notice = (Button) findViewById(R.id.btn_send_notice);
btn_send_notice.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_send_notice:
//步骤四:点击通知没有效果,使用pendingIntent来实现(延迟执行的Intent)
//可以getActivity、getService、getBroadcast,第一个参数context,第二个没用,第三个intent对象,第四个是PI的行为,一般为零。
//最后记得setContentIntent(pi)
Intent intent = new Intent(MainActivity.this,Main2Activity.class);
PendingIntent pi = PendingIntent.getActivity(this,0,intent,0);
//步骤一:NotificationManager对通知进行管理,调用Context的getSystemService可获得,该API可用于确定获取系统的哪一个服务
NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
//步骤二:通过Builder构造器来创建Notification对象,可以连缀任意多的设置方法来创建对象,注意是support-v4库的内容,兼容性好
//设置内容包括标题、正文、创建时间、通知小图标、大图标。
Notification notification = new NotificationCompat.Builder(this)
.setContentTitle("This is content title")
.setContentText("This is content text")
.setWhen(System.currentTimeMillis())
.setSmallIcon(R.mipmap.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher))
.setContentIntent(pi)
.setAutoCancel(true)
.build();
//步骤三:调用NotificationManager的notify就可以让通知显示出来了,notify接收两个参数,一个是id,确保每个通知的id是不同的,
//另一个是Notification对象
notificationManager.notify(1,notification);
//步骤五:点击之后取消通知,在新的Activity中写这个,cancel的1指的是刚才notify接收两个参数中,或者setAutoCancel
// NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
// manager.cancel(1);
break;
default:
break;
}
}
}
1.1.RemoteViews在通知栏上的应用
传统默认样式如上所示,比较简单,为了满足个性化需求,自定义使用RemoteViews来加载自定义的布局文件即可改变通知样式。NotificationManager+PendingIntent+RemoteViews
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_send_notice:
Intent intent = new Intent(MainActivity.this, Main2Activity.class);
PendingIntent pi = PendingIntent.getActivity(this, 0, intent, 0);
NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.layout_notification);
remoteViews.setTextViewText(R.id.msg, "chapter_5");
remoteViews.setImageViewResource(R.id.icon, R.mipmap.ic_launcher);
remoteViews.setOnClickPendingIntent(R.id.open_activity2, pi);
Notification notification = new NotificationCompat.Builder(this)
.setContentTitle("This is content title")
.setContentText("This is content text")
.setWhen(System.currentTimeMillis())
.setSmallIcon(R.mipmap.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher))
.setContentIntent(pi)
.setCustomContentView(remoteViews)
.setAutoCancel(true)
.build();
notificationManager.notify(2, notification);
break;
default:
break;
}
}
RemoteViews的使用也很简单,只要提供当前应用的包名和布局文件的资源id就可以创建一个RemoteViews了,如何更新呢?这一点和View还是有很大的不同,RemoteViews更新的时候,无法直接访问里面的View,必须通过RemoteViews所提供的一系列方法来更新,比如设置TextView,那就需要remoteViews.setTextViewText(R.id.msg, "chapter_5");
如果需要点击事件的话,需要setOnClickPendingIntent来触发了,关于PendingIntent,他表示点击就是一种待定的意图,触发后才会操作。
1.2.RemoteViews 在桌面小部件的应用
AppWidgetProvider是Android提供给我们的用于实现桌面小部件的类,其本质也就是一个广播BroadCastReceiver,继承自Object和BroadCastReceiver。因此将它当做BroadCastReceiver即可。
步骤一:定义小部件视图:在res/layout下我们先写个widget.xml,这里就是小部件的视图了。
步骤二:定义小部件配置信息:在res/xml中定义一个appwidget_info.xml,initialLayout是小工具所使用的初始化布局,minHeight和minWidth是最小尺寸,updatePeriodMillis是自动更新周期,每隔一个周期,小工具自动更新会触发。
步骤三:实现小部件的实现类,继承自AppWidgetProvider,初始化界面和后续的更新界面都必须使用RemoteViews来完成。
步骤四:在XML文件中声明小部件:本质是小部件,必须要注册。有两个Action,其中第一个是识别小部件的动作,第二个就是他的标识。
1.3.AppWidgetProvider:
AppWidgetProvider 除了最常用的onUpdate方法,还有其他的方法,onEnable,onDisabled,onDeleted以及onReceiver,这些方法都会被onReceiver根据Action进行调用:
onEnable:当该窗口小部件第一次添加到桌面的时候调用该方法,可添加多次但是只有第一次调用;
onUpdate:小部件被添加或者第一次更新的时候都会调用一次该方法,小部件的更新机制由updatePeriodMillis来指定;
onDeleted:每删除一次小部件都会调用
onDisabled:当最后一个该类型的小部件会删除时调用
onReceiver:广播内置的方法,用具体事件的分发。
1.4.BroadCastReceiver复习:
广播类型分为标准广播(异步执行,同时收到、无法截断)和有序广播(同步执行、先后顺序,可截断);
1.4.1.接收系统广播:
代码中注册称为动态注册;在AndroidManifest.xml中注册称为静态注册。
动态注册监听网络变化;步骤一:新建类继承自BroadCastReceiver,重写父类的onReceiver方法;具体处理逻辑放在其中;步骤二:onCreate方法中创建IntentFilter实例,系统发出android.net.conn.CONNECTIVITY_CHANGE的广播,在IntentFilter添加该Action;步骤三:在onCreate方法中使用registerReceiver进行注册;在onDestroy中使用unregisterReceiver进行取消注册。步骤四:添加权限:android.permission.ACCESS_NETWORK_STATE。
public class MainActivity extends AppCompatActivity {
private IntentFilter intentFilter;
private NetworkChangedReceiver networkChangedReceiver;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
intentFilter = new IntentFilter();
intentFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE");
networkChangedReceiver = new NetworkChangedReceiver();
registerReceiver(networkChangedReceiver,intentFilter);
}
@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(networkChangedReceiver);
}
class NetworkChangedReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
//getSystemService获取ConnectivityManager的实例,这是系统服务类,专门用来网络管理
ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
//getActiveNetworkInfo得到NetworkInfo的实例;
NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
//isAvailable判断网络是否可用
if(networkInfo!=null &&networkInfo.isAvailable()){
Toast.makeText(context,"有网",Toast.LENGTH_SHORT).show();
}else{
Toast.makeText(context,"没网",Toast.LENGTH_SHORT).show();
}
}
}
}
静态注册实现开机启动:动态广播虽然灵活,但只有在程序启动之后才能接收到广播,静态注册可以在程序未启动的情况下接收到广播。步骤一:右键快速新建BroadCastReceiver,在XML中注册,在onReceiver中写一个简单的Toast;步骤二:系统启动完成之后会发出一条android.intent.action.BOOT_COMPLETED的广播,需要在
....
public class BootCompleReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// TODO: This method is called when the BroadcastReceiver is receiving
// an Intent broadcast.
Toast.makeText(context,"启动成功啦",Toast.LENGTH_SHORT).show();
}
}
1.4.2.发送自定义广播:
发送标准广播:步骤一:快速新建广播接收器,复写onReceiver方法,简单Toast,在xml中修改receiver:
btn_click.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent("com.example.hzk.broadcastreceiver.MyBroadCastReceiver");
sendBroadcast(intent);
}
});
发送有序广播;将sendBroadcast(intent);改成 sendOrderedBroadcast(intent,null);即可,可以在onReceiver方法中使用abortBroadcast();来拦截广播;也可以使用
1.4.3.使用本地广播
系统全局广播可被任何程序接收到,不安全,Android引入一套本地广播机制,使得发出广播只能在应用程序内部传递。而且广播接收器只能接收到来自本应用程序发出的广播,安全性问题得以解决。优势安全高效。
LocalBroadcastManager的getInstance得到它的一个实例,注册、注销都一样,发出一条com.example.hzk.LOCAL_BROADCAST的广播。
public class MainActivity extends AppCompatActivity {
private Button btn_click, btn_nativeclick;
private IntentFilter intentFilter;
private NetworkChangedReceiver networkChangedReceiver;
private LocalReceiver localReceiver;
private LocalBroadcastManager localBroadcastManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
localBroadcastManager = LocalBroadcastManager.getInstance(this);//获取实例
btn_nativeclick.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent("com.example.hzk.LOCAL_BROADCAST");
localBroadcastManager.sendBroadcast(intent);//发送本地广播
}
});
intentFilter.addAction("com.example.hzk.LOCAL_BROADCAST");
localReceiver = new LocalReceiver();
localBroadcastManager.registerReceiver(localReceiver,intentFilter);
}
@Override
protected void onDestroy() {
super.onDestroy();
localBroadcastManager.unregisterReceiver(localReceiver);
}
class LocalReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "received local receiver", Toast.LENGTH_LONG).show();
}
}
}
1.5.PendingIntent概述
pendingIntent与Intent的区别:PendingIntent是在将来某个不确定的时刻发生;而Intent(意图)是立刻发生。典型应用场景是给RemoteViews添加点击事件。RemoteViews运行于远程进程中,不像View可以setOnClickListener设置点击事件。PendingIntent通过send和cancel方法来发送和取消特定的待定Intent。
PendingIntent支持三种特定意图:启动Activity、Service和BroadCastReceiver。其中方法中第一个和第三个参数容易理解,第二个是发送方请求码,多数为零。第四个标志位:一般使用FLAG_UPDATE_CURRENT。
PendingIntent的匹配规则为:如果两个PendingIntent他们内部的Intent相同并且requstCode也相同的话,那么PendingIntent就是相同的,Intent的匹配规则是:只要Intent之间的ComponentName和intent-filter相同,那么这两个intent就相同,需要注意的是Extras不参与匹配过程,只要intent之间的name和intent-filter相同就行。
(二)RemoteViews的内部机制
2.1.RemoteViews支持的类型及相关set的API
RemoteViews的作用在其他进程中显示并且更新View的界面,为了更好的理解他的内部机制。其构造方法为 public RemoteViews(String packageName, int layoutId);第一个表示当前的包名,第二个是加载的布局,RemoteViews目前并不能支持所有的View类型。Layout包括FrameLayout、LinearLayout、RelativeLayout、GridLayout;View 包括ButtonImageButton,ImageView,ProgressBar,TextView,ListView,GridView,stackView,AdapterViewFlipper,ViewStub等。譬如RemoteViews无法使用EditText,报错。
RemoteViews也没有提供findViewById方法,因此无法直接访问里面的View元素,而必须通过RemoteViews所提供的一系列set方法来完成,当然这是因为RemoteViews在远程进程中显示。
RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.notification_item);
//viewId是被操作的id,setText是一个方法名,text是给TextView要设置的文本
remoteViews.setTextViewText(R.id.tv_title, "This is Title");
remoteViews.setTextViewText(R.id.tv_content, "This is Notification Content");
remoteViews.setImageViewResource(R.id.iv_img, R.mipmap.ic_launcher);
remoteViews.setOnClickPendingIntent(R.id.ll_open, pendingIntent);
2.2.RemoteViews的内部机制
RemoteViews主要用于通知栏和通知栏和桌面小部件,通知栏和小部件分别由NotificationManager和AppWidgetProvider管理。而NotificationManager和AppWidgetProvider通过Binder分别为SystemService进程中的NotificationManagerService和AppWidgetService。与我们的进程构成跨进程通信。
系统完全可以通过Binder去支持所有的View和View操作,但是这样做的话代价太大。步骤一:系统首先将Vew操作封装装到Action对象并将这些对象通过Binder跨进程传输到远程进程,步骤二:在远程进程中执行Action对象中的具体操作。Action同样实现了Parcelable接口。eg:当我们通过NotificationManager和AppWidgetManager来提交我们的更新时,这些Action对象就会传输到远程进程并在远程进程中依次执行。远程进程通过RemoteViews的apply方法来进行View的更新操作。优势:无需大量Binder,批量进行,避免大量IPC操作。
Action对象的apply方法是真正操作View的,每一次的set操作都会对应着它里面的Action对象,将其添加进ArrayList列表中;performApply的作用是遍历mActions列表并执行每一个Action对象的apply方法。
ReflectionAction 表示的是一个反射的动作,他通过对View的操作会以反射的方式调用,其中getMethod就是根据方法名来反射所需要的方法。Set方法包括了setTextViewText、setBoolean等。
2.3.setOnClickPendingIntent,setPendingIntentTemplate,以及setonClickFillinIntent的区别
第一个用于给普通view设置单击事件;但不能给集合(ListView和StackView)中的View设置单击事件,比如ListView中的item不能通过setOnClickPendingIntent添加单击事件。其次,如果要对ListView和StackView设置点击事件,则需要将后两者组合使用。因为开销大。
(三)RemoteViews的意义
场景:打造一个模拟通知栏效果以实现跨进程UI更新。
解决:B用来不停发送通知栏信息,A显示这个RemoteViews对象。
B的代码:构造RemoteViews对象并将其传输给A。
public void onButtonClick(View v) {
//步骤一:新建RemoteViews对象,并反射调用设置文本内容、图片资源
RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.layout_simulated_notification);
remoteViews.setTextViewText(R.id.msg, "msg from process:" + Process.myPid());
remoteViews.setImageViewResource(R.id.icon, R.drawable.icon1);
//步骤二:构建待定意图:getActivity实现启动Activity企图,设置点击事件Intent。
PendingIntent pendingIntent = PendingIntent.getActivity(this,
0, new Intent(this, DemoActivity_1.class), PendingIntent.FLAG_UPDATE_CURRENT);
PendingIntent openActivity2PendingIntent = PendingIntent.getActivity(
this, 0, new Intent(this, DemoActivity_2.class), PendingIntent.FLAG_UPDATE_CURRENT);
remoteViews.setOnClickPendingIntent(R.id.item_holder, pendingIntent);
remoteViews.setOnClickPendingIntent(R.id.open_activity2, openActivity2PendingIntent);
//步骤三:以广播的形式发送Intent
Intent intent = new Intent(MyConstants.REMOTE_ACTION);
intent.putExtra(MyConstants.EXTRA_REMOTE_VIEWS, remoteViews);
sendBroadcast(intent);
}
A的代码:接收B中的广播并显示RemoteViews即可。
public class MainActivity extends Activity {
private LinearLayout mLinearLayout;
private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
//从Intent中取出RemoteViews对象
RemoteViews remoteViews = intent.getParcelableExtra("send_bro");
if (remoteViews != null) {
updateUI(remoteViews);
}
}
};
//通过apply方法加载布局并且执行更新操作,最后将得到的View添加到A的布局中即可
private void updateUI(RemoteViews remoteViews) {
View view = remoteViews.apply(this, mLinearLayout);
mLinearLayout.addView(view);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_a);
initView();
}
//注册广播,初始化视图
private void initView() {
mLinearLayout = (LinearLayout) findViewById(R.id.mLinearLayout);
IntentFilter intent = new IntentFilter("send_bro");
registerReceiver(mBroadcastReceiver, intent);
}
//销毁时解除广播
@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(mBroadcastReceiver);
//A和B属于不同应用,那么B中的布局文件的资源id传输到A中以后很有可能是无效的
//就通过资源名称来加载布局文件。首先两个应用要提前约定好RemoteViews中的布局的文件名称,比如“layout simulated notification”,然后在A中根据名称找到并加载。
// int layoutId = getResources().getIdentifier("layout_simulated_notification","layout",getPackageName());
// View view = getLayoutInflater().inflate(layoutId,mLinearLayout,false);
// remoteViews.reapply(this,view);
// mLinearLayout.addView(view);
}
}