5 理解RemoteViews
什么是远程view呢?它和远程service一样,RemoteViews可以在其他进程中显示。我们可以跨进程更新它的界面。在Android中,主要有两种场景:通知栏和桌面小部件。
本章先简单介绍通知栏和桌面小部件应用,接着分析RemoteViews内部机制,最后分析RemoteViews的意义并给出一个实例。
5.1 RemoteViews的应用
通知栏主要是通过NotificationManager的notify方法实现。桌面小部件是通过APPWidgetProvider来实现。APPWidgetProvider本质是一个广播。RemoteViews运行在系统的SystemServer进程。
5.1.1 RemoteViews在通知栏的应用
我们用到自定义通知,首先要提供一个布局文件,然后通过RemoteViews来加载,可以自定义通知的样式。更新view时,通过RemoteViews提供的一系列方法。如果给一个控件加点击事件,要使用PendingIntent。
5.1.2 RemoteViews在桌面小部件的应用
AppWidgetProvider是实现桌面小部件的类,本质是一个BroadcastReceiver。开发步骤如下:
- 定义小部件界面 代码
- 定义小部件配置信息 代码
- 定义小部件实现类,继承AppWidgetProvider 代码
上面的例子实现了一个简单地桌面小部件,在小部件上显示一张图片,点击后会旋转一周。 - 在AndroidManifest.mxl中声明小部件
receiver android:name=".MyAppWidgetProvider" >
第一个action用于识别小部件的单击,第二个action作为小部件的标识必须存在。
AppWidgetProvider除了onUpdate方法,还有一系列方法。这些方法会自动被onReceive方法调用。当广播到来以后,AppWidgetProvider会自动根据广播的action通过onReceive方法分发广播。
- onEnable:该小部件第一次添加到桌面时调用,添加多次只在第一次调用
- onUpdate:小部件被添加或者每次小部件更新时调用,更新时机由updatePeriodMillis指定,每个周期小部件都会自动更新一次。
- onDeleted:每删除一次桌面小部件都会调用一次
- onDisabled:最后一个该类型的桌面小部件被删除时调用
- onReceive:内置方法,用于分发具体事件给以上方法
5.1.3 PendingIntent概述
PendingIntent表示一种处于待定的状态的intent。典型场景是RemoteViews添加单击事件。通过send和cancel方法来发送和取消待定intent。
PendingIntent支持三种待定意图:
static PendingIntent | getActivity(Context context, int requestCode, Intent intent, int flags)获得一个PendingIntent,效果相当于Context.startActivity(Intent) |
static PendingIntent | getService(Context context, int requestCode, Intent intent, int flags)获得一个PendingIntent,效果相当于Context.startService(Intent) |
static PendingIntent | getBroadcast(Context context, int requestCode, Intent intent, int flags)获得一个PendingIntent,效果相当于Context.sendBroadcast(Intent) |
其中requestCode多数情况下设为0即可,requestCode会影响flags的效果。
PendingIntent的匹配规则:
如果两个PendingIntent,它们内部的Intent相同且requestCode也相同,那这两个PendingIntent就是相同的。
Intent的匹配规则:
如果两个intent的ComponentName和intent-filter相同,那么这两个intent相同。Extras不参与匹配过程。
flags参数的含义
- FLAG_ONE_SHOT
当前的PendingIntent只能被使用一次,然后就会被自动cancel,如果后续还有相同的PendingIntent,它们的send方法会调用失败。对于通知栏来说,同类的通知只能使用一次,后续的通知将无法打开。 - FLAG_NO_CREATE
当前的PendingIntent不会主动创建,如果当前PendingIntent之前不存在(匹配的PendingIntent),那么获取PendingIntent失败。这个flag很少使用。 - FLAG_CANCEL_CURRENT
当前的PendingIntent如果存在(匹配的PendingIntent),那么它们都会被cancel,然后系统创建一个新的PendingIntent。对于通知栏来说,那些被cancel的消息单击后将无法打开。 - FLAG_UPDATE_CURRENT
当前PendingIntent如果已经存在(匹配的PendingIntent),那么它们都会被更新。即intent中的extras会被替换成最新的。
举例:
在manager.notify(id,notification)
中,如果id是常量,那么多次调用notify只能弹出一个通知,后续的通知会把前面的通知完全替代。而如果每次id都不同,那么会弹出多个通知。
如果id每次都不同且PendingIntent不匹配,那么flags不会对通知之间造成干扰。
如果id不同且PendingIntent匹配:
- 如果采用了FLAG_ONE_SHOT标记位,那么后续通知中的PendingIntent会和第一条通知完全一致,包括extras,单击任何一条通知后,剩下的通知均无法再打开,当所有的通知被清除后,会再次重复这一过程。
- 如果采用FLAG_CANCEL_CURRENT,那么只有最新的通知可以打开。
- 如果采用FLAG_UPDATE_CURRENT,那么之前弹出的通知中的PendingIntent会被更新,与最新一条的通知完全一致,包括extras,并且这些通知都可以打开。
5.2 RemoteViews的内部机制
构造方法
public RemoteViews(String packageName,int layoutId)
第一个参数是当前应用的包名,第二个参数是待加载的布局文件。
RemoteViews并不支持所有的view类型,支持类型如下:
- Layout:FrameLayout、LinearLayout、RelativeLayout、GridLayout
- View:AnalogClock、Button、Chronometer、ImageButton、ImageView、ProgressBar、TextView、ViewFlipper,ListView,GridView、StackView、AdapterViewFlipper、ViewStub。
- RemoteViews不支持以上view的子类
访问RemoteViews的view元素,必须通过一系列set方法完成:
方法名 | 作用 |
---|---|
setTextViewText(int viewId,CharSequence text) | 设置TextView的文本内容 第一个参数是TextView的id 第二个参数是设置的内容 |
setTextViewTextSize(int viewId,int units,float size) | 设置TextView的字体大小 第二个参数是字体的单位 |
setTextColor(int viewId,int color) | 设置TextView字体颜色 |
setImageViewResource(int viewId,int srcId) | 设置ImageView的图片 |
setInt(int viewId,String methodName,int value) | 反射调用View对象的参数类型为Int的方法 比如上述的setImageViewResource的方法内部就是这个方法实现 因为srcId为int型参数 |
setLong setBoolean | 类似于setInt |
setOnClickPendingIntent(int viewId,PendingIntent pendingIntent) | 添加点击事件的方法 |
大部分set方法是通过反射来完成的。
RemoteViews内部机制
通知栏和小组件分别由NotificationManager(NM)和AppWidgetManager(AWM)管理,而NM和AWM通过Binder分别和SystemService进程中的NotificationManagerService以及AppWidgetService中加载的,而它们运行在系统的SystemService中,这就和我们进程构成了跨进程通讯。
首先RemoteViews会通过Binder传递到SystemService进程,因为RemoteViews实现了Parcelable接口,因此它可以跨进程传输,系统会根据RemoteViews的包名等信息拿到该应用的资源;然后通过LayoutInflater去加载RemoteViews中的布局文件。接着系统会对View进行一系列界面更新任务,这些任务就是之前我们通过set来提交的。set方法对View的更新并不会立即执行,会记录下来,等到RemoteViews被加载以后才会执行。
为了提高效率,系统没有直接通过Binder去支持所有的View和View操作。而是提供一个Action概念,Action同样实现Parcelable接口。系统首先将View操作封装到Action对象并将这些对象跨进程传输到SystemService进程,接着SystemService进程执行Action对象的具体操作。远程进程通过RemoteViews的apply方法来进行View的更新操作,RemoteViews的apply方法会去遍历所有的Action对象并调用他们的apply方法。这样避免了定义大量的Binder接口,也避免了大量IPC操作。
apply和reApply的区别在于:apply会加载布局并更新界面,而reApply则只会更新界面。RemoteViews在初始化界面时会调用apply方法,后续更新界面调用reApply方法。
关于单击事件,RemoteViews中只支持发起PendingIntent,不支持onClickListener那种模式。setOnClickPendingIntent用于给普通的View设置单击事件,不能给集合(ListView/StackView)中的View设置单击事件(开销大,系统禁止了这种方式)。如果要给ListView/StackView中的item设置单击事件,必须将setPendingIntentTemplate和setOnClickFillInIntent组合使用才可以。
5.3 RemoteViews的意义
当一个应用需要更新另一个应用的某个界面,我们可以选择用AIDL来实现,但如果更新比较频繁,效率会有问题,同时AIDL接口就可能变得很复杂。如果采用RemoteViews就没有这个问题,但RemoteViews仅支持一些常用的View,如果界面的View都是RemoteViews所支持的,那么就可以考虑采用RemoteViews。
demo A 、B
6 Android的Drawable
Drawable表示的是一种可以在Canvas上进行绘制的抽象概念,它的种类有很多,最常见的就是颜色和图片。优点:使用简单,比自定义View成本低很多,非图片类型的Drawable占用空间较小。本章中,首先描述Drawable的层次关系,接着介绍Drawable的分类,最后介绍自定义Drawable相关的知识。
6.1 Drawable简介
Drawable有很多种,都表示图像的概念,但不全是图片,通过颜色也可以构造出各式各样的图像效果。实际开发中,Drawable常被用来作为View的背景使用。Drawable一般是通过XML来定义的,Drawable是所有Drawable对象的基类。
Drawable的内部宽、高这个参数比较重要,通过getIntrinsicWidth/getIntrinsicHeight这两个方法获取。但并不是所有Drawable都有宽高;图片Drawable的内部宽/高就是图片的宽/高,但是颜色形成的Drawable并没有宽/高的概念。
6.2 Drawable的分类
常见的有BitmapDrawable、ShapeDrawable、LayerDrawable以及StateListDrawable等。
6.2.1 BitmapDrawable
表示的就是一张图片,可以直接引用原始图片即可,也可以通过XML描述它,从而设置更多效果。
属性分析
android:src
图片资源idandroid:antialias
是否开启图片抗锯齿功能。开启后会让图片变得平滑,同时也会一定程度上降低图片的清晰度,建议开启;android:dither
是否开启抖动效果。当图片的像素配置和手机屏幕像素配置不一致时,开启这个选项可以让高质量的图片在低质量的屏幕上还能保持较好的显示效果,建议开启。android:filter
是否开启过滤效果。当图片尺寸被拉伸或压缩时,开启过滤效果可以保持较好的显示效果,建议开启;-
android:gravity
当图片小于容器的尺寸时,设置此选项可以对图片进行定位。
android:tileMode
平铺模式,有四种选项["disabled" | "clamp" | "repeat" | "mirror"]。当开启平铺模式后,gravity属性会被忽略。repeat是指水平和竖直方向上的平铺效果;mirror是指在水平和竖直方向上的镜面投影效果;clamp是指图片四周的像素会扩展到周围区域,这个比较特别。
NinePatchDrawable
表示一张.9格式的图片,它和BitmapDrawable都表示一张图片。用XML描述的方式也和BitmapDrawable一样。在bitmap标签中也可以使用.9图。
6.2.2 ShapeDrawable
可以理解为通过颜色来构造的图形,可以是纯色或渐变的图形。
属性分析
- android:shape
表示图片的形状,选项:rectangle(矩形)、oval(椭圆)、line(横线)、ring(圆环)。默认值是矩形,另外line和ring这两个选项必须通过标签来指定宽度和颜色,否则看不到效果。
其中,ring有其特殊的5种属性
属性 | 含义 |
---|---|
android:innerRadius | 圆环的内半径,和innerRadiusRatio同时存在,以innerRadius为准 |
android:thickness | 圆环的厚度,外半径减去内半径的大小,和android:thinknessRatio同时存在,以thickness为准 |
android:innerRadiusRatio | 内半径占整个Drawable的宽度比例,默认值为9,如果为n,内半径=宽度/n |
android:thicknessRatio | 厚度占整个Drawable宽度的比例,默认值为3,如果为n,厚度=宽度/n |
android:useLevel | 一般都用false |
表示shape的四个角的角度(圆角程度)。只适用于矩形shape。其中android:radius是同时为4个角设置相同的角度,优先级较低,会被topLeftRadius这种具体指定角度的属性所覆盖。-
与
标签相互排斥的,其中solid表示纯色填充,而gradient表示渐变效果;gradient有如下几个属性:- android:angle——渐变的角度,默认为0,其值必须是45的倍数,0表示从左往右,90表示从下到上。
- android:centerX 渐变的中心点的横坐标
- android:centerY 渐变的中心点的纵坐标;
- android:startColor 渐变的起始色
- android:centerColor 渐变的中间色
- android:endColor 渐变的结束色
- android:gradientRadius 渐变半径,仅当android:type="radial"时有效。
- android:type 渐变的类型,有linear(线性渐变)、radial(镜像渐变)、swepp(扫描线渐变)三种,默认是线性渐变。
表示纯色填充,通过android:color即可指定shape中填充的颜色。-
Shape的描边,有如下属性:- android:width 描边的宽度
- android:color 描边的颜色
- android:dashWidth 组成虚线的线段的宽度
- android:dashGap 组成虚线之间的间距。dashWidth和dashGap有任何一个为0,虚线效果都不能生效。
表示空白,但不是shape的空白,而是包含它的View的空白。
shape的大小,有两个属性:android:width和android:height,分别表示shape的宽高。通过标签指定宽高后,ShapeDrawable就有固定宽/高了。但是作为view的背景来说,shape还是会被拉伸或者缩小为view的大小。
更多参考:Android样式的开发:shape篇
6.2.3 LayerDrawable
它表示一种层次化的Drawable集合,通过将不同的Drawable放置在不同层后达到一种叠加效果。
6.2.4 StateListDrawable
对应
标签。它表示Drawable集合,每个Drawable对应View的一种状态,这样系统就会根据View的状态来选择合适的Drawable。主要用于设置可点击View的背景。
属性分析
- android:constantSize
StateListDrawable的固有大小是否随着其状态的变化而变化,因为不同的Drawable有不同的固有大小。true表示固有大小保持不变,这时它的固有大小是内部所有Drawable的固有大小的最大值。默认值为false。 - android:dither
是否开启抖动效果,默认true - android:variablePadding
StateListDrawable的padding是否随着状态变化而变化。true表示变化,false表示padding是内部所有Drawable的padding的最大值。默认为false。
view的常见状态
状态 | 含义 |
---|---|
android:state_pressed | 按下状态,Button按下之后没有松开 |
android:state_focused | View获取了焦点 |
android:state_selected | 用户选择了View,如RadioButton |
android:state_checked | 用户选中了View,适用于CheckBox |
android:state_enable | View处于可用状态 |
默认的item一般放在最后并且不添加任何状态,这样当系统在之前的item无法选择的时候,就会匹配默认的item,因为item的默认状态不附带任何状态,所以它可以适配任何状态。
6.2.5 LevelListDrawable
对应
标签。同样表示Drawable集合,集合中的每个Drawable都会有一个等级的概念,根据等级不同来切换对于的Drawable。当它作为View的背景时,可以通过Drawable的setLevel方法来设置不同的等级从而切换具体的Drawable。level的值从0-10000,默认为0。
```
###6.2.6 TransitionDrawable
对应``标签。用来实现两个Drawable之间淡入淡出的效果。
TransitionDrawable drawable = (TransitionDrawable) imageView.getBackground();
drawable.startTransition(1000);
startTransition和reverseTransition方法实现淡入淡出的效果以及它的逆过程。
###6.2.7 InsetDrawable
对应于``标签。它可以将其他Drawable内嵌到自己当中,并可以在四周留下一定的间距。当一个View希望自己的背景比自己的实际区域小的时候,可以采用InsetDrawable来实现。通过LayerDrawable也可以实现。
其中,inset中的shape距离view边界为10dp。
###6.2.8 ScaleDrawable
ScaleDrawable对应于xml文件中的``标签,可以根据自己的level将指定的drawable缩放到一定比例。
其中,android:scaleGravity属性相当于gravity属性。android:scaleHeight/scaleWidth 表示Drawable的缩放比例。
缩放公式: `w -= (int) (w * (10000 - level) * mState.mScaleWidth / 10000)`
可见,level越大,Drawable看起来越大;scaleHeight/scaleWidth越大,Drawable看起来越小。注意的是,level设置为0时,Drawable不可见。level不应超过10000。
### 6.2.9 ClipDrawable
ClipDrawabe对应于``标签,他可以根据自己当前的等级(level)来裁剪一个Drawable,裁剪方向可以通过Android:clipOrientation和android:gravity两个属性共同控制。
clipOrientation表示裁剪方向。gravity需要和clipOrientation一起才能发挥作用。如下所示:
选项 | 含义 |
---|---|
top | 将内部的Drawable放在容器的顶部,不改变大小,如果为竖直裁剪,就从底部开始裁剪 |
bottom | 将内部的Drawable放在容器的底部,不改变大小,如果为竖直裁剪,就从顶部开始裁剪 |
left | 默认值。内部Drawable放在容器左边,不改变大小,如果为水平裁剪,就从右边开始裁剪。 |
right | 内部Drawable放在容器右边,不改变大小,如果为水平裁剪,就从左边开始裁剪 |
center_vertical | Drawable在容器中竖直居中,不改变大小,竖直裁剪的时候上下同时开始裁剪 |
fill_vertical | Drawable在竖直方向填充容器,如果为竖直裁剪,仅当ClipDrawable的等级为0(level=0,完全不可见)时,才会有裁剪行为 |
center_horizontal | Drawable水平居中,不改变大小,水平裁剪的时候从左右两边开始裁剪 |
fill_horizontal | Drawable在水平方向填充,如果为水平裁剪,仅当ClipDrawable等级=0的时候,才能有裁剪行为。 |
center Drawable | 在水平和竖直方向居中,不改变大小,水平裁剪的时候从左右开始裁剪,竖直裁剪的时候从上下开始裁剪。 |
fill Drawable | 在竖直和水平方向填充容器,仅当level=0的时候才有裁剪行为 |
clip_vertical | 附加选项,竖直方向的裁剪,少使用 |
clip_horizontal | 附加选项,水平方向的裁剪,少使用 |
使用步骤:
- 定义ClipDrawable
- 布局文件引用
- 代码控制level
ImageView imageClip = (ImageView) findViewById(R.id.image_clip);
ClipDrawable drawable = (ClipDrawable) imageClip.getDrawable();
drawable.setLevel(5000);
level=0的时候,表示完全裁剪,level=10000的时候表示完全不裁剪,level=5000的时候表示裁剪了一半。即等级越大,裁剪的区域越小。
6.3 自定义Drawable
在第5章中,我们分析了View的工作原理,系统会调用Drawable的draw方法绘制view的背景。所以我们可以通过重写Drawable的draw方法来自定义Drawable。但是,通常我们没必要自定义Drawable,因为自定义Drawable无法在XML中使用。只有在特殊情况下可以使用自定义Drawable。
圆形自定义Drawable demo,半径随着view的变化而变化
- draw、setAlpha、setColorFilter和getOpacity这几个方法都是必须要实现的,其中draw是最重要的。当自定义Drawable有固有大小的时候最好重写getIntrinsicWidth和getIntrinsicHeight这两个方法,因为会影响到view的wrap_content布局。上面的例子中,没有重写,其内部大小为-1。
- 内部大小不等于Drawable的实际区域大小,Drawable实际区域的大小可以通过getBounds方法来得到,一般来说它和View的尺寸相同。