Android 使用WindowManager实现悬浮窗及源码解析
Posted 一口仨馍
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 使用WindowManager实现悬浮窗及源码解析相关的知识,希望对你有一定的参考价值。
本文已授权微信公众号《鸿洋》原创首发,转载请务必注明出处。
使用
效果预览
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;
在设置各种属性之后,直接向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"/>
现在点击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;
这里在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;
至此为止。悬浮窗已经显示出来,点击拖动事件也已经搞定。虽然和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;
首先获取应用程序的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);
成员变量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);
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());
);
...
这里返回了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);
首先验证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?
给我们的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)
...
mWindowSession
是IWindowSession
对象。在创建ViewRootImpl
对象时被实例化。
public ViewRootImpl(Context context, Display display)
mWindowSession = WindowManagerGlobal.getWindowSession();
...
跟进。
源码位置: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;
这里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;
返回一个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);
这里的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);
...
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);
将传入的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);
这个过程要稍微麻烦点,首先调用root.die()
,接着将View
添加进mDyingViews
。跟进。
源码位置:frameworks/base/core/java/android/view/ViewRootImpl.java
ViewRootImpl#die()
boolean die(boolean immediate)
...
mHandler.sendEmptyMessage(MSG_DIE);
return true;
这里的参数immediate
默认为false
,也就是说这里只是发送了一个what=MSG_DIE
的空消息。ViewRootHandler
收到这条消息会执行doDie()
。
void doDie()
checkThread();
...
WindowManagerGlobal.getInstance().doRemoveView(this);
跟进。
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();
经过一圈效验最终还是回到WindowManagerGlobal
中移除View
。
至此,本文已经全部结束,感谢耐心阅读到最后~
更多Framework源码解析,请移步 Framework源码解析系列[目录]
以上是关于Android 使用WindowManager实现悬浮窗及源码解析的主要内容,如果未能解决你的问题,请参考以下文章
Android 使用WindowManager实现悬浮窗及源码解析
Android 使用WindowManager实现悬浮窗及源码解析