Android窗口机制

来源:互联网 发布:阿凡达妹妹实力知乎 编辑:程序博客网 时间:2024/05/22 11:36

总览

在Android中,窗口的管理系统是基于C/S模式的。其中,客户端负责请求创建窗口、使用窗口,而服务端则完成窗口的维护、显示。


如图所示,在Client端,并不是直接与Wms交互,而是通过本地对象WindowManager,然后由WindowManager完成和WmS的交互。对于Android应用来说,这个交互是透明的,即应用不能感知到WmS的存在。


窗口的类型

FrameWork定义了三种窗口的类型,定义在WindowManager中,

  • 第一种是应用窗口:该窗口对应一个Activity,由于加载Activity是由AmS完成的,所以在应用程序中创建一个应用类窗口,只能在Activity内部完成。
  • 第二种是子窗口:该窗口必须有一个父窗口,父窗口可以是应用窗口或者其他任何类型窗口。
  • 第三种是系统窗口:系统窗口不需要对应任何Activity或是得有一个父窗口。所有的应用程序都没有创建该窗口的权限。但是系统进程可以创建系统窗口。
WindowManager对这三种类型采用了Z-order顺序管理,即每一个类型用一个int常量表示,代表了窗口对应的层(layer)。WmS在进行窗口叠加时,会按照int大小分配不同层,int值越大,代表层的位置越靠上。

1.应用窗口类型

定义意义FIRST_APPLICATION_WINDOW = 1
第一个应用窗口TYPE_BASE_APPLICATION = 1
基窗口,所有其他类型的
应用窗口将出现在基窗口上层

TYPE_APPLICATION = 2
所有Activity对应的窗口TYPE_APPLICATION_STARTING = 3
应用程序启动时先显示此窗口,当真正的窗口配置完成后,此窗口被关闭
LAST_APPLICATION_WINDOW = 99
最后一个应用窗口
所有的Activity默认的窗口类型都是TYPE_APPLICATION,WmS在进行窗口叠加时,会动态改变应用窗口的层值,但层值不会大于99。

2.子窗口类型
定义意义FIRST_SUB_WINDOW(F) = 1000第一个子窗口TYPE_APPLICATION_PANEL = FIRST_SUB_WINDOW
应用窗口的子窗口,PopupWindow的默认类型
TYPE_APPLICATION_MEDIA = FIRST_SUB_WINDOW + 1
用来显示Media的窗口
TYPE_APPLICATION_SUB_PANEL = FIRST_SUB_WINDOW + 2
TYPE_APPLICATION_PANEL的子窗口
TYPE_APPLICATION_ATTACHED_DIALOG = FIRST_SUB_WINDOW + 3
OptionMenu、ContextMenu的默认类型
TYPE_APPLICATION_MEDIA_OVERLAY = FIRST_SUB_WINDOW + 4
TYPE_APPLICATION_MEDIA的重影窗口,显示在TYPE_APPLICATION_MEDIA和应用窗口之间
LAST_SUB_WINDOW = 1999
最后一个子窗口
创建子窗口时,客户端可以指定窗口类型介于1000-1999之间,而WmS在进行窗口叠加时,会动态调整层值。

3.系统窗口


创建系统窗口可以指定层值2000~2999之间,WmS在进行窗口d叠加时,会动态改变窗口的层值。系统窗口所独特的一点是,有的系统窗口只能出现一个,例如输入法窗口,否则用户会觉得很乱。因此,WmS在接受到创建窗口的消息时,会进行一定的检查,确保该窗口只能被创建一次。

Token变量的意义

token翻译为象征,符号,代表。在创建窗口的时候,多处定义了和token有关的变量,该变量的一般类型都是一个IBinder对象。IBinder对象的作用就是为了IPC调用(进程间通讯)。

代码路径类名
变量frameworks/base/core/java/android/app/Activity.java
Activity.JavaIBinder mTokenframeworks/base/core/java/android/view/Window.java
Window.java 
IBinder mAppToken
frameworks/base/core/java/android/view/WindowManager.java
WindowManager.LayoutParams
IBinder token
frameworks/base/core/java/android/view/ViewRootImpl.java
ViewRootImpl  
View.AttachInfo mAttacheInfo
frameworks/base/core/java/android/view/View.java 
View 
View.AttachInfo mAttacheInfo
frameworks/base/core/java/android/view/View.java
View.AttachInfo
IBinder mWindowToken;
IBinder mPanelParentWindowToken;
IWindow mWindow;


1.Activity中的mToken


AmS内部为每一个运行的的Activity都创建了一个HistoryRecord对象,该对象的基本类型是Binder,因此mToken变量的意义正是指向了该HistoryRecord类。该变量的值是在Activity.init()函数中完成的。



2.Window中的mAppToken

每一个Window对象中都有一个mAppToken变量,这里的Window对象不是窗口。窗口的本质是View,而Window类是一个应用窗口的抽象。比如,Window侧重于一个窗口的交互,而窗口(View)侧重于窗口的显示。若Window类不属于某个Activity,mAppToken的变量为空,否则mAppToken的值与Activity中的mToken的值是相同的。


3.WindowManager.LayoutParams中的token


WindowManager.LayoutParams类是指在添加窗口时指定该窗口的布局参数,token的意义正是指定该窗口所对应的Binder对象。因为WmS需要该Binder对象,以便对客户端进行IPC调用。具体说,该token变量的值有三种:
  • 如果创建的窗口为应用窗口,token的值与Window中的mAppToken值相同。
  • 如果创建的窗口为子窗口,token为其父窗口的W对象。
  • 如果创建的窗口为系统窗口,token为空。


4.View中的token

首先看ViewRoot,客户端的每一个窗口都对应一个ViewRoot对象,在其对象内部的mAttachInfo是该对象被构造时同时创建出来的。该变量的类型与View中的mAttachInfo相同。
View类中的mAttachInfo,其含义是当该View对象被真正作为某个窗口W类的内部View时,该变量就会被赋值为mAttachInfo。同一个窗口包含的所有View对象,其内部的mAttachInfo内容都是相同的。

mAttachInfo包含了三个Binder变量:
  • mWindowToken,指窗口对应的W对象。
  • mPanelParentWindowToken,如果该窗口为子窗口,那么该变量为父窗口中的W对象。该变量赋值与mWindowToken是互斥的,因为如果mWindowToken不为空,则意味着没有父窗口。
  • mWindow,既是一个Binder对象,也是一个IWindow对象。

以上所有token的关系如下:







创建应用窗口


1.每一个应用窗口都对应一个Activity,所以要创建一个应用窗口必须先创建一个Activity对象。当AmS决定启动某一个Activity时,会先通知客户端进程,每一个客户端进程对应一个ActivityThread类,所以启动Activity任务由ActivityThread完成。启动某一个Activity代码本质是构造一个Activity对象。代码如下,


<span style="font-family:Microsoft YaHei;font-size:14px;">Activity activity = null;        try {            java.lang.ClassLoader cl = r.packageInfo.getClassLoader();//使用ClassLoader从程序文件中装载指定的Activity对应的Class文件            activity = mInstrumentation.newActivity(                    cl, component.getClassName(), r.intent);            r.intent.setExtrasClassLoader(cl);            if (r.state != null) {                r.state.setClassLoader(cl);            }        } catch (Exception e) {            if (!mInstrumentation.onException(activity, e)) {                throw new RuntimeException(                    "Unable to instantiate activity " + component                    + ": " + e.toString(), e);            }</span>

以上代码运用Java反射机制,把我们配置好的activity对象实例化出来,然后如果成功if(activity != null),就调用activity.attch(xxxx)方法,代码如下

if (activity != null) {...activity.attach(appContext, this, getInstrumentation(), r.token,                        r.ident, app, r.intent, r.activityInfo, title, r.parent,                        r.embeddedID, r.lastNonConfigurationInstance,                        r.lastNonConfigurationChildInstances, config);...}



2.attach()的作用是为刚构造好的Activity设置内部变量,包括一下几项:
  • appContext:该对象将作为Activity的BaseContext。因为Context是abstract的,所以该类中需要一个真正的Context对象,而appContext就是。appContext由new ContextImpl()方法创建。
  • this:指ActivityThread对象,Activity对象内部可能需要主程序的引用。
  • r.token:r是一个ActivityThread对象,其内部变量token的含义是AmS中的一个HistoryRecord对象。
  • r.parent:一个Activity可以有一个父Activity类,这种概念是为了把Activity嵌套到另一个Activity内部执行。在应用程序使用时,常用ActivityGroup类,ActivityGroup功能的内部变量正是它。

3.在attach()方法内部除了为变量赋值外,另一件重要的事情就是为该Activity创建Window对象,通过调用PolicyManager的静态方法makeNewWindow()完成。创建好Window对象后,将其赋值给Activity的内部变量mWindow,并设置该Window的Callback接口为当前的Activity对象。这就是为什么用户消息能够传递到Activity中的原因。

代码如下,

<span style="font-family:Microsoft YaHei;font-size:14px;"> 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,            Object lastNonConfigurationInstance,            HashMap<String,Object> lastNonConfigurationChildInstances,            Configuration config) {        attachBaseContext(context);        mWindow = PolicyManager.makeNewWindow(this);        mWindow.setCallback(this);//设置该Window的Callback接口为当前的Activity对象</span>


4.创建好Window对象后,需要给Window对象中的mWindowManager变量赋值,WindowManager是一个接口类,真正实现该接口的有两个类,一个是Window.LocalWindowManager,另一个是WindowManagerImpl类。LocalWindowManager是一个类似ContextWrapper的包装类,真正实现这些功能的是WindowManagerImpl类。它们之间类的关系如下,


给mWindowManager赋值的代码如下,

<span style="font-family:Microsoft YaHei;font-size:14px;">mWindow.setWindowManager(null, mToken, mComponent.flattenToString());//在Window类该方法实现中,如果第一个参数为null,内部自动创建一个默认的WindowManagerImpl对象        if (mParent != null) {            mWindow.setContainer(mParent.getWindow());        }        mWindowManager = mWindow.getWindowManager();</span>

每一个Activity内部也有一个mWindowManager对象,其值和Window类中的同名变量相同。

5.配置好了的Activity和Window对象,之后需要给该窗口中添加真正的显示元素View或者ViewGroup。在Activity中添加界面是在onCreate()方法中调用setContentView()方法,代码如下

<span style="font-family:Microsoft YaHei;font-size:14px;">public void setContentView(int layoutResID) {        getWindow().setContentView(layoutResID);//又调用到了Window对象的<span style="font-family: 'Microsoft YaHei';">setContentView方法</span>    }</span>

6.因此,分析Window类中如何把一个layout.xml文件作为Window界面。PhoneWindow的setContentView()方法如下,

<span style="font-family:Microsoft YaHei;font-size:14px;">public void setContentView(int layoutResID) {        if (mContentParent == null) {//若为空,调用installDecor为Window类安装一个窗口修饰,就是界面上的标题栏,     //指定的layout.xml被包含在窗口修饰中,称为窗口内容            installDecor();        } else {            mContentParent.removeAllViews();        }        mLayoutInflater.inflate(layoutResID, mContentParent);        final Callback cb = getCallback();        if (cb != null) {            cb.onContentChanged();        }    }</span>



窗口的修饰也是一个ViewGroup,窗口修饰及其内部的窗口内容加起来就是我们所说的窗口,或者称为Window的界面。关系图如下所示,




FrameWork中定义了多种窗口修饰,installDecor()代码如下,

<span style="font-family:Microsoft YaHei;font-size:14px;">private void installDecor() {        if (mDecor == null) {            mDecor = generateDecor();            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);//将窗口修饰做为mDecor的子窗口            mDecor.setIsRootNamespace(true);        }        if (mContentParent == null) {            mContentParent = generateLayout(mDecor);</span>

  • 使用generateDecor()创建一个DecorView对象,赋值给mDecor变量。
  • 根据用户指定的参数选择不同的窗口装饰,并将窗口修饰做为mDecor的子窗口。
  • 给mContentParent赋值,其值是通过调用 ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT)获得,ID_ANDROID_CONTENT正是id = content 的FrameLayout。

通过在setContentView()中调用inflate()方法,可以把用户界面layout.xml文件添加到窗口修饰中,inflate()方法的第二个参数正是mContentParent,即id = content 的FrameLayout。

最后,回调cb.onContentChanged()方法,通知应用程序窗口内容发生了改变。cb为Activity本身,因为Activity实现了Window.Callback的接口,并且在attach()方法中将自身作为Window对象的Callback接口实现。

以上“根据用户指定的参数”中“用户指定”有两个地方可以指定,



7.Window类设置完成其视图元素后,需要将创建的这个窗口告诉WmS,通过WmS将窗口显示在屏幕上。Activity准备好后会通知AmS,Ams通过各种条件判断最终调用Activity中的makeVisible()方法,该方法以及后续的调用将真正完成将窗口加进WmS之中。代码如下,

<span style="font-family:Microsoft YaHei;">void makeVisible() {        if (!mWindowAdded) {<span style="white-space:pre"></span>//若没有添加窗口(</span><span style="font-family: 'Microsoft YaHei';">mWindowAdded == false</span><span style="font-family: 'Microsoft YaHei';">),则执行一下代码</span><span style="font-family:Microsoft YaHei;">            ViewManager wm = getWindowManager();<span style="white-space:pre"></span>//获取Activity内部的WM对象,实际上是Window.LocalWindowManager对象            wm.addView(mDecor, getWindow().getAttributes());<span style="white-space:pre"></span>//此addView()方法,不是WindowManageImpl类中的</span><span style="font-family: 'Microsoft YaHei';">addView()方法</span><span style="font-family:Microsoft YaHei;">            mWindowAdded = true;<span style="white-space:pre"></span>//添加窗口为真        }        mDecor.setVisibility(View.VISIBLE);    }</span>

addView()中第一个参数mDecor是一个DecorView对象,也就是用户所能看到的Activity对应的全部界面内容。第二个参数是在构造Window对象时默认构造的WindowManager.LayoutParams对象,该代码在Window类的初始化代码中,

 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;        }

以上构造函数说明,在默认情况下的窗口参数的类型是TYPE_APPLICATION,即应用程序类型的窗口。

Activity添加窗口时为何不直接使用WindowManagerImpl类,而是使用LocalWindowManager类?因为后者会检查WindowManager.LayoutParams的值,并给token赋值,而直接使用WindowManagerImpl则不会检查Params的值,所以可以将LocalWindowManager看成一道关卡。

if (wp.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&                wp.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {<span style="white-space:pre"></span>//如果应用窗口的类型大于等于第一个应用窗口且小于等于最后一个应用窗口,执行以下代码。                if (wp.token == null) {<span style="white-space:pre"></span>//如果token为空,则把Activity对应的窗口token赋值给params的token                    View decor = peekDecorView();                    if (decor != null) {                        wp.token = decor.getWindowToken();                    }                }

9.过了LocalWindowManager检查之后,需要调用的addView()方法。一个应用程序内部无论有多少个Activity,但只有一个WindowManagerImpl对象。WindowManagerImpl类中维护三个数组,用于保存该应用程序中所拥有的窗口状态,
  • View[] mViews:这里的每一个View对象都将成为WmS所认为的一个窗口。
  • ViewRoot[] mRoots:所有的ViewRoot对象,mViews中每一个View对象都对应一个ViewRoot对象。
  • WindowManager.LayoutParams[] mParams:当把mViews中的View对象当做一个窗口添加进一个WmS中,WmS要求每个被添加的窗口都要对应一个LayoutParams对象,用mParams来保存。

<span style="white-space:pre"></span> private void addView(View view, ViewGroup.LayoutParams params, boolean nest)    {        if (Config.LOGV) Log.v("WindowManager", "addView view=" + view);        if (!(params instanceof WindowManager.LayoutParams)) {            throw new IllegalArgumentException(                    "Params must be WindowManager.LayoutParams");        }        final WindowManager.LayoutParams wparams                = (WindowManager.LayoutParams)params;                ViewRoot root;        View panelParentView = null;                synchronized (this) {            // Here's an odd/questionable case: if someone tries to add a            // view multiple times, then we simply bump up a nesting count            // and they need to remove the view the corresponding number of            // times to have it actually removed from the window manager.            // This is useful specifically for the notification manager,            // which can continually add/remove the same view as a            // notification gets updated.            int index = findViewLocked(view, false);            if (index >= 0) {                if (!nest) {<span style="white-space:pre"></span>//nest为false抛出异常,检查所添加的窗口是否已经添加过了,不允许重复添加                    throw new IllegalStateException("View " + view                            + " has already been added to the window manager.");                }                root = mRoots[index];                root.mAddNesting++;                // Update layout parameters.                view.setLayoutParams(wparams);                root.setLayoutParams(wparams, true);                return;            }                        // If this is a panel window, then find the window it is being            // attached to for future reference.            if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&                    wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {                final int count = mViews != null ? mViews.length : 0;                for (int i=0; i<count; i++) {                    if (mRoots[i].mWindow.asBinder() == wparams.token) {            <span style="white-space:pre"></span>panelParentView = mViews[i]<span style="white-space:pre"></span>//若所添加的窗口为子窗口类型,找到其父窗口,并保存在panelParentView,该变量将做为后面调用ViewRoot的setView()参数                   }                }            }                        root = new ViewRoot(view.getContext());<span style="white-space:pre"></span>//创建一个新的ViewRoot            root.mAddNesting = 1;            view.setLayoutParams(wparams);                        if (mViews == null) {                index = 1;                mViews = new View[1];                mRoots = new ViewRoot[1];                mParams = new WindowManager.LayoutParams[1];            } else {                index = mViews.length + 1;                Object[] old = mViews;                mViews = new View[index];                System.arraycopy(old, 0, mViews, 0, index-1);<span style="white-space:pre"></span>//使用System.arraycopy()实现数组之间的复制old = mRoots;                mRoots = new ViewRoot[index];                System.arraycopy(old, 0, mRoots, 0, index-1);                old = mParams;                mParams = new WindowManager.LayoutParams[index];                System.arraycopy(old, 0, mParams, 0, index-1);            }            index--;            mViews[index] = view;<span style="white-space:pre"></span>//把新建的View,ViewRoot,WindowManager.LayoutParams对象保存到数组的最后            mRoots[index] = root;            mParams[index] = wparams;        }        // do this last because it fires off messages to start doing things        root.setView(view, wparams, panelParentView);<span style="white-space:pre"></span>//调用ViewRoot的setView()方法完成窗口的添加工作    }

11.调用ViewRoot的setView(View view, WindowManager.LayoutParams attrs, View panelParentView)方法完成最后的窗口添加工作,三个参数的主要意义如下,

  • view:是WindowManagerImpl中mViews的一个元素,即新建的窗口界面
  • attrs:添加窗口的参数,该参数描述该窗口的风格,大小,位置等等。尤其是内部变量token,指明该窗口和相关Activity的关系。
  • panelParentView:该对象也WindowManagerImpl中mViews的一个元素。仅当该窗口有父窗口时,其值才有意义。
setView的执行流程如下,

(1)给ViewRoot变量赋值。包括mViews,mWindowAttributes,mAttachInfo,代码如下

<span style="font-size:14px;"><span style="font-family:Microsoft YaHei;"><span style="white-space: pre;"></span></span><span style="font-family:Microsoft YaHei;">mView = view;                mWindowAttributes.copyFrom(attrs);</span></span>
<span style="font-family:Microsoft YaHei;font-size:14px;"><span style="white-space:pre"></span>mSoftInputMode = attrs.softInputMode;                mWindowAttributesChanged = true;                mAttachInfo.mRootView = view;<span style="white-space:pre"></span>//mAttachInfo变量其成员mRootView赋值参数为View</span>
<span style="font-family:Microsoft YaHei;font-size:14px;"><span style="white-space: pre;"></span>if (panelParentView != null) {<span style="white-space: pre;"></span>//若添加的是子窗口                    mAttachInfo.mPanelParentWindowToken<span style="white-space: pre;"></span>//同时给mPanelParentWindowToken赋值,其值为父窗口的token                            = panelParentView.getApplicationWindowToken();                }</span>

(2)调用requestLayout(),发出界面重绘请求。发出一个异步的消息,便于UI线程下一个处理的消息是界面重绘,从而让该窗口在响应任何其他用户消息之前首先变得可见

(3)调用sWindowSession.add(),通知WmS添加窗口,代码如下,

<span style="font-family:Microsoft YaHei;font-size:14px;"> <span style="white-space:pre"></span>try {                    res = sWindowSession.add(mWindow, mWindowAttributes,                            getHostVisibility(), mAttachInfo.mContentInsets,                            mInputChannel);                } catch (RemoteException e) {</span>

ssWindowSession在ViewRoot中是一个静态的变量,

<span style="font-family:Microsoft YaHei;font-size:14px;">static IWindowSession sWindowSession;<span style="white-space:pre"></span>//<strong>不加任何权限修饰符即为包内访问</strong></span>

每一个应用程序仅有一个sWindowSession对象,该对象类型为IWindowSession,即为一个Binder引用,该引用对应着WmS中的Session子类,WmS为每一个应用程序分配一个Session(会话)对象。
因为sWindowSession变量的访问权限为包内访问,所以不能不经过Activity,或者WindowManager,或者不创建Window对象来创建窗口。
sWindowSession变量是在ViewRoot中,构造函数调用getWindowSession()方法返回的。该方法内部会自动判断sWindowSession是否为空,为空则创建一个。这也是所谓的工厂模式创建对象

public static IWindowSession getWindowSession(Looper mainLooper) {        synchronized (mStaticInit) {            if (!mInitialized) {                try {                    InputMethodManager imm = InputMethodManager.getInstance(mainLooper);                    sWindowSession = IWindowManager.Stub.asInterface(<span style="white-space:pre"></span>//sWindowSession是通过IWindowManager而来的,IWindowManager在源码中是@hide的,意味着SDK中将不包含<span style="white-space:pre"></span> //该类                            ServiceManager.getService("window"))                            .openSession(imm.getClient(), imm.getInputContext());                    mInitialized = true;                } catch (RemoteException e) {                }            }            return sWindowSession;        }    }

所以在标准的程序设计中,只能通过WindowManager类来创建窗口。若有特殊需要,可通过改变Framework,以达到灵活配置。


创建子窗口

下面介绍几个具体的子窗口,

一、Dialog的创建

<span style="font-family:Microsoft YaHei;font-size:14px;">public Dialog(Context context, int theme) {<span style="white-space:pre"></span>//创建Dialog内部的Context对象,并赋值给mContext        mContext = new ContextThemeWrapper(            context, theme == 0 ? com.android.internal.R.style.Theme_Dialog : theme);<span style="white-space:pre"></span>//真正实现Context的是ContextImpl,所以mContext也是一个壳(包装)。没有指定主题就使用默认值        mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);        Window w = PolicyManager.makeNewWindow(mContext);<span style="white-space:pre"></span>//调用makeNewWindow()创建一个Window对象,与Activity中创建Window对象方法相同        mWindow = w;        w.setCallback(this);<span style="white-space:pre"></span>//setCallback()设置该Window的消息回调接口为本Dialog对象。(类似于Activity的消息处理方式)        w.setWindowManager(mWindowManager, null, null);        w.setGravity(Gravity.CENTER);        mUiThread = Thread.currentThread();        mListenersHandler = new ListenersHandler(this);<span style="white-space:pre"></span>//创建一个回调句柄mListenerHandler(Handler为句柄),在内部发送异步消息。    }</span>

在调用show()方法前,Dialog内部仅仅创建了一个Window对象,没有告知WmS添加一个可以显示的窗口。所以show()方法中必须告知WmS添加一个真正可以显示的窗口。在show()方法内部主要完成以下几件事情,

第一,如果窗口存在,则直接显示窗口,并返回,代码如下

<span style="font-family:Microsoft YaHei;font-size:14px;"> if (mShowing) {            if (mDecor != null) {                mDecor.setVisibility(View.VISIBLE);            }            return;        }</span>

第二,如果窗口还不存在,给应用程序一个设置窗口的机会,回调Dialog的onCreate()方法

<span style="font-family:Microsoft YaHei;font-size:14px;"> if (!mCreated) {            dispatchOnCreate(null);        }</span>

第三,为该Dialog中的mDecor变量赋值,并设置添加Dialog窗口所使用的LayoutParams参数

<span style="font-family:Microsoft YaHei;font-size:14px;"><span style="white-space:pre"></span>mDecor = mWindow.getDecorView();        WindowManager.LayoutParams l = mWindow.getAttributes();        if ((l.softInputMode                & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) == 0) {            WindowManager.LayoutParams nl = new WindowManager.LayoutParams();            nl.copyFrom(l);            nl.softInputMode |=                    WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;            l = nl;        }</span>

第四,调用WindowManager的addView()方法,添加窗口。

<span style="font-family:Microsoft YaHei;font-size:14px;">try {            mWindowManager.addView(mDecor, l);            mShowing = true;</span>

第五,可以调用Dialog的setonShowListener()为Dialog添加一个回调接口。执行该回调函数是异步的。下面来区别异步和同步,

  • 若回调目的是为本次循环设置某些参数,必须采用同步回调。
  • 若回调函数中指定的代码依赖本次循环后续代码的执行结果,必须采用异步回调。否则,回调代码会因某种条件不具备而无法正常执行。
  • 若回调代码的执行时间比较长,尽量采用异步回调。系统为每一个循环的执行时间设置的阈值是5秒。5秒内还没有执行下一个循环,系统会弹出应用程序无响应(ANR,Application Not Responding)的对话框。



二、PopupWindow(弹出窗口)的创建

弹出窗口是Framework提供的一种UI控件,例如下拉列表就是一种PopupWindow。AutoText控件(自动提示、补全输入)也是基于PopupWindow实现。
PopupWindow不继承与Window类,所以该类不属于窗口类。该类必须完成以下几件事情,

第一,为该类中的mContext变量赋值,该值可直接来源于构造函数的参数,也可以根据参数中的View对象获得,代码如下,

public PopupWindow(View contentView, int width, int height, boolean focusable) {        if (contentView != null) {            mContext = contentView.getContext();

第二,为该类中的mWindowManager变量赋值。该变量的值实际上是创建该PopupWindow对象的Activity中的mWindowManager对象。

mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);

第三,PopupWindow提供以下API将mContentView对应的窗口显示到界面上的任何位置。

<span style="font-family:Microsoft YaHei;font-size:14px;">public void showAtLocation(View parent, int gravity, int x, int y) {}public void showAsDropDown(View anchor, int xoff, int yoff) {}</span>

其中,parent与anchor本质没有区别,它们俩唯一的区别是指定窗口的位置方式不同。两个方法的内部调用流程相同,如下,

首先看,LayoutParams对象。通过调用createPopupLayout()方法。该方法参数是一个IBinder对象,其值应该为父窗口的token值,通过调用parent.getWindowToken()获得。创建LayoutParams的内部变量值如下,

<span style="font-family:Microsoft YaHei;font-size:14px;"><span style="white-space:pre"></span>WindowManager.LayoutParams p = new WindowManager.LayoutParams();        // these gravity settings put the view at the top left corner of the        // screen. The view is then positioned to the appropriate location        // by setting the x and y offsets to match the anchor's bottom        // left corner        p.gravity = Gravity.LEFT | Gravity.TOP;        p.width = mLastWidth = mWidth;        p.height = mLastHeight = mHeight;        if (mBackground != null) {            p.format = mBackground.getOpacity();        } else {            p.format = PixelFormat.TRANSLUCENT;        }        p.flags = computeFlags(p.flags);        p.type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;</span>
<span style="font-family:Microsoft YaHei;font-size:14px;"><span style="white-space:pre"></span>p.token = token;</span>

注意该LayoutParams对象的type类型,“即应用程序的子窗口,PopupWindow的默认类型”。添加子窗口时,要LocalWindowManager要检查其token值必须为父窗口的token。所以给p.token赋上父窗口的token值。

在PopupWindow中,提供了setContentView()方法用于设置窗口内容,setBackgroundDrawable()用于设置窗口背景。所以在准备窗口内容的preparePopup()方法中,会判断是否有窗口背景,若没有,窗口内容就是mContentView,否则创建一个PopupViewContainer,这个类继承于FrameLayout,最后将mContentView添加到该FrameLayout中,具体代码如下,

if (mBackground != null) {<span style="white-space:pre"></span>//窗口背景存在            final ViewGroup.LayoutParams layoutParams = mContentView.getLayoutParams();            int height = ViewGroup.LayoutParams.MATCH_PARENT;            if (layoutParams != null &&                    layoutParams.height == ViewGroup.LayoutParams.WRAP_CONTENT) {                height = ViewGroup.LayoutParams.WRAP_CONTENT;            }            // when a background is available, we embed the content view            // within another view that owns the background drawable            PopupViewContainer popupViewContainer = new PopupViewContainer(mContext);            PopupViewContainer.LayoutParams listParams = new PopupViewContainer.LayoutParams(                    ViewGroup.LayoutParams.MATCH_PARENT, height            );            popupViewContainer.setBackgroundDrawable(mBackground);            popupViewContainer.addView(mContentView, listParams);            mPopupView = popupViewContainer;        } else {            mPopupView = mContentView;<span style="white-space:pre"></span>//若没有窗口背景        }

三、ContextMenu(情景菜单)的创建

情景菜单一般是指长按ListView的某个Item时弹出的菜单。不过,程序可以给任何一个View设置情景菜单,触发的方式不一定是长按,也可能是双击,单击等等。
情景菜单实际上是一个Dialog,但情景菜单把创建好的Dialog添加到WmS时,会修改该Dialog窗口的类型,即从默认的应用类型修改为子窗口类型。

1.触发情景菜单的消息

显示情景菜单有两种方式:第一、当用户长按某个View时,若该View已经添加过场景菜单,则会弹出一个场景菜单窗口;第二,调用openContextMenu()方法。关系如下,


<span style="font-family:Microsoft YaHei;font-size:14px;">        boolean handled = false;        if (mOnLongClickListener != null) {<span style="white-space:pre"></span>//长按参数不为空时            handled = mOnLongClickListener.onLongClick(View.this);<span style="white-space:pre"></span>//执行长按的回调函数        }        if (!handled) {<span style="white-space:pre"></span>//如果用户没有处理            handled = showContextMenu();<span style="white-space:pre"></span>//调用showContextMenu()        }</span>

2.菜单功能中几个类的关系

在showContextMenu()内部调用其父类方法showContextMenuForChild()。对于应用类窗口,任何View的根视图都是DecorView对象

 public boolean showContextMenuForChild(View originalView) {            // Reuse the context menu builder            if (mContextMenu == null) {                mContextMenu = new ContextMenuBuilder(getContext());                mContextMenu.setCallback(mContextMenuCallback);            } else {                mContextMenu.clearAll();            }            mContextMenuHelper = mContextMenu.show(originalView, originalView.getWindowToken());            return mContextMenuHelper != null;        }















1 0