您的位置:首页 > 移动开发 > Android开发

Android 使用WindowManager实现悬浮窗及源码解析

2017-08-16 16:48 886 查看
本文已授权微信公众号《鸿洋》原创首发,转载请务必注明出处。

使用

效果预览



Demo结构



一个
Activity
、一个
Service
和两个布局文件。布局十分简单,这里就不贴了,大概描述下。
activity_main.xml
中俩按钮,
layout_window.xml
中一个
TextView
。ok,首先看下
MainActivity
MainActivity
中只有俩按钮,点击启动
WindowService
,点击停止
WindowService
。没啥好说的。直接看
WindowService


/**
* @author CSDN 一口仨馍
*/
public class WindowService extends Service {

private final String TAG = this.getClass().getSimpleName();

private WindowManager.LayoutParams wmParams;
private WindowManager mWindowManager;
private View mWindowView;
private TextView mPercentTv;

private int mStartX;
private int mStartY;
private int mEndX;
private int mEndY;

@Override
public void onCreate() {
super.onCreate();
Log.i(TAG, "onCreate");
initWindowParams();
initView();
addWindowView2Window();
initClick();
}

private void initWindowParams() {
mWindowManager = (WindowManager) getApplication().getSystemService(getApplication().WINDOW_SERVICE);
wmParams = new WindowManager.LayoutParams();
// 更多type:https://developer.android.com/reference/android/view/WindowManager.LayoutParams.html#TYPE_PHONE
wmParams.type = WindowManager.LayoutParams.TYPE_PHONE;
wmParams.format = PixelFormat.TRANSLUCENT;
// 更多falgs:https://developer.android.com/reference/android/view/WindowManager.LayoutParams.html#FLAG_NOT_FOCUSABLE
wmParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
wmParams.gravity = Gravity.LEFT | Gravity.TOP;
wmParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
wmParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
}

private void initView() {
mWindowView = LayoutInflater.from(getApplication()).inflate(R.layout.layout_window, null);
mPercentTv = (TextView) mWindowView.findViewById(R.id.percentTv);
}

private void addWindowView2Window() {
mWindowManager.addView(mWindowView, wmParams);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i(TAG, "onStartCommand");
return super.onStartCommand(intent, flags, startId);
}

@Override
public void onDestroy() {
super.onDestroy();
if (mWindowView != null) {
//移除悬浮窗口
Log.i(TAG, "removeView");
mWindowManager.removeView(mWindowView);
}
Log.i(TAG, "onDestroy");
}

@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
[/code]

在设置各种属性之后,直接向
WindowManager
中添加
mWindowView
(也就是我们自己的布局
layout_window.xml
)。在此之前需要在
AndroidManifest。xml
中注册
Service
和添加相应的限权。

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.GET_TASKS" />

<service android:name=".WindowService"/>
1
2
3
4


1
2
3
4
[/code]

现在点击
startBtn
,桌面上已经可以出现悬浮窗。但是没有拖动啦点击啦这些动作。小意思,重写点击事件。根据拖动距离,判断是点击还是滑动。由于
onTouchEvent()
的优先级比
onClick
高,拖动时在需要的拦截的地方,
return true
就ok了。具体如下:

private void initClick() {
mPercentTv.setOnTouchListener(new View.OnTouchListener() {

@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mStartX = (int) event.getRawX();
mStartY = (int) event.getRawY();
break;
case MotionEvent.ACTION_MOVE:
mEndX = (int) event.getRawX();
mEndY = (int) event.getRawY();
if (needIntercept()) {
//getRawX是触摸位置相对于屏幕的坐标,getX是相对于按钮的坐标
wmParams.x = (int) event.getRawX() - mWindowView.getMeasuredWidth() / 2;
wmParams.y = (int) event.getRawY() - mWindowView.getMeasuredHeight() / 2;
mWindowManager.updateViewLayout(mWindowView, wmParams);
return true;
}
break;
case MotionEvent.ACTION_UP:
if (needIntercept()) {
return true;
}
break;
default:
break;
}
return false;
}
});

mPercentTv.setOnClickListener(new View.OnClickListener() {

@Override
public void onClick(View v) {
if (isAppAtBackground(WindowService.this)) {
Intent intent = new Intent(WindowService.this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
}
});
}

/**
* 是否拦截
* @return true:拦截;false:不拦截.
*/
private boolean needIntercept() {
if (Math.abs(mStartX - mEndX) > 30 || Math.abs(mStartY - mEndY) > 30) {
return true;
}
return false;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
[/code]

这里在
onClick
中进行了一个程序前后台的判断操作,方法如下:

/**
*判断当前应用程序处于前台还是后台
*/
private boolean isAppAtBackground(final Context context) {
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.RunningTaskInfo> tasks = am.getRunningTasks(1);
if (!tasks.isEmpty()) {
ComponentName topActivity = tasks.get(0).topActivity;
if (!topActivity.getPackageName().equals(context.getPackageName())) {
return true;
}
}
return false;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14


1
2
3
4
5
6
7
8
9
10
11
12
13
14
[/code]

至此为止。悬浮窗已经显示出来,点击拖动事件也已经搞定。虽然和360悬浮窗差距还蛮大,但是剩下的只剩具体实现。像
addView()
removeView()
和动画等等,这里就不再具体实现。本着知其然知其所以然的精神,下文是整个流程的源码解析。

源码解析

初始化解析

WindowService
中通过
getApplication().getSystemService(getApplication().WINDOW_SERVICE)
获取到一个
WindowManager
,姑且称这么过程为初始化。

源码位置:frameworks/base/core/Java/Android/app/Service.java

Service#getApplication()

public final Application getApplication() {
return mApplication;
}
1
2
3


1
2
3
[/code]

首先获取应用程序的
Application
对象,然后调用
Application#getSystemService()
。但是,在
Application
中并没有
getSystemService()
这个方法,那么这个方法肯定在父类中或在某个接口中。追踪发现在其父类
ContextWrapper
中。跟进。

源码位置:frameworks/base/core/java/android/content/ContextWrapper.java

ContextWrapper#getSystemServiceName()

@Override
public String getSystemServiceName(Class<?> serviceClass) {
return mBase.getSystemServiceName(serviceClass);
}
1
2
3
4


1
2
3
4
[/code]

成员变量
mBase
Context
对象,跟进。

源码位置:frameworks/base/core/java/Android/content/Context.java

Context#getSystemServiceName()

public final <T> T getSystemService(Class<T> serviceClass) {
String serviceName = getSystemServiceName(serviceClass);
return serviceName != null ? (T)getSystemService(serviceName) : null;
}

public abstract Object getSystemService(@ServiceName @NonNull String name);
1
2
3
4
5
6


1
2
3
4
5
6
[/code]

Context
的实现类是
ContextImpl
,接下来获取服务的方式和Android XML布局文件解析过程源码解析中一样,为了节省篇幅,直接进入到
SystemServiceRegistry
中的静态代码快

源码位置:frameworks/base/core/java/android/app/SystemServiceRegistry.java

static {
...
registerService(Context.WINDOW_SERVICE, WindowManager.class,
new CachedServiceFetcher<WindowManager>() {
@Override
public WindowManager createService(ContextImpl ctx) {
return new WindowManagerImpl(ctx.getDisplay());
}});
...
}
1
2
3
4
5
6
7
8
9
10


1
2
3
4
5
6
7
8
9
10
[/code]

这里返回了
WindowManagerImpl
对象,不过最后强转称了父类
WindowManager
。目前为止,已经获取到了
WindowManager
对象,各种参数也已经初始化完成。接下来只有一行
WindowManager.addView()
。真可谓简单到极致。极度的简单往往是繁琐的假象。接下来,才是本文真正的开始。

WindowManager.addView()解析

源码位置:frameworks/base/core/Java/Android/view/WindowManagerImpl.java

WindowManagerImpl#addView()

@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mDisplay, mParentWindow);
}
1
2
3
4
5


1
2
3
4
5
[/code]

首先验证
Token
,这里不作为重点。接下来还有个
addView()
跟进。

源码位置:frameworks/base/core/Java/Android/view/WindowManagerGlobal.java

WindowManagerGlobal#addView()

public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
// 参数效验
...
ViewRootImpl root;
synchronized (mLock) {
// 查找缓存,类型效验
...
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
}
try {
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
// who care?
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[/code]

给我们的
View
设置参数并添加到
mRoots
中,由
WindowManagerGlobal
进行管理,之后的事情就和View没什么关系了。接着调用
ViewRootImpl#setView()
。跟进。下面是个关键点,同学们注意力要集中。

源码位置:frameworks/base/core/Java/Android/view/ViewRootImpl.java

ViewRootImpl#setView()

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
// 各种属性读取,赋值及效验
...
try {
...
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mInputChannel);
} catch (RemoteException e) {
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14


1
2
3
4
5
6
7
8
9
10
11
12
13
14
[/code]

mWindowSession
IWindowSession
对象。在创建
ViewRootImpl
对象时被实例化。

public ViewRootImpl(Context context, Display display) {
mWindowSession = WindowManagerGlobal.getWindowSession();
...
}
1
2
3
4


1
2
3
4
[/code]

跟进。

源码位置:frameworks/base/core/Java/Android/view/WindowManagerGlobal.java

WindowManagerGlobal#getWindowSession()

public static IWindowSession getWindowSession() {
synchronized (WindowManagerGlobal.class) {
if (sWindowSession == null) {
try {
InputMethodManager imm = InputMethodManager.getInstance();
IWindowManager windowManager = getWindowManagerService();
sWindowSession = windowManager.openSession(
new IWindowSessionCallback.Stub() {
@Override
public void onAnimatorScaleChanged(float scale) {
ValueAnimator.setDurationScale(scale);
}
},
imm.getClient(), imm.getInputContext());
} catch (RemoteException e) {
Log.e(TAG, "Failed to open window session", e);
}
}
return sWindowSession;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[/code]

这里
getWindowManagerService()
通过
AIDL
返回
WindowManagerService
实例。之后调用
WindowManagerService#openSession()
。跟进。

源码位置:frameworks/base/services/java/com/android/server/wm/WindowManagerService.java

WindowManagerService#getWindowSession()

public IWindowSession openSession(IWindowSessionCallback callback, IInputMethodClient client,
IInputContext inputContext) {
if (client == null) throw new IllegalArgumentException("null client");
if (inputContext == null) throw new IllegalArgumentException("null inputContext");
Session session = new Session(this, callback, client, inputContext);
return session;
}
1
2
3
4
5
6
7


1
2
3
4
5
6
7
[/code]

返回一个
Session
对象。也就是说在
ViewRootImpl#setView()
中调用的是
Session#addToDisplay()
。跟进。

源码位置:frameworks/base/services/java/com/android/server/wm/Session.java

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);
}
1
2
3
4
5
6
7


1
2
3
4
5
6
7
[/code]

这里的
mService
是个
WindowManagerService
对象,也就是说最后调用的是
WindowManagerService#addWindow()


源码位置:frameworks/base/services/java/com/android/server/wm/WindowManagerService.java

WindowManagerService#addWindow()

public int addWindow(...) {
...
WindowState win = new WindowState(this, session, client, token,
attachedWindow, appOp[0], seq, attrs, viewVisibility, displayContent);
win.attach();
mWindowMap.put(client.asBinder(), win);
...
}
1
2
3
4
5
6
7
8


1
2
3
4
5
6
7
8
[/code]

mWindowMap
是个
Map
实例,将
WindowManager
添加进
WindowManagerService
统一管理。至此,整个添加视图操作解析完毕。

WindowManager.updateViewLayout()解析

addView()
过程一样,最终会进入到
WindowManagerGlobal#updateViewLayout()


源码位置:frameworks/base/core/Java/Android/view/WindowManagerGlobal.java

WindowManagerGlobal#getWindowSession()

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.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);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[/code]

将传入的
View
设置参数之后,更新
mRoot
中View的参数。没撒好说的。next one。

WindowManager.removeView()解析

和上两个过程一样,最终会进入到
WindowManagerGlobal#removeView()


public void removeView(View view, boolean immediate) {
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}

synchronized (mLock) {
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);
}
}

private void removeViewLocked(int index, boolean immediate) {
ViewRootImpl root = mRoots.get(index);
View view = root.getView();
...
boolean deferred = root.die(immediate);
if (view != null) {
view.assignParent(null);
if (deferred) {
mDyingViews.add(view);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
[/code]

这个过程要稍微麻烦点,首先调用
root.die()
,接着将
View
添加进
mDyingViews
。跟进。

源码位置:frameworks/base/core/java/android/view/ViewRootImpl.java

ViewRootImpl#die()

boolean die(boolean immediate) {
...
mHandler.sendEmptyMessage(MSG_DIE);
return true;
}
1
2
3
4
5


1
2
3
4
5
[/code]

这里的参数
immediate
默认为
false
,也就是说这里只是发送了一个
what=MSG_DIE
的空消息。
ViewRootHandler
收到这条消息会执行
doDie()


void doDie() {
checkThread();
...
WindowManagerGlobal.getInstance().doRemoveView(this);
}
1
2
3
4
5


1
2
3
4
5
[/code]

跟进。

void doRemoveView(ViewRootImpl root) {
synchronized (mLock) {
final int index = mRoots.indexOf(root);
if (index >= 0) {
mRoots.remove(index);
mParams.remove(index);
final View view = mViews.remove(index);
mDyingViews.remove(view);
}
}
if (HardwareRenderer.sTrimForeground && HardwareRenderer.isAvailable()) {
doTrimForeground();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14


1
2
3
4
5
6
7
8
9
10
11
12
13
14
[/code]

经过一圈效验最终还是回到
WindowManagerGlobal
中移除
View


至此,本文已经全部结束,感谢耐心阅读到最后~

更多Framework源码解析,请移步 Framework源码解析系列[目录]
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: