[源码分析]悬浮框工具条实现
有几个关于window的基础知识需要知道
1.每个Window都对应了一个View和一个ViewRootImpl
2.View是Window存在的实体
3.Window的具体实现位于WindowManagerService中
4.WindowManager是外界访问Window的入口
5.WindowManager和WindowManagerService交互是一个IPC过程
6.Window的添加过程实际上是一次IPC的调用(为什么会有token的原因)
通常有3中window类型
1.Application windows
取值范围从FIRST_APPLICATION_WINDOW(Constant Value: 1 (0x00000001))到 LAST_APPLICATION_WINDOW(Constant Value: 99 (0x00000063))
这种window是普通的顶层window.
这些种类的window的token必须设置成Activity的token(如果这个token是null,那么需要你来提供)
2.Sub-windows
取值范围从FIRST_SUB_WINDOW(Constant Value: 1000 (0x000003e8))到 LAST_SUB_WINDOW(Constant Value: 1999 (0x000007cf))
这种window一般都和其他顶层window关联在一起,
这种window的token必须是关联的window的token
3.System windows
取值范围为从 FIRST_SYSTEM_WINDOW(Constant Value: 2000 (0x000007d0)) 到 LAST_SYSTEM_WINDOW(Constant Value: 2999 (0x00000bb7))
这种window是特殊的window类型,一般是系统用户特殊目的使用的
这种window不应该被普通程序使用,想要使用他们必须拥有特别的权限
(也就是说从api23开始不要想通过这种创建这种window的方式构造悬浮窗了,系统默认就不允许)
type有如下类型
源码android.viewWindowManager.java文件中可以看到,并有相关的解释,总得来说有个原则,type值越大则显示的越靠上层,上面的这些type常量都是系统中各种UI默认的使用的值,如果要达到你想要达到的效果甚至可以自己设置想要的int值,比如想要覆盖在状态栏之上,就设置个大于2001且小于2999的值就行。具体内容如下:
/**
* Start of types of sub-windows. The {@link #token} of these windows
* must be set to the window they are attached to. These types of
* windows are kept next to their attached window in Z-order, and their
* coordinate space is relative to their attached window.
*/
public static final int FIRST_SUB_WINDOW = 1000;
/**
* Window type: a panel on top of an application window. These windows
* appear on top of their attached window.
*/
public static final int TYPE_APPLICATION_PANEL = FIRST_SUB_WINDOW;
/**
* Window type: window for showing media (such as video). These windows
* are displayed behind their attached window.
*/
public static final int TYPE_APPLICATION_MEDIA = FIRST_SUB_WINDOW + 1;
...
/**
* Window type: a above sub-panel on top of an application window and it's
* sub-panel windows. These windows are displayed on top of their attached window
* and any {@link #TYPE_APPLICATION_SUB_PANEL} panels.
* @hide
*/
public static final int TYPE_APPLICATION_ABOVE_SUB_PANEL = FIRST_SUB_WINDOW + 5;
/**
* End of types of sub-windows.
*/
public static final int LAST_SUB_WINDOW = 1999;
/**
* Start of system-specific window types. These are not normally
* created by applications.
*/
public static final int FIRST_SYSTEM_WINDOW = 2000;
/**
* Window type: the status bar. There can be only one status bar
* window; it is placed at the top of the screen, and all other
* windows are shifted down so they are below it.
* In multiuser systems shows on all users' windows.
*/
public static final int TYPE_STATUS_BAR = FIRST_SYSTEM_WINDOW;
/**
* Window type: the search bar. There can be only one search bar
* window; it is placed at the top of the screen.
* In multiuser systems shows on all users' windows.
*/
public static final int TYPE_SEARCH_BAR = FIRST_SYSTEM_WINDOW+1;
...
/**
* Window type: Application overlay windows are displayed above all activity windows
* (types between {@link #FIRST_APPLICATION_WINDOW} and {@link #LAST_APPLICATION_WINDOW})
* but below critical system windows like the status bar or IME.
* <p>
* The system may change the position, size, or visibility of these windows at anytime
* to reduce visual clutter to the user and also manage resources.
* <p>
* Requires {@link android.Manifest.permission#SYSTEM_ALERT_WINDOW} permission.
* <p>
* The system will adjust the importance of processes with this window type to reduce the
* chance of the low-memory-killer killing them.
* <p>
* In multi-user systems shows only on the owning user's screen.
*/
public static final int TYPE_APPLICATION_OVERLAY = FIRST_SYSTEM_WINDOW + 38;
/**
* End of types of system windows.
*/
public static final int LAST_SYSTEM_WINDOW = 2999;
/**
* @hide
* Used internally when there is no suitable type available.
*/
public static final int INVALID_WINDOW_TYPE = -1;
自定义一个View,在初始化时,添加属性,就能使其可以悬浮
windowManager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
wmLayoutParams = new WindowManager.LayoutParams();
//4.4以上toast能获取到焦点,能处理事件,但以下则不能
//4.4以上system_error程序退到后台就不能悬浮于系统上
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
wmLayoutParams.type = WindowManager.LayoutParams.TYPE_TOAST;
} else {
wmLayoutParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR;
}
wmLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
wmLayoutParams.format = PixelFormat.RGBA_8888;
wmLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
wmLayoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
wmLayoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
重写onInterceptTouchEvent、onTouchEvent方法添加控制处理
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
if (isCanTouchMove()) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
xDownInScreen = event.getRawX();
yDownInScreen = event.getRawY() - getStatusBarHeight();
break;
case MotionEvent.ACTION_MOVE:
xInScreen = event.getRawX();
yInScreen = event.getRawY() - getStatusBarHeight();
float dx = Math.abs(xInScreen - xDownInScreen);
float dy = Math.abs(yInScreen - yDownInScreen);
if (dx < 10f && dy < 10f) {
return false;
} else {
//拦截,交给自己的onTouch事件处理
return true;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
xDownInScreen = 0;
yDownInScreen = 0;
break;
}
}
return super.onInterceptTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (isCanTouchMove()) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
xInScreen = event.getRawX();
yInScreen = event.getRawY() - getStatusBarHeight();
break;
case MotionEvent.ACTION_MOVE:
xInScreen = event.getRawX();
yInScreen = event.getRawY() - getStatusBarHeight();
wmLayoutParams.x = (int) (xInScreen - getWidth() / 2);
wmLayoutParams.y = (int) (yInScreen - getHeight() / 2);
updatePosition();
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
updatePosition();
if (alignSide)
alignSide();
break;
default:
break;
}
}
return super.onTouchEvent(event);
}
添加更新位置方法
private void updatePosition() {
windowManager.updateViewLayout(this, wmLayoutParams);
}
public void updatePosition(int x, int y) {
if (x >= 0)
wmLayoutParams.x = x;
if (y >= 0)
wmLayoutParams.y = y;
// 刷新
updatePosition();
}
模拟手机系统悬浮框,可靠边处理
@Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
alignSide();
}
/**
* 靠边
*/
private void alignSide() {
int width = getResources().getDisplayMetrics().widthPixels;
int height = getResources().getDisplayMetrics().heightPixels;
if (-1 != alignGravity) {
switch (alignGravity) {
case Gravity.LEFT:
wmLayoutParams.x = 0;
break;
case Gravity.TOP:
wmLayoutParams.y = 0;
break;
case Gravity.RIGHT:
wmLayoutParams.x = width;
break;
case Gravity.BOTTOM:
wmLayoutParams.y = height;
break;
default:
break;
}
} else {
//以屏幕中点为原点,横向为X轴,纵向为Y轴计算
if (xInScreen < width / 2) {//第二、三象限
if (yInScreen < height / 2) {//第二象限
if (xInScreen < yInScreen) {
wmLayoutParams.x = 0;
} else {
wmLayoutParams.y = 0;
}
} else {//第三象限
if (xInScreen < height - yInScreen) {
wmLayoutParams.x = 0;
} else {
wmLayoutParams.y = height;
}
}
} else {//第一、四象限
if (yInScreen < height / 2) {//第一象限
if (width - xInScreen < yInScreen) {
wmLayoutParams.x = width;
} else {
wmLayoutParams.y = 0;
}
} else {//第四象限
if (width - xInScreen < height - yInScreen) {
wmLayoutParams.x = width;
} else {
wmLayoutParams.y = height;
}
}
}
}
updatePosition();
}
然后添加创建方法和移除方法:
public boolean addToWindow() {
boolean result;
if (windowManager != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (!isAttachedToWindow()) {
windowManager.addView(this, wmLayoutParams);
result = true;
} else {
result = false;
}
} else {
try {
if (getParent() == null) {
windowManager.addView(this, wmLayoutParams);
}
result = true;
} catch (Exception e) {
result = false;
}
}
} else {
result = false;
}
if (result && null != addedListener) {
addedListener.onAddedToWindow(getPosition().x, getPosition().y);
}
return result;
}
public boolean removeFromWindow() {
boolean result;
if (windowManager != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (isAttachedToWindow()) {
windowManager.removeViewImmediate(this);
result = true;
} else {
result = false;
}
} else {
try {
if (getParent() != null) {
windowManager.removeViewImmediate(this);
}
result = true;
} catch (Exception e) {
result = false;
}
}
} else {
result = false;
}
if (result && null != removedListener) {
removedListener.onRemovedToWindow(getPosition().x, getPosition().y);
}
return result;
}
接下来就可以愉快的创建它了,如果在service中启动它的话,那么它就是一个依赖于应用的全局悬浮框
if (null == floatView) {
floatView = new FloatView(this);
}
floatView.addToWindow();
WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
int width = wm.getDefaultDisplay().getWidth();
int height = wm.getDefaultDisplay().getHeight();
floatView.updatePosition(width, height);
附上github,view类完整代码:https://github.com/YJF-Lib/android-floatview
怎么创建的,为什么不需要权限,分析一下源码
首先,自定义的view,在创建时使用的是windowManager.addView(this, wmLayoutParams)方法,所以先看一下WindowManager类:
WindowManager继承ViewManager
@SystemService(Context.WINDOW_SERVICE)
public interface WindowManager extends ViewManager
{
...
}
而addView是定义在ViewManager的接口
/** 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);
}
在WindowManagerImpl继承了WindowManager接口并实现addView方法
public final class WindowManagerImpl implements WindowManager {
private final Display mDisplay;
private final WindowManagerGlobal mGlobal;
private final Window mParentWindow;
public WindowManagerImpl(Display var1) {
this(var1, (Window)null);
}
private WindowManagerImpl(Display var1, Window var2) {
this.mGlobal = WindowManagerGlobal.getInstance();
this.mDisplay = var1;
this.mParentWindow = var2;
}
public void addView(View var1, LayoutParams var2) {
this.mGlobal.addView(var1, var2, this.mDisplay, this.mParentWindow);
}
...
}
mGlobal是WindowManagerGlobal的实例,所以调用的的是WindowManagerGlobal.addView(),代码中创建了一个ViewRootImpl实例root,并且调用root.setView传入view
public void addView(View view, android.view.ViewGroup.LayoutParams params, Display display, Window parentWindow) {
...
ViewRootImpl root;
synchronized(this.mLock) {
...
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
this.mViews.add(view);
this.mRoots.add(root);
this.mParams.add(wparams);
}
try {
root.setView(view, wparams, panelParentView);
} catch (RuntimeException var15) {
Object var18 = this.mLock;
synchronized(this.mLock) {
index = this.findViewLocked(view, false);
if (index >= 0) {
this.removeViewLocked(index, true);
}
}
throw var15;
}
}
在ViewRootImpl.setView里最关键的代码是:
public void setView(View view, LayoutParams attrs, View panelParentView) {
synchronized(this) {
if (this.mView == null) {
...
int res;
try {
this.mOrigWindowType = this.mWindowAttributes.type;
this.mAttachInfo.mRecomputeGlobalAttributes = true;
this.collectViewAttributes();
res = this.mWindowSession.addToDisplay(this.mWindow, this.mSeq, this.mWindowAttributes, this.getHostVisibility(), this.mDisplay.getDisplayId(), this.mAttachInfo.mContentInsets, this.mAttachInfo.mStableInsets, this.mInputChannel);
} catch (RemoteException var20) {
this.mAdded = false;
this.mView = null;
this.mAttachInfo.mRootView = null;
this.mInputChannel = null;
this.mFallbackEventHandler.setView((View)null);
this.unscheduleTraversals();
this.setAccessibilityFocus((View)null, (AccessibilityNodeInfo)null);
throw new RuntimeException("Adding window failed", var20);
} finally {
if (restore) {
attrs.restore();
}
}
...
}
}
}
mWindowSession的类型是IWindowSession,mWindow的类型是IWindow.Stub,这句代码就是利用AIDL进行IPC,实际被调用的是Session.addToDisplay:
@Override
public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets,
Rect outOutsets, InputChannel outInputChannel) {
return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,
outContentInsets, outStableInsets, outOutsets, outInputChannel);
}
由addToDisplay调用WindowManagerService的addWindow:
public int addWindow(Session session, IWindow client, int seq,
WindowManager.LayoutParams attrs, int viewVisibility, int displayId,
Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
InputChannel outInputChannel) {
int[] appOp = new int[1];
int res = mPolicy.checkAddPermission(attrs, appOp);
if (res != WindowManagerGlobal.ADD_OKAY) {
return res;
}
...
synchronized(mWindowMap) {
...
mPolicy.adjustWindowParamsLw(win.mAttrs);
win.setShowToOwnerOnlyLocked(mPolicy.checkShowToOwnerOnly(attrs));
res = mPolicy.prepareAddWindowLw(win, attrs);
if (res != WindowManagerGlobal.ADD_OKAY) {
return res;
}
...
}
...
return res;
}
从这方法中,第一段就能看到使用mPolicy.checkAddPermission(attrs, appOp)检查权限,mPolicy是标记为final的成员变量:
final WindowManagerPolicy mPolicy =PolicyManager.makeNewWindowManager();
继续看PolicyManager.makeNewWindowManager,实际是Policy.makeNewWindowManager()
public WindowManagerPolicy makeNewWindowManager(){
return new PhoneWindowManager();
}
现在我们知道mPolicy实际上是PhoneWindowManager,那么intres =mPolicy.checkAddPermission(attrs, appOp);实际调用的代码是PhoneWindowManager.checkAddPermission(),可以看到TYPE_TOAST不检查permission,TYPE_PHONE检查SYSTEM_ALERT_WINDOW权限
/** {@inheritDoc} */
@Override
public int checkAddPermission(WindowManager.LayoutParams attrs, int[] outAppOp) {
...
if (!isSystemAlertWindowType(type)) {
switch (type) {
case TYPE_TOAST:
// Only apps that target older than O SDK can add window without a token, after
// that we require a token so apps cannot add toasts directly as the token is
// added by the notification system.
// Window manager does the checking for this.
outAppOp[0] = OP_TOAST_WINDOW;
return ADD_OKAY;
case TYPE_DREAM:
case TYPE_INPUT_METHOD:
case TYPE_WALLPAPER:
case TYPE_PRESENTATION:
case TYPE_PRIVATE_PRESENTATION:
case TYPE_VOICE_INTERACTION:
case TYPE_ACCESSIBILITY_OVERLAY:
case TYPE_QS_DIALOG:
// The window manager will check these.
return ADD_OKAY;
}
return mContext.checkCallingOrSelfPermission(INTERNAL_SYSTEM_WINDOW)
== PERMISSION_GRANTED ? ADD_OKAY : ADD_PERMISSION_DENIED;
}
...
}
接着刚才addWindow方法的分析,继续看下面一句,:
public int addWindow(Session session, IWindow client, int seq,
WindowManager.LayoutParams attrs, int viewVisibility, int displayId,
Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
InputChannel outInputChannel) {
int[] appOp = new int[1];
int res = mPolicy.checkAddPermission(attrs, appOp);
if (res != WindowManagerGlobal.ADD_OKAY) {
return res;
}
...
synchronized(mWindowMap) {
...
mPolicy.adjustWindowParamsLw(win.mAttrs);
win.setShowToOwnerOnlyLocked(mPolicy.checkShowToOwnerOnly(attrs));
res = mPolicy.prepareAddWindowLw(win, attrs);
if (res != WindowManagerGlobal.ADD_OKAY) {
return res;
}
...
}
...
return res;
}
还是在PhoneWindowManager里的方法adjustWindowParamsLw(),给出了三个版本的实现,一个是2.0到2.3.7实现的版本,一个是4.0.1到4.3.1实现的版本,一个是4.4实现的版本:
//Android 2.0 - 2.3.7 PhoneWindowManager
public void adjustWindowParamsLw(WindowManager.LayoutParams attrs) {
switch (attrs.type) {
case TYPE_SYSTEM_OVERLAY:
case TYPE_SECURE_SYSTEM_OVERLAY:
case TYPE_TOAST:
// These types of windows can't receive input events.
attrs.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
break;
}
}
//Android 4.0.1 - 4.3.1 PhoneWindowManager
public void adjustWindowParamsLw(WindowManager.LayoutParams attrs) {
switch (attrs.type) {
case TYPE_SYSTEM_OVERLAY:
case TYPE_SECURE_SYSTEM_OVERLAY:
case TYPE_TOAST:
// These types of windows can't receive input events.
attrs.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
attrs.flags &= ~WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
break;
}
}
//Android 4.4 PhoneWindowManager
@Override
public void adjustWindowParamsLw(WindowManager.LayoutParams attrs) {
switch (attrs.type) {
case TYPE_SYSTEM_OVERLAY:
case TYPE_SECURE_SYSTEM_OVERLAY:
// These types of windows can't receive input events.
attrs.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
attrs.flags &= ~WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
break;
}
}
小结
在4.0.1以前,当我们使用TYPE_TOAST, Android会偷偷给我们加上FLAG_NOT_FOCUSABLE和FLAG_NOT_TOUCHABLE,4.0.1开始,会额外再去掉FLAG_WATCH_OUTSIDE_TOUCH,这样真的是什么事件都没了.而4.4开始,TYPE_TOAST被移除了,所以从4.4开始,使用TYPE_TOAST的同时还可以接收触摸事件和按键事件了,而4.4以前只能显示出来,不能交互。