【工匠若水 http://blog.csdn.net/yanbober 转载烦请注明出处。尊重劳动成果】
1 背景
之所以写这一篇博客的原因是由于之前有写过一篇《Android应用setContentView与LayoutInflater载入解析机制源代码分析》。然后有人在文章以下评论和微博私信中问我关于Android应用Activity、Dialog、PopWindow载入显示机制是咋回事,所以我就写一篇文章来分析分析吧(本文以Android5.1.1 (API 22)源代码为基础分析),以便大家在应用层开发时不再迷糊。
PS一句:不仅有人微博私信我这个问题,还有人问博客插图这些是用啥画的,这里告诉大家。就是我,快来猛戳我
还记得之前《Android应用setContentView与LayoutInflater载入解析机制源代码分析》这篇文章的最后分析结果吗?就是例如以下这幅图:
在那篇文章里我们当时重点是Activity的View载入解析xml机制分析,当时说到了Window的东西,但仅仅是皮毛的分析了Activity相关的一些逻辑。(PS:看到这不清楚上面啥意思的建议先移步到《Android应用setContentView与LayoutInflater载入解析机制源代码分析》,完事再回头继续看这篇文章。)当时给大家承诺过我们要从应用控件一点一点往下慢慢深入分析,所以如今開始深入,可是本篇的深入也仅仅是仅限Window相关的东东。之后文章还会继续慢慢深入。
【工匠若水 http://blog.csdn.net/yanbober 转载烦请注明出处。尊重劳动成果】
2 浅析Window与WindowManager相关关系及源代码
通过上面那幅图能够非常直观的看见,Android屏幕显示的就是Window和各种View,Activity在当中的作用主要是管理生命周期、建立窗体等。也就是说Window相关的东西对于Android屏幕来说是至关重要的(尽管前面分析Activity的setContentView等原理时说过一点Window。但那仅仅是皮毛。),所以有必要在分析Android应用Activity、Dialog、PopWindow载入显示机制前再看看Window相关的一些东西。
2-1 Window与WindowManager基础关系
在分析Window与WindowManager之前我们先看一张图:
接下来看一点代码,例如以下:
/** Interface to let you add and remove child views to an Activity. To get an instance
* of this class, call {@link android.content.Context#getSystemService(java.lang.String) Context.getSystemService()}.
*/
public interface ViewManager
{
public void addView(View view, ViewGroup.LayoutParams params);
public void updateViewLayout(View view, ViewGroup.LayoutParams params);
public void removeView(View view);
}
能够看见,ViewManager接口定义了一组规则。也就是add、update、remove的操作View接口。也就是说ViewManager是用来加入和移除activity中View的接口。
继续往下看:
public interface WindowManager extends ViewManager {
......
public Display getDefaultDisplay();
public void removeViewImmediate(View view);
......
public static class LayoutParams extends ViewGroup.LayoutParams
implements Parcelable {
......
}
}
看见没有。WindowManager继承自ViewManager,然后自己还是一个接口,同一时候又定义了一个静态内部类LayoutParams(这个类比較重要。后面会分析。提前透漏下。假设你在APP做过相似360助手屏幕的那个悬浮窗或者做过那种相似IOS的小白圆点,点击展开菜单功能,你或多或少就能猜到这个类的重要性。)。
WindowManager用来在应用与Window之间的接口、窗体顺序、消息等的管理。继续看下ViewManager的还有一个实现子类ViewGroup。例如以下:
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
//protected ViewParent mParent;
//这个成员是View定义的,ViewGroup继承自View,所以也能够拥有。
//这个变量就是前面我们一系列文章分析View向上传递的父节点,相似于一个链表Node的next一样
//终于指向了ViewRoot
......
public void addView(View child, LayoutParams params) {
addView(child, -1, params);
}
......
public void addView(View child, int index, LayoutParams params) {
......
// addViewInner() will call child.requestLayout() when setting the new LayoutParams
// therefore, we call requestLayout() on ourselves before, so that the child's request
// will be blocked at our level
requestLayout();
invalidate(true);
addViewInner(child, index, params, false);
}
......
}
这下理解上面那幅图了吧,所以说View通过ViewGroup的addView方法加入到ViewGroup中。而ViewGroup层层嵌套到最*都会显示在在一个窗体Window中(正如上面背景介绍中《Android应用setContentView与LayoutInflater载入解析机制源代码分析》的示意图一样)。当中每一个View都有一个ViewParent类型的父节点mParent,最顶上的节点也是一个viewGroup,也即前面文章分析的Window的内部类DecorView(从《Android应用setContentView与LayoutInflater载入解析机制源代码分析》的总结部分或者《Android应用层View绘制流程与源代码分析》的5-1小节都能够验证这个结论)对象。同一时候通过上面背景中那幅图能够看出来。对于一个Activity仅仅有一个DecorView(ViewRoot)。也仅仅有一个Window。
2-2 Activity窗体加入流程拓展
前面文章说过。ActivityThread类的performLaunchActivity方法中调运了activity.attach(…)方法进行初始化。例如以下是Activity的attach方法源代码:
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor) {
......
//创建Window类型的mWindow对象。实际为PhoneWindow类实现了抽象Window类
mWindow = PolicyManager.makeNewWindow(this);
......
//通过抽象Window类的setWindowManager方法给Window类的成员变量WindowManager赋值实例化
mWindow.setWindowManager(
(WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
mToken, mComponent.flattenToString(),
(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
......
//把抽象Window类相关的WindowManager对象拿出来关联到Activity的WindowManager类型成员变量mWindowManager
mWindowManager = mWindow.getWindowManager();
......
}
看见没有。Activity类中的attach方法又创建了Window类型的新成员变量mWindow(PhoneWindow实现类)与Activity相关联,接着在Activity类的attach方法最后又通过mWindow.setWindowManager(…)方法创建了与Window相关联的WindowManager对象,最后又通过mWindow.getWindowManager()将Window的WindowManager成员变量赋值给Activity的WindowManager成员变量mWindowManager。
接下来我们看下上面代码中的mWindow.setWindowManager(…)方法源代码(PhoneWindow没有重写抽象Window的setWindowManager方法,所以直接看Window类的该方法源代码),例如以下:
public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
boolean hardwareAccelerated) {
......
if (wm == null) {
wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
}
//实例化Window类的WindowManager类型成员mWindowManager
mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
}
能够看见,Window的setWindowManager方法中通过WindowManagerImpl实例的createLocalWindowManager方法获取了WindowManager实例。例如以下:
public final class WindowManagerImpl implements WindowManager {
......
private WindowManagerImpl(Display display, Window parentWindow) {
mDisplay = display;
mParentWindow = parentWindow;
}
......
public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
return new WindowManagerImpl(mDisplay, parentWindow);
}
......
}
看见没有?这样就把Activity的Window与WindowManager关联起来了。Activity类的Window类型成员变量mWindow及WindowManager类型成员变量mWindowManager就是这么来的。
回过头继续看上面刚刚贴的Activity的attach方法代码,看见mWindow.setWindowManager方法传递的第一个參数没?有人会想(WindowManager)context.getSystemService(Context.WINDOW_SERVICE)这行代码是什么意思。如今告诉你。
《Android应用Context具体解释及源代码解析》一文中第三部分以前说过ActivityThread中创建了Acitivty(运行attach等方法)等东东,在创建这个Activity之前得到了Context的实例。
记不记得当时说Context的实现类就是ContextImpl吗?以下我们看下ContextImpl类的静态方法块,例如以下:
class ContextImpl extends Context {
......
//静态代码块。类载入时运行一次
static {
......
//这里有一堆相似的XXX_SERVICE的注冊
......
registerService(WINDOW_SERVICE, new ServiceFetcher() {
Display mDefaultDisplay;
public Object getService(ContextImpl ctx) {
//搞一个Display实例
Display display = ctx.mDisplay;
if (display == null) {
if (mDefaultDisplay == null) {
DisplayManager dm = (DisplayManager)ctx.getOuterContext().
getSystemService(Context.DISPLAY_SERVICE);
mDefaultDisplay = dm.getDisplay(Display.DEFAULT_DISPLAY);
}
display = mDefaultDisplay;
}
//返回一个WindowManagerImpl实例
return new WindowManagerImpl(display);
}});
......
}
//这就是你在外面调运Context的getSystemService获取到的WindowManagerImpl实例
@Override
public Object getSystemService(String name) {
ServiceFetcher fetcher = SYSTEM_SERVICE_MAP.get(name);
return fetcher == null ? null : fetcher.getService(this);
}
//上面static代码块创建WindowManagerImpl实例用到的方法
private static void registerService(String serviceName, ServiceFetcher fetcher) {
if (!(fetcher instanceof StaticServiceFetcher)) {
fetcher.mContextCacheIndex = sNextPerContextServiceCacheIndex++;
}
SYSTEM_SERVICE_MAP.put(serviceName, fetcher);
}
}
看见没有,我们都知道Java的静态代码块是类载入是运行一次的。也就相当于一个全局的,这样就相当于每一个Application仅仅有一个WindowManagerImpl(display)实例。
还记不记得《Android应用setContentView与LayoutInflater载入解析机制源代码分析》一文2-6小节中说的,setContentView的实质显示是触发了Activity的resume状态。也就是触发了makeVisible方法。那我们再来看下这种方法。例如以下:
void makeVisible() {
if (!mWindowAdded) {
//也就是获取Activity的mWindowManager
//这个mWindowManager是在Activity的attach中通过mWindow.getWindowManager()获得
ViewManager wm = getWindowManager();
//调运的实质就是ViewManager接口的addView方法,传入的是mDecorView
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}
特别注意,看见makeVisible方法的wm变量没。这个变量就是Window类中通过调运WindowManagerImpl的createLocalWindowManager创建的实例,也就是说每一个Activity都会新创建这么一个WindowManager实例来显示Activity的界面的,有点和上面分析的ContextImpl中static块创建的WindowManager不太一样的地方就在于Context的WindowManager对每一个APP来说是一个全局单例的,而Activity的WindowManager是每一个Activity都会新创建一个的(事实上你从上面分析的两个实例化WindowManagerImpl的构造函数參数传递就能够看出来,Activity中Window的WindowManager成员在构造实例化时传入给WindowManagerImpl中mParentWindow成员的是当前Window对象,而ContextImpl的static块中单例实例化WindowManagerImpl时传入给WindowManagerImpl中mParentWindow成员的是null值)。
继续看makeVisible中调运的WindowManagerImpl的addView方法例如以下:
public final class WindowManagerImpl implements WindowManager {
//继承自Object的单例类
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
......
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
//mParentWindow是上面分析的在Activity中获取WindowManagerImpl实例化时传入的当前Window
//view是Activity中最顶层的mDecor
mGlobal.addView(view, params, mDisplay, mParentWindow);
}
......
}
这里当前传入的view是mDecor。LayoutParams呢?能够看见是getWindow().getAttributes()。那我们进去看看Window类的这个属性。例如以下:
// The current window attributes.
private final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams();
原来是WindowManager的静态内部类LayoutParams的默认构造函数:
public LayoutParams() {
super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
type = TYPE_APPLICATION;
format = PixelFormat.OPAQUE;
}
看见没有。Activity窗体的WindowManager.LayoutParams类型是TYPE_APPLICATION的。
继续回到WindowManagerImpl的addView方法。分析能够看见WindowManagerImpl中有一个单例模式的WindowManagerGlobal成员mGlobal。addView终于调运了WindowManagerGlobal的addView,源代码例如以下:
public final class WindowManagerGlobal {
......
private final ArrayList<View> mViews = new ArrayList<View>();
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
private final ArrayList<WindowManager.LayoutParams> mParams =
new ArrayList<WindowManager.LayoutParams>();
private final ArraySet<View> mDyingViews = new ArraySet<View>();
......
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
......
//获取Activity的Window的getWindow().getAttributes()的LayoutParams
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
//假设是Activity中调运的,parentWindow=Window。假设不是Activity的。譬如是Context的静态代码块的实例化则parentWindow为null
if (parentWindow != null) {
//根据当前Activity的Window调节sub Window的LayoutParams
parentWindow.adjustLayoutParamsForSubWindow(wparams);
} else {
......
}
ViewRootImpl root;
......
synchronized (mLock) {
......
//为当前Window创建ViewRoot
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
//把当前Window相关的东西存入各自的List中,在remove中会删掉
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
}
// do this last because it fires off messages to start doing things
try {
//把View和ViewRoot关联起来。非常重要!!!
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
......
}
}
......
}
能够看见,在addView方法中会利用LayoutParams获得Window的属性,然后为每一个Window创建ViewRootImpl。最后通过ViewRootImpl的setView方法通过mSession向WindowManagerService发送加入窗体请求把窗体加入到WindowManager中。并且由WindowManager来管理窗体的view、事件、消息收集处理等(ViewRootImpl的这一加入过程后面会写文章分析,这里先记住这个概念就可以)。
至此我们对上面背景中那幅图,也就是《Android应用setContentView与LayoutInflater载入解析机制源代码分析》这篇文章总结部分的那幅图又进行了更深入的一点分析。其目的也就是为了以下分析Android应用Dialog、PopWindow、Toast载入显示机制做铺垫准备。
2-3 继续顺藤摸瓜WindowManager.LayoutParams类的源代码
上面2-1分析Window与WindowManager基础关系时提到了WindowManager有一个静态内部类LayoutParams。它继承于ViewGroup.LayoutParams。用于向WindowManager描写叙述Window的管理策略。如今我们来看下这个类(PS:在AD上也能够看见,自备*点我看AD的),例如以下:
public static class LayoutParams extends ViewGroup.LayoutParams
implements Parcelable {
//窗体的绝对XY位置,须要考虑gravity属性
public int x;
public int y;
//在横纵方向上为相关的View预留多少扩展像素。假设是0则此view不能被拉伸。其它情况下扩展像素被widget均分
public float horizontalWeight;
public float verticalWeight;
//窗体类型
//有3种主要类型例如以下:
//ApplicationWindows取值在FIRST_APPLICATION_WINDOW与LAST_APPLICATION_WINDOW之间,是经常使用的顶层应用程序窗体,须将token设置成Activity的token;
//SubWindows取值在FIRST_SUB_WINDOW和LAST_SUB_WINDOW之间,与顶层窗体相关联。需将token设置成它所附着宿主窗体的token。
//SystemWindows取值在FIRST_SYSTEM_WINDOW和LAST_SYSTEM_WINDOW之间,不能用于应用程序。使用时须要有特殊权限。它是特定的系统功能才干使用;
public int type;
//WindowType:開始应用程序窗体
public static final int FIRST_APPLICATION_WINDOW = 1;
//WindowType:全部程序窗体的base窗体,其它应用程序窗体都显示在它上面
public static final int TYPE_BASE_APPLICATION = 1;
//WindowType:普通应用程序窗体,token必须设置为Activity的token来指定窗体属于谁
public static final int TYPE_APPLICATION = 2;
//WindowType:应用程序启动时所显示的窗体,应用自己不要使用这样的类型,它被系统用来显示一些信息,直到应用程序能够开启自己的窗体为止
public static final int TYPE_APPLICATION_STARTING = 3;
//WindowType:结束应用程序窗体
public static final int LAST_APPLICATION_WINDOW = 99;
//WindowType:SubWindows子窗体。子窗体的Z序和坐标空间都依赖于他们的宿主窗体
public static final int FIRST_SUB_WINDOW = 1000;
//WindowType: 面板窗体。显示于宿主窗体的上层
public static final int TYPE_APPLICATION_PANEL = FIRST_SUB_WINDOW;
//WindowType:媒体窗体(比如视频)。显示于宿主窗体下层
public static final int TYPE_APPLICATION_MEDIA = FIRST_SUB_WINDOW+1;
//WindowType:应用程序窗体的子面板。显示于全部面板窗体的上层
public static final int TYPE_APPLICATION_SUB_PANEL = FIRST_SUB_WINDOW+2;
//WindowType:对话框,相似于面板窗体,绘制相似于顶层窗体,而不是宿主的子窗体
public static final int TYPE_APPLICATION_ATTACHED_DIALOG = FIRST_SUB_WINDOW+3;
//WindowType:媒体信息。显示在媒体层和程序窗体之间。须要实现半透明效果
public static final int TYPE_APPLICATION_MEDIA_OVERLAY = FIRST_SUB_WINDOW+4;
//WindowType:子窗体结束
public static final int LAST_SUB_WINDOW = 1999;
//WindowType:系统窗体,非应用程序创建
public static final int FIRST_SYSTEM_WINDOW = 2000;
//WindowType:状态栏,仅仅能有一个状态栏。位于屏幕顶端,其它窗体都位于它下方
public static final int TYPE_STATUS_BAR = FIRST_SYSTEM_WINDOW;
//WindowType:搜索栏。仅仅能有一个搜索栏,位于屏幕上方
public static final int TYPE_SEARCH_BAR = FIRST_SYSTEM_WINDOW+1;
//WindowType:电话窗体,它用于电话交互(特别是呼入),置于全部应用程序之上,状态栏之下
public static final int TYPE_PHONE = FIRST_SYSTEM_WINDOW+2;
//WindowType:系统提示,出如今应用程序窗体之上
public static final int TYPE_SYSTEM_ALERT = FIRST_SYSTEM_WINDOW+3;
//WindowType:锁屏窗体
public static final int TYPE_KEYGUARD = FIRST_SYSTEM_WINDOW+4;
//WindowType:信息窗体。用于显示Toast
public static final int TYPE_TOAST = FIRST_SYSTEM_WINDOW+5;
//WindowType:系统顶层窗体。显示在其它一切内容之上,此窗体不能获得输入焦点。否则影响锁屏
public static final int TYPE_SYSTEM_OVERLAY = FIRST_SYSTEM_WINDOW+6;
//WindowType:电话优先,当锁屏时显示,此窗体不能获得输入焦点,否则影响锁屏
public static final int TYPE_PRIORITY_PHONE = FIRST_SYSTEM_WINDOW+7;
//WindowType:系统对话框
public static final int TYPE_SYSTEM_DIALOG = FIRST_SYSTEM_WINDOW+8;
//WindowType:锁屏时显示的对话框
public static final int TYPE_KEYGUARD_DIALOG = FIRST_SYSTEM_WINDOW+9;
//WindowType:系统内部错误提示,显示于全部内容之上
public static final int TYPE_SYSTEM_ERROR = FIRST_SYSTEM_WINDOW+10;
//WindowType:内部输入法窗体,显示于普通UI之上,应用程序可又一次布局以免被此窗体覆盖
public static final int TYPE_INPUT_METHOD = FIRST_SYSTEM_WINDOW+11;
//WindowType:内部输入法对话框。显示于当前输入法窗体之上
public static final int TYPE_INPUT_METHOD_DIALOG= FIRST_SYSTEM_WINDOW+12;
//WindowType:墙纸窗体
public static final int TYPE_WALLPAPER = FIRST_SYSTEM_WINDOW+13;
//WindowType:状态栏的滑动面板
public static final int TYPE_STATUS_BAR_PANEL = FIRST_SYSTEM_WINDOW+14;
//WindowType:安全系统覆盖窗体。这些窗户必须不带输入焦点。否则会干扰键盘
public static final int TYPE_SECURE_SYSTEM_OVERLAY = FIRST_SYSTEM_WINDOW+15;
//WindowType:拖放伪窗体,仅仅有一个阻力层(最多),它被放置在全部其它窗体上面
public static final int TYPE_DRAG = FIRST_SYSTEM_WINDOW+16;
//WindowType:状态栏下拉面板
public static final int TYPE_STATUS_BAR_SUB_PANEL = FIRST_SYSTEM_WINDOW+17;
//WindowType:鼠标指针
public static final int TYPE_POINTER = FIRST_SYSTEM_WINDOW+18;
//WindowType:导航栏(有别于状态栏时)
public static final int TYPE_NAVIGATION_BAR = FIRST_SYSTEM_WINDOW+19;
//WindowType:音量级别的覆盖对话框。显示当用户更改系统音量大小
public static final int TYPE_VOLUME_OVERLAY = FIRST_SYSTEM_WINDOW+20;
//WindowType:起机进度框,在一切之上
public static final int TYPE_BOOT_PROGRESS = FIRST_SYSTEM_WINDOW+21;
//WindowType:假窗,消费导航栏隐藏时触摸事件
public static final int TYPE_HIDDEN_NAV_CONSUMER = FIRST_SYSTEM_WINDOW+22;
//WindowType:梦想(屏保)窗体,略高于键盘
public static final int TYPE_DREAM = FIRST_SYSTEM_WINDOW+23;
//WindowType:导航栏面板(不同于状态栏的导航栏)
public static final int TYPE_NAVIGATION_BAR_PANEL = FIRST_SYSTEM_WINDOW+24;
//WindowType:universe背后真正的窗户
public static final int TYPE_UNIVERSE_BACKGROUND = FIRST_SYSTEM_WINDOW+25;
//WindowType:显示窗体覆盖,用于模拟辅助显示设备
public static final int TYPE_DISPLAY_OVERLAY = FIRST_SYSTEM_WINDOW+26;
//WindowType:放大窗体覆盖。用于突出显示的放大部分可訪问性放大时启用
public static final int TYPE_MAGNIFICATION_OVERLAY = FIRST_SYSTEM_WINDOW+27;
//WindowType:......
public static final int TYPE_KEYGUARD_SCRIM = FIRST_SYSTEM_WINDOW+29;
public static final int TYPE_PRIVATE_PRESENTATION = FIRST_SYSTEM_WINDOW+30;
public static final int TYPE_VOICE_INTERACTION = FIRST_SYSTEM_WINDOW+31;
public static final int TYPE_ACCESSIBILITY_OVERLAY = FIRST_SYSTEM_WINDOW+32;
//WindowType:系统窗体结束
public static final int LAST_SYSTEM_WINDOW = 2999;
//MemoryType:窗体缓冲位于主内存
public static final int MEMORY_TYPE_NORMAL = 0;
//MemoryType:窗体缓冲位于能够被DMA訪问,或者硬件加速的内存区域
public static final int MEMORY_TYPE_HARDWARE = 1;
//MemoryType:窗体缓冲位于可被图形加速器訪问的区域
public static final int MEMORY_TYPE_GPU = 2;
//MemoryType:窗体缓冲不拥有自己的缓冲区,不能被锁定,缓冲区由本地方法提供
public static final int MEMORY_TYPE_PUSH_BUFFERS = 3;
//指出窗体所使用的内存缓冲类型,默觉得NORMAL
public int memoryType;
//Flag:当该window对用户可见的时候。同意锁屏
public static final int FLAG_ALLOW_LOCK_WHILE_SCREEN_ON = 0x00000001;
//Flag:让该window后全部的东西都成暗淡
public static final int FLAG_DIM_BEHIND = 0x00000002;
//Flag:让该window后全部东西都模糊(4.0以上已经放弃这样的毛玻璃效果)
public static final int FLAG_BLUR_BEHIND = 0x00000004;
//Flag:让window不能获得焦点,这样用户快就不能向该window发送按键事
public static final int FLAG_NOT_FOCUSABLE = 0x00000008;
//Flag:让该window不接受触摸屏事件
public static final int FLAG_NOT_TOUCHABLE = 0x00000010;
//Flag:即使在该window在可获得焦点情况下,依然把该window之外的不论什么event发送到该window之后的其它window
public static final int FLAG_NOT_TOUCH_MODAL = 0x00000020;
//Flag:当手机处于睡眠状态时,假设屏幕被按下。那么该window将第一个收到
public static final int FLAG_TOUCHABLE_WHEN_WAKING = 0x00000040;
//Flag:当该window对用户可见时,让设备屏幕处于高亮(bright)状态
public static final int FLAG_KEEP_SCREEN_ON = 0x00000080;
//Flag:让window占满整个手机屏幕。不留不论什么边界
public static final int FLAG_LAYOUT_IN_SCREEN = 0x00000100;
//Flag:window大小不再不受手机屏幕限制大小,即window可能超出屏幕之外
public static final int FLAG_LAYOUT_NO_LIMITS = 0x00000200;
//Flag:window全屏显示
public static final int FLAG_FULLSCREEN = 0x00000400;
//Flag:恢复window非全屏显示
public static final int FLAG_FORCE_NOT_FULLSCREEN = 0x00000800;
//Flag:开启抖动(dithering)
public static final int FLAG_DITHER = 0x00001000;
//Flag:当该window在进行显示的时候,不同意截屏
public static final int FLAG_SECURE = 0x00002000;
//Flag:一个特殊模式的布局參数用于运行扩展表面合成时到屏幕上
public static final int FLAG_SCALED = 0x00004000;
//Flag:用于windows时,经常会使用屏幕用户持有反对他们的脸,它将积极过滤事件流,以防止意外按在这样的情况下,可能不须要为特定的窗体,在检測到这样一个事件流时,应用程序将接收取消运动事件表明,这样应用程序能够处理这相应地採取不论什么行动的事件,直到手指释放
public static final int FLAG_IGNORE_CHEEK_PRESSES = 0x00008000;
//Flag:一个特殊的选项仅仅用于结合FLAG_LAYOUT_IN_SC
public static final int FLAG_LAYOUT_INSET_DECOR = 0x00010000;
//Flag:转化的状态FLAG_NOT_FOCUSABLE对这个窗体当前怎样进行交互的方法
public static final int FLAG_ALT_FOCUSABLE_IM = 0x00020000;
//Flag:假设你设置了该flag,那么在你FLAG_NOT_TOUNCH_MODAL的情况下。即使触摸屏事件发送在该window之外。其事件被发送到了后面的window,那么该window仍然将以MotionEvent.ACTION_OUTSIDE形式收到该触摸屏事件
public static final int FLAG_WATCH_OUTSIDE_TOUCH = 0x00040000;
//Flag:当锁屏的时候,显示该window
public static final int FLAG_SHOW_WHEN_LOCKED = 0x00080000;
//Flag:在该window后显示系统的墙纸
public static final int FLAG_SHOW_WALLPAPER = 0x00100000;
//Flag:当window被显示的时候,系统将把它当做一个用户活动事件,以点亮手机屏幕
public static final int FLAG_TURN_SCREEN_ON = 0x00200000;
//Flag:消失键盘
public static final int FLAG_DISMISS_KEYGUARD = 0x00400000;
//Flag:当该window在能够接受触摸屏情况下。让因在该window之外,而发送到后面的window的触摸屏能够支持split touch
public static final int FLAG_SPLIT_TOUCH = 0x00800000;
//Flag:对该window进行硬件加速,该flag必须在Activity或Dialog的Content View之前进行设置
public static final int FLAG_HARDWARE_ACCELERATED = 0x01000000;
//Flag:让window占满整个手机屏幕。不留不论什么边界
public static final int FLAG_LAYOUT_IN_OVERSCAN = 0x02000000;
//Flag:请求一个半透明的状态栏背景以最小的系统提供保护
public static final int FLAG_TRANSLUCENT_STATUS = 0x04000000;
//Flag:请求一个半透明的导航栏背景以最小的系统提供保护
public static final int FLAG_TRANSLUCENT_NAVIGATION = 0x08000000;
//Flag:......
public static final int FLAG_LOCAL_FOCUS_MODE = 0x10000000;
public static final int FLAG_SLIPPERY = 0x20000000;
public static final int FLAG_LAYOUT_ATTACHED_IN_DECOR = 0x40000000;
public static final int FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS = 0x80000000;
//行为选项标记
public int flags;
//PrivateFlags:......
public static final int PRIVATE_FLAG_FAKE_HARDWARE_ACCELERATED = 0x00000001;
public static final int PRIVATE_FLAG_FORCE_HARDWARE_ACCELERATED = 0x00000002;
public static final int PRIVATE_FLAG_WANTS_OFFSET_NOTIFICATIONS = 0x00000004;
public static final int PRIVATE_FLAG_SHOW_FOR_ALL_USERS = 0x00000010;
public static final int PRIVATE_FLAG_NO_MOVE_ANIMATION = 0x00000040;
public static final int PRIVATE_FLAG_COMPATIBLE_WINDOW = 0x00000080;
public static final int PRIVATE_FLAG_SYSTEM_ERROR = 0x00000100;
public static final int PRIVATE_FLAG_INHERIT_TRANSLUCENT_DECOR = 0x00000200;
public static final int PRIVATE_FLAG_KEYGUARD = 0x00000400;
public static final int PRIVATE_FLAG_DISABLE_WALLPAPER_TOUCH_EVENTS = 0x00000800;
//私有的行为选项标记
public int privateFlags;
public static final int NEEDS_MENU_UNSET = 0;
public static final int NEEDS_MENU_SET_TRUE = 1;
public static final int NEEDS_MENU_SET_FALSE = 2;
public int needsMenuKey = NEEDS_MENU_UNSET;
public static boolean mayUseInputMethod(int flags) {
......
}
//SOFT_INPUT:用于描写叙述软键盘显示规则的bite的mask
public static final int SOFT_INPUT_MASK_STATE = 0x0f;
//SOFT_INPUT:没有软键盘显示的约定规则
public static final int SOFT_INPUT_STATE_UNSPECIFIED = 0;
//SOFT_INPUT:可见性状态softInputMode,请不要改变软输入区域的状态
public static final int SOFT_INPUT_STATE_UNCHANGED = 1;
//SOFT_INPUT:用户导航(navigate)到你的窗体时隐藏软键盘
public static final int SOFT_INPUT_STATE_HIDDEN = 2;
//SOFT_INPUT:总是隐藏软键盘
public static final int SOFT_INPUT_STATE_ALWAYS_HIDDEN = 3;
//SOFT_INPUT:用户导航(navigate)到你的窗体时显示软键盘
public static final int SOFT_INPUT_STATE_VISIBLE = 4;
//SOFT_INPUT:总是显示软键盘
public static final int SOFT_INPUT_STATE_ALWAYS_VISIBLE = 5;
//SOFT_INPUT:显示软键盘时用于表示window调整方式的bite的mask
public static final int SOFT_INPUT_MASK_ADJUST = 0xf0;
//SOFT_INPUT:不指定显示软件盘时,window的调整方式
public static final int SOFT_INPUT_ADJUST_UNSPECIFIED = 0x00;
//SOFT_INPUT:当显示软键盘时,调整window内的控件大小以便显示软键盘
public static final int SOFT_INPUT_ADJUST_RESIZE = 0x10;
//SOFT_INPUT:当显示软键盘时,调整window的空白区域来显示软键盘,即使调整空白区域,软键盘还是有可能遮挡一些有内容区域。这时用户就仅仅有退出软键盘才干看到这些被遮挡区域并进行
public static final int SOFT_INPUT_ADJUST_PAN = 0x20;
//SOFT_INPUT:当显示软键盘时,不调整window的布局
public static final int SOFT_INPUT_ADJUST_NOTHING = 0x30;
//SOFT_INPUT:用户导航(navigate)到了你的window
public static final int SOFT_INPUT_IS_FORWARD_NAVIGATION = 0x100;
//软输入法模式选项
public int softInputMode;
//窗体怎样停靠
public int gravity;
//水平边距,容器与widget之间的距离,占容器宽度的百分率
public float horizontalMargin;
//纵向边距
public float verticalMargin;
//积极的insets画图表面和窗体之间的内容
public final Rect surfaceInsets = new Rect();
//期望的位图格式,默觉得不透明,參考android.graphics.PixelFormat
public int format;
//窗体所使用的动画设置,它必须是一个系统资源而不是应用程序资源,由于窗体管理器不能訪问应用程序
public int windowAnimations;
//整个窗体的半透明值,1.0表示不透明,0.0表示全透明
public float alpha = 1.0f;
//当FLAG_DIM_BEHIND设置后生效,该变量指示后面的窗体变暗的程度。1.0表示全然不透明,0.0表示没有变暗
public float dimAmount = 1.0f;
public static final float BRIGHTNESS_OVERRIDE_NONE = -1.0f;
public static final float BRIGHTNESS_OVERRIDE_OFF = 0.0f;
public static final float BRIGHTNESS_OVERRIDE_FULL = 1.0f;
public float screenBrightness = BRIGHTNESS_OVERRIDE_NONE;
//用来覆盖用户设置的屏幕亮度,表示应用用户设置的屏幕亮度。从0到1调整亮度从暗到最亮发生变化
public float buttonBrightness = BRIGHTNESS_OVERRIDE_NONE;
public static final int ROTATION_ANIMATION_ROTATE = 0;
public static final int ROTATION_ANIMATION_CROSSFADE = 1;
public static final int ROTATION_ANIMATION_JUMPCUT = 2;
//定义出入境动画在这个窗体旋转设备时使用
public int rotationAnimation = ROTATION_ANIMATION_ROTATE;
//窗体的标示符
public IBinder token = null;
//此窗体所在的包名
public String packageName = null;
//屏幕方向
public int screenOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
//首选的刷新率的窗体
public float preferredRefreshRate;
//控制status bar是否显示
public int systemUiVisibility;
//ui能见度所请求的视图层次结构
public int subtreeSystemUiVisibility;
//得到关于系统ui能见度变化的回调
public boolean hasSystemUiListeners;
public static final int INPUT_FEATURE_DISABLE_POINTER_GESTURES = 0x00000001;
public static final int INPUT_FEATURE_NO_INPUT_CHANNEL = 0x00000002;
public static final int INPUT_FEATURE_DISABLE_USER_ACTIVITY = 0x00000004;
public int inputFeatures;
public long userActivityTimeout = -1;
......
public final int copyFrom(LayoutParams o) {
......
}
......
public void scale(float scale) {
......
}
......
}
看见没有。从上面类能够看出,Android窗体类型主要分成了三大类:
- 应用程序窗体。
一般应用程序的窗体,比方我们应用程序的Activity的窗体。
- 子窗体。一般在Activity里面的窗体,比方对话框等。
- 系统窗体。系统的窗体,比方输入法,Toast,墙纸等。
同一时候还能够看见,WindowManager.LayoutParams里面窗体的type类型值定义是一个递增保留的连续增大数值,从凝视能够看出来事实上就是窗体的Z-ORDER序列(值越大显示的位置越在上面,你须要将屏幕想成三维坐标模式)。创建不同类型的窗体须要设置不同的type值,譬如上面拓展Activity窗体载入时分析的makeVisible方法中的Window默认属性的type=TYPE_APPLICATION。
既然说这个类非常重要。那总得感性的体验一下重要性吧,所以我们先来看几个实例。
2-4 通过上面WindowManager.LayoutParams分析引出的应用层开发经常使用经典实例
有了上面分析相信你一定觉得WindowManager.LayoutParams还是蛮熟悉的,不信我们来看下。
Part1:开发APP时设置Activity全屏常亮的一种办法(设置Activity也就是Activity的Window):
public class MainActivity extends ActionBarActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//设置Activity的Window为全屏。当然也能够在xml中设置
Window window = getWindow();
WindowManager.LayoutParams windowAttributes = window.getAttributes();
windowAttributes.flags = WindowManager.LayoutParams.FLAG_FULLSCREEN | windowAttributes.flags;
window.setAttributes(windowAttributes);
//设置Activity的Window为保持屏幕亮
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
setContentView(R.layout.activity_main);
}
}
这是运行结果:
Part2:App开发中弹出软键盘时以下的输入框被软件盘挡住问题的解决的方法:
在Activity中的onCreate中setContentView之前写例如以下代码:
//你也能够在xml文件里设置。一样的效果
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE|WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);
Part3:创建悬浮窗体(仿IPhone的小圆点或者魅族的小白点或者360手机卫士的小浮标)。退出当前Activity依然可见的一种实现方法:
省略了Activity的start与stop Service的按钮代码,直接给出了核心代码例如以下:
/**
* Author : yanbo
* Time : 14:47
* Description : 手机屏幕悬浮窗,仿IPhone小圆点
* (未全然实现。仅仅提供思路,如需请自行实现)
* Notice : <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
*/
public class WindowService extends Service {
private WindowManager mWindowManager;
private ImageView mImageView;
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
//创建悬浮窗
createFloatWindow();
}
private void createFloatWindow() {
//这里的參数设置上面刚刚讲过。不再说明
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
mWindowManager = (WindowManager) getApplication().getSystemService(Context.WINDOW_SERVICE);
//设置window的type
layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
//设置效果为背景透明
layoutParams.format = PixelFormat.RGBA_8888;
//设置浮动窗体不可聚焦
layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
layoutParams.gravity = Gravity.BOTTOM | Gravity.RIGHT;
layoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
layoutParams.x = -50;
layoutParams.y = -50;
mImageView = new ImageView(this);
mImageView.setImageResource(android.R.drawable.ic_menu_add);
//加入到Window
mWindowManager.addView(mImageView, layoutParams);
//设置监听
mImageView.setOnTouchListener(touchListener);
}
@Override
public void onDestroy() {
super.onDestroy();
if (mImageView != null) {
//讲WindowManager时说过。add,remove成对出现,所以须要remove
mWindowManager.removeView(mImageView);
}
}
private View.OnTouchListener touchListener = new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
//模拟触摸触发的事件
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
return false;
}
};
}
例如以下是运行过程模拟,特别留意屏幕右下角的变化:
怎么样,通过最后这个样例你是不是就能体会到WindowManager.LayoutParams的Z-ORDER序列类型,值越大显示的位置越在上面。
2-5 总结Activity的窗体加入机制
有了上面这么多分析和前几篇的分析,我们对Activity的窗体载入再次深入分析总结例如以下:
能够看见Context的WindowManager对每一个APP来说是一个全局单例的。而Activity的WindowManager是每一个Activity都会新创建一个的(事实上你从上面分析的两个实例化WindowManagerImpl的构造函数參数传递就能够看出来,Activity中Window的WindowManager成员在构造实例化时传入给WindowManagerImpl中mParentWindow成员的是当前Window对象。而ContextImpl的static块中单例实例化WindowManagerImpl时传入给WindowManagerImpl中mParentWindow成员的是null值)。所以上面模拟苹果浮动小图标使用了Application的WindowManager而不是Activity的,原因就在于这里;使用Activity的WindowManager时当Activity结束时WindowManager就无效了。所以使用Activity的getSysytemService(WINDOW_SERVICE)获取的是Local的WindowManager。同一时候能够看出来Activity中的WindowManager.LayoutParams的type为TYPE_APPLICATION。
好了。上面也说了不少了,有了上面这些知识点以后我们就来開始分析Android应用Activity、Dialog、PopWindow窗体显示机制。
【工匠若水 http://blog.csdn.net/yanbober 转载烦请注明出处,尊重劳动成果】
3 Android应用Dialog窗体加入显示机制源代码
3-1 Dialog窗体源代码分析
写过APP都知道,Dialog是一系列XXXDialog的基类,我们能够new随意Dialog或者通过Activity提供的onCreateDialog(……)、onPrepareDialog(……)和showDialog(……)等方法来管理我们的Dialog,可是究事实上质都是来源于Dialog基类,所以我们对于各种XXXDialog来说仅仅用分析Dialog的窗体载入就能够了。
例如以下从Dialog的构造函数開始分析:
public class Dialog implements DialogInterface, Window.Callback,
KeyEvent.Callback, OnCreateContextMenuListener, Window.OnWindowDismissedCallback {
......
public Dialog(Context context) {
this(context, 0, true);
}
//构造函数终于都调运了这个默认的构造函数
Dialog(Context context, int theme, boolean createContextThemeWrapper) {
//默认构造函数的createContextThemeWrapper为true
if (createContextThemeWrapper) {
//默认构造函数的theme为0
if (theme == 0) {
TypedValue outValue = new TypedValue();
context.getTheme().resolveAttribute(com.android.internal.R.attr.dialogTheme,
outValue, true);
theme = outValue.resourceId;
}
mContext = new ContextThemeWrapper(context, theme);
} else {
mContext = context;
}
//mContext已经从外部传入的context对象获得值(通常是个Activity)。!。非常重要。先记住!!。
//获取WindowManager对象
mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
//为Dialog创建新的Window
Window w = PolicyManager.makeNewWindow(mContext);
mWindow = w;
//Dialog能够接受到按键事件的原因
w.setCallback(this);
w.setOnWindowDismissedCallback(this);
//关联WindowManager与新Window。特别注意第二个參数token为null。也就是说Dialog没有自己的token
//一个Window属于Dialog的话。那么该Window的mAppToken对象是null
w.setWindowManager(mWindowManager, null, null);
w.setGravity(Gravity.CENTER);
mListenersHandler = new ListenersHandler(this);
}
......
}
能够看到。Dialog构造函数首先把外部传入的參数context对象赋值给了当前类的成员(我们的Dialog一般都是在Activity中启动的。所以这个context通常是个Activity),然后调用context.getSystemService(Context.WINDOW_SERVICE)获取WindowManager。这个WindowManager是哪来的呢?先依照上面说的context通常是个Activity来看待,能够发现这句实质就是Activity的getSystemService方法,我们看下源代码,例如以下:
@Override
public Object getSystemService(@ServiceName @NonNull String name) {
if (getBaseContext() == null) {
throw new IllegalStateException(
"System services not available to Activities before onCreate()");
}
//我们Dialog中获得的WindowManager对象就是这个分支
if (WINDOW_SERVICE.equals(name)) {
//Activity的WindowManager
return mWindowManager;
} else if (SEARCH_SERVICE.equals(name)) {
ensureSearchManager();
return mSearchManager;
}
return super.getSystemService(name);
}
看见没有。Dialog中的WindowManager成员实质和Activity里面是一样的,也就是共用了一个WindowManager。
回到Dialog的构造函数继续分析,在得到了WindowManager之后,程序又新建了一个Window对象(类型是PhoneWindow类型,和Activity的Window新建过程相似)。接着通过w.setCallback(this)设置Dialog为当前window的回调接口。这样Dialog就能够接收事件处理了;接着把从Activity拿到的WindowManager对象关联到新创建的Window中。
至此Dialog的创建过程Window处理已经完成,非常easy。所以接下来我们继续看看Dialog的show与cancel方法。例如以下:
public void show() {
......
if (!mCreated) {
//回调Dialog的onCreate方法
dispatchOnCreate(null);
}
//回调Dialog的onStart方法
onStart();
//相似于Activity,获取当前新Window的DecorView对象,所以有一种自己定义Dialog布局的方式就是重写Dialog的onCreate方法,使用setContentView传入布局。就像前面文章分析Activity相似
mDecor = mWindow.getDecorView();
......
//获取新Window的WindowManager.LayoutParams參数。和上面分析的Activity一样type为TYPE_APPLICATION
WindowManager.LayoutParams l = mWindow.getAttributes();
......
try {
//把一个View加入到Activity共用的windowManager里面去
mWindowManager.addView(mDecor, l);
......
} finally {
}
}
能够看见Dialog的新Window与Activity的Window的type相同都为TYPE_APPLICATION,上面介绍WindowManager.LayoutParams时TYPE_APPLICATION的凝视明白说过,普通应用程序窗体TYPE_APPLICATION的token必须设置为Activity的token来指定窗体属于谁。所以能够看见,既然Dialog和Activity共享同一个WindowManager(也就是上面分析的WindowManagerImpl),而WindowManagerImpl里面有个Window类型的mParentWindow变量。这个变量在Activity的attach中创建WindowManagerImpl时传入的为当前Activity的Window,而当前Activity的Window里面的mAppToken值又为当前Activity的token。所以Activity与Dialog共享了同一个mAppToken值,仅仅是Dialog和Activity的Window对象不同。
3-2 Dialog窗体载入总结
通过上面分析Dialog的窗体载入原理,我们总结例如以下图:
从图中能够看出。Activity和Dialog共用了一个Token对象,Dialog必须依赖于Activity而显示(通过别的context搞完之后token都为null,终于会在ViewRootImpl的setView方法中载入时由于token为null抛出异常)。所以Dialog的Context传入參数通常是一个存在的Activity,假设Dialog弹出来之前Activity已经被销毁了,则这个Dialog在弹出的时候就会抛出异常。由于token不可用了。在Dialog的构造函数中我们关联了新Window的callback事件监听处理,所以当Dialog显示时Activity无法消费当前的事件。
到此Dialog的窗体载入机制就分析完成了,接下来我们说说应用开发中常见的一个诡异问题。
3-3 从Dialog窗体载入分析引出的应用开发问题
有了上面的分析我们接下来看下平时开发App刚開始学习的人easy犯的几个错误。
实如今一个Activity中显示一个Dialog,例如以下代码:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
setContentView(R.layout.activity_main);
//重点关注构造函数的參数,创建一个Dialog然后显示出来
Dialog dialog = new ProgressDialog(this);
dialog.setTitle("TestDialogContext");
dialog.show();
}
}
分析:使用了Activity为context,也即和Activity共用token,符合上面的分析。所以不会报错,正常运行。
实如今一个Activity中显示一个Dialog,例如以下代码:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
setContentView(R.layout.activity_main);
//重点关注构造函数的參数,创建一个Dialog然后显示出来
Dialog dialog = new ProgressDialog(getApplicationContext());
dialog.setTitle("TestDialogContext");
dialog.show();
}
}
分析:传入的是Application的Context,导致TYPE_APPLICATION类型Dialog的token为null。所以抛出例如以下异常,无法显示对话框。
Caused by: android.view.WindowManager$BadTokenException: Unable to add window -- token null is not for an application
at android.view.ViewRootImpl.setView(ViewRootImpl.java:566)
at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:272)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:69)
at android.app.Dialog.show(Dialog.java:298)
实如今一个Service中显示一个Dialog。例如以下代码:
public class WindowService extends Service {
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
//重点关注构造函数的參数
Dialog dialog = new ProgressDialog(this);
dialog.setTitle("TestDialogContext");
dialog.show();
}
}
分析:传入的Context是一个Service,相似上面传入ApplicationContext一样的后果,一样的原因。抛出例如以下异常:
Caused by: android.view.WindowManager$BadTokenException: Unable to add window -- token null is not for an application
at android.view.ViewRootImpl.setView(ViewRootImpl.java:566)
at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:272)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:69)
at android.app.Dialog.show(Dialog.java:298)
至此通过我们平时使用最多的Dialog也验证了Dialog成功显示的必要条件,同一时候也让大家避免了再次使用Dialog不当出现异常的情况,或者出现相似异常后知道真实的背后原因是什么的问题。
能够看见。Dialog的实质无非也是使用WindowManager的addView、updateViewLayout、removeView进行一些操作展示。
【工匠若水 http://blog.csdn.net/yanbober 转载烦请注明出处,尊重劳动成果】
4 Android应用PopWindow窗体加入显示机制源代码
PopWindow实质就是弹出式菜单。它与Dialag不同的地方是不会使依赖的Activity组件失去焦点(PopupWindow弹出后能够继续与依赖的Activity进行交互)。Dialog却不能这样。同一时候PopupWindow与Dialog还有一个不同点是PopupWindow是一个堵塞的对话框。假设你直接在Activity的onCreate等方法中显示它则会报错,所以PopupWindow必须在某个事件中显示地或者是开启一个新线程去调用。
说这么多还是直接看代码吧。
4-1 PopWindow窗体源代码分析
根据PopWindow的使用,我们选择最经常使用的方式来分析。例如以下先看当中经常使用的一种构造函数:
public class PopupWindow {
......
//我们仅仅分析最经常使用的一种构造函数
public PopupWindow(View contentView, int width, int height, boolean focusable) {
if (contentView != null) {
//获取mContext。contentView实质是View,View的mContext都是构造函数传入的,View又层级传递,所以终于这个mContext实质是Activity。!!
非常重要
mContext = contentView.getContext();
//获取Activity的getSystemService的WindowManager
mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
}
//进行一些Window类的成员变量初始化赋值操作
setContentView(contentView);
setWidth(width);
setHeight(height);
setFocusable(focusable);
}
......
}
能够看见,构造函数仅仅是初始化了一些变量,看完构造函数继续看下PopWindow的展示函数。例如以下:
public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) {
......
//anchor是Activity中PopWindow准备依附的View,这个View的token实质也是Activity的Window中的token,也即Activity的token
//第一步 初始化WindowManager.LayoutParams
WindowManager.LayoutParams p = createPopupLayout(anchor.getWindowToken());
//第二步
preparePopup(p);
......
//第三步
invokePopup(p);
}
能够看见,当我们想将PopWindow展示在anchor的下方向(Z轴是在anchor的上面)旁边时经理了上面三步。我们一步一步来分析。先看第一步。源代码例如以下:
private WindowManager.LayoutParams createPopupLayout(IBinder token) {
//实例化一个默认的WindowManager.LayoutParams。当中type=TYPE_APPLICATION
WindowManager.LayoutParams p = new WindowManager.LayoutParams();
//设置Gravity
p.gravity = Gravity.START | Gravity.TOP;
//设置宽高
p.width = mLastWidth = mWidth;
p.height = mLastHeight = mHeight;
//根据背景设置format
if (mBackground != null) {
p.format = mBackground.getOpacity();
} else {
p.format = PixelFormat.TRANSLUCENT;
}
//设置flags
p.flags = computeFlags(p.flags);
//改动type=WindowManager.LayoutParams.TYPE_APPLICATION_PANEL,mWindowLayoutType有初始值,type类型为子窗体
p.type = mWindowLayoutType;
//设置token为Activity的token
p.token = token;
......
return p;
}
接着回到showAsDropDown方法看看第二步,例如以下源代码:
private void preparePopup(WindowManager.LayoutParams p) {
......
//有无设置PopWindow的background差别
if (mBackground != null) {
......
//假设有背景则创建一个PopupViewContainer对象的ViewGroup
PopupViewContainer popupViewContainer = new PopupViewContainer(mContext);
PopupViewContainer.LayoutParams listParams = new PopupViewContainer.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, height
);
//把背景设置给PopupViewContainer的ViewGroup
popupViewContainer.setBackground(mBackground);
//把我们构造函数传入的View加入到这个ViewGroup
popupViewContainer.addView(mContentView, listParams);
//返回这个ViewGroup
mPopupView = popupViewContainer;
} else {
//假设没有通过PopWindow的setBackgroundDrawable设置背景则直接赋值当前传入的View为PopWindow的View
mPopupView = mContentView;
}
......
}
能够看见preparePopup方法的作用就是推断设置View。假设有背景则会在传入的contentView外面包一层PopupViewContainer(实质是一个重写了事件处理的FrameLayout)之后作为mPopupView。假设没有背景则直接用contentView作为mPopupView。
我们再来看下这里的PopupViewContainer类。例如以下源代码:
private class PopupViewContainer extends FrameLayout {
......
@Override
protected int[] onCreateDrawableState(int extraSpace) {
......
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
......
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mTouchInterceptor != null && mTouchInterceptor.onTouch(this, ev)) {
return true;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
......
if(xxx) {
dismiss();
}
......
}
@Override
public void sendAccessibilityEvent(int eventType) {
......
}
}
能够看见,这个PopupViewContainer是一个PopWindow的内部私有类,它继承了FrameLayout。在当中重写了Key和Touch事件的分发处理逻辑。同一时候查阅PopupView能够发现,PopupView类自身没有重写Key和Touch事件的处理,所以假设没有将传入的View对象放入封装的ViewGroup中。则点击Back键或者PopWindow以外的区域PopWindow是不会消失的(事实上PopWindow中没有向Activity及Dialog一样new新的Window,所以不会有新的callback设置。也就没法处理事件消费了)。
接着继续回到showAsDropDown方法看看第三步。例如以下源代码:
private void invokePopup(WindowManager.LayoutParams p) {
if (mContext != null) {
p.packageName = mContext.getPackageName();
}
mPopupView.setFitsSystemWindows(mLayoutInsetDecor);
setLayoutDirectionFromAnchor();
mWindowManager.addView(mPopupView, p);
}
能够看见。这里使用了Activity的WindowManager将我们的PopWindow进行了显示。
到此能够发现。PopWindow的实质无非也是使用WindowManager的addView、updateViewLayout、removeView进行一些操作展示。与Dialog不同的地方是没有新new Window而已(也就没法设置callback。无法消费事件,也就是前面说的PopupWindow弹出后能够继续与依赖的Activity进行交互的原因)。
到此PopWindw的窗体载入显示机制就分析完成了,接下来进行总结与应用开发技巧提示。
4-2 PopWindow窗体源代码分析总结及应用开发技巧提示
通过上面分析能够发现总结例如以下图:
能够看见,PopWindow全然使用了Activity的Window与WindowManager,相对来说比較简单easy记理解。
再来看一个开发技巧:
假设设置了PopupWindow的background。则点击Back键或者点击PopupWindow以外的区域时PopupWindow就会dismiss;假设不设置PopupWindow的background。则点击Back键或者点击PopupWindow以外的区域PopupWindow不会消失。
【工匠若水 http://blog.csdn.net/yanbober 转载烦请注明出处,尊重劳动成果】
5 Android应用Toast窗体加入显示机制源代码
5-1 基础知识准备
在開始分析这几个窗体之前须要脑补一点东东,我们从应用层开发来直观脑补,这样以下分析源代码时就不蛋疼了。
例如以下是一个我们写的两个应用实现Service跨进程调用服务ADIL的样例,client调运远程Service的start与stop方法控制远程Service的操作。
Android系统中的应用程序都运行在各自的进程中,进程之间是无法直接交换数据的。可是Android为开发人员提供了AIDL跨进程调用Service的功能。事实上AIDL就相当于两方约定的一个规则而已。
先看下在Android Studio中AIDL开发的project文件夹结构,例如以下:
由于AIDL文件里不能出现訪问修饰符(如public),同一时候AIDL文件在两个项目中要全然一致并且仅仅支持基本类型。所以我们定义的AIDL文件例如以下:
ITestService.aidl
package io.github.yanbober.myapplication;
interface ITestService {
void start(int id);
void stop(int id);
}
再来看下根据aidl文件自己主动生成的ITestService.java文件吧。例如以下:
/*
* This file is auto-generated. DO NOT MODIFY.
*/
package io.github.yanbober.myapplication;
public interface ITestService extends android.os.IInterface
{
//Stub类是ITestService接口的内部静态抽象类,该类继承了Binder类
public static abstract class Stub extends android.os.Binder implements io.github.yanbober.myapplication.ITestService
{
......
//这是抽象静态Stub类中的asInterface方法,该方法负责将service返回至client的对象转换为ITestService.Stub
//把远程Service的Binder对象传递进去,得到的是远程服务的本地代理
public static io.github.yanbober.myapplication.ITestService asInterface(android.os.IBinder obj)
{
......
}
......
//远程服务的本地代理,也会继承自ITestService
private static class Proxy implements io.github.yanbober.myapplication.ITestService
{
......
@Override
public void start(int id) throws android.os.RemoteException
{
......
}
@Override
public void stop(int id) throws android.os.RemoteException
{
......
}
}
......
}
//两个方法是aidl文件里定义的方法
public void start(int id) throws android.os.RemoteException;
public void stop(int id) throws android.os.RemoteException;
}
这就是自己主动生成的java文件,接下来我们看看服务端的Service源代码,例如以下:
//记得在AndroidManifet.xml中注冊Service的<action android:name="io.github.yanbober.myapplication.aidl" />
public class TestService extends Service {
private TestBinder mTestBinder;
//该类继承ITestService.Stub类而不是Binder类,由于ITestService.Stub是Binder的子类
//进程内的Service定义TestBinder内部类是继承Binder类
public class TestBinder extends ITestService.Stub {
@Override
public void start(int id) throws RemoteException {
Log.i(null, "Server Service is start!");
}
@Override
public void stop(int id) throws RemoteException {
Log.i(null, "Server Service is stop!");
}
}
@Override
public IBinder onBind(Intent intent) {
//返回Binder
return mTestBinder;
}
@Override
public void onCreate() {
super.onCreate();
//实例化Binder
mTestBinder = new TestBinder();
}
}
如今服务端App的代码已经OK,我们来看下client的代码。client首先也要像上面的project结构一样,把AIDL文件放好。接着在client使用远程服务端的Service代码例如以下:
public class MainActivity extends Activity {
private static final String REMOT_SERVICE_ACTION = "io.github.yanbober.myapplication.aidl";
private Button mStart, mStop;
private ITestService mBinder;
private ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
//获得还有一个进程中的Service传递过来的IBinder对象
//用IMyService.Stub.asInterface方法转换该对象
mBinder = ITestService.Stub.asInterface(service);
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mStart = (Button) this.findViewById(R.id.start);
mStop = (Button) this.findViewById(R.id.stop);
mStart.setOnClickListener(clickListener);
mStop.setOnClickListener(clickListener);
//绑定远程跨进程Service
bindService(new Intent(REMOT_SERVICE_ACTION), connection, BIND_AUTO_CREATE);
}
@Override
protected void onDestroy() {
super.onDestroy();
//取消绑定远程跨进程Service
unbindService(connection);
}
private View.OnClickListener clickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
////调用远程Service中的start与stop方法
switch (v.getId()) {
case R.id.start:
try {
mBinder.start(0x110);
} catch (RemoteException e) {
e.printStackTrace();
}
break;
case R.id.stop:
try {
mBinder.stop(0x120);
} catch (RemoteException e) {
e.printStackTrace();
}
break;
}
}
};
}
到此你相应用层通过AIDL使用远程Service的形式已经非常熟悉了,至于实质的通信使用Binder的机制我们后面会写文章一步一步往下分析。到此的准备知识已经足够用来理解以下我们的源代码分析了。
5-2 Toast窗体源代码分析
我们经常使用的Toast窗体事实上和前面分析的Activity、Dialog、PopWindow都是不同的。由于它和输入法、墙纸相似。都是系统窗体。
我们还是依照最经常使用的方式来分析源代码吧。
我们先看下Toast的静态makeText方法吧,例如以下:
public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
//new一个Toast对象
Toast result = new Toast(context);
//获取前面有篇文章分析的LayoutInflater
LayoutInflater inflate = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
//载入解析Toast的布局。实质transient_notification.xml是一个LinearLayout中套了一个@android:id/message的TextView而已
View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
//取出布局中的TextView
TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
//把我们的文字设置到TextView上
tv.setText(text);
//设置一些属性
result.mNextView = v;
result.mDuration = duration;
//返回新建的Toast
return result;
}
能够看见。这种方法构造了一个Toast,然后把要显示的文本放到这个View的TextView中,然后初始化相关属性后返回这个新的Toast对象。
当我们有了这个Toast对象之后。能够通过show方法来显示出来。例如以下看下show方法源代码:
public void show() {
......
//通过AIDL(Binder)通信拿到NotificationManagerService的服务訪问接口。当前Toast类相当于上面样例的client!
!。相当重要!!!
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
try {
//把TN对象和一些參数传递到远程NotificationManagerService中去
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
}
我们看看show方法中调运的getService方法,例如以下:
//远程NotificationManagerService的服务訪问接口
private static INotificationManager sService;
static private INotificationManager getService() {
//单例模式
if (sService != null) {
return sService;
}
//通过AIDL(Binder)通信拿到NotificationManagerService的服务訪问接口
sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
return sService;
}
通过上面我们的基础脑补实例你也能看懂这个getService方法了吧。那接着我们来看mTN吧,好像mTN在Toast的构造函数里见过一眼,我们来看看。例如以下:
public Toast(Context context) {
mContext = context;
mTN = new TN();
mTN.mY = context.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.toast_y_offset);
mTN.mGravity = context.getResources().getInteger(
com.android.internal.R.integer.config_toastDefaultGravity);
}
能够看见mTN确实是在构造函数中实例化的,那我们就来看看这个TN类,例如以下:
//相似于上面样例的服务端实例化的Service内部类Binder
private static class TN extends ITransientNotification.Stub {
......
//实现了AIDL的show与hide方法
@Override
public void show() {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.post(mShow);
}
@Override
public void hide() {
if (localLOGV) Log.v(TAG, "HIDE: " + this);
mHandler.post(mHide);
}
......
}
看见没有,TN是Toast内部的一个私有静态类,继承自ITransientNotification.Stub。你这时指定好奇ITransientNotification.Stub是个啥玩意,对吧?事实上你在上面的脑补实例中见过它的,他出如今服务端实现的Service中,就是一个Binder对象。也就是对一个aidl文件的实现而已,我们看下这个ITransientNotification.aidl文件。例如以下:
package android.app;
/** @hide */
oneway interface ITransientNotification {
void show();
void hide();
}
看见没有,和我们上面的样例非常相似吧。
再回到上面分析的show()方法中能够看到。我们的Toast是传给远程的NotificationManagerService管理的,为了NotificationManagerService回到我们的应用程序(回调)。我们须要告诉NotificationManagerService我们当前程序的Binder引用是什么(也就是TN)。是不是觉得和上面样例有些不同,这里感觉Toast又充当client。又充当服务端的样子,实质就是一个回调过程而已。
继续来看Toast中的show方法的service.enqueueToast(pkg, tn, mDuration);语句。service实质是远程的NotificationManagerService。所以enqueueToast方法就是NotificationManagerService类的。例如以下:
private final IBinder mService = new INotificationManager.Stub() {
// Toasts
// ============================================================================
@Override
public void enqueueToast(String pkg, ITransientNotification callback, int duration)
{
......
synchronized (mToastQueue) {
int callingPid = Binder.getCallingPid();
long callingId = Binder.clearCallingIdentity();
try {
ToastRecord record;
//查看该Toast是否已经在队列当中
int index = indexOfToastLocked(pkg, callback);
// If it's already in the queue, we update it in place, we don't
// move it to the end of the queue.
//凝视说了,已经存在则直接取出update
if (index >= 0) {
record = mToastQueue.get(index);
record.update(duration);
} else {
// Limit the number of toasts that any given package except the android
// package can enqueue. Prevents DOS attacks and deals with leaks.
......
//将Toast封装成ToastRecord对象,放入mToastQueue中
record = new ToastRecord(callingPid, pkg, callback, duration);
//把他加入到ToastQueue队列中
mToastQueue.add(record);
index = mToastQueue.size() - 1;
//将当前Toast所在的进程设置为前台进程
keepProcessAliveLocked(callingPid);
}
//假设index为0,说明当前入队的Toast在队头,须要调用showNextToastLocked方法直接显示
if (index == 0) {
showNextToastLocked();
}
} finally {
Binder.restoreCallingIdentity(callingId);
}
}
}
}
继续看下该方法中调运的showNextToastLocked方法,例如以下:
void showNextToastLocked() {
//取出ToastQueue中队列最前面的ToastRecord
ToastRecord record = mToastQueue.get(0);
while (record != null) {
try {
//Toast类中实现的ITransientNotification.Stub的Binder接口TN,调运了那个类的show方法
record.callback.show();
scheduleTimeoutLocked(record);
return;
} catch (RemoteException e) {
......
}
}
}
继续先看下该方法中调运的scheduleTimeoutLocked方法。例如以下:
private void scheduleTimeoutLocked(ToastRecord r)
{
//移除上一条消息
mHandler.removeCallbacksAndMessages(r);
//根据Toast传入的duration參数LENGTH_LONG=1来推断决定多久发送消息
Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);
long delay = r.duration == Toast.LENGTH_LONG ?
LONG_DELAY : SHORT_DELAY;
//根据设置的MESSAGE_TIMEOUT后发送消息
mHandler.sendMessageDelayed(m, delay);
}
能够看见这里先回调了Toast的TN的show,以下timeout可能就是hide了。接着还在该类的mHandler处理了这条消息。然后调运了例如以下处理方法:
private void handleTimeout(ToastRecord record)
{
......
synchronized (mToastQueue) {
int index = indexOfToastLocked(record.pkg, record.callback);
if (index >= 0) {
cancelToastLocked(index);
}
}
}
我们继续看cancelToastLocked方法,例如以下:
void cancelToastLocked(int index) {
ToastRecord record = mToastQueue.get(index);
try {
//回调Toast的TN中实现的hide方法
record.callback.hide();
} catch (RemoteException e) {
......
}
//从队列移除当前显示的Toast
mToastQueue.remove(index);
keepProcessAliveLocked(record.pid);
if (mToastQueue.size() > 0) {
//假设当前的Toast显示完成队列里还有其它的Toast则显示其它的Toast
showNextToastLocked();
}
}
到此能够发现,Toast的远程管理NotificationManagerService类的处理实质是通过Handler发送延时消息显示取消Toast的,并且在远程NotificationManagerService类中又远程回调了Toast的TN类实现的show与hide方法。
如今我们就回到Toast的TN类再看看这个show与hide方法,例如以下:
```java
private static class TN extends ITransientNotification.Stub {
......
//仅仅是实例化了一个Handler。非常重要!!!
!。!
!!
final Handler mHandler = new Handler();
......
final Runnable mShow = new Runnable() {
@Override
public void run() {
handleShow();
}
};
final Runnable mHide = new Runnable() {
@Override
public void run() {
handleHide();
// Don't do this in handleHide() because it is also invoked by handleShow()
mNextView = null;
}
};
......
//实现了AIDL的show与hide方法
@Override
public void show() {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.post(mShow);
}
@Override
public void hide() {
if (localLOGV) Log.v(TAG, "HIDE: " + this);
mHandler.post(mHide);
}
......
}
能够看见。这里实现aidl接口的方法实质是通过handler的post来运行的一个方法,而这个Handler仅仅仅仅是new了一下。也就是说。假设我们写APP时使用Toast在子线程中则须要自行准备Looper对象。仅仅有主线程Activity创建时帮忙准备了Looper(关于Handler与Looper假设整不明白请阅读《Android异步消息处理机制具体解释及源代码分析》)。
那我们重点关注一下handleShow与handleHide方法。例如以下:
public void handleShow() {
if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
+ " mNextView=" + mNextView);
if (mView != mNextView) {
// remove the old view if necessary
//假设有必要就通过WindowManager的remove删掉旧的
handleHide();
mView = mNextView;
Context context = mView.getContext().getApplicationContext();
String packageName = mView.getContext().getOpPackageName();
if (context == null) {
context = mView.getContext();
}
//通过得到的context(通常是ContextImpl的context)获取WindowManager对象(上一篇文章分析的单例的WindowManager)
mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
......
//在把Toast的View加入之前发现Toast的View已经被加入过(有partent)则删掉
if (mView.getParent() != null) {
......
mWM.removeView(mView);
}
......
//把Toast的View加入到窗体,当中mParams.type在构造函数中赋值为TYPE_TOAST!!!。!
!
特别重要
mWM.addView(mView, mParams);
......
}
}
public void handleHide() {
if (mView != null) {
// note: checking parent() just to make sure the view has
// been added... i have seen cases where we get here when
// the view isn't yet added, so let's try not to crash.
//凝视说得非常清楚了,不解释,就是remove
if (mView.getParent() != null) {
mWM.removeView(mView);
}
mView = null;
}
}
到此Toast的窗体加入原理就分析完成了,接下来我们进行总结。
5-3 Toast窗体源代码分析总结及应用开发技巧
经过上面的分析我们总结例如以下:
通过上面分析及上图直观描写叙述能够发现,之所以Toast的显示交由远程的NotificationManagerService管理是由于Toast是每一个应用程序都会弹出的,并且位置和UI风格都差点儿相同。所以假设我们不统一管理就会出现覆盖叠加现象,同一时候导致不好控制。所以Google把Toast设计成为了系统级的窗体类型,由NotificationManagerService统一队列管理。
在我们开发应用程序时使用Toast注意事项:
通过分析TN类的handler能够发现,假设想在非UI线程使用Toast须要自行声明Looper。否则运行会抛出Looper相关的异常;UI线程不须要,由于系统已经帮忙声明。
在使用Toast时context參数尽量使用getApplicationContext()。能够有效的防止静态引用导致的内存泄漏。
有时候我们会发现Toast弹出过多就会延迟显示。由于上面源代码分析能够看见Toast.makeText是一个静态工厂方法,每次调用这种方法都会产生一个新的Toast对象,当我们在这个新new的对象上调用show方法就会使这个对象加入到NotificationManagerService管理的mToastQueue消息显示队列里排队等候显示;所以假设我们不每次都产生一个新的Toast对象(使用单例来处理)就不须要排队。也就能及时更新了。
6 Android应用Activity、Dialog、PopWindow、Toast窗体显示机制总结
能够看见上面不管Acitivty、Dialog、PopWindow、Toast的实质事实上都是例如以下接口提供的方法操作:
public interface ViewManager
{
public void addView(View view, ViewGroup.LayoutParams params);
public void updateViewLayout(View view, ViewGroup.LayoutParams params);
public void removeView(View view);
}
整个应用各种窗体的显示都离不开这三个方法而已。仅仅是token及type与Window是否共用的问题。
【工匠若水 http://blog.csdn.net/yanbober 转载烦请注明出处,尊重劳动成果】