7 Android动画深入分析
Android动画分为三种:
- View动画
- 帧动画
- 属性动画
本章学习内容:
- 介绍View动画和自定义View动画
- View动画一些特殊的使用场景
- 对属性动画全面性的介绍
- 使用动画的一些注意事项
7.1 View动画
View动画的作用对象是View,支持四种动画效果:
- 平移
- 缩放
- 旋转
- 透明
7.1.1 View动画的种类
上述四种变换效果对应着Animation四个子类: TranslateAnimation 、 ScaleAnimation 、 RotateAnimation 和 AlphaAnimation 。这四种动画皆可以通过XML定义,也可以通过代码来动态创建。
名称 | 标签 | 子类 | 效果 |
---|---|---|---|
平移动画 |
|
TranslateAnimation | 移动View |
缩放动画 |
|
ScaleAnimation | 放大或缩小View |
旋转动画 |
|
RotateAnimation | 旋转View |
透明度动画 |
|
AlphaAnimation | 改变View的透明度 |
xml定义动画
//透明度动画,对应 AlphaAnimation 类,可以改变 View 的透明度
//旋转动画,对应着 RotateAnimation ,它可以使 View 具有旋转的动画效果
//平移动画,对应 TranslateAnimation 类,可以使 View 完成垂直或者水平方向的移动效果。
//缩放动画,对应 ScaleAnimation 类,可以使 View 具有放大和缩小的动画效果。
标签表示动画集合,对应AnimationSet类,可以包含一个或若干个动画,内部还可以嵌套其他动画集合。 - android:interpolator 表示动画集合所采用的插值器,插值器影响动画速度,比如非匀速动画就需要通过插值器来控制动画的播放过程。
- android:shareInterpolator 表示集合中的动画是否和集合共享同一个插值器,如果集合不指定插值器,那么子动画就需要单独指定所需的插值器或默认值。
-
、
、
、
这几个子标签分别代表四种变换效果。 - android:fillAfter属性表示动画结束以后, View 是否停留在结束动画的位置,如果为 false , View 会回到动画开始的位置。这个参数在动画 XML 文件的
节点中设置或在程序 Java 代码中进行设置:
setFillAfter(true)
。 - 定义完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
7.2 View动画的特殊使用场景
View 动画除了可以实现的四种基本的动画效果外,还可以在一些特殊的场景下使用,比如在 ViewGroup 中可以控制子元素的出场效果,在 Activity 中可以实现不同 Activity 之间的切换效果。
7.2.1 LayoutAnimation
作用于ViewGroup,为ViewGroup指定一个动画,当它的子元素出场时都会具有这种动画效
果,一般用在ListView上。
//res/anim/layout_animation.xml
- 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是动画集合。
举例:
- 改变一个对象 TranslationY 属性,让其沿着 Y 轴平移一段距离
private void translateViewByObjectAnimator(View targetView){
//TranslationY 目标 View 要改变的属性
//ivShow.getHeight() 要移动的距离
ObjectAnimator objectAnimator=ObjectAnimator.ofFloat(targetView,"TranslationY",ivShow.getHeight());
objectAnimator.start();
}
- 改变一个对象的背景色属性,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();
}
- 动画集合,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
```
使用动画
AnimatorSet set = (AnimatorSet) AnimatorInflater.loadAnimator(context , R.animator.ani
m);
set.setTarget(view);
set.start();
实际开发中建议使用代码实现属性动画。很多时候一个属性的起始值是无法提前确定的。
7.3.2 理解差值器和估值器
- 时间插值器( TimeInterpolator) 的作用是根据时间流逝的百分比来计算出当前属性值改变的百分比,系统预置的有LinearInterpolator( 线性插值器:匀速动画) ,AccelerateDecelerateInterpolator( 加速减速插值器:动画两头慢中间快) ,DecelerateInterpolator(减速插值器:动画越来越慢) 。
- 估值器(类型估值算法, TypeEvaluator) 的作用是根据当前属性改变的百分比来计算改变后的属性值。系统预置有IntEvaluator(针对整型属性) 、FloatEvaluator(浮点型属性) 、ArgbEvaluator(针对 Color 属性)。
如图所示,表示一个匀速动画,采用了线性插值器和整型估值算法,在 40ms 内,View 的 X 属性实现了从 0 到 40 的变化。
属性动画要求对象的该属性有 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 做动画,如果想要动画生效,要同时满足两个条件:
- object 必须要提供 setAbc() 方法,如果动画的时候没有传递初始值,那么还要提供 getAbc() 方法,因为系统要去取 abc 属性的初始值(如果这条不满足,程序直接crash)。
- object 的 setAbc() 对属性 abc 所做的改变必须能够通过某种方法反应出来(即最终体现了 UI 的变化),比如会带来 UI 的改变之类(如果这条不满足,动画无效果,但是程序不会crash)。
我们给 Button 的 width 属性做动画无效果但是没有crash的原因就是 Button 内部提供了 setWidth 和 getWidth 方法,但是这个 setWidth 方法并不是改变 UI 大小的,而是用来设置最大宽度和最小宽度的。对于上面属性动画的两个条件来说,这个例子只满足了条件 1 而未满足条件 2。
针对上面问题,官方文档给出了 3 种解决方法:
- 请给你的对象加上get和set方法,如果你有权限的话
对于SDK或者其他第三方类库的类无法加上的 - 用一个类来包装原始对象,间接为其提供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();
}
}
- 采用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 使用动画的注意事项
- OOM问题
使用帧动画时,当图片数量较多且图片分辨率较大的时候容易出现OOM,需注意,尽量避免使用帧动画。 - 内存泄漏
使用无限循环的属性动画时,在Activity退出时即使停止,否则将导致Activity无法释放从而造成内存泄露。 - 兼容性问题
动画在3.0以下的系统存在兼容性问题,特殊场景可能无法正常工作,需做好适配工作。 - View动画的问题
View动画是对View的影像做动画,并不是真正的改变了View的状态,因此有时候会出现动画完成后View无法隐藏( setVisibility(View.GONE) 失效) ,这时候调用 view.clearAnimation() 清理View动画即可解决。 - 不要使用px
使用px会导致不同设备上有不同的效果。 - 动画元素的交互
View动画是对View的影像做动画,View的真实位置没有变动,动画完成后的新位置是无法触发点击事件的。属性动画是真实改变了View的属性,所以动画完成后的位置可以接受触摸事件。 - 硬件加速
使用动画的过程中,使用硬件加速可以提高动画的流畅度。
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的显示特性
- FLAG_NOT_FOCUSABLE
在此模式下,Window不需要获取焦点,也不需要接收各种输入事件,这个标记同时会启用FLAG_NOT_TOUCH_MODAL,最终事件会直接传递给下层具有焦点的Window。 - FLAG_NOT_TOUCH_MODAL
在此模式下,系统将当前Window区域以外的点击事件传递给底层的Window,当前Window区域内的单击事件则自己处理。一般需要开启此标记。 - FLAG_SHOW_WHEN_LOCKED
开启此模式Window将显示在锁屏界面上。
type参数表示Window的类型
- 应用Window:Activity
- 子Window: 如Dialog
- 系统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 ( 同时需要权限
) 。
WindowManager提供的功能很简单,常用的只有三个方法:
- 添加View
- 更新View
- 删除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来实现.
- 检查所有参数是否合法, 如果是子Window那么还需要调整一些布局参数.
- 创建ViewRootImpl并将View添加到列表中.
- 通过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的逻辑在这个方法内部实现. 其中主要做了四件事:
- 垃圾回收的相关工作, 比如清除数据和消息,移除回调.
- 通过Session的remove方法删除Window: mWindowSession.remove(mWindow), 这同样是一个IPC过程, 最终会调用WMS的removeWindow()方法.
- 调用View的dispatchDetachedFromWindow()方法, 内部会调用View的onDetachedFromWindow()以及onDetachedFromWindowInternal(). 而对于onDetachedFromWindow()就是在View从Window中移除时, 这个方法就会被调用, 可以在这个方法内部做一些资源回收的工作. 比如停止动画,停止线程
- 调用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的相关逻辑。分为以下几步
- 如果没有DecorView, 那么就创建它. 由installDecor()-->generateDecor()触发
- 将View添加到DecorView的mContentParent中
- 回调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类似, 有如下几步
- 创建Window
Dialog的创建后的实际就是PhoneWindow, 这个过程和Activity的Window创建过程一致 - 初始化DecorView并将Dialog的视图添加到DecorView中
这个过程也类似, 都是通过Window去添加指定的布局文件. - 将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 声明权限
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中移除.
以上三节的总结
- 在创建视图并显示出来时,首先是通过创建一个Window对象,然后通过WindowManager对象的 addView(View view, ViewGroup.LayoutParams params); 方法将 contentView 添加到Window中,完成添加和显示视图这两个过程。
- 在关闭视图时,通过WindowManager来移除DecorView, mWindowManager.removeViewImmediate( view); 。
- 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。