继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

转 <android开发>学习笔记

一只名叫tom的猫
关注TA
已关注
手记 346
粉丝 62
获赞 329

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。开发步骤如下:

  1. 定义小部件界面 代码

  2. 定义小部件配置信息 代码

  3. 定义小部件实现类,继承AppWidgetProvider 代码
    上面的例子实现了一个简单地桌面小部件,在小部件上显示一张图片,点击后会旋转一周。

  4. 在AndroidManifest.mxl中声明小部件

     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>

    第一个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 PendingIntentgetActivity(Context context, int requestCode, Intent intent, int flags)获得一个PendingIntent,效果相当于Context.startActivity(Intent)
static PendingIntentgetService(Context context, int requestCode, Intent intent, int flags)获得一个PendingIntent,效果相当于Context.startService(Intent)
static PendingIntentgetBroadcast(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匹配:

  1. 如果采用了FLAG_ONE_SHOT标记位,那么后续通知中的PendingIntent会和第一条通知完全一致,包括extras,单击任何一条通知后,剩下的通知均无法再打开,当所有的通知被清除后,会再次重复这一过程。

  2. 如果采用FLAG_CANCEL_CURRENT,那么只有最新的通知可以打开。

  3. 如果采用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描述它,从而设置更多效果。

<?xml version="1.0" encoding="utf-8"?>
<bitmap / nine-patch
xmlns:android="http://schemas.android.com/apk/res/android"
android:src="@[package:]drawable/drawable_resource"
android:antialias=["true" | "false"]
android:dither=["true" | "false"]
android:filter=["true" | "false"]
android:gravity=["top" | "bottom" | "left" | "right" | "center_vertical" |
                  "fill_vertical" | "center_horizontal" | "fill_horizontal" |
                  "center" | "fill" | "clip_vertical" | "clip_horizontal"]
android:tileMode=["disabled" | "clamp" | "repeat" | "mirror"] />

属性分析

  • android:src
    图片资源id

  • android: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

可以理解为通过颜色来构造的图形,可以是纯色或渐变的图形。

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners
    android:bottomLeftRadius="10dp"
    android:bottomRightRadius="10dp"
    android:radius="5dp"
    android:topLeftRadius="10dp"
    android:topRightRadius="10dp" />

<gradient
    android:angle="0"
    android:centerColor="#cccccc"
    android:centerX="100"
    android:centerY="20"
    android:endColor="#abcdef"
    android:gradientRadius="100dp"
    android:startColor="#000000"
    android:type="linear"
    android:useLevel="false" />

<solid android:color="#cccccc" />

<stroke
    android:width="1dp"
    android:color="#cccccc"
    android:dashGap="2dp"
    android:dashWidth="50dp" />


属性分析

  • 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
  • <corners>
    表示shape的四个角的角度(圆角程度)。只适用于矩形shape。其中android:radius是同时为4个角设置相同的角度,优先级较低,会被topLeftRadius这种具体指定角度的属性所覆盖。

  • <gradient>
    <solid>标签相互排斥的,其中solid表示纯色填充,而gradient表示渐变效果;gradient有如下几个属性:

    1. android:angle——渐变的角度,默认为0,其值必须是45的倍数,0表示从左往右,90表示从下到上。

    2. android:centerX 渐变的中心点的横坐标

    3. android:centerY 渐变的中心点的纵坐标;

    4. android:startColor 渐变的起始色

    5. android:centerColor 渐变的中间色

    6. android:endColor 渐变的结束色

    7. android:gradientRadius 渐变半径,仅当android:type=”radial”时有效。

    8. android:type 渐变的类型,有linear(线性渐变)、radial(镜像渐变)、swepp(扫描线渐变)三种,默认是线性渐变。

  • <solid>
    表示纯色填充,通过android:color即可指定shape中填充的颜色。

  • <stroke>
    Shape的描边,有如下属性:

    1. android:width 描边的宽度

    2. android:color 描边的颜色

    3. android:dashWidth 组成虚线的线段的宽度

    4. android:dashGap 组成虚线之间的间距。dashWidth和dashGap有任何一个为0,虚线效果都不能生效。

  • <padding>
    表示空白,但不是shape的空白,而是包含它的View的空白。

  • <size>
    shape的大小,有两个属性:android:width和android:height,分别表示shape的宽高。通过标签指定宽高后,ShapeDrawable就有固定宽/高了。但是作为view的背景来说,shape还是会被拉伸或者缩小为view的大小。

更多参考:Android样式的开发:shape篇

6.2.3 LayerDrawable

它表示一种层次化的Drawable集合,通过将不同的Drawable放置在不同层后达到一种叠加效果。

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
    android:id="@+id/res_haimei1"
    android:bottom="10dp"
    android:drawable="@mipmap/haimei1"
    android:left="10dp"
    android:right="10dp"
    android:top="10dp" />

<item
    android:id="@+id/res_icon"
    android:width="30dp"
    android:height="30dp"
    android:drawable="@mipmap/ic_launcher"
    android:gravity="center" />


6.2.4 StateListDrawable

对应<selector>标签。它表示Drawable集合,每个Drawable对应View的一种状态,这样系统就会根据View的状态来选择合适的Drawable。主要用于设置可点击View的背景。

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" 
android:constantSize="false" 
android:dither="true" 
android:variablePadding="false">
<item android:drawable="@mipmap/ic_launcher" android:state_pressed="true" />
<item android:drawable="@mipmap/haimei1" android:state_pressed="false" />


属性分析

  • 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_focusedView获取了焦点
android:state_selected用户选择了View,如RadioButton
android:state_checked用户选中了View,适用于CheckBox
android:state_enableView处于可用状态

默认的item一般放在最后并且不添加任何状态,这样当系统在之前的item无法选择的时候,就会匹配默认的item,因为item的默认状态不附带任何状态,所以它可以适配任何状态。

6.2.5 LevelListDrawable

对应<level-list>标签。同样表示Drawable集合,集合中的每个Drawable都会有一个等级的概念,根据等级不同来切换对于的Drawable。当它作为View的背景时,可以通过Drawable的setLevel方法来设置不同的等级从而切换具体的Drawable。level的值从0-10000,默认为0。

<?xml version="1.0" encoding="utf-8"?>
 <level-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:maxLevel="0" android:drawable="@drawable/ic_playmethod_normal" />
    <item android:maxLevel="1" android:drawable="@drawable/ic_playmethod_repeat_list" />
    <item android:maxLevel="2" android:drawable="@drawable/ic_playmethod_repeat_one" />
    <item android:maxLevel="3" android:drawable="@drawable/ic_playmethod_random" />
</level-list>

6.2.6 TransitionDrawable

对应<transition>标签。用来实现两个Drawable之间淡入淡出的效果。

<?xml version="1.0" encoding="utf-8"?>
<transition xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@mipmap/haimei2" />
    <item android:drawable="@mipmap/haimei3" />
</transition>

TransitionDrawable drawable = (TransitionDrawable) imageView.getBackground();
drawable.startTransition(1000);

startTransition和reverseTransition方法实现淡入淡出的效果以及它的逆过程。

6.2.7 InsetDrawable

对应于<inset>标签。它可以将其他Drawable内嵌到自己当中,并可以在四周留下一定的间距。当一个View希望自己的背景比自己的实际区域小的时候,可以采用InsetDrawable来实现。通过LayerDrawable也可以实现。

<?xml version="1.0" encoding="utf-8"?>
<inset xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@mipmap/haimei1"
    android:insetBottom="10dp"
    android:insetLeft="10dp"
    android:insetRight="10dp"
    android:insetTop="10dp">
    <shape android:shape="rectangle">
        <solid android:color="#abcdef" />
    </shape>
</inset>

其中,inset中的shape距离view边界为10dp。

6.2.8 ScaleDrawable

ScaleDrawable对应于xml文件中的<scale>标签,可以根据自己的level将指定的drawable缩放到一定比例。

<?xml version="1.0" encoding="utf-8"?>
<scale xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@color/blue"
    android:level="1"
    android:scaleGravity="center"
    android:scaleHeight="20%"
    android:scaleWidth="20%" />

其中,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对应于<clip>标签,他可以根据自己当前的等级(level)来裁剪一个Drawable,裁剪方向可以通过Android:clipOrientation和android:gravity两个属性共同控制。

<?xml version="1.0" encoding="utf-8"?> 
<clip xmlns:android="http://schemas.android.com/apk/res/android" 
android:clipOrientation="vertical\horizontal" 
android:drawable="@drawable/bitmapdrawable" 
android:gravity="bottom|top|left|right|center|fill|center_vertical|center_horizontal|fill_vertical|fill_horizontal|clip_vertical|clip_horizontal" />

clipOrientation表示裁剪方向。gravity需要和clipOrientation一起才能发挥作用。如下所示:

选项含义
top将内部的Drawable放在容器的顶部,不改变大小,如果为竖直裁剪,就从底部开始裁剪
bottom将内部的Drawable放在容器的底部,不改变大小,如果为竖直裁剪,就从顶部开始裁剪
left默认值。内部Drawable放在容器左边,不改变大小,如果为水平裁剪,就从右边开始裁剪。
right内部Drawable放在容器右边,不改变大小,如果为水平裁剪,就从左边开始裁剪
center_verticalDrawable在容器中竖直居中,不改变大小,竖直裁剪的时候上下同时开始裁剪
fill_verticalDrawable在竖直方向填充容器,如果为竖直裁剪,仅当ClipDrawable的等级为0(level=0,完全不可见)时,才会有裁剪行为
center_horizontalDrawable水平居中,不改变大小,水平裁剪的时候从左右两边开始裁剪
fill_horizontalDrawable在水平方向填充,如果为水平裁剪,仅当ClipDrawable等级=0的时候,才能有裁剪行为。
center Drawable在水平和竖直方向居中,不改变大小,水平裁剪的时候从左右开始裁剪,竖直裁剪的时候从上下开始裁剪。
fill Drawable在竖直和水平方向填充容器,仅当level=0的时候才有裁剪行为
clip_vertical附加选项,竖直方向的裁剪,少使用
clip_horizontal附加选项,水平方向的裁剪,少使用

使用步骤:

  1. 定义ClipDrawable

  2. 布局文件引用

  3. 代码控制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的尺寸相同。

7 Android动画深入分析

Android动画分为三种:

  1. View动画

  2. 帧动画

  3. 属性动画

本章学习内容:

  • 介绍View动画和自定义View动画

  • View动画一些特殊的使用场景

  • 对属性动画全面性的介绍

  • 使用动画的一些注意事项

7.1 View动画

View动画的作用对象是View,支持四种动画效果:

  1. 平移

  2. 缩放

  3. 旋转

  4. 透明

7.1.1 View动画的种类

上述四种变换效果对应着Animation四个子类: TranslateAnimation 、 ScaleAnimation 、 RotateAnimation 和 AlphaAnimation 。这四种动画皆可以通过XML定义,也可以通过代码来动态创建。

名称标签子类效果
平移动画<translate>TranslateAnimation移动View
缩放动画<scale>ScaleAnimation放大或缩小View
旋转动画<rotate>RotateAnimation旋转View
透明度动画<alpha>AlphaAnimation改变View的透明度

xml定义动画

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="300"
  //动画插值器,影响动画的播放速度
android:interpolator="@android:anim/accelerate_interpolator"
  //表示集合中的动画是否和集合共享一个插值器
android:shareInterpolator="true" >
//透明度动画,对应 AlphaAnimation 类,可以改变 View 的透明度
  <alpha
        android:duration="3000"
        android:fromAlpha="0.0"
        android:toAlpha="1.0" />
          //旋转动画,对应着 RotateAnimation ,它可以使 View 具有旋转的动画效果
    <rotate
        android:duration="2000"
        android:fromDegrees="0"
        android:interpolator="@android:anim/accelerate_decelerate_interpolator"
        android:pivotX="50%"
        android:pivotY="50%"
        android:startOffset="3000"
        android:toDegrees="180" />
       <!--通过设置第一个alpha动画播放3s后启动rotate动画实现组合动画,如果不设置startOffset则同时播放
    pivotX:表示旋转时候的相对轴的坐标点,即围绕哪一点进行旋转,默认情况下轴点是 View 中心
    -->
      //平移动画,对应 TranslateAnimation 类,可以使 View 完成垂直或者水平方向的移动效果。
    <translate
        android:fromXDelta="500"
        android:toXDelta="0" />
          //缩放动画,对应 ScaleAnimation 类,可以使 View 具有放大和缩小的动画效果。
    <scale
        android:duration="1000"
        android:fromXScale="0.0"
        android:fromYScale="0.0"
        android:interpolator="@android:anim/accelerate_decelerate_interpolator"
        android:pivotX="50"
        android:pivotY="50"
        android:toXScale="2"
        android:toYScale="2" />
    </set>
  1. 标签表示动画集合,对应AnimationSet类,可以包含一个或若干个动画,内部还可以嵌套其他动画集合。

    • android:interpolator 表示动画集合所采用的插值器,插值器影响动画速度,比如非匀速动画就需要通过插值器来控制动画的播放过程。

    • android:shareInterpolator 表示集合中的动画是否和集合共享同一个插值器,如果集合不指定插值器,那么子动画就需要单独指定所需的插值器或默认值。

  2. <translate><scale>、 <rotate> 、 <alpha> 这几个子标签分别代表四种变换效果。

  3. android:fillAfter属性表示动画结束以后, View 是否停留在结束动画的位置,如果为 false , View 会回到动画开始的位置。这个参数在动画 XML 文件的 </set> 节点中设置或在程序 Java 代码中进行设置:setFillAfter(true)

  4. 定义完View动画的xml后,通过以下代码应用动画:

     Aniamation anim = AnimationUtils.loadAnimation(context,R.anim.animation_test);
     view.startAnimation(anim);

代码动态创建动画

AlphaAnimation alphaAnimation = new AlphaAnimation(0,1);
alphaAnimation.setDuration(1500);
view.startAnimation(alphaAnimation);

通过 setAnimationListener 给 View 动画添加过程监听

public static interface AnimationListener {
    void onAnimationStart(Animation animation);
    void onAnimationEnd(Animation animation);
    void onAnimationRepeat(Animation animation);
}

7.1.2 自定义View动画

除了系统提供的四种动画外,我们可以根据需求自定义动画,自定义一个新的动画只需要继承 Animation 这个抽象类,然后重写它的 inatialize 和 applyTransformation 这两个方法,在 initialize 方法中做一些初始化工作,在 Transformation 方法中进行矩阵变换即可,很多时候才有 Camera 来简化矩阵的变换过程,其实自定义动画的主要过程就是矩阵变换的过程,矩阵变换是数学上的概念,需要掌握该方面知识方能轻松实现自定义动画,例子可以参考 Android 的 APIDemos 中的一个自定义动画 Rotate3dAnimation ,这是一个可以围绕 Y 轴旋转并同时沿着 Z 轴平移从而实现类似一种 3D 效果的动画。

7.1.3 帧动画

帧动画是顺序播放一组预先定义好的图片,使用简单,但容易引起OOM,所以在使用帧动画时应尽量避免使用过多尺寸较大的图片。系统提供了另一个类 AnimationDrawble 来使用帧动画,使用的时候,需要通过 XML 定义一个 AnimationDrawble ,如下:

//\res\drawable\frame_animation_list.xml
<?xml version="1.0" encoding="utf-8"?>
<!--
    根标签为 animation-list,其中 oneshot 代表着是否只展示一遍,设置为 false 会不停的循环播放动画
    根标签下,通过 item 标签对动画中的每一个图片进行声明
    android:duration 表示展示所用的该图片的时间长度
 -->
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:oneshot="false">
    <item
        android:drawable="@drawable/one"
        android:duration="2000"/>
    <item
        android:drawable="@drawable/two"
        android:duration="2000"/>
    <item
        android:drawable="@drawable/three"
        android:duration="2000"/>
</animation-list

7.2 View动画的特殊使用场景

View 动画除了可以实现的四种基本的动画效果外,还可以在一些特殊的场景下使用,比如在 ViewGroup 中可以控制子元素的出场效果,在 Activity 中可以实现不同 Activity 之间的切换效果。

7.2.1 LayoutAnimation

作用于ViewGroup,为ViewGroup指定一个动画,当它的子元素出场时都会具有这种动画效
果,一般用在ListView上。

//res/anim/layout_animation.xml
<?xml version="1.0" encoding="utf-8"?>
<layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"
    android:delay="0.5"
    android:animationOrder="normal"
    android:animation="@anim/zoom_in">
</layoutAnimation>
  • android:delay
    表示子元素开始动画的延时时间,取值为子元素入场动画时间 duration 的倍数,比如子元素入场动画时间周期为 300ms ,那么 0.5 表示每个子元素都需要延迟 150ms 才能播放入场动画,即第一个子元素延迟 150ms 开始播放入场动画,第二个子元素延迟 300ms 开始播放入场动画,依次类推进行。

  • android:animationOrder
    表示子元素动画的开场顺序,normal(正序)、reverse(倒序)、random(随机)。

  • 为 ViewGroup 指定属性
    android:layoutAnimation="@anim/layout_animation"

通过 LayoutAnimationController 来实现

//用于控制子 view 动画效果
LayoutAnimationController layoutAnimationController= new LayoutAnimationController(AnimationUtils.loadAnimation(this,R.anim.zoom_in));
layoutAnimationController.setOrder(LayoutAnimationController.ORDER_NORMAL);
listView.setLayoutAnimation(layoutAnimationController);
listView.startLayoutAnimation();

7.2.2 Activity的切换效果

我们可以自定义Activity的切换效果,主要通过overridePendingTransition(int enterAnim , int exitAnim) 方法。该方法必须要在 startActivity(intent) 和 finish() 方法之后调用才会有效。

//启动新的Activity带动画
Intent intent=new Intent(MainActivity.this,Main2Activity.class);
startActivity(intent);
overridePendingTransition(R.anim.zoom_in,R.anim.zoom_out);
//退出Activity本身带动画
@Override
public void finish() {
    super.finish();
    overridePendingTransition(R.anim.zoom_in,R.anim.zoom_out);
}

Fragment 也可以添加切换动画,通过 FragmentTransation 中的 setCustomAnimations() 方法来实现切换动画,这个动画需要的是 View 动画,不能使用属性动画,因为属性动画也是 API11 才引入的,不兼容。

7.3 属性动画

属性动画是 API 11 引入的新特性,属性动画可以对任何对象做动画,甚至还可以没有对象。可以在一个时间间隔内完成对象从一个属性值到另一个属性值的改变。与View动画相比,属性动画几乎无所不能,只要对象有这个属性,它都能实现动画效果。API11以下可以通过 nineoldandroids 库来兼容以前版本。

属性动画有ValueAnimator、ObjectAnimator和AnimatorSet等概念。其中ObjectAnimator继承自ValueAnimator,AnimatorSet是动画集合。

举例:

  1. 改变一个对象 TranslationY 属性,让其沿着 Y 轴平移一段距离

    private void translateViewByObjectAnimator(View targetView){
    //TranslationY 目标 View 要改变的属性
    //ivShow.getHeight() 要移动的距离
        ObjectAnimator objectAnimator=ObjectAnimator.ofFloat(targetView,"TranslationY",ivShow.getHeight());
        objectAnimator.start();
    }
  2. 改变一个对象的背景色属性,3秒内从0xFFFF8080到0xFF8080FF渐变,无限循环且有反转效果

     private void changeViewBackGroundColor(View targetView){
         ValueAnimator valueAnimator=ObjectAnimator.ofInt(targetView,"backgroundColor", Color.RED,Color.BLUE);
         valueAnimator.setDuration(3000);
         //设置估值器,该处插入颜色估值器
         valueAnimator.setEvaluator(new ArgbEvaluator());
         //无限循环
         valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
         //反转模式
         valueAnimator.setRepeatMode(ValueAnimator.REVERSE);
         valueAnimator.start();
     }
  3. 动画集合,5 秒内对 View 旋转、平移、缩放和透明度进行了改变

     private void startAnimationSet(View targetView){
         AnimatorSet animatorSet=new AnimatorSet();
         animatorSet.playTogether(ObjectAnimator.ofFloat(targetView,"rotationX",0,360),
                  //旋转
                 ObjectAnimator.ofFloat(targetView,"rotationY",0,360),
                 ObjectAnimator.ofFloat(targetView,"rotation",0,-90),
                  //平移
                 ObjectAnimator.ofFloat(targetView,"translationX",0,90),
                 ObjectAnimator.ofFloat(targetView,"translationY",0,90),
                  //缩放
                 ObjectAnimator.ofFloat(targetView,"scaleX",1,1.5f),
                 ObjectAnimator.ofFloat(targetView,"scaleY",1,1.5f),
                  //透明度
                 ObjectAnimator.ofFloat(targetView,"alpha",1,0.25f,1));
                 animatorSet.setDuration(3000).start();
     }

    也可以通过在xml中定义在 res/animator/ 目录下。具体如下:

     \res\animator\value_animator.xml
     <?xml version="1.0" encoding="utf-8"?><!--set 标签对应着 AnimatorSet-->
     <set xmlns:android="http://schemas.android.com/apk/res/android"
         android:ordering="together">
         <!--对应着 ObjectAnimator-->
         <objectAnimator
             android:propertyName="x"
             android:repeatCount="infinite"
             android:repeatMode="reverse"
             android:startOffset="10"
             android:valueTo="300"
             android:valueType="floatType" />
         <!--其中propertyName 属性设置为translationX ,valueType 设置为floatType 可以正常启动
            如果 valueType 设置为 intType 将报错,即属性类型必须为 floatType 类型,并且android:propertyName="translationX" 表示移动 300 ,而  android:propertyName="x"表示移动到300 ,是两个不同属性-->
         <!--startOffset 指定延迟多久开始播放动画-->
         <!--valueType 表示指定的 android:propertyName 所指定属性的类型,intType 表示指定属性是整型的,
          如果指定属性为颜色,那么不需要指定 valueType 属性,系统会自动处理
          repeatCount 表示动画循环次数,默认值为0,-1 表示无限循环-->
    
         <!--对应着 ValueAnimator-->
        <animator
         android:duration="300"
         android:valueFrom="0"
         android:valueTo="360"
         android:startOffset="10"
         android:repeatCount="infinite"
         android:repeatMode="reverse"
         android:valueType="intType"/>
     </set>

    使用动画

     AnimatorSet set = (AnimatorSet) AnimatorInflater.loadAnimator(context , R.animator.ani
     m);
     set.setTarget(view);
     set.start();

    实际开发中建议使用代码实现属性动画。很多时候一个属性的起始值是无法提前确定的。

7.3.2 理解差值器和估值器

  1. 时间插值器( TimeInterpolator) 的作用是根据时间流逝的百分比来计算出当前属性值改变的百分比,系统预置的有LinearInterpolator( 线性插值器:匀速动画) ,AccelerateDecelerateInterpolator( 加速减速插值器:动画两头慢中间快) ,DecelerateInterpolator(减速插值器:动画越来越慢) 。

  2. 估值器(类型估值算法, TypeEvaluator) 的作用是根据当前属性改变的百分比来计算改变后的属性值。系统预置有IntEvaluator(针对整型属性) 、FloatEvaluator(浮点型属性) 、ArgbEvaluator(针对 Color 属性)。

属性动画要求对象的该属性有 set 和 get(可选) 方法,插值器和估值算法除了系统提供的外,我们还可以自己定义,插值器或者估值算法都是一个接口,且内部只有一个方法,我们只需要派生一个类实现该接口即可,然后就可以做出千变万化的动画效果了。具体而言是:自定义插值器需要实现 Interpolator 或者 TimeInterpolator ,自定义估值算法需要实现 TypeEvaluator 。如果要对其他类型(非int,float,color)做动画,必须要自定义类型估值算法。

7.3.3 属性动画的监听器

属性动画提供了监听器用于监听动画的播放过程,主要有如下两个接口:AnimatorUpdateListener 和 AnimatorListener 。

public static interface AnimatorListener {
void onAnimationStart(Animator animation); //动画开始
void onAnimationEnd(Animator animation); //动画结束
void onAnimationCancel(Animator animation); //动画取消
void onAnimationRepeat(Animator animation); //动画重复播放
}

为了方便开发,系统提供了AnimatorListenerAdapter类,它是AnimatorListener的适配器类,可以有选择的实现以上4个方法。

public static interface AnimatorUpdateListener {
void onAnimationUpdate(ValueAnimator animation);
}

AnimatorUpdateListener会监听整个动画的过程,动画由许多帧组成的,每播放一帧,onAnimationUpdate就会调用一次。利用这个特性,我们可以做一些特殊的事情。

7.3.4 对任意属性做动画

属性动画原理:属性动画要求动画作用的对象提供 get 方法和 set 方法,属性动画根据外界传递该属性的初始值和最终值以动画的效果去多次调用 set 方法,每次传递给 set 方法的值都不一样,确切的来说是随着时间的推移,所传递的值越来越接近最终值。总结一下,我们对 object 对象属性 abc 做动画,如果想要动画生效,要同时满足两个条件:

  1. object 必须要提供 setAbc() 方法,如果动画的时候没有传递初始值,那么还要提供 getAbc() 方法,因为系统要去取 abc 属性的初始值(如果这条不满足,程序直接crash)。

  2. object 的 setAbc() 对属性 abc 所做的改变必须能够通过某种方法反应出来(即最终体现了 UI 的变化),比如会带来 UI 的改变之类(如果这条不满足,动画无效果,但是程序不会crash)。

我们给 Button 的 width 属性做动画无效果但是没有crash的原因就是 Button 内部提供了 setWidth 和 getWidth 方法,但是这个 setWidth 方法并不是改变 UI 大小的,而是用来设置最大宽度和最小宽度的。对于上面属性动画的两个条件来说,这个例子只满足了条件 1 而未满足条件 2。

针对上面问题,官方文档给出了 3 种解决方法:

  1. 请给你的对象加上get和set方法,如果你有权限的话
    对于SDK或者其他第三方类库的类无法加上的

  2. 用一个类来包装原始对象,间接为其提供get和set方法

     /**
      * 将 Button 沿着 X 轴方向放大
      * @param button
      */
     private void performAnimationByWrapper(View button){
         ViewWrapper viewWrapper=new ViewWrapper(button);
         ObjectAnimator.ofInt(viewWrapper,"width",800)
                 .setDuration(5000)
                 .start();
     }
      private class ViewWrapper {
             private View targetView;
             public ViewWrapper(View targetView) {
                 this.targetView = targetView;
             }
             public int getWidth() {
                 //注意调用此函数能得到 View 的宽度的前提是, View 的宽度是精准测量模式,即不可以是 wrap_content
                 //否则得不到正确的测量值
                 return targetView.getLayoutParams().width;
             }
             public void setWidth(int width) {
               //重写设置目标 view 的布局参数,使其改变大小
                 targetView.getLayoutParams().width = width;
               //view 大小改变需要调用重新布局
                 targetView.requestLayout();
             }
         }
  3. 采用ValueAnimator,监听动画过程,自己实现属性的改变
    ValueAnimator 本身不作用于任何对象,也就是说直接使用它没有任何动画效果(所以系统提供了它的子类 ObjectAnimator 供我们直接使用,作用于对象直接执行动画效果,而 ValueAnimator 只是提供改变一个值的过程,并能监听到整个值的改变过程,我们基于这个过程可以自己去实现动画效果,在这个过程中做想要达到的效果,自己去实现)。它可以对一个值做动画,然后我们可以监听其动画过程,在动画过程中修改我们对象的属性,这样我们自己就实现了对对象做了动画。

     //new 一个整型估值器,用于下面比例值计算使用(可以自己去计算,这里直接使用系统的)
     private IntEvaluator intEvaluator = new IntEvaluator();
     private void performAnimatorByValue(final View targetView, final int start, final int end) {
         ValueAnimator valueAnimator = ValueAnimator.ofInt(1, 100);
         valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
             @Override
             public void onAnimationUpdate(ValueAnimator animation) {
                 //获取当前动画进度值
                 int currentValue = (int) animation.getAnimatedValue();
                 //获取当前进度占整个动画比例
                 int fraction = (int) animation.getAnimatedFraction();
                 //直接通过估值器根据当前比例计算当前 View 的宽度,然后设置给 View
                 targetView.getLayoutParams().width = intEvaluator.evaluate(fraction, start, end);
                 targetView.requestLayout();
             }
         });
         valueAnimator.setDuration(5000)
                 .start();
     }

7.3.5 属性动画的工作原理

属性动画需要运行在有Looper的线程中,系统通过反射调用被作用对象get/set方法。

7.4 使用动画的注意事项

  1. OOM问题
    使用帧动画时,当图片数量较多且图片分辨率较大的时候容易出现OOM,需注意,尽量避免使用帧动画。

  2. 内存泄漏
    使用无限循环的属性动画时,在Activity退出时即使停止,否则将导致Activity无法释放从而造成内存泄露。

  3. 兼容性问题
    动画在3.0以下的系统存在兼容性问题,特殊场景可能无法正常工作,需做好适配工作。

  4. View动画的问题
    View动画是对View的影像做动画,并不是真正的改变了View的状态,因此有时候会出现动画完成后View无法隐藏( setVisibility(View.GONE) 失效) ,这时候调用 view.clearAnimation() 清理View动画即可解决。

  5. 不要使用px
    使用px会导致不同设备上有不同的效果。

  6. 动画元素的交互
    View动画是对View的影像做动画,View的真实位置没有变动,动画完成后的新位置是无法触发点击事件的。属性动画是真实改变了View的属性,所以动画完成后的位置可以接受触摸事件。

  7. 硬件加速
    使用动画的过程中,使用硬件加速可以提高动画的流畅度。

8 理解Window和WindowMananger

Window是一个抽象类,具体实现是 PhoneWindow 。不管是 Activity 、 Dialog 、 Toast 它们的视图都是附加在Window上的,因此Window实际上是View的直接管理者。WindowManager 是外界访问Window的入口,通过WindowManager可以创建Window,而Window的具体实现位于 WindowManagerService 中,WindowManager和WindowManagerService的交互是一个IPC过程。

8.1 Window和WindowManager

下面代码演示了通过WindowManager添加Window的过程:

mWindowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
mFloatingButton = new Button(this);
mFloatingButton.setText("click me");
mLayoutParams = new WindowManager.LayoutParams(
LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, 0, 0,
PixelFormat.TRANSPARENT);
mLayoutParams.flags = LayoutParams.FLAG_NOT_TOUCH_MODAL
| LayoutParams.FLAG_NOT_FOCUSABLE
| LayoutParams.FLAG_SHOW_WHEN_LOCKED;
mLayoutParams.type = LayoutParams.TYPE_SYSTEM_ERROR;
mLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
mLayoutParams.x = 100;
mLayoutParams.y = 300;
mFloatingButton.setOnTouchListener(this);
mWindowManager.addView(mFloatingButton, mLayoutParams);

上述代码将一个button添加到屏幕坐标为(100,300)的位置上。WindowManager的flags和type这两个属性比较重要。
Flags代表Window的属性,控制Window的显示特性

  1. FLAG_NOT_FOCUSABLE
    在此模式下,Window不需要获取焦点,也不需要接收各种输入事件,这个标记同时会启用FLAG_NOT_TOUCH_MODAL,最终事件会直接传递给下层具有焦点的Window。

  2. FLAG_NOT_TOUCH_MODAL
    在此模式下,系统将当前Window区域以外的点击事件传递给底层的Window,当前Window区域内的单击事件则自己处理。一般需要开启此标记。

  3. FLAG_SHOW_WHEN_LOCKED
    开启此模式Window将显示在锁屏界面上。

type参数表示Window的类型

  1. 应用Window:Activity

  2. 子Window: 如Dialog

  3. 系统Window :如Toast和系统状态栏
    Window是分层的,每个Window对应一个z-ordered,层级大的会覆盖在层级小的上面,和HTM的z-index概念一样。在三类Window中,应用Window的层级范围是199,子Window的层级范围是10001999,系统Window的层级范围是2000~2999,这些值对应WindowManager.LayoutParams的type参数。一般系统Window选用 TYPE_SYSTEM_OVERLAY 或者 TYPE_SYSTEM_ERROR ( 同时需要权限 <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> ) 。

WindowManager提供的功能很简单,常用的只有三个方法:

  1. 添加View

  2. 更新View

  3. 删除View

这个三个方法定义在 ViewManager 中,而WindowManager继承了ViewManager。

    public interface ViewManager
    {
        public void addView(View view, ViewGroup.LayoutParams params);
        public void updateViewLayout(View view, ViewGroup.LayoutParams params);
        public void removeView(View view);
    }

如何拖动window?
给view设置onTouchListener:mFloatingButton.setOnTouchListener(this)。在onTouch方法中更新view的位置,这个位置根据手指的位置设定。

8.2 Window的内部机制

Window是一个抽象的概念,每一个Window都对应着一个View和一个ViewRootImpl,Window和View通过ViewRootImpl来建立联系。因此Window并不是实际存在的,它是以View的形式存在的。所以WindowManager的三个方法都是针对View的,说明View才是Window存在的实体。在实际使用中无法直接访问Window,必须通过WindowManager来访问Window。

8.2.1 Window的添加过程

Window的添加过程需要通过WindowManager的addView()来实现, 而WindowManager是一个接口, 它的真正实现是WindowManagerImpl类。

WindowManagerImpl并没有直接实现Window的三大操作, 而是全部交给了WindowManagerGlobal来处理. WindowManagerGlobal以工厂的形式向外提供自己的实例. 而WindowManagerImpl这种工作模式就典型的桥接模式, 将所有的操作全部委托给WindowManagerGlobal来实现.

  1. 检查所有参数是否合法, 如果是子Window那么还需要调整一些布局参数.

  2. 创建ViewRootImpl并将View添加到列表中.

  3. 通过ViewRootImpl来更新界面并完成Window的添加过程.
    这个过程是通过ViewRootImpl的setView()来完成的. View的绘制过程是由ViewRootImpl来完成的, 在内部会调用requestLayout()来完成异步刷新请求. 而scheduleTraversals()实际上是View绘制的入口. 接着会通过WindowSession完成Window的添加过程(Window的添加过程是一次IPC调用). 最终会通过WindowManagerService来实现Window的添加.

WindowManagerService内部会为每一个应用保留一个单独的Session.

8.2.2 Window的删除过程

Window 的删除过程和添加过程一样, 都是先通过WindowManagerImpl后, 在进一步通过WindowManagerGlobal的removeView()来实现的.

方法内首先通过findViewLocked来查找待删除的View的索引, 这个过程就是建立数组遍历, 然后调用removeViewLocked来做进一步的删除.

这里通过ViewRootImpl的die()完成来完成删除操作. die()方法只是发送了请求删除的消息后就立刻返回了, 这个时候View并没有完成删除操作, 所以最后会将其添加到mDyingViews中, mDyingViews表示待删除的View的列表.

die方法中只是做了简单的判断, 如果是异步删除那么就发送一个MSG_DIE的消息, ViewRootImpl中的Handler会处理此消息并调用doDie(); 如果是同步删除, 那么就不发送消息直接调用doDie()方法.

在doDie()方法中会调用dispatchDetachedFromWindow()方法, 真正删除View的逻辑在这个方法内部实现. 其中主要做了四件事:

  1. 垃圾回收的相关工作, 比如清除数据和消息,移除回调.

  2. 通过Session的remove方法删除Window: mWindowSession.remove(mWindow), 这同样是一个IPC过程, 最终会调用WMS的removeWindow()方法.

  3. 调用View的dispatchDetachedFromWindow()方法, 内部会调用View的onDetachedFromWindow()以及onDetachedFromWindowInternal(). 而对于onDetachedFromWindow()就是在View从Window中移除时, 这个方法就会被调用, 可以在这个方法内部做一些资源回收的工作. 比如停止动画,停止线程

  4. 调用WindowManagerGlobal#doRemoveView方法刷新数据, 包括mRoots, mParams, mDyingViews, 需要将当前Window所关联的这三类对象从列表中删除.

8.2.3 Window的更新过程

WindowManagerGlobal#updateViewLayout()方法做的比较简单, 它需要更新View的LayoutParams并替换掉老的LayoutParams, 接着在更新ViewRootImpl中的LayoutParams. 这一步主要是通过setLayoutParams()方法实现.

在ViewRootImpl中会通过scheduleTraversals()来对View重新布局, 包括测量,布局,重绘. 除了View本身的重绘以外, ViewRootImpl还会通过WindowSession来更新Window的视图, 这个过程最后由WMS的relayoutWindow()实现同样是一个IPC过程.

8.3 Window的创建过程

由之前的分析可以知道,View是Android中视图的呈现方式,但是View不能单独存在,必须附着在Window这个抽象的概念上面,因此有视图的地方就有Window。这些视图包括:Activity、Dialog、Toast、PopUpWindow、菜单等等。

8.3.1 Activity的Window创建过程

Activity的大体启动流程: 最终会由ActivityThread中的PerformLaunchActivity()来完成整个启动过程, 这个方法内部会通过类加载器创建Activity的实例对象, 并调用其attach()方法为其关联运行过程中所依赖的一系列上下文环境变量。

在attach()方法里, 系统会创建Activity所属的Window对象并为其设置回调接口, Window对象的创建是通过PolicyManager#makeNewWindow()方法实现. 由于Activity实现了Window的CallBack接口, 因此当Window接收到外界的状态改变的时候就会回调Activity方法. 比如说我们熟悉的onAttachedToWindow(), onDetachedFromWindow(), dispatchTouchEvent()等等。

Activity将具体实现交给了Window处理, 而Window的具体实现就是PhoneWindow, 所以只需要看PhoneWindow的相关逻辑。分为以下几步

  1. 如果没有DecorView, 那么就创建它. 由installDecor()—>generateDecor()触发

  2. 将View添加到DecorView的mContentParent中

  3. 回调Activity的onContentChanged()通知activity视图已经发生改变

这个时候DecorView已经被创建并初始化完毕, Activity的布局文件也已经添加成功到DecorView的mContentParent中. 但是这个时候DecorView还没有被WindowManager正式添加到Window中. 虽然早在Activity的attach方法中window就已经被创建了, 但是这个时候由于DecorView并没有被WindowManager识别, 所以这个时候的Window无法提供具体功能, 因为他还无法接收外界的输入信息.

在ActivityThread#handleResumeActivity()方法中, 首先会调用Activity#onResume(), 接着会调用Activity#makeVisible(), 正是在makeVisible方法中, DecorView真正的完成了添加和显示这两个过程。

8.3.2 Dialog的Window创建过程

Dialog的Window的创建过程和Activity类似, 有如下几步

  1. 创建Window
    Dialog的创建后的实际就是PhoneWindow, 这个过程和Activity的Window创建过程一致

  2. 初始化DecorView并将Dialog的视图添加到DecorView中
    这个过程也类似, 都是通过Window去添加指定的布局文件.

  3. 将DecorView添加到Window中并显示
    在Dialog的show方法中, 会通过WindowManager将DecorView添加Window中.

普通的Dialog有一个特殊之处, 那就是必须采用Activity的Content, 如果采用Application的Content, 那么就会报错. 报的错是没有应用token所导致的, 而应用token一般只有Activity才拥有.

还有一种方法. 系统Window比较特殊, 他可以不需要token, 因此只需要指定对话框的Window为系统类型就可以正常弹出对话框.

//JAVA 给Dialog的Window改变为系统级的Window
dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ERROR);
//XML 声明权限
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>

8.3.3 Toast的Window创建过程

Toast和Dialog不同, 它的工作过程就稍显复杂. 首先Toast也是基于Window来实现的. 但是由于Toast具有定时取消的功能, 所以系统采用了Handler. 在Toast的内部有两类IPC过程, 第一类是Toast访问NotificationManagerService()后面简称NMS. 第二类是NotificationManagerService回调Toast里的TN接口.

Toast属于系统Window, 它内部的视图有两种方式指定, 一种是系统默认的样式, 另一种是通过setView方法来指定一个自定义View. 不管如何, 他们都对应Toast的一个View类型的内部成员mNextView. Toast内部提供了cancel和show两个方法. 分别用于显示和隐藏Toast. 他们内部是一个IPC过程.

显示和隐藏Toast都是需要通过NMS来实现的. 由于NMS运行在系统的进程中, 所以只能通过远程调用的方式来显示和隐藏Toast. 而TN这个类是一个Binder类. 在Toast和NMS进行IPC的过程中, 当NMS处理Toast的显示或隐藏请求时会跨进程回调TN的方法. 这个时候由于TN运行在Binder线程池中, 所以需要通过Handler将其切换到当前主线程. 所以由其可知, Toast无法在没有Looper的线程中弹出, 因为Handler需要使用Looper才能完成切换线程的功能.

对于非系统应用来说, 最多能同时存在对Toast封装的ToastRecord上限为50个. 这样做是为了防止DOS(Denial of Service). 如果不这样, 当通过大量循环去连续的弹出Toast, 这将会导致其他应用没有机会弹出Toast, 那么对于其他应用的Toast请求, 系统的行为就是拒绝服务, 这就是拒绝服务攻击的含义.

在ToastRecord被添加到mToastQueue()中后, NMS就会通过showNextToastLocked()方法来显示当前的Toast.

Toast的显示是由ToastRecord的callback来完成的. 这个callback实际上就是Toast中的TN对象的远程Binder. 通过callback来访问TN中的方法是需要跨进程的. 最终被调用的TN中的方法会运行在发起Toast请求的应用的Binder线程池.

Toast的隐藏也会通过ToastRecord的callback完成的.同样是一次IPC过程. 方式和Toast显示类似.

以上基本说明Toast的显示和影响过程实际上是通过Toast中的TN这个类来实现的. 他有两个方法show(), hide(). 分别对应着Toast的显示和隐藏. 由于这两个方法是被NMS以跨进程的方式调用的, 因此他们运行在Binder线程池中. 为了将执行环境切换到Toast请求所在线程中, 在他们内部使用了handler。

TN的handleShow中会将Toast的视图添加到Window中.
TN的handleHide中会将Toast的视图从Window中移除.

以上三节的总结

  1. 在创建视图并显示出来时,首先是通过创建一个Window对象,然后通过WindowManager对象的 addView(View view, ViewGroup.LayoutParams params); 方法将 contentView 添加到Window中,完成添加和显示视图这两个过程。

  2. 在关闭视图时,通过WindowManager来移除DecorView, mWindowManager.removeViewImmediate( view); 。

  3. Toast比较特殊,具有定时取消功能,所以系统采用了Handler,内部有两类IPC过程:

    • Toast访问 NotificationManagerService

    • NotificationManagerService 回调Toast里的 TN 接口

显示和隐藏Toast都通过NotificationManagerService( NMS) 来实现,而NMS运行在系统进程中,所以只能通过IPC来进行显示/隐藏Toast。而TN是一个Binder类,在Toast和NMS进行IPC的过程中,当NMS处理Toast的显示/隐藏请求时会跨进程回调TN中的方法,这时由于TN运行在Binder线程池中,所以需要通过Handler将其切换到当前线程( 即发起Toast请求所在的线程) ,然后通过WindowManager的 addView/removewView 方法真正完成显示和隐藏Toast。

原文链接:http://www.apkbus.com/blog-892197-78027.html


打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP