读书笔记(5) 理解RemoteViews

这些读书笔记是根据《Android开发艺术探索》和《Android群英传》这两本书,然后将书上的一些知识点做一下记录。方便学习和理解,如果有存在侵犯版权的地方,还麻烦告知。个人强烈建议购买这两本书。真心不错。

本节是和《Android开发艺术探索》中的第5章 “理解RemoteViews” 有关系,建议先买书查看这一章。

[]RemoteViews概述

RemoteViews是一种远程的View。表示的是一个View结构,它可以在其他进程中显示。提供了一组操作用于跨进程更新界面。

RemoteViews的使用场景有两种:通知栏和桌面小部件。它们的界面都运行在其它进程(系统的SystemServer进程),为了跨进程更新界面,RemoteViews提供了一系列set方法(大部分set方法是通过反射来完成的),并且这些方法只是View全部方法的子集。

[]RemoteViews在通知栏(NotificationManager)上的应用

Notification notification = new Notification();

//设置状态栏中的通知图标
notification.icon = R.drawable.ic_launcher;
//设置状态栏中的通知标题
notification.tickerText = "状态栏通知标题";
//设置通知显示的时间
notification.when = System.currentTimeMillis();
//设置通知能否被清除
notification.flags = Notification.FLAG_AUTO_CANCEL;


//设置通知点击处理
Intent intent = new Intent(this, TextActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(
this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
notification.contentIntent = pendingIntent;


//设置通知布局及布局控件设置
RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.notification);

//设置RemoteViews布局中TextView的文本值
remoteViews.setTextViewText(R.id.tv, "自定义文本值");

//设置RemoteViews布局中ImageView的图片资源
remoteViews.setImageViewResource(R.id.iv, R.drawable.src);

//设置RemoteViews布局中View的点击事件
remoteViews.setOnClickPendingIntent(R.id.view, clickPendingIntent);
notification.contentView = remoteViews;


//发送通知
NotificationManager notificationManager = (NotificationManager) getSystemService(
Context.NOTIFICATION_SERVICE);
notificationManager.notify(1, notification);

更新RemoteViews时,没有提供findViewById()方法来直接获取里面的View,而是必须通过一系列的set方法来更新View。

setOnClickPendingIntent():用于给普通View设置点击事件。但是不能给ListView的item来设置点击事件。因为开销太大,所以系统就禁止了这种方式。

如果给ListView的item设置点击事件需要将setPendingIntentTemplate()和setOnClickFillInIntent()组合使用才行。

高版本(Api版本高于16)还可以这样发出通知

Notification notification = new Notification.Builder(this)
                    .setSmallIcon(int icon)
                    .setTicker(CharSequence tickerText)
                    .setWhen(long when)                       
                    .setContentIntent(PendingIntent intent)
                    .setContent(RemoteViews views)
                    .build();

NotificationManager notificationManager = (NotificationManager) getSystemService(
Context.NOTIFICATION_SERVICE);
notificationManager.notify(2, notification);

通知在Android的O版本(26)必须要设置notificationChannel,否则会发送失败。

[]RemoteViews在桌面小部件(AppWidgetProvider)上的应用

AppWidgetProvider本质上是一个广播。由系统发出广播,程序注册广播。

{}定义小部件布局文件
在res/layout下新建一个widget.xml布局文件。

{}定义小部件配置信息
在res/xml下新建一个widget_provider.xml配置文件。

<appwidget-provider 
    ......
    android:initialLayout="@layout/widget"  //小部件的布局文件
    android:minHeight=""  //小部件的最小高度
    android:minWidth=""  //小部件的最小宽度
    android:updatePeriodMillis=""  //小部件的自动更新周期
    >

</appwidget-provider>

自动更新周期的单位是毫秒,每隔一个周期,小部件的自动更新就会触发。

{}定义小部件的实现类

继承AppWidgetProvider(AppWidgetProvider是BroadcastReceiver的子类)。可能需要重写下列方法

onEnabled():小部件第一次添加到桌面时调用,多次添加只会在第一次调用。

onUpdate():小部件被添加或者每次小部件更新时都会调用,
小部件的更新时间由android:updatePeriodMillis决定,每个周期小部件都会自动更新一次。

onDeleted():每删除一次小部件调用一次。

onDisabled():当最后一个该类型的小部件被删除时调用。

onReceive():广播的内置方法,用于分发具体的事件。上面的方法会自动的被该方法在合适的时间调用。

{}在AndroidManifest.xml声明小部件

<receiver android:name=".MyWidgetProvider">

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

    </meta-data>

    <intent-filter>
        <action android:name="包名.action.CLICK"/>
        <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>

    </intent-filter>

</receiver>

第一个action用于识别小部件的单击行为,并且是在onReceive()中判断。第二个action作为小部件的标识而必须存在,这是系统的规范。如果不加,那么这个receiver就不是一个小部件。

[]PendingIntent概述

PendingIntent表示一种处于pending状态的Intent,pending状态表示一种待定,等待,即将发生,也就是说接下来有一个Intent将在某个待定的时刻发生。

PendingIntent和Intent的区别是:PendingIntent是在将来的某个不确定的时刻发生。而Intent是立刻发生。

PendingIntent典型的使用场景是给RemoteViews中的View添加点击事件。PendingIntent通过send()和cancel()来发送和取消特定的Intent。

PendingIntent支持三种待定Intent:

//启动Activity
getActivity(Context context, int requestCode,Intent intent, int flags)

//启动Service
getService(Context context, int requestCode,Intent intent, int flags)

//发送Broadcast 
getBroadcast(Context context, int requestCode,Intent intent,  int flags)

requestCode参数:PendingIntent发送方的请求码,多数情况设为0。requestCode会影响到flags的效果。

flags参数常见的值:

FLAG_ONE_SHOT:当前的PendingIntent只能被使用一次,然后它就会被自动cancel。如果后续还有相同的PendingIntent,那么它的send()就会调用失败。

FLAG_NO_CREATE:当前的PendingIntent不会主动创建,如果当前PendingIntent之前不存在,那么getActivity(),getService(),getBroadcast()就会直接返回null。即获取PendingIntent失败。这个标记为很少见。在开发中没有很大意义。

FLAG_CANCEL_CURRENT:当前的PendingIntent如果已经存在,那么它们都会被cancel,然后系统会创建一个新的PendingIntent。

FLAG_UPDATE_CURRENT:当前的PendingIntent如果已经存在,那么它们都会被更新,即它们的Intent中的Extras会被替换成最新的。

结合通知栏来理解flags参数,比如NotificationManager对象.notify(int id, Notification notification)

如果参数id为常量,那么多次调用notify()只能弹出一个通知,并且后续的通知会把前面的通知替换掉。即它们的Intent中的Extras会被替换成最新的。

如果参数id每次都不同,那么多次调用notify()会弹出多个通知栏,如果PendingIntent不匹配时,不管采用何种flags,这些通知之间不会互相干扰,如果PendingIntent匹配时,如果flags是FLAG_ONE_SHOT,后续的通知中的PendingIntent会和第一条通知保持一致,包括Extras,单击任何一条通知,剩下的通知均无法打开,当所有的通知被清除后,会再次重复这个过程。flags是FLAG_CANCEL_CURRENT,那么只有最新的通知可以打开,之前的通知均无法打开。flags是FLAG_UPDATE_CURRENT,之前的通知中的PendingIntent会被更新,最终会和最新的一条通知保持一致,包括Extras,并且这些通知都是可以打开的。

PendingIntent匹配过程:如果两个PendingIntent的Intent和requestCode都相同,那么这两个PendingIntent就是相同的,

Intent匹配过程:如果两个Intent的ComponentName和intent-filter都相同,那么这两个Intent就是相同的。需要注意的是Extras不参与Intent匹配。也就是说即使Extras不同,但是ComponentName和intent-filter都相同,那么这两个Intent还是相同。

[]RemoteViews的内部机制

RemoteViews的作用是在其他进程中显示并更新View界面。

RemoteViews所支持的View类型也是有限的。支持的Layout有FrameLayout,LinearLayout,RelativeLayout,GridLayout,支持的View有AnalogClock,Button,Chronometer,ImageButton,ImageView,ProgressBar,TextView,ViewFlipper,ListView,GridView,StackView,AdapterViewFlipper,ViewStub。不支持以上列表之外的View。也无法使用自定义View。

通知栏和小部件分别由NotificationManager和AppWidgetManager管理,而NotificationManager和AppWidgetManager通过Binder分别和SystemServer进程中的NotificationManagerService和AppWidgetManagerService进行通讯。因此通知栏和小部件的布局文件是在NotificationManagerService和AppWidgetManagerService被加载的。而它们运行在系统的SystemServer中。

RemoteViews能够跨进程,因为实现了Parcelable接口,通过Binder传递到SystemServer进程甚至是另一个应用。系统会根据RemoteViews中的包名信息获取到应用中的资源,从而完成布局文件的加载。

系统将view操作封装成Action对象,Action同样实现了Parcelable接口,通过Binder传递到SystemServer进程。远程进程通过RemoteViews的apply方法来进行view的更新操作,RemoteViews的apply方法内部则会去遍历所有的action对象并调用它们的apply方法来进行view的更新操作。这样做的好处是不需要定义大量的Binder接口,其次批量执行RemoteViews中的更新操作提高了程序性能。

每执行一次set操作,RemoteViews就会添加一个Action对象。

RemoteViews类中的源码
public void setTextViewText(int viewId, CharSequence text) {
    setCharSequence(viewId, "setText", text);
}

public void setCharSequence(int viewId, String methodName, CharSequence value) {
    addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR_SEQUENCE, value));
}

private void addAction(Action a) {
    ......
    if (mActions == null) {
        mActions = new ArrayList<Action>();
    }
    mActions.add(a);

   ......
}

通过NotificationManager执行notify()或者AppWidgetManager执行updateAppWidget()来提交更新时。这些Action对象跨进程传输到远程进程,远程进程通过RemoteViews的apply()来进行View的更新操作。RemoteViews的apply()内部去遍历所有的Action对象并调用apply(),具体的View更新操作是由Action对象的apply()来完成的。

RemoteViews类中的源码
public View apply(Context context, ViewGroup parent, OnClickHandler handler) {
     ......

    LayoutInflater inflater = (LayoutInflater)
            context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

    inflater = inflater.cloneInContext(inflationContext);
    inflater.setFilter(this);
    result = inflater.inflate(rvToApply.getLayoutId(), parent, false);

    rvToApply.performApply(result, parent, handler);

    return result;
}


public void reapply(Context context, View v, OnClickHandler handler) {
    ......

    rvToApply.performApply(v, (ViewGroup) v.getParent(), handler);
}

private void performApply(View v, ViewGroup parent, OnClickHandler handler) {
    ......
    final int count = mActions.size();
        for (int i = 0; i < count; i++) {
            Action a = mActions.get(i);
            a.apply(v, parent, handler);
        }
    }
}

ReflectionAction源码
@Override
public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
        ......
    try {
        getMethod(view, this.methodName, param).invoke(view, wrapArg(this.value));
    } catch (ActionException e) {
        throw e;
    } catch (Exception ex) {
        throw new ActionException(ex);
    }
}

apply()和reapply()的区别:apply()会加载布局并更新界面。reapply()则只会更新界面。

[]RemoteViews的意义

比如有两个应用,一个应用需要更新另一个应用的某个界面。当然可以选择AIDL去实现。但是如果对界面的更新比较频繁,这时就会有效率问题。同时AIDL接口就有可能变得很复杂。这时选择RemoteViews来实现就没有这个问题。但是RemoteViews仅支持部分View。对自定义View也是不支持的。

如果界面的View都是一些简单的且被RemoteViews支持的,可以采用RemoteViews。否则就不适应。

修改Activity的process属性可以使其运行在其他单独的进程中。

应用A加载应用B的布局文件。B的布局文件的资源id传输到A中以后很有可能是无效的。因为A中的布局文件的资源id不可能刚好和B的资源id是一样的。那么可以通过资源名称来加载布局文件。首先A和B约定好RemoteViews中的布局文件的资源名称,然后在A中根据名称查找对应的布局文件并加载。接着调用RemoteViews的reapply()即可将B中的对View所做的一系列更新操作全部作用到A中加载的View上。

B中通过广播将RemoteViews传递给A,将B中的布局文件复制到到A中

RemoteViews remoteViews = intent.getParcelableExtra("remoteViews");

int layoutId = getResources().getIdentifier("remote_view", "layout", getPackageName());

View view = getLayoutInflater().inflate(layoutId, mLinearLayout, false);
remoteViews.reapply(this, view);

mLinearLayout.addView(view);

但是B对RemoteViews的一系列set方法并没有效果,原因暂时不清楚。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值