理解Window和WindowManager
一、Window简介
二、Window和WindowManager
1、使用WindowManager添加一个Window
/** 下面的代码演示了通过WindowManager添加Window的过程:* (1)创建Button按钮。* (2)为Button设置文字内容。* (3)创建一个WindowManager.LayoutParams的参数,设置Button的宽高之类的。* (4)为WindowManager.LayoutParams设置Flags参数。* (5)为WindowManager.LayoutParams设置type参数。* (6、7、8)为WindowManager.LayoutParams设置中心点、坐标位置。* (9)给Button设置触摸事件。* (10)将Button按照WindowManager.LayoutParams参数用WindowManager添加View即可。* */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);
FLAG_NOT_FOCUSABLE:表示Window不需要获取焦点,也不需要接收各种输入事件,此标记会同时启用FLAG_NOT_TOUCH_MODAL,最终事件会直接传递给下层的具有焦点的Window。
FLAG_NOT_TOUCH_MODAL:在此模式下,系统会将当前Window区域以外的单击事件传递给底层的Window,当前Window区域以内的单击事件则自己处理。 一般来说都需要开启此标记,否则其他Window将无法收到单击事件。
FLAG_SHOW_WHEN_LOCKED:此模式可以让Window显示在锁屏的界面上。
应用Window(层级范围1~99):对应着一个Activity。
子Window(层级范围1000~1999):不能单独存在,它需要附属在特定的父Window之中,比如常见的Dialog就是一个子Window。
系统Window(层级范围2000~2999):是需要声明权限才能创建的Window,比如Toast和系统状态栏都属于系统Window。
层级范围对应这WindowManager.LayoutParams的type参数,层级大的会覆盖在层级小的Window上面。所以系统层级是最大的,系统层级一般选用TYPE_SYSTEM_ERROR或者TYPE_SYSTEM_OVERLAY。 还有哦,系统层级需要声明权限:不然会报错:
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
(3)将Button按照WindowManager.LayoutParams参数用WindowManager添加View即可。 所以这个Button就作为一个新的Window了。
!!!!!Window并不是实际存在的,它以View的形式存在。!!!!!!
WindowManager所提供的功能很简单,常用的只有三个方法:即添加View、更新View和删除View。这三个方法定义在ViewManager中,ViewManager是一个接口,而WindowManager正是继承了ViewManager。
package android.view;/** 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
{/*** Assign the passed LayoutParams to the passed View and add the view to the window.* <p>Throws {@link android.view.WindowManager.BadTokenException} for certain programming* errors, such as adding a second view to a window without removing the first view.* <p>Throws {@link android.view.WindowManager.InvalidDisplayException} if the window is on a* secondary {@link Display} and the specified display can't be found* (see {@link android.app.Presentation}).* @param view The view to be added to this window.* @param params The LayoutParams to assign to view.*/public void addView(View view, ViewGroup.LayoutParams params);public void updateViewLayout(View view, ViewGroup.LayoutParams params);public void removeView(View view);
}
* (1)ViewManager这个接口是由WindowManager来继承的,WindowManager可以用来创建Window。
* ViewManager这里面提供了三个方法,分别是添加、更新和删除View。
* (2)WindowManagerImpl继承自WindowManager,
* 而WindowManager又是继承自ViewManager,
* addView、updateViewLayout、removeView都是来自ViewManager的。
* 所以在WindowManagerImpl具体实现了这三个方法。
2、下面这个例子中的onTouch实现了拖动的Window效果
package com.ryg.chapter_8;import com.ryg.chapter_8.R;import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.graphics.PixelFormat;
import android.os.Bundle;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;
import android.widget.Button;public class TestActivity extends Activity implements OnTouchListener {private static final String TAG = "TestActivity";private Button mCreateWindowButton;private Button mFloatingButton;private WindowManager.LayoutParams mLayoutParams;private WindowManager mWindowManager;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_test);initView();}private void initView() {mCreateWindowButton = (Button) findViewById(R.id.button1);mWindowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);}public void onButtonClick(View v) {if (v == mCreateWindowButton) {/** 下面的代码演示了通过WindowManager添加Window的过程:* */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);}}@SuppressLint("ClickableViewAccessibility") @Overridepublic boolean onTouch(View v, MotionEvent event) {int rawX = (int) event.getRawX();int rawY = (int) event.getRawY();switch (event.getAction()) {case MotionEvent.ACTION_DOWN: {break;}case MotionEvent.ACTION_MOVE: {/** 如果想要实现可以拖动的Window效果,* 只需要根据手指的位置来设定LayoutParams中的x和y的值即可改变Window的位置。* */int x = (int) event.getX();int y = (int) event.getY();mLayoutParams.x = rawX;mLayoutParams.y = rawY;mWindowManager.updateViewLayout(mFloatingButton, mLayoutParams);break;}case MotionEvent.ACTION_UP: {break;}default:break;}return false;}@Overrideprotected void onDestroy() {try {mWindowManager.removeView(mFloatingButton);} catch (IllegalArgumentException e) {e.printStackTrace();}super.onDestroy();}
}
三、Window的内部机制
1、综述
2、Window的添加过程
package android.view;/*** Provides low-level communication with the system window manager for* operations that are bound to a particular context, display or parent window.* Instances of this object are sensitive to the compatibility info associated* with the running application.** This object implements the {@link ViewManager} interface,* allowing you to add any View subclass as a top-level window on the screen.* Additional window manager specific layout parameters are defined for* control over how windows are displayed. It also implements the {@link WindowManager}* interface, allowing you to control the displays attached to the device.* * <p>Applications will not normally use WindowManager directly, instead relying* on the higher-level facilities in {@link android.app.Activity} and* {@link android.app.Dialog}.* * <p>Even for low-level window manager access, it is almost never correct to use* this class. For example, {@link android.app.Activity#getWindowManager}* provides a window manager for adding windows that are associated with that* activity -- the window manager will not normally allow you to add arbitrary* windows that are not associated with an activity.** @see WindowManager* @see WindowManagerGlobal* @hide*/
public final class WindowManagerImpl implements WindowManager {private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();private final Display mDisplay;private final Window mParentWindow;public WindowManagerImpl(Display display) {this(display, null);}private WindowManagerImpl(Display display, Window parentWindow) {mDisplay = display;mParentWindow = parentWindow;}public WindowManagerImpl createLocalWindowManager(Window parentWindow) {return new WindowManagerImpl(mDisplay, parentWindow);}public WindowManagerImpl createPresentationWindowManager(Display display) {return new WindowManagerImpl(display, mParentWindow);}/** Window的添加,即View的添加:* */@Overridepublic void addView(View view, ViewGroup.LayoutParams params) {/** 这里变成了四个参数:* */ mGlobal.addView(view, params, mDisplay, mParentWindow);}@Overridepublic void updateViewLayout(View view, ViewGroup.LayoutParams params) {mGlobal.updateViewLayout(view, params);}/** Window的删除,即View的删除:* */@Overridepublic void removeView(View view) {mGlobal.removeView(view, false);}@Overridepublic void removeViewImmediate(View view) {mGlobal.removeView(view, true);}@Overridepublic Display getDefaultDisplay() {return mDisplay;}
}
if (view == null) {throw new IllegalArgumentException("view must not be null");}if (display == null) {throw new IllegalArgumentException("display must not be null");}if (!(params instanceof WindowManager.LayoutParams)) {throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");}/** 如果是子Window,那么还需要调整一些布局参数:* */final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;if (parentWindow != null) {parentWindow.adjustLayoutParamsForSubWindow(wparams);}
(4)在WindowManagerGlobal内部有如下几个列表比较重要:
// 存储Window对应的所有View:private final ArrayList<View> mViews = new ArrayList<View>();// 存储Window对应的所有ViewRootImpl:private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();// 存储Window对应的所有的布局参数:private final ArrayList<WindowManager.LayoutParams> mParams =new ArrayList<WindowManager.LayoutParams>();// 存储正在被删除的View对象,或者说是那些已经调用removeView方法但是删除操作还未完成的Window对象:private final ArraySet<View> mDyingViews = new ArraySet<View>();
root = new ViewRootImpl(view.getContext(), display);view.setLayoutParams(wparams);mViews.add(view);mRoots.add(root);mParams.add(wparams);
(5)WindowManagerGlobal.java文件中的addView方法中通过ViewRootImpl来更新界面并完成Window的添加过程:这一步骤由ViewRootImpl的setView方法来完成。
/** setView方法的内部会通过requestLayout方法来完成异步刷新请求。* */@Overridepublic void requestLayout() {if (!mHandlingLayoutInLayoutRequest) {checkThread();mLayoutRequested = true;/** 这里实际才是View绘制的入口:* */scheduleTraversals();}}
在ViewRootImpl的setView方法中调用requestLayout方法后会接着通过WindowSession最终来完成Window的添加过程。
下面的mWindowSession的类型是IWindowSession,它是一个Binder对象,真正的实现类是Session,也就是Window的添加过程试一次IPC调用。在Session的内部会通过WindowManagerService来实现Window的添加: mWindowSession.addToDisplay。如此一来,Window的添加请求就交给WindowManagerService去处理了, 在WindowManagerService内部会为每一个应用保留一个单独的Session。具体的WindowManagerService的内部实现,我们就不讲了,深入进去没有太大的意义, 可以自行查看。
try {mOrigWindowType = mWindowAttributes.type;mAttachInfo.mRecomputeGlobalAttributes = true;collectViewAttributes();res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,getHostVisibility(), mDisplay.getDisplayId(),mAttachInfo.mContentInsets, mInputChannel);} catch (RemoteException e) {mAdded = false;mView = null;mAttachInfo.mRootView = null;mInputChannel = null;mFallbackEventHandler.setView(null);unscheduleTraversals();setAccessibilityFocus(null, null);throw new RuntimeException("Adding window failed", e);}
在Session内部会通过WindowManagerService来实现Window的添加:
public void addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow) {/** 第一步:下面的四个if语句都是在检查参数是否合法:* */if (view == null) {throw new IllegalArgumentException("view must not be null");}if (display == null) {throw new IllegalArgumentException("display must not be null");}if (!(params instanceof WindowManager.LayoutParams)) {throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");}/** 如果是子Window,那么还需要调整一些布局参数:* */final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;if (parentWindow != null) {parentWindow.adjustLayoutParamsForSubWindow(wparams);}ViewRootImpl root;View panelParentView = null;synchronized (mLock) {// Start watching for system property changes.if (mSystemPropertyUpdater == null) {mSystemPropertyUpdater = new Runnable() {@Override public void run() {synchronized (mLock) {for (int i = mRoots.size() - 1; i >= 0; --i) {mRoots.get(i).loadSystemProperties();}}}};SystemProperties.addChangeCallback(mSystemPropertyUpdater);}int index = findViewLocked(view, false);if (index >= 0) {if (mDyingViews.contains(view)) {// Don't wait for MSG_DIE to make it's way through root's queue.mRoots.get(index).doDie();} else {throw new IllegalStateException("View " + view+ " has already been added to the window manager.");}// The previous removeView() had not completed executing. Now it has.}// 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.size();for (int i = 0; i < count; i++) {if (mRoots.get(i).mWindow.asBinder() == wparams.token) {panelParentView = mViews.get(i);}}}/** WindowManagerGlobal内部有一些比较重要的列表,这些列表上面有声明过,* 下面的几条语句将Window的一系列对象添加到这些列表中,* */root = new ViewRootImpl(view.getContext(), display);view.setLayoutParams(wparams);mViews.add(view);mRoots.add(root);mParams.add(wparams);}// do this last because it fires off messages to start doing thingstry {/** 通过ViewRootImpl来更新界面并完成Window的添加过程。* 这个root就是ViewRootImpl类对象。* View的绘制过程就是由ViewRootImpl来完成的。* setView内部会通过requestLayout来完成异步刷新请求。* */root.setView(view, wparams, panelParentView);} catch (RuntimeException e) {// BadTokenException or InvalidDisplayException, clean up.synchronized (mLock) {final int index = findViewLocked(view, false);if (index >= 0) {removeViewLocked(index, true);}}throw e;}}
(7)整体的调用关系是:
3、Window的删除过程
/**** WindowManager的removeView的最终源头:* @param view* @param immediate*/public void removeView(View view, boolean immediate) {if (view == null) {throw new IllegalArgumentException("view must not be null");}synchronized (mLock) {// 首先查找待删除的View的索引,查找过程就是遍历建立的数组:int index = findViewLocked(view, true);View curView = mRoots.get(index).getView();// 然后调用这个方法进一步删除:removeViewLocked(index, immediate);if (curView == view) {return;}throw new IllegalStateException("Calling with view " + view+ " but the ViewAncestor is attached to " + curView);}}
/** 根据待删除View的index来做进一步删除View,* removeViewLocked是通过ViewRootImpl来完成删除操作的。* */private void removeViewLocked(int index, boolean immediate) {ViewRootImpl root = mRoots.get(index);View view = root.getView();if (view != null) {InputMethodManager imm = InputMethodManager.getInstance();if (imm != null) {imm.windowDismissed(mViews.get(index).getWindowToken());}}boolean deferred = root.die(immediate);if (view != null) {view.assignParent(null);if (deferred) {mDyingViews.add(view);}}}
(4)具体的删除操作由ViewRootImpl的die方法来完成。 在die的内部会判断是异步删除还是同步删除。在异步删除的情况下,die方法只是发送了一个请求删除的消息后就立刻返回了,这个时候View并没有完成删除操作。doDie内部会调用dispatchDetachedFromWindow方法,真正删除View的逻辑在dispatchDetachedFromWindow方法的内部实现。
boolean die(boolean immediate) {// Make sure we do execute immediately if we are in the middle of a traversal or the damage// done by dispatchDetachedFromWindow will cause havoc on return./** 如果是同步删除(立即删除),那么就不发送消息直接调用doDie方法。* */if (immediate && !mIsInTraversal) {doDie();return false;}if (!mIsDrawing) {destroyHardwareRenderer();} else {Log.e(TAG, "Attempting to destroy the window while drawing!\n" +" window=" + this + ", title=" + mWindowAttributes.getTitle());}/** 如果是异步操作,那么就发送一个MSG_DIE的消息,* ViewRootImpl中的Handler会处理此消息并调用doDie方法。* */mHandler.sendEmptyMessage(MSG_DIE);return true;}
(5)ViewRootImpl.java中doDie方法:
/** 在Die方法中会判断是用异步删除还是同步删除,* 但归根结底还是要用doDie来完成删除View的操作。* 在doDie的内部会调用dispatchDetachedFromWindow方法,* 真正删除View的逻辑在dispatchDetachedFromWindow方法的内部实现。* */void doDie() {checkThread();if (LOCAL_LOGV) Log.v(TAG, "DIE in " + this + " of " + mSurface);synchronized (this) {if (mRemoved) {return;}mRemoved = true;if (mAdded) {/** 这儿才是重点呢:* */dispatchDetachedFromWindow();}if (mAdded && !mFirst) {invalidateDisplayLists();destroyHardwareRenderer();if (mView != null) {int viewVisibility = mView.getVisibility();boolean viewVisibilityChanged = mViewVisibility != viewVisibility;if (mWindowAttributesChanged || viewVisibilityChanged) {// If layout params have been changed, first give them// to the window manager to make sure it has the correct// animation info.try {if ((relayoutWindow(mWindowAttributes, viewVisibility, false)& WindowManagerGlobal.RELAYOUT_RES_FIRST_TIME) != 0) {mWindowSession.finishDrawing(mWindow);}} catch (RemoteException e) {}}mSurface.release();}}mAdded = false;}WindowManagerGlobal.getInstance().doRemoveView(this);}
(5)doDie方法中调用的dispatchDetachedFromWindow是真正删除View的逻辑。
(1)垃圾回收相关的工作,比如清除数据和消息、移除回调。
(2)通过Session的remove方法删除Window:mWindowSession.remove(mWindow),这同样是一个IPC过程, 最终会调用WindowManagerService的removeWindow方法。
(3)调用View的dispatchDetachedFromWindow方法:
* 在内部会调用View的onDetachedFromWindow()以及onDetachedFromWindowInternal()。
* 对于onDetachedFromWindow()大家一定不陌生,当View从Window中移除时,这个方法就会被调用,
* 可以在这个方法内部做一些资源回收的工作,
* 比如终止动画、停止线程等。
void dispatchDetachedFromWindow() {if (mView != null && mView.mAttachInfo != null) {if (mAttachInfo.mHardwareRenderer != null &&mAttachInfo.mHardwareRenderer.isEnabled()) {mAttachInfo.mHardwareRenderer.validate();}mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(false);mView.dispatchDetachedFromWindow();}mAccessibilityInteractionConnectionManager.ensureNoConnection();mAccessibilityManager.removeAccessibilityStateChangeListener(mAccessibilityInteractionConnectionManager);removeSendWindowContentChangedCallback();destroyHardwareRenderer();setAccessibilityFocus(null, null);mView.assignParent(null);mView = null;mAttachInfo.mRootView = null;mAttachInfo.mSurface = null;mSurface.release();if (mInputQueueCallback != null && mInputQueue != null) {mInputQueueCallback.onInputQueueDestroyed(mInputQueue);mInputQueue.dispose();mInputQueueCallback = null;mInputQueue = null;}if (mInputEventReceiver != null) {mInputEventReceiver.dispose();mInputEventReceiver = null;}try {mWindowSession.remove(mWindow);} catch (RemoteException e) {}// Dispose the input channel after removing the window so the Window Manager// doesn't interpret the input channel being closed as an abnormal termination.if (mInputChannel != null) {mInputChannel.dispose();mInputChannel = null;}unscheduleTraversals();}
4、Window的更新过程
public void updateViewLayout(View view, ViewGroup.LayoutParams params) {if (view == null) {throw new IllegalArgumentException("view must not be null");}if (!(params instanceof WindowManager.LayoutParams)) {throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");}final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;/** 首先它需要更新View的LayoutParams并替换掉老的LayoutParams,* */view.setLayoutParams(wparams);synchronized (mLock) {int index = findViewLocked(view, true);ViewRootImpl root = mRoots.get(index);mParams.remove(index);mParams.add(index, wparams);root.setLayoutParams(wparams, false);}}
(2)整体的调用关系是:
四、Window的创建过程
1、Window和View之间的关系
2、Activity的Window创建过程
Activity activity = null;
try {
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
/*
* 通过类加载器创建Activity的实例对象:
* */
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
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);
}
}
.....
if (activity != null) {
Context appContext = createBaseContextForActivity(r, activity);
CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
Configuration config = new Configuration(mCompatConfiguration);
if (DEBUG_CONFIGURATION) Slog.v(TAG, "Launching activity "
+ r.activityInfo.name + " with config " + config);
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config);
...
}
(2)在Activity的attach方法里,系统会创建Activity所属的Window对象并为其设置回调接口,Window对象的创建是通过PolicyManager的makeNewWindow方法实现的。由于Activity实现了Window的Callback接口,因此当Window接收到外界的状态改变时就会回调Activity的方法。Callback接口中的方法很多,但是有几个确实非常熟悉的,比如onAttachToWindow、onDetachedFromWindow、dispatchTouchEvent等,代码如下,在Activity.java文件中的attach方法中:
mWindow = PolicyManager.makeNewWindow(this);mWindow.setCallback(this);mWindow.getLayoutInflater().setPrivateFactory(this);if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {mWindow.setSoftInputMode(info.softInputMode);}if (info.uiOptions != 0) {mWindow.setUiOptions(info.uiOptions);}
(3)从上面的分析可以看出,Activity的Window是通过PolicyManager的一个工厂方法来创建的。但从PolicyManager的类名可以看出,它不是一个普通的类,它是一个策略类。PolicyManager中实现的几个工厂方法全部在策略接口IPolicy中声明了,IPolicy的定义如下,IPolicy.java文件:
/** Copyright (C) 2008 The Android Open Source Project** Licensed under the Apache License, Version 2.0 (the "License");* you may not use this file except in compliance with the License.* You may obtain a copy of the License at** http://www.apache.org/licenses/LICENSE-2.0** Unless required by applicable law or agreed to in writing, software* distributed under the License is distributed on an "AS IS" BASIS,* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.* See the License for the specific language governing permissions and* limitations under the License.*/package com.android.internal.policy;import android.content.Context;
import android.view.FallbackEventHandler;
import android.view.LayoutInflater;
import android.view.Window;
import android.view.WindowManagerPolicy;/*** {@hide}*//* The implementation of this interface must be called Policy and contained* within the com.android.internal.policy.impl package */
public interface IPolicy {public Window makeNewWindow(Context context);public LayoutInflater makeNewLayoutInflater(Context context);public WindowManagerPolicy makeNewWindowManager();public FallbackEventHandler makeNewFallbackEventHandler(Context context);
}
在实际的调用中,PolicyManager的真正实现类是Policy类,Policy类中的makeNewWindow方法的实现如下,在Policy.java文件中:
public Window makeNewWindow(Context context) {return new PhoneWindow(context);}
可以看出Window的具体实现确实是PhoneWindow。
public void setContentView(int layoutResID) {getWindow().setContentView(layoutResID);initActionBar();}
Activity的视图由setContentView方法提供,在这里面Activity将具体实现交给了Window处理,而Window的具体实现是由PhoneWindow, 所以只需要看PhoneWindow的相关逻辑即可。
@Override
public void setContentView(int layoutResID) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
<span style="white-space:pre"> </span>/*
<span style="white-space:pre"> </span>第一步,先要创建DecorView才行:
<span style="white-space:pre"> </span>*/
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
/*
<span style="white-space:pre"> </span>第二步,将Activity的布局文件添加到DecorView的mContentParent中了:
<span style="white-space:pre"> </span>*/
mLayoutInflater.inflate(layoutResID, mContentParent);
}
<span style="white-space:pre"> </span>/*
<span style="white-space:pre"> </span>第三步,
<span style="white-space:pre"> </span>*/
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
第一步:如果没有DecorView,那么就创建它。这个DecorView是Activity中的顶级View,包含标题栏和内容栏,内容栏的id就是“content”,完整id是android.R.id.content。DecorView的创建过程由installDecor方法来完成,在方法内部会通过generateDecor方法来直接创建DecorView,这个时候DecorView还只是一个空白的FrameLayout:
protected DecorView generateDecor(){return new DecorView(getContext(), -1);}
为了初始化DecorView的结构,PhoneWindow还需要通过generateLayout方法来加载具体的布局文件到DecorView中,具体的布局文件和系统版本以及主题有关,这个过程如下所示,在PhoneWindow.java文件中的generateLayout方法中:
View in = mLayoutInflater.inflate(layoutResource, null);decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));mContentRoot = (ViewGroup) in;ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
其中ID_ANDROID_CONTENT的定义如下,在Window.java文件中:这个id对应的ViewGroup就是mContentParent。
public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;
第二步:将View添加到DecorView的mContentParent中。就是上面的这句话:mLayoutInflater.inflate(layoutResID, mContentParent);。到此为止,Activity的布局文件已经加载到DecorView里面了,由此可以理解Activity的setContentView这个方法的来历了。Activity的布局文件只是被添加到DecorView的mContentParent中。
第三步:回调Activity的onContentChanged方法通知Activity视图已经发生改变。由于Activity实现了Window的Callback接口,于是要通知Activity,使其可以做相应的处理。Activity的onContentChanged方法是个空实现,我们可以在子Activity中处理这个回调。
(6)经过上面的三个步骤,到这里为止DecorView已经被创建并初始化完毕,Activity的布局文件也已经成功添加到了DecorView的mContentParent中,但是这个时候DecorView还没有被WindowManager正式添加到Window中。这里需要正确理解Window的概念,Window更多表示的是一种抽象的功能集合,虽然说早在Activity的attach方法中Window就已经被创建了,但是这个时候由于DecorView并没有被WindowManager识别,所以这个时候的Window无法提供具体功能,因为它还无法接收外界的输入信息。在ActivityThread的handleResumeActivity方法中,首先会调用Activity的onResume方法,接着会调用Activity的makeVisible方法,正是在makeVisible方法中,DecorView真正地完成了添加和显示这两个过程,到这里Activity的视图才能被用户看到,如下所示,在Activity.java文件中:
void makeVisible() {if (!mWindowAdded) {ViewManager wm = getWindowManager();wm.addView(mDecor, getWindow().getAttributes());mWindowAdded = true;}mDecor.setVisibility(View.VISIBLE);}
五、Dialog的Window创建过程
1、创建Window
Dialog(Context context, int theme, boolean createContextThemeWrapper) {if (createContextThemeWrapper) {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;}
//主要看这里:mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);Window w = PolicyManager.makeNewWindow(mContext);mWindow = w;w.setCallback(this);w.setOnWindowDismissedCallback(this);w.setWindowManager(mWindowManager, null, null);w.setGravity(Gravity.CENTER);mListenersHandler = new ListenersHandler(this);}
2、初始化DecorView并将Dialog的视图添加到DecorView中
public void setContentView(View view) {mWindow.setContentView(view);}
3、将DecorView添加到Window中并显示
mWindowManager.addView(mDecor, 1);mShowing = true;
以上可以发现Activity的Window创建过程和Dialog的Window创建过程很类似,两者几乎没有什么区别。
4、关闭Dialog
mWindowManager.removeViewImmediate(mDecor);
5、一个注意点
Dialog dailog = new Dialog(this.getApplicationContext());TextView textView = new TextView(this);textView.setText("this is a toast");dialog.setContentView(textView);dialog.show();
dialog.getWindow().setType(LayoutParams.TYPE_SYSTEM_OVERLAY);
然后别忘了在AndroidManifest文件中声明权限从而可以使用系统Window,如下所示:
<user-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
六、Toast的Window创建过程
1、Toast和Dialog不同,它的工作过程就稍显复杂
(5)在Toast.java文件中:show方法
/**
* Show the view for the specified duration.
*/
/*
* 显示Toast:
* 内部是一个IPC过程。
* */
public void show() {
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}
/*
* 由于NotificationManagerService运行在系统的进程中,
* 所以只能通过远程调用的方式来显示和隐藏Toast。
* TN是一个Binder类,在Toast和NotificationManagerService进行IPC的过程中,
* 当NotificationManagerService处理Toast的显示或隐藏时会跨进程回调TN中的方法,
* 这个时候由于TN运行在Binder线程池中,所以需要通过Handler将其切换到当前线程中。
* 这里的当前线程指的是发送Toast请求所在的线程。
* 注意,由于这里使用了Handler,所以这意味着Toast无法在没有Looper的线程中弹出,
* 这是因为Handler需要使用Looper才能完成切换线程的功能。
* */
INotificationManager service = getService();
String pkg = mContext.getPackageName();
/*
* TN是Binder类,是Toast的内部类
* */
TN tn = mTN;
tn.mNextView = mNextView;
try {
<span style="white-space:pre"> </span>/*
<span style="white-space:pre"> </span> * IPC体现在,我们这里可以使用NotificationManagerService内部的方法了呢。
<span style="white-space:pre"> </span> * 第一个参数:表示当前应用的包名,
<span style="white-space:pre"> </span> * 第二个参数:表示远程回调,
<span style="white-space:pre"> </span> * 第三个参数:表示Toast的显示时长。
<span style="white-space:pre"> </span> * 这个方法首先将Toast请求封装为ToastRecord对象,并将其添加到一个名为mToastQueue的队列中,
<span style="white-space:pre"> </span> * mToastQueue其实是一个ArrayList。
<span style="white-space:pre"> </span> * 对于非系统应用来说,mToastQueue中最多能同时存在50个ToastRecord,
<span style="white-space:pre"> </span> * 这样做是为了防止DOS(Denial of Service),防止拒绝服务攻击。
<span style="white-space:pre"> </span> * */
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
}
/**
* Close the view if it's showing, or don't show it if it isn't showing yet.
* You do not normally have to call this. Normally view will disappear on its own
* after the appropriate duration.
*/
/*
* 关闭Toast:
* 内部是一个IPC过程。
* */
public void cancel() {
mTN.hide();
try {
getService().cancelToast(mContext.getPackageName(), mTN);
} catch (RemoteException e) {
// Empty
}
}
/*
* 第一个参数:表示当前应用的包名,
<span style="white-space:pre"> </span> * 第二个参数:表示远程回调,这个参数是一个Binder类,也就是Toast中的TN对象。
<span style="white-space:pre"> </span> * 第三个参数:表示Toast的显示时长。
<span style="white-space:pre"> </span> * 这个方法首先将Toast请求封装为ToastRecord对象,并将其添加到一个名为mToastQueue的队列中,
<span style="white-space:pre"> </span> * mToastQueue其实是一个ArrayList。
<span style="white-space:pre"> </span> * 对于非系统应用来说,mToastQueue中最多能同时存在50个ToastRecord,
<span style="white-space:pre"> </span> * 这样做是为了防止DOS(Denial of Service),防止拒绝服务攻击。
* */
public void enqueueToast(String pkg, ITransientNotification callback, int duration)
{
if (DBG) Slog.i(TAG, "enqueueToast pkg=" + pkg + " callback=" + callback + " duration=" + duration);
if (pkg == null || callback == null) {
Slog.e(TAG, "Not doing toast. pkg=" + pkg + " callback=" + callback);
return ;
}
final boolean isSystemToast = isCallerSystem() || ("android".equals(pkg));
if (ENABLE_BLOCKED_TOASTS && !noteNotificationOp(pkg, Binder.getCallingUid())) {
if (!isSystemToast) {
Slog.e(TAG, "Suppressing toast from package " + pkg + " by user request.");
return;
}
}
synchronized (mToastQueue) {
int callingPid = Binder.getCallingPid();
long callingId = Binder.clearCallingIdentity();
try {
ToastRecord record;
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.
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的队列中,
<span style="white-space:pre"> </span> * mToastQueue其实是一个ArrayList。
<span style="white-space:pre"> </span> * 对于非系统应用来说,mToastQueue中最多能同时存在50个ToastRecord,
<span style="white-space:pre"> </span> * 这样做是为了防止DOS(Denial of Service),防止拒绝服务攻击。
* */
<span style="white-space:pre"> </span>if (!isSystemToast) {
int count = 0;
final int N = mToastQueue.size();
for (int i=0; i<N; i++) {
final ToastRecord r = mToastQueue.get(i);
if (r.pkg.equals(pkg)) {
count++;
if (count >= MAX_PACKAGE_NOTIFICATIONS) {
Slog.e(TAG, "Package has already posted " + count
+ " toasts. Not showing more. Package=" + pkg);
return;
}
}
}
}
<span style="white-space:pre"> </span>/*
<span style="white-space:pre"> </span> * 在ToastQueue中添加的ToastRecord对象的callback字段就是Toast的TN对象。
<span style="white-space:pre"> </span> * */
record = new ToastRecord(callingPid, pkg, callback, duration);
mToastQueue.add(record);
index = mToastQueue.size() - 1;
keepProcessAliveLocked(callingPid);
}
// If it's at index 0, it's the current toast. It doesn't matter if it's
// new or just been updated. Call back and tell it to show itself.
// If the callback fails, this will remove it from the list, so don't
// assume that it's valid after this.
if (index == 0) {
<span style="white-space:pre"> </span>/*
<span style="white-space:pre"> </span> * 通过该方法来显示当前的Toast:
<span style="white-space:pre"> </span> * */
showNextToastLocked();
}
} finally {
Binder.restoreCallingIdentity(callingId);
}
}
}
在enqueueToast中首先将Toast请求封装成ToastRecord对象并将其添加到一个名为mToastQueue的队列中。mToastQueue其实是一个ArrayList。对于非系统应用来说,mToastQueue中最多能同时存在50个ToastRecord,这样做是为了防止DOS(Denial of Service),放置拒绝服务攻击。
(6)正常情况下,一个应用不可能达到上限,当ToastRecord被添加到mToastQueue中后,NotificationManagerService就会通过showNextToastLocked方法来显示当前的Toast。
/** 通过showNextToastLocked来显示当前的Toast。* Toast的显示是由ToastRecord的callback字段来完成的,* 这个callback实际上就是Toast中的TN对象的远程Binder,* 通过callback来访问TN中的方法是需要跨进程来完成的,* 最终被调用的TN中的方法会运行在发起Toast请求的应用的Binder线程池中。也就是在客户端中运行的* */private void showNextToastLocked() {ToastRecord record = mToastQueue.get(0);while (record != null) {if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback);try {/** 在这里啦:调用Toast中TN对象的show方法:* */record.callback.show();/** 在Toast显示之后,NotificationManagerService通过该方法来发送一个延时消息,* 具体延时取决于Toast的时长:* */scheduleTimeoutLocked(record);return;} catch (RemoteException e) {Slog.w(TAG, "Object died trying to show notification " + record.callback+ " in package " + record.pkg);// remove it from the list and let the process dieint index = mToastQueue.indexOf(record);if (index >= 0) {mToastQueue.remove(index);}keepProcessAliveLocked(record.pid);if (mToastQueue.size() > 0) {record = mToastQueue.get(0);} else {record = null;}}}}
scheduleTimeoutLocked方法:用到了Handler
/** 在Toast显示之后,NotificationManagerService通过该方法来发送一个延时消息,* 具体延时取决于Toast的时长:* */private void scheduleTimeoutLocked(ToastRecord r){mHandler.removeCallbacksAndMessages(r);Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);/** LONG_DELAY:3.5s* SHORT_DELAY:2.5s* */long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;mHandler.sendMessageDelayed(m, delay);}
(7)延迟相应时间后,NotificationManagerService会通过cancelToastLocked方法来隐藏Toast并将其从mToastQueue中移除,这个时候如果mToastQueue中还有其他Toast,那么NotificationManagerService就继续显示其他Toast:
/** Toast的隐藏也是通过ToastRecord的callback来完成的,* 这同样是一次IPC过程,它的工作方式和Toast的显示过程是类似的。* */private void cancelToastLocked(int index) {ToastRecord record = mToastQueue.get(index);try {/** 这个callback就是TN对象,远程调用TN对象的hide方法。* */record.callback.hide();} catch (RemoteException e) {Slog.w(TAG, "Object died trying to hide notification " + record.callback+ " in package " + record.pkg);// don't worry about this, we're about to remove it from// the list anyway}mToastQueue.remove(index);keepProcessAliveLocked(record.pid);if (mToastQueue.size() > 0) {// Show the next one. If the callback fails, this will remove// it from the list, so don't assume that the list hasn't changed// after this point.showNextToastLocked();}}
(8)通过上面的分析,大家知道Toast的显示和影响过程实际上是通过Toast中的TN这个类来实现的,它有两个方法show和hide,分别对应Toast的显示和隐藏。由于这两个方法是被NMS以跨进程的方式调用的,因此它们运行在Binder线程池中。为了将执行环境切换到Toast请求所在的线程,在它们的内部使用了Handler:mShow和mHide分别是两个Runnable,它们内部分别调用了handleShow和handleHide方法。由此可见,handleShow和handleHide才是真正完成显示和隐藏Toast的地方。
/*** schedule handleShow into the right thread*//** 在NotificationManagerService中会通过TN对象远程调用TN对象的show方法来实现Toast的显示,* 因此show方法运行在Binder线程池中。* 为了将执行环境切换到Toast请求所在的线程,在它们的内部都使用了Handler,* 所以Toast的显示又跳转到mShow中。* */@Overridepublic void show() {if (localLOGV) Log.v(TAG, "SHOW: " + this);mHandler.post(mShow);}/*** schedule handleHide into the right thread*//** 在NotificationManagerService中会通过TN对象远程调用TN对象的hide方法来实现Toast的隐藏,* 因此hide方法运行在Binder线程池中。* 为了将执行环境切换到Toast请求所在的线程,在它们的内部都使用了Handler,* 所以Toast的隐藏又跳转到mHide中。* */@Overridepublic void hide() {if (localLOGV) Log.v(TAG, "HIDE: " + this);mHandler.post(mHide);}
private static class TN extends ITransientNotification.Stub {/** mShow是Runnable,内部调用了handleShow方法,* 可见handleShow方法才是真正完成显示Toast的地方。* TN的handleShow中会将Toast的视图添加到Window中。 * */final Runnable mShow = new Runnable() {@Overridepublic void run() {handleShow();}};/** mHide是Runnable,内部调用了handleHide方法,* 可见handleHide方法才是真正完成隐藏Toast的地方。* TN的handleHide中会将Toast的视图从Window中移除。 * */final Runnable mHide = new Runnable() {@Overridepublic void run() {handleHide();// Don't do this in handleHide() because it is also invoked by handleShow()mNextView = null;}};private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();final Handler mHandler = new Handler(); int mGravity;int mX, mY;float mHorizontalMargin;float mVerticalMargin;View mView;View mNextView;WindowManager mWM;TN() {// XXX This should be changed to use a Dialog, with a Theme.Toast// defined that sets up the layout params appropriately.final WindowManager.LayoutParams params = mParams;params.height = WindowManager.LayoutParams.WRAP_CONTENT;params.width = WindowManager.LayoutParams.WRAP_CONTENT;params.format = PixelFormat.TRANSLUCENT;params.windowAnimations = com.android.internal.R.style.Animation_Toast;params.type = WindowManager.LayoutParams.TYPE_TOAST;params.setTitle("Toast");params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;}/*** schedule handleShow into the right thread*//** 在NotificationManagerService中会通过TN对象远程调用TN对象的show方法来实现Toast的显示,* 因此show方法运行在Binder线程池中。* 为了将执行环境切换到Toast请求所在的线程,在它们的内部都使用了Handler,* 所以Toast的显示又跳转到mShow中。* */@Overridepublic void show() {if (localLOGV) Log.v(TAG, "SHOW: " + this);mHandler.post(mShow);}/*** schedule handleHide into the right thread*//** 在NotificationManagerService中会通过TN对象远程调用TN对象的hide方法来实现Toast的隐藏,* 因此hide方法运行在Binder线程池中。* 为了将执行环境切换到Toast请求所在的线程,在它们的内部都使用了Handler,* 所以Toast的隐藏又跳转到mHide中。* */@Overridepublic void hide() {if (localLOGV) Log.v(TAG, "HIDE: " + this);mHandler.post(mHide);}/** handleShow方法才是真正完成显示Toast的地方。* TN的handleShow中会将Toast的视图添加到Window中。 * */public void handleShow() {if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView+ " mNextView=" + mNextView);if (mView != mNextView) {// remove the old view if necessaryhandleHide();mView = mNextView;Context context = mView.getContext().getApplicationContext();if (context == null) {context = mView.getContext();}mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);// We can resolve the Gravity here by using the Locale for getting// the layout directionfinal Configuration config = mView.getContext().getResources().getConfiguration();final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection());mParams.gravity = gravity;if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {mParams.horizontalWeight = 1.0f;}if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {mParams.verticalWeight = 1.0f;}mParams.x = mX;mParams.y = mY;mParams.verticalMargin = mVerticalMargin;mParams.horizontalMargin = mHorizontalMargin;if (mView.getParent() != null) {if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);mWM.removeView(mView);}if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this);/** 在这里呢* */mWM.addView(mView, mParams);trySendAccessibilityEvent();}}private void trySendAccessibilityEvent() {AccessibilityManager accessibilityManager =AccessibilityManager.getInstance(mView.getContext());if (!accessibilityManager.isEnabled()) {return;}// treat toasts as notifications since they are used to// announce a transient piece of information to the userAccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);event.setClassName(getClass().getName());event.setPackageName(mView.getContext().getPackageName());mView.dispatchPopulateAccessibilityEvent(event);accessibilityManager.sendAccessibilityEvent(event);} /** handleHide方法才是真正完成隐藏Toast的地方。* TN的handleHide中会将Toast的视图从Window中移除。* */public void handleHide() {if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);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.if (mView.getParent() != null) {if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);/** 在这里呢* */mWM.removeView(mView);}mView = null;}}}
(10)整体过程:
理解Window和WindowManager相关推荐
- Android开发艺术探索》读书笔记 (8) 第8章 理解Window和WindowManager
第8章 理解Window和WindowManager 8.1 Window和WindowManager (1)Window是抽象类,具体实现是PhoneWindow,通过WindowManager就可 ...
- Android 带你彻底理解 Window 和 WindowManager
有时候我们需要在桌面上显示一个类似悬浮窗的东西,这种效果就需要用 Window 来实现,Window 是一个抽象类,表示一个窗口,它的具体实现类是 PhoneWindow,实现位于 WindowMan ...
- 带你彻底理解 Window 和 WindowManager
有时候我们需要在桌面上显示一个类似悬浮窗的东西,这种效果就需要用 Window 来实现,Window 是一个抽象类,表示一个窗口,它的具体实现类是 PhoneWindow,实现位于 WindowMan ...
- Android源码分析之理解Window和WindowManager
Window和WindowManager概述 Window是一个抽象类,它的具体实现是PhoneWindow,创建一个Window通过WindowManager 就可以完成.WindowManager ...
- Android中的Window、WindowManager以及悬浮框视频播放的实现
摘要:近日看公司直播项目,其中有一个功能就是退出某房间之后,直播界面会以悬浮窗的形式出现,并且可以拖动悬浮窗到界面中任意位置,点击悬浮框之后,又可以回到房间中继续观看直播.现在这个功能在主流的直播或者 ...
- Android之Window与WindowManager
Window表示一个窗口的概念,在日常开发中直接接触Window的机会并不多,但却会经常用到Window,activity.toast.dialog.PopupWindow.状态栏等都是Window ...
- Android GUI之Window、WindowManager
通过前几篇的文章(查看系列文章:http://www.cnblogs.com/jerehedu/p/4607599.html#gui ),我们清楚了Activity实际上是将视图的创建和显示交给了Wi ...
- Activity Window View WindowManager关系Touch事件分发机制
http://www.cnblogs.com/linjzong/p/4191891.html https://www.cnblogs.com/kest/p/5141817.html https://b ...
- Window与WindowManager
相关类: Window:是一个抽象类,只是一个概念并不实际存在,唯一实现类是PhoneWindow,其对View进行管理 WindowManager:一个接口类,继承自ViewManager,字面意思 ...
- [转]Android 之 Window、WindowManager 与窗口管理
这篇是对前两天研究的悬浮窗的内容的一个小小的资料整理吧.首先是转载一篇介绍Android窗口的内容,觉得写得不错:http://blog.csdn.net/xieqibao/article/detai ...
最新文章
- 实现数据集多( 高 )维可视化(附代码)
- doc es 中type_Elasticsearch(024):es常见的字段映射类型之 连接类型(join type)
- phpcmsv9mysql扩展_phpcmsV9升级到php7.1+mysql5.6+nginx1.9迁移步骤nginx中文伪静态rewrite配置...
- 聊聊分布式锁——Redis和Redisson的方式
- 租赁mt4虚拟服务器,mt4服务器出租
- es6 --- 模块
- 剑指offer:39-42记录
- 计算机科学文章,计算机科学导论论文范文
- 圆弧裁剪算法c++_箍筋算法之争:按外皮长度计算与按中心线长度计算究竟相差多少?...
- URLEncoder URLDecoder
- 经典算法:蒙特卡洛方法(MCMC)
- ajax 跨域 iis7,IIS中使用URL rewrite配置跨域调用
- blender快捷键记录-基本所有场景通用
- 匹配问题——匈牙利算法
- 笔记本电脑设置WiFi共享
- ABP VNext学习日记30
- 文件类型关联的文件图标
- 深入了解JVM之内存区域(一)
- 向量的平面投影 ProjectOnPlane
- Latex中插入.eps图片遇到的问题 (Unknown graphics extension:.eps)